mcp-client-kit 0.4.0__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/CHANGELOG.md +85 -1
  2. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/PKG-INFO +93 -15
  3. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/README.md +91 -13
  4. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/doc/USAGE.md +203 -18
  5. mcp_client_kit-0.6.0/docs/superpowers/plans/2026-08-10-audit-m1-m2.md +3817 -0
  6. mcp_client_kit-0.6.0/docs/superpowers/specs/2026-08-10-audit-m1-m2-design.md +386 -0
  7. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/mcpgen/__init__.py +4 -0
  8. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/mcpgen/_bridge.py +387 -38
  9. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/mcpgen/cli.py +206 -12
  10. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/mcpgen/discovery.py +14 -5
  11. mcp_client_kit-0.6.0/mcpgen/manifest.py +246 -0
  12. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/pyproject.toml +1 -1
  13. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/skills/generate-mcp-runner/SKILL.md +16 -11
  14. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/skills/generate-mcp-runner/runner_templates/http_bearer.py +4 -1
  15. mcp_client_kit-0.6.0/skills/generate-mcp-runner/runner_templates/http_oauth.py +44 -0
  16. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/skills/generate-mcp-runner/runner_templates/http_public.py +4 -1
  17. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/skills/generate-mcp-runner/runner_templates/stdio.py +3 -0
  18. mcp_client_kit-0.6.0/tests/integration/__init__.py +0 -0
  19. mcp_client_kit-0.6.0/tests/integration/conftest.py +27 -0
  20. mcp_client_kit-0.6.0/tests/integration/fixtures/stdio_server.py +49 -0
  21. mcp_client_kit-0.6.0/tests/integration/fixtures/stdio_server_changed.py +46 -0
  22. mcp_client_kit-0.6.0/tests/integration/test_generated_wrapper_e2e.py +188 -0
  23. mcp_client_kit-0.6.0/tests/integration/test_stdio_e2e.py +110 -0
  24. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/tests/test_bridge.py +1330 -2
  25. mcp_client_kit-0.6.0/tests/test_cli_check.py +353 -0
  26. mcp_client_kit-0.6.0/tests/test_cli_codegen_manifest.py +116 -0
  27. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/tests/test_config.py +26 -0
  28. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/tests/test_discovery.py +96 -0
  29. mcp_client_kit-0.6.0/tests/test_manifest.py +309 -0
  30. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/uv.lock +1 -1
  31. mcp_client_kit-0.4.0/skills/generate-mcp-runner/runner_templates/http_oauth.py +0 -34
  32. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/.claude/skills/releasing-a-version/SKILL.md +0 -0
  33. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/.claude-plugin/marketplace.json +0 -0
  34. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/.claude-plugin/plugin.json +0 -0
  35. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/.github/workflows/ci.yml +0 -0
  36. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/.github/workflows/publish.yml +0 -0
  37. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/.gitignore +0 -0
  38. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/CLAUDE.md +0 -0
  39. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/LICENSE +0 -0
  40. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/doc/CODEGEN_SKILL_IDEA.md +0 -0
  41. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/doc/DISTRIBUTION.md +0 -0
  42. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/doc/EXTRACTION_ANALYSIS.md +0 -0
  43. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/doc/LANDSCAPE.md +0 -0
  44. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/doc/MARKET_RESEARCH.md +0 -0
  45. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/doc/RUNNING_LOCALLY.md +0 -0
  46. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/doc/VERDICT.md +0 -0
  47. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/mcpgen/codegen.py +0 -0
  48. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/mcpgen/seam.py +0 -0
  49. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/servers.example.json +0 -0
  50. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/skills/generate-mcp-wrappers/SKILL.md +0 -0
  51. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/tests/test_cli_call.py +0 -0
  52. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/tests/test_cli_env.py +0 -0
  53. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/tests/test_cli_list.py +0 -0
  54. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/tests/test_cli_merge.py +0 -0
  55. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/tests/test_cli_probe.py +0 -0
  56. {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/tests/test_codegen.py +0 -0
@@ -1,6 +1,90 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased] — 0.5.0
3
+ ## [Unreleased] — 0.7.0
4
+
5
+ ## [0.6.0] — 2026-08-24
6
+
7
+ ### Fixed
8
+
9
+ - **A completed login was thrown away whenever the server failed right after it, turning a
10
+ transient outage into an endless browser re-prompt.** `login()` stashed the existing
11
+ credential, ran the OAuth flow, then smoke-tested the result with `initialize()` +
12
+ `list_tools()` — with the whole block under one `except BaseException` that restored the
13
+ stash. But the SDK's `OAuthClientProvider` saves the exchanged token from inside the auth
14
+ handshake, before `initialize()` returns, so any failure after that point rolled a valid
15
+ new token back to the stale one. A `502` from the resource server was indistinguishable
16
+ from "your login failed", and nothing was ever cached, so the next run re-prompted — one
17
+ reported batch produced 16 completed browser logins, all discarded, and no useful work.
18
+ The restore is now conditional: it fires only when the flow produced no token at all. When
19
+ a token *was* issued it is kept, together with the token endpoint needed to refresh it later,
20
+ and the failure surfaces as the new `PostLoginCheckFailed` rather than the raw transport
21
+ error. A login that fails *before* the token exchange — a cancelled consent screen, a
22
+ callback timeout — still restores the previous credential whole, `client_info` included:
23
+ pairing a freshly registered `client_id` with a refresh token the authorization server
24
+ issued to the *old* client would break the refresh the restore exists to preserve.
25
+
26
+ - **Servers that publish no OAuth discovery document re-prompted at every token expiry.**
27
+ The token endpoint was cached only when discovery returned metadata, so for those servers
28
+ `login()` stored a token with no way to renew it, and the next pre-flight refresh had no
29
+ choice but to demand a new browser login — every hour, forever. The endpoint now comes from
30
+ the SDK's own resolution, which covers the no-discovery case with its `<origin>/token`
31
+ fallback, so the cached URL is the one that demonstrably just issued the token rather than a
32
+ guess.
33
+
34
+ ### Added
35
+
36
+ - **`PostLoginCheckFailed`**, exported from `mcpgen`. The counterpart to
37
+ `ReauthenticationRequired`, and deliberately not named for a cause: the token was issued
38
+ and cached, which says nothing about whether the resource server accepted it. A `502` from
39
+ the origin, a post-login `401` over scope or audience, and an MCP-level error from
40
+ `list_tools()` all raise it; what they share is that another browser round cannot fix them.
41
+ Batch callers can now abort on the first failure instead of walking a user through one
42
+ browser prompt per item. `mcpgen login` reports it as a single `[login] error: …` line —
43
+ naming the underlying cause, which anyio otherwise hides behind "unhandled errors in a
44
+ TaskGroup (1 sub-exception)" — and exits `1`. User interrupts (`KeyboardInterrupt`,
45
+ `SystemExit`, cancellation) are never relabelled, even when a task group wraps them.
46
+
47
+ ### Changed
48
+
49
+ - **Generated OAuth runners now stop on `PostLoginCheckFailed` instead of printing a
50
+ traceback.** The `generate-mcp-runner` skill's HTTP + OAuth template called `ensure_login()`
51
+ bare, so the one place mcpgen writes caller code did not demonstrate the contract it
52
+ documents. It now prints the message and exits `1` — the failure is not something a rerun,
53
+ or another trip through the browser, can clear.
54
+
55
+ ## [0.5.0] — 2026-08-09
56
+
57
+ ### Fixed
58
+
59
+ - **A caller-supplied credentials path was silently ignored once past login, so tokens were
60
+ written to one file and read from another.** `login()` and `ensure_login()` accepted
61
+ `creds_path` and honoured it, but the call path never did: `_http_session()` built its
62
+ `FileTokenStorage` on the default `~/.mcpgen/credentials.json`, and neither `session()` nor
63
+ `McpBridgeCaller` even accepted the parameter, so there was no way to route a call to the file
64
+ the login had just written. Anyone keeping tokens outside the shared store — a project-local
65
+ credentials file, two identities side by side, a test fixture — logged in successfully and then
66
+ got `ReauthenticationRequired` on the very next call, in a loop, with both files on disk looking
67
+ plausible. `creds_path` now flows through `_http_session()`, `session()`, and
68
+ `McpBridgeCaller`, defaulting to `DEFAULT_CREDS_PATH` when omitted, so existing behaviour is
69
+ unchanged. The stdio, bearer, static-header, and raw-URL transports do not take it: they store
70
+ no credentials at all, and a parameter that silently does nothing is worse than its absence.
71
+
72
+ ### Added
73
+
74
+ - **`--creds PATH`** on every command that touches stored credentials — `codegen`, `list`,
75
+ `probe`, `call`, `login`, and the management trio `list-creds`, `delete-creds`,
76
+ `migrate-creds` — the CLI surface for the fix above. `mcpgen login` previously dropped the
77
+ path entirely, having no flag to drop it from; it now logs in to exactly the file the
78
+ subsequent calls will read. The management commands were the other half of the same gap: all
79
+ three already took a `credentials_path` internally but exposed no way to set it, so after a
80
+ `--creds` login `list-creds` reported an empty store, `delete-creds` deleted nothing while
81
+ saying so, and `migrate-creds` migrated the wrong file. A flag that exists on half the
82
+ commands that read a file is worse than no flag at all — it makes the other half look broken.
83
+
84
+ - **`DEFAULT_CREDS_PATH` is exported from `mcpgen`** — consumers that thread a credentials path
85
+ through their own CLI need the default as a value. Re-deriving
86
+ `Path.home() / ".mcpgen" / "credentials.json"` downstream would silently drift the day mcpgen
87
+ changes it.
4
88
 
