Skip to main content

DuckTap

Turn any OpenAPI spec into a working MCP server in one command — deterministically.

No LLM. No API key. No Claude Code. ducktap press ./openapi.yaml reads the spec and prints an MCP server — plus a Python CLI and an agent skill — that any agent can use. Same spec → same output, every time. Runs in CI.

Website: ducktap-website.vercel.app

CI Release PyPI License: MIT Python 3.11+

DuckTap: OpenAPI spec → MCP server in one command

$ ducktap press ./openapi.yaml --name petstore
Pressed petstore (19 operations) -> out
  python-cli: 11 files
  mcp-server: 5 files
  skill:      3 files

Scorecard: 87/100 (B)
  - coverage:           95  -- 19 operations exposed
  - documentation:      100 -- 19/19 operations have docs
  - auth:               100 -- 2 auth scheme(s)
  - typed_params:       54  -- 29/53 params typed/enum
  - artifacts:          100 -- 3/3 expected artifact dirs present
  - naming:             100 -- 19/19 unique operation ids
  - domain_correctness: 72  -- restful=66, responses=100, base_url=50

Why deterministic? DuckTap parses the spec and emits code directly — no model in the loop. That means no API key, no per-run token cost, reproducible output you can diff and review, and generation that runs in CI. (An LLM can optionally sharpen the generated insight — ducktap press --llm, needing the [llm] extra — but it is off by default so press stays reproducible.)

Inspired by Printing Press by @mvanhorn — same north star (muscle memory for agents). Printing Press is the prompt-driven, Claude-Code-native take; DuckTap is the spec-first, deterministic one. Here's an honest DuckTap vs Printing Press on when to use which.

Why a CLI factory?

In a world of AI agents, a well-designed CLI is muscle memory. No hunting through docs, no wrong turns, no wasted tokens. DuckTap reads the spec, sniffs the traffic when no spec exists, and prints:

  • A Python CLI (<api>-dt-cli) -- Click-based, auth from env vars, JSON by default, pretty mode for humans, local SQLite mirror with FTS5 full-text search, compound query commands (stale, health, bottleneck), retries on transient errors.

  • TypeScript CLI (<api>-dt-ts) -- oclif-based for Node.js teams.

  • Go CLI (<api>-dt-go) -- cobra-based, single binary.

  • Rust CLI (<api>-dt-rs) -- clap-based, single binary distribution.

    Generate any of these with ducktap press <spec> -t go-cli,rust-cli,typescript-cli. Each language's generated project is compiled in CI (go build, cargo build, tsc) so the templates can't silently drift. As of v0.7.0 all four languages share an agent-parity bundle: a --dry-run flag, an agent-context JSON self-introspection command, and typed exit codes (3/4/5/7).

  • An MCP server (<api>-dt-mcp) -- every operation exposed as an MCP tool, stdio transport, drop into Claude Desktop or Cursor in 60 seconds.

  • A skill for Claude Code, Cursor (.mdc), and a generic tools.json -- so any agent harness can pick up where the others left off.

  • A scorecard grading coverage, docs, auth clarity, typed params, artifacts, and naming.

Install

pip install ducktap
ducktap --version

The core install is deterministic-only and stays small. Optional extras:

pip install "ducktap[sniff]"    # browser + proxy sniffing (playwright, mitmproxy)
playwright install chromium
pip install "ducktap[llm]"      # optional LLM steps: polish, rename, insight, vision
pip install "ducktap[search]"   # crowd-sniff web search
pip install "ducktap[all]"      # everything

Requires Python 3.11+.

Developer install (contributing to DuckTap itself)
git clone https://github.com/zanni098/DuckTap
cd DuckTap
pip install -e ".[dev]"
python -m pytest tests/ -q   # 145 passed; language compile tests skipped by default

# Opt into the heavy compile tests (needs go, cargo, and node installed).
# These press the fixture into Go/Rust/TS and actually build each project.
DUCKTAP_COMPILE_TESTS=1 pytest -q tests/test_generated_multilang.py

Quick start

# 1. Press a built-in catalog entry (no URL needed)
ducktap catalog list                  # browse 30 built-in APIs
ducktap catalog print stripe          # press Stripe CLI + MCP + skill

# 2. From any OpenAPI spec URL
ducktap press https://petstore3.swagger.io/api/v3/openapi.yaml

