mcp-client-kit 0.2.0__tar.gz → 0.4.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 (44) hide show
  1. mcp_client_kit-0.4.0/CHANGELOG.md +154 -0
  2. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/PKG-INFO +5 -2
  3. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/README.md +4 -1
  4. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/doc/USAGE.md +46 -1
  5. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/mcpgen/__init__.py +2 -0
  6. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/mcpgen/_bridge.py +268 -48
  7. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/mcpgen/cli.py +66 -12
  8. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/mcpgen/codegen.py +161 -27
  9. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/pyproject.toml +1 -1
  10. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/skills/generate-mcp-wrappers/SKILL.md +39 -21
  11. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/tests/test_bridge.py +824 -0
  12. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/tests/test_cli_list.py +111 -2
  13. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/tests/test_cli_merge.py +51 -1
  14. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/tests/test_cli_probe.py +60 -7
  15. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/tests/test_codegen.py +305 -0
  16. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/uv.lock +2 -2
  17. mcp_client_kit-0.2.0/CHANGELOG.md +0 -68
  18. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/.claude/skills/releasing-a-version/SKILL.md +0 -0
  19. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/.claude-plugin/marketplace.json +0 -0
  20. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/.claude-plugin/plugin.json +0 -0
  21. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/.github/workflows/ci.yml +0 -0
  22. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/.github/workflows/publish.yml +0 -0
  23. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/.gitignore +0 -0
  24. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/CLAUDE.md +0 -0
  25. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/LICENSE +0 -0
  26. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/doc/CODEGEN_SKILL_IDEA.md +0 -0
  27. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/doc/DISTRIBUTION.md +0 -0
  28. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/doc/EXTRACTION_ANALYSIS.md +0 -0
  29. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/doc/LANDSCAPE.md +0 -0
  30. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/doc/MARKET_RESEARCH.md +0 -0
  31. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/doc/RUNNING_LOCALLY.md +0 -0
  32. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/doc/VERDICT.md +0 -0
  33. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/mcpgen/discovery.py +0 -0
  34. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/mcpgen/seam.py +0 -0
  35. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/servers.example.json +0 -0
  36. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/skills/generate-mcp-runner/SKILL.md +0 -0
  37. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/skills/generate-mcp-runner/runner_templates/http_bearer.py +0 -0
  38. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/skills/generate-mcp-runner/runner_templates/http_oauth.py +0 -0
  39. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/skills/generate-mcp-runner/runner_templates/http_public.py +0 -0
  40. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/skills/generate-mcp-runner/runner_templates/stdio.py +0 -0
  41. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/tests/test_cli_call.py +0 -0
  42. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/tests/test_cli_env.py +0 -0
  43. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/tests/test_config.py +0 -0
  44. {mcp_client_kit-0.2.0 → mcp_client_kit-0.4.0}/tests/test_discovery.py +0 -0