5
89
  ## [0.4.0] — 2026-08-09
6
90
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: mcp-client-kit
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: mcp-client-kit — generate typed Python wrappers for any MCP server (codegen skill + CLI).
5
5
  Project-URL: Homepage, https://github.com/svd/mcp-client-kit
6
6
  Project-URL: Source, https://github.com/svd/mcp-client-kit
@@ -26,7 +26,9 @@ Description-Content-Type: text/markdown
26
26
 
27
27
  **Write your MCP server wrappers once — from the live server. Keep them as real Python source you can diff, review, and pin.**
28
28
 
29
- `mcpgen` turns any MCP server into a typed Python module: one `async def` per tool, real return types, no live server needed to read it. Call your tools from code instead of pumping their schemas through the model's context — the pattern Anthropic measured at up to **98% token reduction**.
29
+ `mcpgen` turns an MCP server reachable over **stdio or Streamable HTTP** into a Python module: one `async def` per tool, typed inputs from the start, and typed returns once you've probed the real responses. No live server needed to read it.
30
+
31
+ `mcpgen` generates the Python wrapper layer for MCP code-mode architectures. Token savings occur when your agent or runtime executes these wrappers instead of loading the full MCP tool catalog into model context. `mcpgen` does not itself alter host tool registration and does not provide a sandbox.
30
32
 