# 3. From a local spec or HAR file
ducktap press ./openapi.yaml --name myapi
ducktap press ./traffic.har  --name myapi

# 4. From a website with no public spec (needs [sniff] extra)
ducktap sniff https://example.com

# 5. Open the dashboard
ducktap ui    # http://127.0.0.1:8765

Running ducktap press prints something like:

Pressed petstore (19 operations) -> ./out
  python-cli: 11 files
  mcp-server: 5 files
  skill:      3 files

Scorecard: 87/100 (B)
  - coverage:           95  -- 19 operations exposed
  - documentation:      100 -- 19/19 operations have docs
  - auth:               100 -- 2 auth scheme(s)
  - typed_params:       54  -- 29/53 params typed/enum
  - artifacts:          100 -- 3/3 expected artifact dirs present
  - naming:             100 -- 19/19 unique operation ids
  - domain_correctness: 72  -- restful=66, responses=100, base_url=50

What you get under ./out/:

out/
├── petstore-dt-cli/        # pip install -e .  →  petstore-dt-cli --help
│   ├── pyproject.toml
│   ├── README.md
│   ├── petstore_dt_cli/
│   │   ├── __main__.py
│   │   ├── main.py
│   │   ├── commands.py     # one click subcommand per API operation
│   │   ├── client.py       # httpx + env-var auth + retries
│   │   └── mirror.py       # local SQLite cache
│   └── tests/test_smoke.py
├── petstore-dt-mcp/        # pip install -e .  →  add to Claude Desktop config
│   └── petstore_dt_mcp/server.py
└── skills/ducktap-petstore/
    ├── SKILL.md            # Claude Code skill
    ├── ducktap-petstore.mdc  # Cursor rule
    └── tools.json          # generic agent tool definitions

Correctness

Generation is only useful if the output is right, so the guarantees are tested rather than asserted:

  • Deterministic. Two presses of the same spec produce byte-identical output. The .ducktap.json manifest records a checksum of the normalized spec so you can tell "the API changed" from "DuckTap changed".
  • It parses, installs, and runs. Reserved words (operationId: class), colliding operation ids, colliding parameter names, and non-PEP 440 versions (Stripe ships 2022-11-15) are all handled — each one used to produce code that wouldn't compile, silently dropped a command, or built the wrong request.
  • Recursive $ref schemas work. Self-referencing models are the norm in real specs; cycles are cut at the discovery boundary.
  • The skill matches the CLI. SKILL.md, cursor.mdc and tools.json are cross-checked against the generated Click command tree by a test, so an agent reading the skill never sees a command or flag that doesn't exist.
  • Generated CLIs don't leak credentials. Redirects are followed by hand and only benign headers cross an origin boundary; path parameters are percent-encoded; <cli> query runs on a read-only SQLite connection.
  • ducktap verify mechanically proves the generated CLI matches the spec — no hallucinated paths, every operation reachable, auth headers correct, no write-only mirror tables.

How DuckTap improves on Printing Press

Printing Press DuckTap
Generation Prompt-driven, a fresh model run each time Deterministic -- same spec, same bytes, runs in CI
Needs to run Go + Node + Claude Code A Python interpreter
Output languages Go Python, Go, Rust, TypeScript from one spec
Skills Claude Code Claude Code + Cursor .mdc + generic tools.json
UI None Local FastAPI dashboard (ducktap ui)
Plugins Source fork Entry-point plugin system -- drop-in discoverers & generators
Browser sniff Custom Go browser Playwright -- full HAR export, scriptable actions
LLM (optional) Claude only Multi-LLM via LiteLLM (Anthropic, OpenAI, Gemini, Ollama, Groq, Azure)

Printing Press wins on maturity and on its 250+ CLI community library. The honest comparison covers when to reach for which.

See docs/COMPARISON.md for the full feature matrix.

Commands