@@ -0,0 +1,154 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased] — 0.5.0
4
+
5
+ ## [0.4.0] — 2026-08-09
6
+
7
+ ### Added
8
+
9
+ - **Headless OAuth login — `mcpgen login <server> --headless`** — logging in no longer requires
10
+ a browser on the machine running the command. Instead of opening a browser and binding a local
11
+ callback server, mcpgen prints the authorization URL to stderr, you authorize on any device,
12
+ and paste the resulting callback URL back on stdin. This is the only workable flow inside
13
+ containers, over SSH, and in CI, where `webbrowser.open()` silently does nothing and the
14
+ callback port is unreachable from wherever the browser actually runs. The redirect URI
15
+ registered in headless mode is port-less (`http://localhost/callback`) — nothing ever fetches
16
+ it, and keeping it free of an ephemeral port makes the registered value stable across runs for
17
+ servers that pin `redirect_uris`. Without the flag mcpgen auto-detects: macOS and Windows are
18
+ always treated as interactive, other platforms are headless when neither `DISPLAY` nor
19
+ `WAYLAND_DISPLAY` is set. `MCPGEN_HEADLESS=1`/`0` overrides the detection in either direction
20
+ for environments that guess wrong; an explicit `--headless`/`--no-headless` outranks both.
21
+
22
+ - **`mcpgen login --callback-timeout SECONDS`** — the 300-second bound on the browser redirect
23
+ is a guess about how long a consent screen plus MFA takes, and guesses about human latency are
24
+ wrong for someone: a hardware-token or approve-on-phone flow can outlast it, while a scripted
25
+ login wants to fail fast. The flag sets that bound per invocation, `0` restores the original
26
+ unbounded wait as an escape hatch, and anything negative or non-numeric is rejected by argparse
27
+ with a usage message rather than surfacing as a traceback from inside asyncio. The same value
28
+ is available in code as `callback_timeout=` on `login()`, `ensure_login()`, and
29
+ `ensure_login_all()`. Headless logins ignore it — the pasted-URL prompt is never bounded.
30
+
31
+ - **`ensure_login_all(servers)`** — pre-flight refresh-or-login across several servers in one
32
+ call, for pipelines that talk to more than one MCP server and want every token settled before
33
+ work starts. Deliberately sequential: concurrent logins would open several browser tabs at
34
+ once and, in headless mode, race each other for stdin. Servers with a valid cached token stay
35
+ silent, so the steady-state cost is zero.
36
+
37
+ ### Fixed
38
+
39
+ - **An authorization denial in the browser flow now fails with the same clear error as the
40
+ headless flow.** The local callback server extracted only `code` and `state` and ignored
41
+ `error`/`error_description`, so declining consent resolved the callback to `(None, None)` and
42
+ surfaced later as an opaque downstream failure. Both callback paths now share one parser,
43
+ which raises `ValueError: OAuth authorization failed: <error> — <description>` at the point of
44
+ failure.
45
+
46
+ - **`mcpgen login` no longer hangs forever when the browser never comes back.** The error-redirect
47
+ fix above only helps when the authorization server actually redirects; real ones often don't —
48
+ cancel the consent screen and the tab simply closes, so no request ever reaches the local
49
+ callback server and the wait never ends. The interactive flow now bounds that wait at 300s
50
+ (`_CALLBACK_TIMEOUT`) and raises `TimeoutError` explaining that the browser never returned,
51
+ with a pointer to `--headless` for pasting the redirect URL by hand. The timeout cancels the
52
+ pending callback future, so the background server task shuts down cleanly, and it unwinds
53
+ through the same restore path as any other login failure — a timed-out attempt leaves a
54
+ previously-working credential intact. Headless logins are deliberately unbounded: a human
55
+ pasting a URL may take arbitrarily long.
56
+
57
+ ## [0.3.0] — 2026-07-14
58
+
59
+ ### Fixed
60
+
61
+ - **`mcpgen login` — OAuth token exchange failed with `400 invalid_request`** on authorization
62
+ servers that strictly enforce RFC 6749 §2.3 (`"Client must not use multiple authentication
63
+ methods"`). Dynamic client registration omitted `token_endpoint_auth_method`, so the server
64
+ defaulted us to `client_secret_basic` and issued a `client_secret`; the MCP SDK then sent an
65
+ `Authorization: Basic` header *and* `client_id` in the form body — two client authentication
66
+ methods in one request. mcpgen now registers as a public client
67
+ (`token_endpoint_auth_method: "none"`), which is the correct posture for a distributed CLI
68
+ (RFC 8252 §8.4) and is fully secured by the PKCE (S256) the SDK already performs on every
69
+ authorization code grant. No security regression: the discarded `client_secret` was issued
70
+ per-install by dynamic registration and stored in the same `credentials.json` as the refresh
71
+ token, so anyone who could read the secret could already read the token — it never added a
72
+ layer of defense.
73
+
74
+ - **Discriminated-union codegen mistyped `string` discriminators with numeric-looking
75
+ enum values as `int`.** Discriminator type was inferred solely by sniffing whether
76
+ shape-spec variant keys looked numeric (e.g. `"1"`, `"2"`), so a genuinely
77
+ `type: "string"` discriminator with those enum values got a wrong `int` `Literal`
78
+ in the generated `@overload` stubs. Discriminator type is now resolved from an
79
+ explicit `input_overrides` entry, then the tool's declared `inputSchema` type,
80
+ and only falls back to key-sniffing when neither is available.
81
+
82
+ - **`mcpgen call`/`probe` crashed on `resource_link` content items** — real MCP SDK
83
+ `ResourceLink.uri` is a pydantic `AnyUrl`, not `str`; `json.dumps` on the summarized
84
+ content raised `TypeError`. The summary now coerces `uri` to `str`.
85
+
86
+ - **`mcpgen probe` reported inflated payload sizes for non-ASCII content** — size was
87
+ measured as the character count of `json.dumps`'s default `ensure_ascii=True`
88
+ output, which escapes every non-ASCII character to a `\uXXXX` sequence. Size is
89
+ now measured as real UTF-8 byte length.
90
+
91
+ ## [0.2.0] — 2026-06-20
92
+
93
+ ### Added
94
+
95
+ - **`mcpgen list --schema`** — include raw `inputSchema` JSON per tool in list output.
96
+ Useful for inspecting required params and enum constraints without a separate probe.
97
+
98
+ - **`mcpgen codegen --embed-schema`** — embed `fn.__schema__ = {<inputSchema>}` on each
99
+ generated function, plus an Args docstring section (per-param description, enum values,
100
+ default). Enables introspection at runtime (`mod.get_issue.__schema__`) and richer IDE
101
+ hover docs.
102
+
103
+ - **Enum params → `Literal[...]`** (default, no flag) — `py_type()` now maps JSON Schema
104
+ `enum` arrays to `Literal[v1, v2, ...]` instead of bare `str`/`int`. Applies to direct
105
+ enum params and array items (`list[Literal[...]]`). Static analysis and call-site type
106
+ narrowing work without any extra configuration.
107
+
108
+ ## [0.1.0] — initial release
109
+
110
+ ### Added
111
+
112
+ - **`mcpgen codegen`** — generate typed async Python wrappers from a live MCP server's
113
+ `tools/list`. Every tool becomes an `async def` typed from `inputSchema`; returns `Any`
114
+ by default (shape-spec refines to `TypedDict`).
115
+
116
+ - **Shape-spec sidecar** (`<server>.shapes.json`) — hand-editable file driving typed
117
+ return models. Fields: `unwrap` (key path), `return_model` (TypedDict name), `fields`,
118
+ `input_overrides`, `overloads`. Intermediate probe parts land in `.parts/` until `merge`.
119
+
120
+ - **Discriminator detection + overloads** — `mcpgen list` / `probe` emit a stderr advisory
121
+ when a param is shared across ≥2 tools (polymorphic-suspect). Codegen emits one
122
+ `@overload` per discriminator variant (`Literal[<val>]`) plus a union impl.
123
+
124
+ - **`mcpgen list`** — print all tools on a server as JSON `[{name, description}]`.
125
+ Includes discriminator advisory on stderr.
126
+
127
+ - **`mcpgen probe`** — live call(s) → response-shape skeleton. Writes per-probe
128
+ intermediates to `.parts/` for later merge.
129
+
130
+ - **`mcpgen merge`** — consolidate `.parts/` into `<server>.shapes.json`; emits a
131
+ gitignored `verify.json` sidecar with pre-scrub `probed_args`.
132
+
133
+ - **`mcpgen call`** — single live call, raw payload written to disk; useful for
134
+ bootstrapping ids and inspecting raw output.
135
+
136
+ - **`mcpgen discover`** — enumerate MCP servers configured in installed agent hosts
137
+ (reads Claude Code CLI / `~/.claude.json`).
138
+
139
+ - **`mcpgen login`** — browser OAuth login; tokens stored at
140
+ `~/.mcpgen/credentials.json` (chmod 0600) or OS keychain via `--cred-backend keyring`.
141
+
142
+ - **`mcpgen migrate-creds`** — move stored OAuth tokens between `file` / `keyring`
143
+ backends.
144
+
145
+ - **`mcpgen list-creds` / `delete-creds`** — inspect and remove stored credentials.
146
+
147
+ - **Transport flags** — `--stdio` (launch command), `--url` (HTTP), `--bearer` (PAT),
148
+ `--config` (config-file server resolution); shared across `codegen`/`list`/`probe`/`call`/`login`.
149
+
150
+ - **`generate-mcp-wrappers` plugin skill** — agent skill that drives the full
151
+ probe → shape-spec → codegen workflow interactively.
152
+
153
+ - **`generate-mcp-runner` plugin skill** — agent skill for building typed runner
154
+ scripts on top of generated wrappers.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-client-kit
3
- Version: 0.2.0
3
+ Version: 0.4.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
@@ -168,11 +168,14 @@ Full workflow and flags: [`doc/USAGE.md`](doc/USAGE.md).
168
168
 