31
33
  > **Package / repo / plugin:** `mcp-client-kit` · **CLI command:** `mcpgen`
32
34
  >
@@ -46,6 +48,8 @@ Every tool definition costs **300–600 tokens** for its name, description, and
46
48
 
47
49
  Anthropic's validated fix ("Code execution with MCP," Nov 2025): stop routing schemas through the model. Generate wrapper code and call the tools from code instead — an approach Anthropic measured shrinking one workflow from ~150,000 tokens to ~2,000 (**98.7%**), with independent benchmarks landing around **78–85%** on less extreme workloads.
48
50
 
51
+ Those numbers describe **the pattern**, as reported by Anthropic and by third-party benchmarks — they are not a measurement of this CLI. mcpgen supplies one piece of that pattern: the wrapper layer. What you save depends on how your runtime uses it.
52
+
49
53
  The catch for Python teams: no good tool generated **standalone, importable, reviewable `.py` wrappers** from a live MCP server. So everyone hand-writes `jira.py`, `github.py`, `slack.py` — slowly, inconsistently, and they silently rot when the server changes.
50
54
 
51
55
  That's the gap mcpgen fills.
@@ -57,7 +61,7 @@ That's the gap mcpgen fills.
57
61
  ```bash
58
62
  uv tool install mcp-client-kit # puts `mcpgen` on your PATH
59
63
  mcpgen login github # browser OAuth, tokens persisted
60
- mcpgen codegen github --out github.py # typed wrappers for every tool
64
+ mcpgen codegen github --out github.py # one async def per tool, typed inputs
61
65
  ```
62
66
 
63
67
  ```python
@@ -74,31 +78,61 @@ async def main():
74
78
  asyncio.run(main())
75
79
  ```
76
80
 
