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.
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/CHANGELOG.md +85 -1
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/PKG-INFO +93 -15
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/README.md +91 -13
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/doc/USAGE.md +203 -18
- mcp_client_kit-0.6.0/docs/superpowers/plans/2026-08-10-audit-m1-m2.md +3817 -0
- mcp_client_kit-0.6.0/docs/superpowers/specs/2026-08-10-audit-m1-m2-design.md +386 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/mcpgen/__init__.py +4 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/mcpgen/_bridge.py +387 -38
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/mcpgen/cli.py +206 -12
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/mcpgen/discovery.py +14 -5
- mcp_client_kit-0.6.0/mcpgen/manifest.py +246 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/pyproject.toml +1 -1
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/skills/generate-mcp-runner/SKILL.md +16 -11
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/skills/generate-mcp-runner/runner_templates/http_bearer.py +4 -1
- mcp_client_kit-0.6.0/skills/generate-mcp-runner/runner_templates/http_oauth.py +44 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/skills/generate-mcp-runner/runner_templates/http_public.py +4 -1
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/skills/generate-mcp-runner/runner_templates/stdio.py +3 -0
- mcp_client_kit-0.6.0/tests/integration/__init__.py +0 -0
- mcp_client_kit-0.6.0/tests/integration/conftest.py +27 -0
- mcp_client_kit-0.6.0/tests/integration/fixtures/stdio_server.py +49 -0
- mcp_client_kit-0.6.0/tests/integration/fixtures/stdio_server_changed.py +46 -0
- mcp_client_kit-0.6.0/tests/integration/test_generated_wrapper_e2e.py +188 -0
- mcp_client_kit-0.6.0/tests/integration/test_stdio_e2e.py +110 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/tests/test_bridge.py +1330 -2
- mcp_client_kit-0.6.0/tests/test_cli_check.py +353 -0
- mcp_client_kit-0.6.0/tests/test_cli_codegen_manifest.py +116 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/tests/test_config.py +26 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/tests/test_discovery.py +96 -0
- mcp_client_kit-0.6.0/tests/test_manifest.py +309 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/uv.lock +1 -1
- mcp_client_kit-0.4.0/skills/generate-mcp-runner/runner_templates/http_oauth.py +0 -34
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/.claude/skills/releasing-a-version/SKILL.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/.claude-plugin/marketplace.json +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/.claude-plugin/plugin.json +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/.github/workflows/ci.yml +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/.github/workflows/publish.yml +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/.gitignore +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/CLAUDE.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/LICENSE +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/doc/CODEGEN_SKILL_IDEA.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/doc/DISTRIBUTION.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/doc/EXTRACTION_ANALYSIS.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/doc/LANDSCAPE.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/doc/MARKET_RESEARCH.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/doc/RUNNING_LOCALLY.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/doc/VERDICT.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/mcpgen/codegen.py +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/mcpgen/seam.py +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/servers.example.json +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/skills/generate-mcp-wrappers/SKILL.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/tests/test_cli_call.py +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/tests/test_cli_env.py +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/tests/test_cli_list.py +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/tests/test_cli_merge.py +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.6.0}/tests/test_cli_probe.py +0 -0
- {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.
|
|
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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: mcp-client-kit
|
|
3
|
-
Version: 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
|
|
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 #
|
|
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
|
|
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
|
|
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
|
-
##
|
|
113
|
+
## What mcpgen does not do
|
|
96
114
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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 #
|
|
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
|
|
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
|
|
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
|
-
##
|
|
89
|
+
## What mcpgen does not do
|
|
72
90
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|