169
169
  ```bash
170
170
  mcpgen login <server> # OAuth (most servers)
171
+ mcpgen login <server> --headless # no browser: print URL, paste callback URL back
171
172
  mcpgen codegen <server> --bearer "$TOKEN" --out s.py # PAT / bearer
172
173
  mcpgen codegen <server> --stdio "python server.py" --out s.py # local stdio, no auth
173
174
  ```
174
175
 
175
- Tokens persist in `~/.mcpgen/credentials.json` (chmod 0600) or your OS keystore via `--cred-backend keyring`. In code, `ensure_login(server, url=...)` refreshes silently and only opens a browser when a real login is required.
176
+ Tokens persist in `~/.mcpgen/credentials.json` (chmod 0600) or your OS keystore via `--cred-backend keyring`. In code, `ensure_login(server, url=...)` refreshes silently and only opens a browser when a real login is required; `ensure_login_all([...])` does the same for several servers, one at a time.
177
+
178
+ In containers, over SSH, or in CI there is no browser to open. `--headless` prints the authorization URL, you authorize on any device, and paste the callback URL back on stdin. It is auto-detected when neither `DISPLAY` nor `WAYLAND_DISPLAY` is set (never on macOS or Windows); `MCPGEN_HEADLESS=1`/`0` overrides the detection, and `--headless`/`--no-headless` overrides both.
176
179
 