81
+ The `-> GitHubUser` return type above comes from the full lifecycle — probe the tool, shape the response, regenerate. Straight out of `codegen`, `get_me` takes the same typed arguments but returns `Any`. See [Lifecycle](#lifecycle).
82
+
77
83
  `github.py` is just Python. Open it in your IDE, review it in a PR, pin it to a commit, ship it. No runtime proxy, no framework lock-in, no live server required to read what your tools return.
78
84
 
85
+ **One connection for a series of calls.** Each call opens its own session by default. Wrap a run in `connected()` and every call to a server shares one initialized session — one `initialize()`, one stdio subprocess, one OAuth pre-flight refresh:
86
+
87
+ ```python
88
+ caller = McpBridgeCaller(url="https://api.githubcopilot.com/mcp/")
89
+
90
+ async with caller.connected():
91
+ me = await github.get_me(caller)
92
+ issues = await github.list_issues(caller, owner="octocat", repo="hello-world")
93
+ ```
94
+
95
+ Outside the block nothing changes, so existing code keeps working untouched. Details — per-server sessions, concurrency, cleanup — in [`doc/USAGE.md`](doc/USAGE.md#reusing-one-connection-for-a-series-of-calls).
96
+
79
97
  ---
80
98
 
81
99
  ## Why developers pick it
82
100
 
83
101
  **Real source you own.** Importable `.py` modules — not `.pyi` stubs (mcp2py), not a runtime proxy, not tied to one execution framework (ipybox). You can diff it, review it, pin it, and read it in your IDE without a server running.
84
102
 
85
- **Types that match reality.** A tool's `inputSchema` describes its *inputs* — it tells you nothing about the *output* shape. mcpgen's `--probe` makes one live call and records the actual response, so your return types reflect what the server really sends. No other generator does this.
103
+ **Types that match reality.** A tool's `inputSchema` describes its *inputs* — it tells you nothing about the *output* shape, so `codegen` alone returns `Any`. mcpgen's `probe` makes a live call and records the actual response; you (or the skill) turn that observation into an output model, and regeneration bakes it in. Return types then reflect what the server really sends, not a guess. No other generator does this.
86
104
 
87
105
  **OAuth that survives restarts.** Pre-flight token refresh means a fresh process renews a near-expired token silently from the refresh token — no surprise browser pop-up at cold start. (The official SDK's canonical example is in-memory only; every restart re-authenticates.)
88
106
 
89
107
  **Swap auth without regenerating.** Every wrapper takes an `McpCaller` as its first argument. Change transports or auth backends — bearer, OAuth, stdio, a fake for tests — without touching the generated code.
90
108
 
91
- **Built for production teams.** Works with any MCP server (HTTP URL, stdio, or bearer/PAT). Generated code lives in git like any other module, so it survives code review, audits, and pinning.
109
+ **Built for production teams.** Works with MCP servers reachable over **stdio or Streamable HTTP** (OAuth, bearer/PAT, or no auth). Generated code lives in git like any other module, so it survives code review, audits, and pinning.
92
110
 
93
111
  ---
94
112
 
95
- ## How it works
113
+ ## What mcpgen does not do
96
114
 
97
- | Step | Command | What happens |
98
- |------|---------|--------------|
99
- | 1. Generate | `mcpgen codegen <server> --out <server>.py` | One typed `async def` per tool. |
100
- | 2. Probe (optional) | `mcpgen probe <server> <tool> --args '{}' --emit-shape <server>.shapes.json` | Records the *real* response shape from a live call. |
101
- | 3. Regenerate | `mcpgen codegen <server> --out <server>.py --shapes <server>.shapes.json` | Wrappers now return precise types (`TypedDict`s, unions, lists). |
115
+ - It does not remove tool schemas from Claude Code or Cursor context — host tool registration is not mcpgen's to change.
116
+ - It does not sandbox generated code. Generated wrappers are ordinary Python and run with your process's privileges.
117
+ - It does not infer trustworthy output types from input schemas alone. Output types come from live probes plus a judgment pass (see the lifecycle below).
118
+ - It does not speak SSE. SSE servers are surfaced by `mcpgen discover` but marked unsupported by this version.
119
+ - It does not guarantee a remote server will stay stable. That is what `mcpgen check` is for.
120
+
121
+ ---
122
+
123
+ ## Lifecycle
124
+
125
+ | Step | Command | Result |
126
+ |------|---------|--------|
127
+ | 1. Generate | `mcpgen codegen <server> --out <server>.py` | One `async def` per tool: typed inputs, `Any` return by default. |
128
+ | 2. Probe | `mcpgen probe <server> <tool> --args '{}' --emit-shape <server>.shapes.json` | Observed response-shape skeleton, written as a per-tool part under `<server>.shapes.json.parts/`. |
129
+ | 3. Merge | `mcpgen merge <server> --out <server>.shapes.json` | Consolidates the parts into the committed, hand-editable shape-spec. |
130
+ | 4. Edit the shape-spec (or run the skill) | edit `<server>.shapes.json` | Output model and `unwrap` decisions — the judgment pass. |
131
+ | 5. Regenerate | `mcpgen codegen <server> --out <server>.py --shapes <server>.shapes.json` | `TypedDict` returns, unions, lists, overloads. |
132
+
133
+ Step 3 is not optional: `--emit-shape` writes only into `<server>.shapes.json.parts/`, so `<server>.shapes.json` does not exist until you merge.
134
+
135
+ Typed input wrappers by default; empirically shaped output types after live probes. Steps 2–5 are optional as a group — skip them and you still get working wrappers, just with `Any` returns. Shaping is also per-tool: tools you never probe keep their `Any` return.
102
136
 
103
137
  Polymorphic tools — ones that return different shapes depending on an input (`entityType=1` → `Person`, `=2` → `Position`) — get typed `@overload`s, so your type checker narrows the return at every call site.
104
138
 
@@ -151,19 +185,63 @@ A companion skill, `generate-mcp-runner`, writes a standalone smoke-test `run.py
151
185
 
152
186
  | Command | What it does |
153
187
  |---------|--------------|
154
- | `codegen <server>` | Emit typed wrappers; `--shapes` applies the shape-spec, `--probe` records a response shape inline, `--embed-schema` embeds `fn.__schema__` + Args docstring per function. |
188
+ | `codegen <server>` | Emit wrappers; `--shapes` applies the shape-spec, `--probe` records a response shape inline, `--embed-schema` embeds `fn.__schema__` + Args docstring per function. Also writes `<out-stem>.mcpgen.json` for `check`. |
189
+ | `check <server>` | Compare the server's live tool inventory against the stored manifest. Exit `0` no drift, `1` drift, `2` operational error. |
155
190
  | `list <server>` | Print a server's tools as JSON; `--schema` adds raw `inputSchema` per tool. |
156
191
  | `probe <server> <tool>` | Live call(s) → response-shape skeleton. |
157
192
  | `call <server> <tool> --out <p>` | One live call, raw payload to disk — bootstrap ids or inspect output. |
158
193
  | `merge <server>` | Consolidate probe parts into `<server>.shapes.json`. |
159
194
  | `login <server>` | Browser OAuth login; tokens stored at `~/.mcpgen/credentials.json`. |
160
195
  | `migrate-creds` | Move stored OAuth tokens between `file` / `keyring` backends. |
161
- | `discover` | List MCP servers configured in your installed agent hosts. |
196
+ | `discover` | List MCP servers configured in Claude Code. |
197
+
198
+ `discover` reads Claude Code's configuration — the only host provider implemented today. `discovery.PROVIDERS` is the extension point where more hosts get added.
162
199
 
163
200
  Full workflow and flags: [`doc/USAGE.md`](doc/USAGE.md).
164
201
 
165
202
  ---
166
203
 
204
+ ## Drift detection
205
+
206
+ `codegen` writes a deterministic, non-secret snapshot of the server's tool inventory
207
+ beside the generated module:
208
+
209
+ ```bash
210
+ mcpgen codegen demo --stdio 'python server.py' --out gen/demo.py
211
+ # writes gen/demo.py and gen/demo.mcpgen.json
212
+ ```
213
+
214
+ Commit both. In CI, verify the server still matches what you generated against:
215
+
216
+ ```bash
217
+ mcpgen check demo --stdio 'python server.py' --manifest gen/demo.mcpgen.json
218
+ ```
219
+
220
+ | Exit | Meaning |
221
+ |---|---|
222
+ | 0 | No drift. Description-only changes print as advisories and do not fail. |
223
+ | 1 | Drift: a tool was added, removed, or its input schema or annotations changed. |
224
+ | 2 | Operational error: manifest missing or unreadable, transport failure, auth failure. |
225
+
226
+ Exit 2 is never drift, so a broken runner or an expired token cannot be misread as a
227
+ changed tool contract. `--json` emits a structured report. `check` never calls a tool
228
+ and never touches `<server>.shapes.json`.
229
+
230
+ To accept the new inventory, ask for it explicitly — `check` never writes on its own:
231
+
232
+ ```bash
233
+ mcpgen check demo --stdio 'python server.py' --manifest gen/demo.mcpgen.json --update
234
+ ```
235
+
236
+ GitHub Actions:
237
+
238
+ ```yaml
239
+ - name: Check MCP tool drift
240
+ run: mcpgen check demo --config .mcp.json --manifest gen/demo.mcpgen.json --json
241
+ ```
242
+
243
+ ---
244
+
167
245
  ## Authentication
168
246
 
169
247
  ```bash
@@ -194,7 +272,7 @@ If you write your agent logic in Python and want generated tool wrappers you can
194
272
 
195
273
  ## Status
196
274
 
197
- Early access (`v0.x`). The codegen engine, OAuth persistence, live-probe shaping, and both Claude Code skills are working today. On the roadmap: `--check` drift mode, so CI can flag when a server's tools change out from under your wrappers.
275
+ Early access (`v0.x`). The codegen engine, OAuth persistence, live-probe shaping, tool-inventory drift detection (`mcpgen check`), and both Claude Code skills are working today. Transport support is stdio and Streamable HTTP; SSE is discovered but not implemented.
198
276
 
199
277
  ## License
200
278
 
@@ -2,7 +2,9 @@
2
2
 
3
3
  **Write your MCP server wrappers once — from the live server. Keep them as real Python source you can diff, review, and pin.**
4
4
 
5
- `mcpgen` turns any MCP server into a typed Python module: one `async def` per tool, real return types, no live server needed to read it. Call your tools from code instead of pumping their schemas through the model's context — the pattern Anthropic measured at up to **98% token reduction**.
5
+ `mcpgen` turns an MCP server reachable over **stdio or Streamable HTTP** into a Python module: one `async def` per tool, typed inputs from the start, and typed returns once you've probed the real responses. No live server needed to read it.
6
+
7
+ `mcpgen` generates the Python wrapper layer for MCP code-mode architectures. Token savings occur when your agent or runtime executes these wrappers instead of loading the full MCP tool catalog into model context. `mcpgen` does not itself alter host tool registration and does not provide a sandbox.
6
8
 
7
9
  > **Package / repo / plugin:** `mcp-client-kit` · **CLI command:** `mcpgen`
8
10
  >
@@ -22,6 +24,8 @@ Every tool definition costs **300–600 tokens** for its name, description, and
22
24
 
23
25
  Anthropic's validated fix ("Code execution with MCP," Nov 2025): stop routing schemas through the model. Generate wrapper code and call the tools from code instead — an approach Anthropic measured shrinking one workflow from ~150,000 tokens to ~2,000 (**98.7%**), with independent benchmarks landing around **78–85%** on less extreme workloads.
24
26
 
27
+ Those numbers describe **the pattern**, as reported by Anthropic and by third-party benchmarks — they are not a measurement of this CLI. mcpgen supplies one piece of that pattern: the wrapper layer. What you save depends on how your runtime uses it.
28
+
25
29
  The catch for Python teams: no good tool generated **standalone, importable, reviewable `.py` wrappers** from a live MCP server. So everyone hand-writes `jira.py`, `github.py`, `slack.py` — slowly, inconsistently, and they silently rot when the server changes.
26
30
 
27
31
  That's the gap mcpgen fills.
@@ -33,7 +37,7 @@ That's the gap mcpgen fills.
33
37
  ```bash
34
38
  uv tool install mcp-client-kit # puts `mcpgen` on your PATH
35
39
  mcpgen login github # browser OAuth, tokens persisted
36
- mcpgen codegen github --out github.py # typed wrappers for every tool
40
+ mcpgen codegen github --out github.py # one async def per tool, typed inputs
37
41
  ```
38
42
 
39
43
  ```python
@@ -50,31 +54,61 @@ async def main():
50
54
  asyncio.run(main())
51
55
  ```
52
56
 
57
+ The `-> GitHubUser` return type above comes from the full lifecycle — probe the tool, shape the response, regenerate. Straight out of `codegen`, `get_me` takes the same typed arguments but returns `Any`. See [Lifecycle](#lifecycle).
58
+
53
59
  `github.py` is just Python. Open it in your IDE, review it in a PR, pin it to a commit, ship it. No runtime proxy, no framework lock-in, no live server required to read what your tools return.
54
60
 
61
+ **One connection for a series of calls.** Each call opens its own session by default. Wrap a run in `connected()` and every call to a server shares one initialized session — one `initialize()`, one stdio subprocess, one OAuth pre-flight refresh:
62
+
63
+ ```python
64
+ caller = McpBridgeCaller(url="https://api.githubcopilot.com/mcp/")
65
+
66
+ async with caller.connected():
67
+ me = await github.get_me(caller)
68
+ issues = await github.list_issues(caller, owner="octocat", repo="hello-world")
69
+ ```
70
+
71
+ Outside the block nothing changes, so existing code keeps working untouched. Details — per-server sessions, concurrency, cleanup — in [`doc/USAGE.md`](doc/USAGE.md#reusing-one-connection-for-a-series-of-calls).
72
+
55
73
  ---
56
74
 
57
75
  ## Why developers pick it
58
76
 
59
77
  **Real source you own.** Importable `.py` modules — not `.pyi` stubs (mcp2py), not a runtime proxy, not tied to one execution framework (ipybox). You can diff it, review it, pin it, and read it in your IDE without a server running.
60
78
 
61
- **Types that match reality.** A tool's `inputSchema` describes its *inputs* — it tells you nothing about the *output* shape. mcpgen's `--probe` makes one live call and records the actual response, so your return types reflect what the server really sends. No other generator does this.
79
+ **Types that match reality.** A tool's `inputSchema` describes its *inputs* — it tells you nothing about the *output* shape, so `codegen` alone returns `Any`. mcpgen's `probe` makes a live call and records the actual response; you (or the skill) turn that observation into an output model, and regeneration bakes it in. Return types then reflect what the server really sends, not a guess. No other generator does this.
62
80
 
63
81
  **OAuth that survives restarts.** Pre-flight token refresh means a fresh process renews a near-expired token silently from the refresh token — no surprise browser pop-up at cold start. (The official SDK's canonical example is in-memory only; every restart re-authenticates.)
64
82
 
65
83
  **Swap auth without regenerating.** Every wrapper takes an `McpCaller` as its first argument. Change transports or auth backends — bearer, OAuth, stdio, a fake for tests — without touching the generated code.
66
84
 
67
- **Built for production teams.** Works with any MCP server (HTTP URL, stdio, or bearer/PAT). Generated code lives in git like any other module, so it survives code review, audits, and pinning.
85
+ **Built for production teams.** Works with MCP servers reachable over **stdio or Streamable HTTP** (OAuth, bearer/PAT, or no auth). Generated code lives in git like any other module, so it survives code review, audits, and pinning.
68
86
 
69
87
  ---
70
88
 
71
- ## How it works
89
+ ## What mcpgen does not do
72
90
 
73
- | Step | Command | What happens |
74
- |------|---------|--------------|
75
- | 1. Generate | `mcpgen codegen <server> --out <server>.py` | One typed `async def` per tool. |
76
- | 2. Probe (optional) | `mcpgen probe <server> <tool> --args '{}' --emit-shape <server>.shapes.json` | Records the *real* response shape from a live call. |
77
- | 3. Regenerate | `mcpgen codegen <server> --out <server>.py --shapes <server>.shapes.json` | Wrappers now return precise types (`TypedDict`s, unions, lists). |
91
+ - It does not remove tool schemas from Claude Code or Cursor context — host tool registration is not mcpgen's to change.
92
+ - It does not sandbox generated code. Generated wrappers are ordinary Python and run with your process's privileges.
93
+ - It does not infer trustworthy output types from input schemas alone. Output types come from live probes plus a judgment pass (see the lifecycle below).
94
+ - It does not speak SSE. SSE servers are surfaced by `mcpgen discover` but marked unsupported by this version.
95
+ - It does not guarantee a remote server will stay stable. That is what `mcpgen check` is for.
96
+
97
+ ---
98
+
99
+ ## Lifecycle
100
+
101
+ | Step | Command | Result |
102
+ |------|---------|--------|
103
+ | 1. Generate | `mcpgen codegen <server> --out <server>.py` | One `async def` per tool: typed inputs, `Any` return by default. |
104
+ | 2. Probe | `mcpgen probe <server> <tool> --args '{}' --emit-shape <server>.shapes.json` | Observed response-shape skeleton, written as a per-tool part under `<server>.shapes.json.parts/`. |
105
+ | 3. Merge | `mcpgen merge <server> --out <server>.shapes.json` | Consolidates the parts into the committed, hand-editable shape-spec. |
106
+ | 4. Edit the shape-spec (or run the skill) | edit `<server>.shapes.json` | Output model and `unwrap` decisions — the judgment pass. |
107
+ | 5. Regenerate | `mcpgen codegen <server> --out <server>.py --shapes <server>.shapes.json` | `TypedDict` returns, unions, lists, overloads. |
108
+
109
+ Step 3 is not optional: `--emit-shape` writes only into `<server>.shapes.json.parts/`, so `<server>.shapes.json` does not exist until you merge.
110
+
111
+ Typed input wrappers by default; empirically shaped output types after live probes. Steps 2–5 are optional as a group — skip them and you still get working wrappers, just with `Any` returns. Shaping is also per-tool: tools you never probe keep their `Any` return.
78
112
 
79
113
  Polymorphic tools — ones that return different shapes depending on an input (`entityType=1` → `Person`, `=2` → `Position`) — get typed `@overload`s, so your type checker narrows the return at every call site.
80
114
 
@@ -127,19 +161,63 @@ A companion skill, `generate-mcp-runner`, writes a standalone smoke-test `run.py
127
161
 
128
162
  | Command | What it does |
129
163
  |---------|--------------|
130
- | `codegen <server>` | Emit typed wrappers; `--shapes` applies the shape-spec, `--probe` records a response shape inline, `--embed-schema` embeds `fn.__schema__` + Args docstring per function. |
164
+ | `codegen <server>` | Emit wrappers; `--shapes` applies the shape-spec, `--probe` records a response shape inline, `--embed-schema` embeds `fn.__schema__` + Args docstring per function. Also writes `<out-stem>.mcpgen.json` for `check`. |
165
+ | `check <server>` | Compare the server's live tool inventory against the stored manifest. Exit `0` no drift, `1` drift, `2` operational error. |
131
166
  | `list <server>` | Print a server's tools as JSON; `--schema` adds raw `inputSchema` per tool. |
132
167
  | `probe <server> <tool>` | Live call(s) → response-shape skeleton. |
133
168
  | `call <server> <tool> --out <p>` | One live call, raw payload to disk — bootstrap ids or inspect output. |
134
169
  | `merge <server>` | Consolidate probe parts into `<server>.shapes.json`. |
135
170
  | `login <server>` | Browser OAuth login; tokens stored at `~/.mcpgen/credentials.json`. |
136
171
  | `migrate-creds` | Move stored OAuth tokens between `file` / `keyring` backends. |
137
- | `discover` | List MCP servers configured in your installed agent hosts. |
172
+ | `discover` | List MCP servers configured in Claude Code. |
173
+
174
+ `discover` reads Claude Code's configuration — the only host provider implemented today. `discovery.PROVIDERS` is the extension point where more hosts get added.
138
175
 
139
176
  Full workflow and flags: [`doc/USAGE.md`](doc/USAGE.md).
140
177
 
141
178
  ---
142
179
 
180
+ ## Drift detection
181
+
182
+ `codegen` writes a deterministic, non-secret snapshot of the server's tool inventory
183
+ beside the generated module:
184
+
185
+ ```bash
186
+ mcpgen codegen demo --stdio 'python server.py' --out gen/demo.py
187
+ # writes gen/demo.py and gen/demo.mcpgen.json
188
+ ```
189
+
190
+ Commit both. In CI, verify the server still matches what you generated against:
191
+
192
+ ```bash
193
+ mcpgen check demo --stdio 'python server.py' --manifest gen/demo.mcpgen.json
194
+ ```
195
+
196
+ | Exit | Meaning |
197
+ |---|---|
198
+ | 0 | No drift. Description-only changes print as advisories and do not fail. |
199
+ | 1 | Drift: a tool was added, removed, or its input schema or annotations changed. |
200
+ | 2 | Operational error: manifest missing or unreadable, transport failure, auth failure. |
201
+
202
+ Exit 2 is never drift, so a broken runner or an expired token cannot be misread as a
203
+ changed tool contract. `--json` emits a structured report. `check` never calls a tool
204
+ and never touches `<server>.shapes.json`.
205
+
206
+ To accept the new inventory, ask for it explicitly — `check` never writes on its own:
207
+
208
+ ```bash
209
+ mcpgen check demo --stdio 'python server.py' --manifest gen/demo.mcpgen.json --update
210
+ ```
211
+
212
+ GitHub Actions:
213
+
214
+ ```yaml
215
+ - name: Check MCP tool drift
216
+ run: mcpgen check demo --config .mcp.json --manifest gen/demo.mcpgen.json --json
217
+ ```
218
+
219
+ ---
220
+
143
221
  ## Authentication
144
222
 
145
223
  ```bash
@@ -170,7 +248,7 @@ If you write your agent logic in Python and want generated tool wrappers you can
170
248
 
171
249
  ## Status
172
250
 
173
- Early access (`v0.x`). The codegen engine, OAuth persistence, live-probe shaping, and both Claude Code skills are working today. On the roadmap: `--check` drift mode, so CI can flag when a server's tools change out from under your wrappers.
251
+ Early access (`v0.x`). The codegen engine, OAuth persistence, live-probe shaping, tool-inventory drift detection (`mcpgen check`), and both Claude Code skills are working today. Transport support is stdio and Streamable HTTP; SSE is discovered but not implemented.
174
252
 
175
253
  ## License
176
254