ducktap press <source>          # discover + generate (the default loop)
ducktap research <source>       # discover only -- emit normalized APISpec JSON
ducktap sniff <url>             # browser-sniff a site (needs [sniff] extra)
ducktap scorecard <source>      # quality scorecard
ducktap shipcheck <name>        # structural & runtime sanity checks
ducktap verify <name> -s <src>  # proof of behavior against the spec
ducktap info                    # read back the provenance manifest
ducktap catalog list|print      # browse the recipe library
ducktap plugins list            # show installed discoverers + generators
ducktap library list|add|search # local registry of printed CLIs
ducktap macro list|run|new      # compound command macros
ducktap publish <name>          # push a generated CLI to GitHub + PyPI
ducktap emboss <name>           # brand-stamp a generated CLI
ducktap ui                      # local web dashboard

# Optional, need the [llm] extra:
ducktap insight <source>        # one-sentence Non-Obvious Insight
ducktap polish|rename <source>  # LLM cleanup of summaries / operation ids
ducktap absorb <api>            # feature gate against the agent-CLI playbook
ducktap crowd-sniff <api>       # study community CLIs (needs [search] too)
ducktap vision <url>            # read a docs page from a screenshot

Plugins

Add a discoverer or generator without forking. Register via Python entry points:

# your_plugin/pyproject.toml
[project.entry-points."ducktap.plugins"]
mything = "your_plugin.module"   # module just calls plugins.register_discoverer(...)

See docs/PLUGINS.md and the sample at src/ducktap/plugins/builtin/graphql_intro.py.

Architecture

input (URL | spec | HAR)
        │
        ▼
  ┌─────────────┐
  │  Discovery  │   openapi / har / browser-sniff / graphql (plugin) / ...
  └──────┬──────┘
         ▼
   APISpec (Pydantic) ──── intermediate normalized representation
         │
         ▼
  ┌─────────────┐
  │  Generator  │   python-cli / mcp-server / skill / ...
  └──────┬──────┘
         ▼
  artifacts/       (CLI pkg + MCP pkg + SKILL.md + cursor.mdc + tools.json)
         │
         ▼
  ┌─────────────┐
  │   Verify    │   scorecard + shipcheck + (optional) live smoke test
  └─────────────┘

See docs/ARCHITECTURE.md.

Roadmap

See docs/ROADMAP.md. Highlights for v0.4+:

  • Compound command macros -- YAML recipes that chain API calls with step references
  • FTS5 full-text search over saved JSON responses in the local data lake
  • DuckDB backend as an alternative to SQLite for analytical workloads
  • Top-level query command for SQL over the mirror
  • CLI publish to PyPI + GitHub in one command

License

MIT -- see LICENSE.

Acknowledgements

Inspired by Printing Press by Matt Van Horn and the agent-CLI playbook proved out by discrawl and gogcli.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ducktap-0.8.2.tar.gz (104.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ducktap-0.8.2-py3-none-any.whl (141.2 kB view details)

Uploaded Python 3

File details

Details for the file ducktap-0.8.2.tar.gz.

File metadata

  • Download URL: ducktap-0.8.2.tar.gz
  • Upload date:
  • Size: 104.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for ducktap-0.8.2.tar.gz
Algorithm Hash digest
SHA256 1ac3790f50b66d6a1373d9d5b8d07c0516bb7ae84477e4742756c30312e9d8bf
MD5 c07193ea2e6d6b3f57854049944ddf57
BLAKE2b-256 d21d98d6cbb132f74bd076b5e93a8794b774f0c79b3dd0965b5c647af9af30cb

See more details on using hashes here.

Provenance

The following attestation bundles were made for ducktap-0.8.2.tar.gz:

Publisher: publish.yml on zanni098/DuckTap

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ducktap-0.8.2-py3-none-any.whl.

File metadata

  • Download URL: ducktap-0.8.2-py3-none-any.whl
  • Upload date:
  • Size: 141.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for ducktap-0.8.2-py3-none-any.whl
Algorithm Hash digest
SHA256 bc38d7fb4646123d5376111028cbb9429dea8fd3ee45dc4ac766cf97c9d9655d
MD5 27338b2c3c4146707cb27c8b2e2b8b52
BLAKE2b-256 9c77b17e9d45321a336e1f9f6ace13e31768b1472b5cf5c58813bc2f534ad0b5

See more details on using hashes here.

Provenance

The following attestation bundles were made for ducktap-0.8.2-py3-none-any.whl:

Publisher: publish.yml on zanni098/DuckTap

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.8.3

2 files

This release

0.8.2 This release

2 files

0.8.0

2 files

0.7.0

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.2

2 files

0.2.0

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page