177
180
  ---
178
181
 
@@ -144,11 +144,14 @@ Full workflow and flags: [`doc/USAGE.md`](doc/USAGE.md).
144
144
 
145
145
  ```bash
146
146
  mcpgen login <server> # OAuth (most servers)
147
+ mcpgen login <server> --headless # no browser: print URL, paste callback URL back
147
148
  mcpgen codegen <server> --bearer "$TOKEN" --out s.py # PAT / bearer
148
149
  mcpgen codegen <server> --stdio "python server.py" --out s.py # local stdio, no auth
149
150
  ```
150
151
 
151
- Tokens persist in `~/.mcpgen/credentials.json` (chmod 0600) or your OS keystore via `--cred-backend keyring`. In code, `ensure_login(server, url=...)` refreshes silently and only opens a browser when a real login is required.
152
+ Tokens persist in `~/.mcpgen/credentials.json` (chmod 0600) or your OS keystore via `--cred-backend keyring`. In code, `ensure_login(server, url=...)` refreshes silently and only opens a browser when a real login is required; `ensure_login_all([...])` does the same for several servers, one at a time.
153
+
154
+ In containers, over SSH, or in CI there is no browser to open. `--headless` prints the authorization URL, you authorize on any device, and paste the callback URL back on stdin. It is auto-detected when neither `DISPLAY` nor `WAYLAND_DISPLAY` is set (never on macOS or Windows); `MCPGEN_HEADLESS=1`/`0` overrides the detection, and `--headless`/`--no-headless` overrides both.
152
155
 
153
156
  ---
154
157
 
@@ -79,7 +79,7 @@ uv add mcp-client-kit # or: pip install mcp-client-kit
79
79
  | `probe <server> <tool>` | Live call(s) → shape skeleton | `--args` (repeatable), `--emit-shape <path>` (writes `.parts/`) |
80
80
  | `call <server> <tool>` | One live call, raw payload to disk — bootstrap ids / inspect output | `--out <path>` (required) |
81
81
  | `merge <server>` | Consolidate `.parts/` → `<server>.shapes.json`; emit gitignored `verify.json` | `--out <path>` |
82
- | `login <server>` | Browser OAuth login | connection flags below |
82
+ | `login <server>` | Browser OAuth login | `--headless` / `--no-headless`, `--callback-timeout <seconds>`, connection flags below |
83
83
  | `migrate-creds` | Copy stored OAuth tokens between `file`/`keyring` backends | `--from`, `--to`, `--servers`, `--purge`, `--set-default` |
84
84
  | `discover` | List servers from agent hosts | `--host <id>` (repeatable), `--json` |
85
85
 
@@ -138,6 +138,47 @@ mcpgen login myserver # opens browser; token stored at ~/.mcpgen/credentials.j
138
138
 
139
139
  Re-run when you see `ReauthenticationRequired`.
140
140
 
141
+ The browser flow waits up to 300 seconds for the redirect. If you cancel on the consent
142
+ screen, many authorization servers just close the tab without redirecting back, so mcpgen
143
+ never hears anything — the wait then ends with `TimeoutError` instead of hanging. Your
144
+ existing credential is left untouched by a failed or timed-out attempt.
145
+
146
+ Adjust the bound when the default doesn't fit — a hardware token or an approve-on-phone
147
+ step can outlast it, and a scripted login may want to fail sooner:
148
+
149
+ ```bash
150
+ mcpgen login myserver --callback-timeout 900 # 15 minutes
151
+ mcpgen login myserver --callback-timeout 0 # wait forever (no bound)
152
+ ```
153
+
154
+ Negative or non-numeric values are rejected outright. Headless logins ignore the flag —
155
+ the pasted-URL prompt is never bounded. In code, the same value is `callback_timeout=` on
156
+ `login()`, `ensure_login()`, and `ensure_login_all()`.
157
+
158
+ **No browser available (container, SSH, CI):**
159
+
160
+ ```bash
161
+ mcpgen login myserver --headless
162
+ ```
163
+
164
+ mcpgen prints the authorization URL instead of opening it, and waits:
165
+
166
+ ```
167
+ Open this URL in your browser:
168
+
169
+ https://auth.example.com/authorize?response_type=code&client_id=…
170
+
171
+ After authorizing, paste the full callback URL here (http://localhost.../callback?code=...):
172
+ ```
173
+
174
+ Authorize on any device, then copy the URL from the browser's address bar — the page
175
+ itself will fail to load, which is expected, nothing is listening on that address — and
176
+ paste it at the prompt.
177
+
178
+ Headless mode is auto-detected when neither `DISPLAY` nor `WAYLAND_DISPLAY` is set (macOS
179
+ and Windows are always treated as interactive). Set `MCPGEN_HEADLESS=1` or `0` to override
180
+ the detection; an explicit `--headless` / `--no-headless` overrides both.
181
+
141
182
  **PAT / Bearer token:**
142
183
 
143
184
  ```bash
@@ -323,6 +364,10 @@ How it works:
323
364
  absent), it falls back to a full browser login — the in-code equivalent of
324
365
  `mcpgen login <server>`. Credentials are persisted at
325
366
  `~/.mcpgen/credentials.json`.
367
+ - **Several servers at once** — `await ensure_login_all(["a", "b"])` runs the same
368
+ refresh-or-login for each name in turn. Sequential on purpose: parallel logins would
369
+ open several browser tabs at once and race for stdin in headless mode. Both functions
370
+ accept `headless=True`/`False` to force the paste-the-URL or browser flow explicitly.
326
371
  - **Lower-level alternative** — skip `ensure_login` and catch
327
372
  `ReauthenticationRequired` from the first failing `.call()`, then call
328
373
  `login(SERVER, url=URL)` and retry. `ReauthenticationRequired` and `login` are
@@ -5,6 +5,7 @@ from mcpgen._bridge import (
5
5
  ReauthenticationRequired,
6
6
  delete_cred,
7
7
  ensure_login,
8
+ ensure_login_all,
8
9
  list_creds,
9
10
  login,
10
11
  migrate_creds,
@@ -17,6 +18,7 @@ __all__ = [
17
18
  "ReauthenticationRequired",
18
19
  "delete_cred",
19
20
  "ensure_login",
21
+ "ensure_login_all",
20
22
  "list_creds",
21
23
  "login",
22
24
  "migrate_creds",