mcp-client-kit 0.0.4__tar.gz → 0.3.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.3.0/.claude/skills/releasing-a-version/SKILL.md +127 -0
- mcp_client_kit-0.3.0/CHANGELOG.md +102 -0
- mcp_client_kit-0.3.0/CLAUDE.md +86 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/PKG-INFO +3 -3
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/README.md +2 -2
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/doc/USAGE.md +11 -2
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/mcpgen/_bridge.py +109 -31
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/mcpgen/cli.py +40 -7
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/mcpgen/codegen.py +262 -30
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/pyproject.toml +1 -1
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/skills/generate-mcp-wrappers/SKILL.md +63 -36
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/tests/test_bridge.py +329 -0
- mcp_client_kit-0.3.0/tests/test_cli_list.py +282 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/tests/test_cli_merge.py +51 -1
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/tests/test_cli_probe.py +60 -7
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/tests/test_codegen.py +467 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/uv.lock +2 -2
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/.claude-plugin/marketplace.json +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/.claude-plugin/plugin.json +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/.github/workflows/ci.yml +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/.github/workflows/publish.yml +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/.gitignore +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/LICENSE +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/doc/CODEGEN_SKILL_IDEA.md +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/doc/DISTRIBUTION.md +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/doc/EXTRACTION_ANALYSIS.md +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/doc/LANDSCAPE.md +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/doc/MARKET_RESEARCH.md +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/doc/RUNNING_LOCALLY.md +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/doc/VERDICT.md +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/mcpgen/__init__.py +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/mcpgen/discovery.py +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/mcpgen/seam.py +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/servers.example.json +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/skills/generate-mcp-runner/SKILL.md +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/skills/generate-mcp-runner/runner_templates/http_bearer.py +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/skills/generate-mcp-runner/runner_templates/http_oauth.py +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/skills/generate-mcp-runner/runner_templates/http_public.py +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/skills/generate-mcp-runner/runner_templates/stdio.py +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/tests/test_cli_call.py +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/tests/test_cli_env.py +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/tests/test_config.py +0 -0
- {mcp_client_kit-0.0.4 → mcp_client_kit-0.3.0}/tests/test_discovery.py +0 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: releasing-a-version
|
|
3
|
+
description: Use when releasing, cutting, or shipping a new version of mcp-client-kit — publishing to PyPI, bumping the version for release, or tagging vX.Y.Z / plugin-vX.Y.Z. Covers the engine (PyPI) and plugin-only (marketplace) flows, including the dev-branch dev0→stable→PR→tag→minor+dev sequence.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Releasing a version
|
|
7
|
+
|
|
8
|
+
Two artifacts ship from one repo. Full rationale in `doc/DISTRIBUTION.md`.
|
|
9
|
+
|
|
10
|
+
| Artifact | Version file | Tag form | Fires PyPI | Tag branch |
|
|
11
|
+
|---|---|---|---|---|
|
|
12
|
+
| **engine** | `pyproject.toml` | `vX.Y.Z` | yes | `main` |
|
|
13
|
+
| **plugin** | `.claude-plugin/plugin.json` | `plugin-vX.Y.Z` | no | `main` |
|
|
14
|
+
|
|
15
|
+
**Pick the flow:**
|
|
16
|
+
- `mcpgen/**` code changed → **Engine release**
|
|
17
|
+
- Skill/docs/SKILL.md only → **Plugin-only release**
|
|
18
|
+
- Both changed → run Engine release first (it implicitly bumps the product too)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Engine release
|
|
23
|
+
|
|
24
|
+
Run from the `dev` branch.
|
|
25
|
+
|
|
26
|
+
**1. Precheck**
|
|
27
|
+
```bash
|
|
28
|
+
git checkout dev && git pull
|
|
29
|
+
git status # must be clean
|
|
30
|
+
gh pr checks # CI green
|
|
31
|
+
```
|
|
32
|
+
`CHANGELOG.md` must have a `## [Unreleased] — X.Y.Z` entry ready. If not, add one.
|
|
33
|
+
|
|
34
|
+
**2. Bump to stable**
|
|
35
|
+
```bash
|
|
36
|
+
uv version --bump stable --dry-run # preview: 0.2.0.dev0 => 0.2.0
|
|
37
|
+
uv version --bump stable # writes pyproject.toml + uv.lock
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**3. Finalize CHANGELOG**
|
|
41
|
+
Change the unreleased header to a dated release header, e.g.:
|
|
42
|
+
```
|
|
43
|
+
## [Unreleased] — 0.2.0
|
|
44
|
+
```
|
|
45
|
+
→
|
|
46
|
+
```
|
|
47
|
+
## [0.2.0] — 2026-06-20
|
|
48
|
+
```
|
|
49
|
+
Add a new blank `## [Unreleased] — 0.3.0` placeholder at the top (you'll fill it during the post-release step).
|
|
50
|
+
|
|
51
|
+
**4. Commit + PR → main**
|
|
52
|
+
```bash
|
|
53
|
+
git add pyproject.toml uv.lock CHANGELOG.md
|
|
54
|
+
git commit -m "release: engine v0.2.0"
|
|
55
|
+
git push origin dev
|
|
56
|
+
gh pr create --base main --title "release: engine v0.2.0"
|
|
57
|
+
# merge the PR (squash or merge commit)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**5. Tag on main**
|
|
61
|
+
```bash
|
|
62
|
+
git checkout main && git pull
|
|
63
|
+
git tag v0.2.0
|
|
64
|
+
git push --tags # bare v* tag triggers publish.yml → PyPI
|
|
65
|
+
```
|
|
66
|
+
`publish.yml` asserts the tag (minus `v`) matches `pyproject.toml version` — if they differ the job fails. Fix by ensuring step 2 ran on the same commit.
|
|
67
|
+
|
|
68
|
+
**6. Re-advance dev**
|
|
69
|
+
```bash
|
|
70
|
+
git checkout dev
|
|
71
|
+
uv version --bump minor # 0.2.0 → 0.3.0
|
|
72
|
+
uv version --bump dev # 0.3.0 → 0.3.0.dev0
|
|
73
|
+
# Add fresh unreleased section to CHANGELOG if not already there:
|
|
74
|
+
# ## [Unreleased] — 0.3.0
|
|
75
|
+
git add pyproject.toml uv.lock CHANGELOG.md
|
|
76
|
+
git commit -m "chore: bump version to 0.3.0.dev0"
|
|
77
|
+
git push origin dev
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Plugin-only release
|
|
83
|
+
|
|
84
|
+
Use when only skill files, SKILL.md, or docs changed — no engine code.
|
|
85
|
+
|
|
86
|
+
**1. Bump plugin product version on dev**
|
|
87
|
+
|
|
88
|
+
Files to update:
|
|
89
|
+
- `.claude-plugin/plugin.json` → `"version"` field
|
|
90
|
+
- `.claude-plugin/marketplace.json` → `"plugins"[0]["version"]` field only
|
|
91
|
+
*(leave top-level `"version"` — that's the catalog version, bumped only when the catalog listing itself changes, not per plugin release)*
|
|
92
|
+
|
|
93
|
+
Optionally raise the version floor in `skills/generate-mcp-wrappers/SKILL.md` — but only if the skill now requires a CLI feature from a newer engine. Two coupled spots:
|
|
94
|
+
- Line with `mcpgen >= X.Y.Z` in the prose
|
|
95
|
+
- `min=X.Y.Z` in the bash version check below it
|
|
96
|
+
|
|
97
|
+
**2. Commit + PR → main**
|
|
98
|
+
```bash
|
|
99
|
+
git add .claude-plugin/
|
|
100
|
+
git commit -m "release: plugin v0.1.1 (engine unchanged 0.2.0)"
|
|
101
|
+
git push origin dev
|
|
102
|
+
gh pr create --base main --title "release: plugin v0.1.1"
|
|
103
|
+
# merge the PR
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**3. Tag on main**
|
|
107
|
+
```bash
|
|
108
|
+
git checkout main && git pull
|
|
109
|
+
git tag plugin-v0.1.1
|
|
110
|
+
git push origin plugin-v0.1.1 # plugin-v* does NOT trigger PyPI publish
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**4. Update the marketplace entry**
|
|
114
|
+
In the `svd-agent-skills` marketplace repo, bump the `mcp-client-kit` entry's `ref` to `plugin-v0.1.1`. This points consumers at the stable `main` tag, not a dev pre-release.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Common mistakes
|
|
119
|
+
|
|
120
|
+
| Mistake | Effect |
|
|
121
|
+
|---|---|
|
|
122
|
+
| Tag on `dev` instead of `main` | Marketplace/PyPI points at a dev pre-release |
|
|
123
|
+
| Tag version ≠ `pyproject.toml` version | `publish.yml` fails the tag-vs-version assertion |
|
|
124
|
+
| Skip step 6 (re-advance dev) | dev stays at release version, no `.dev0` signal |
|
|
125
|
+
| Bump `pyproject.toml` for plugin-only | Triggers unnecessary PyPI republish |
|
|
126
|
+
| Bump `marketplace.json` top-level `version` for every plugin release | Top-level is catalog metadata, not plugin version — leave it unless the catalog listing changed |
|
|
127
|
+
| Raise SKILL.md floor preemptively | Floor should only move when the skill actually needs the new engine feature |
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased] — 0.4.0
|
|
4
|
+
|
|
5
|
+
## [0.3.0] — 2026-07-14
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`mcpgen login` — OAuth token exchange failed with `400 invalid_request`** on authorization
|
|
10
|
+
servers that strictly enforce RFC 6749 §2.3 (`"Client must not use multiple authentication
|
|
11
|
+
methods"`). Dynamic client registration omitted `token_endpoint_auth_method`, so the server
|
|
12
|
+
defaulted us to `client_secret_basic` and issued a `client_secret`; the MCP SDK then sent an
|
|
13
|
+
`Authorization: Basic` header *and* `client_id` in the form body — two client authentication
|
|
14
|
+
methods in one request. mcpgen now registers as a public client
|
|
15
|
+
(`token_endpoint_auth_method: "none"`), which is the correct posture for a distributed CLI
|
|
16
|
+
(RFC 8252 §8.4) and is fully secured by the PKCE (S256) the SDK already performs on every
|
|
17
|
+
authorization code grant. No security regression: the discarded `client_secret` was issued
|
|
18
|
+
per-install by dynamic registration and stored in the same `credentials.json` as the refresh
|
|
19
|
+
token, so anyone who could read the secret could already read the token — it never added a
|
|
20
|
+
layer of defense.
|
|
21
|
+
|
|
22
|
+
- **Discriminated-union codegen mistyped `string` discriminators with numeric-looking
|
|
23
|
+
enum values as `int`.** Discriminator type was inferred solely by sniffing whether
|
|
24
|
+
shape-spec variant keys looked numeric (e.g. `"1"`, `"2"`), so a genuinely
|
|
25
|
+
`type: "string"` discriminator with those enum values got a wrong `int` `Literal`
|
|
26
|
+
in the generated `@overload` stubs. Discriminator type is now resolved from an
|
|
27
|
+
explicit `input_overrides` entry, then the tool's declared `inputSchema` type,
|
|
28
|
+
and only falls back to key-sniffing when neither is available.
|
|
29
|
+
|
|
30
|
+
- **`mcpgen call`/`probe` crashed on `resource_link` content items** — real MCP SDK
|
|
31
|
+
`ResourceLink.uri` is a pydantic `AnyUrl`, not `str`; `json.dumps` on the summarized
|
|
32
|
+
content raised `TypeError`. The summary now coerces `uri` to `str`.
|
|
33
|
+
|
|
34
|
+
- **`mcpgen probe` reported inflated payload sizes for non-ASCII content** — size was
|
|
35
|
+
measured as the character count of `json.dumps`'s default `ensure_ascii=True`
|
|
36
|
+
output, which escapes every non-ASCII character to a `\uXXXX` sequence. Size is
|
|
37
|
+
now measured as real UTF-8 byte length.
|
|
38
|
+
|
|
39
|
+
## [0.2.0] — 2026-06-20
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **`mcpgen list --schema`** — include raw `inputSchema` JSON per tool in list output.
|
|
44
|
+
Useful for inspecting required params and enum constraints without a separate probe.
|
|
45
|
+
|
|
46
|
+
- **`mcpgen codegen --embed-schema`** — embed `fn.__schema__ = {<inputSchema>}` on each
|
|
47
|
+
generated function, plus an Args docstring section (per-param description, enum values,
|
|
48
|
+
default). Enables introspection at runtime (`mod.get_issue.__schema__`) and richer IDE
|
|
49
|
+
hover docs.
|
|
50
|
+
|
|
51
|
+
- **Enum params → `Literal[...]`** (default, no flag) — `py_type()` now maps JSON Schema
|
|
52
|
+
`enum` arrays to `Literal[v1, v2, ...]` instead of bare `str`/`int`. Applies to direct
|
|
53
|
+
enum params and array items (`list[Literal[...]]`). Static analysis and call-site type
|
|
54
|
+
narrowing work without any extra configuration.
|
|
55
|
+
|
|
56
|
+
## [0.1.0] — initial release
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- **`mcpgen codegen`** — generate typed async Python wrappers from a live MCP server's
|
|
61
|
+
`tools/list`. Every tool becomes an `async def` typed from `inputSchema`; returns `Any`
|
|
62
|
+
by default (shape-spec refines to `TypedDict`).
|
|
63
|
+
|
|
64
|
+
- **Shape-spec sidecar** (`<server>.shapes.json`) — hand-editable file driving typed
|
|
65
|
+
return models. Fields: `unwrap` (key path), `return_model` (TypedDict name), `fields`,
|
|
66
|
+
`input_overrides`, `overloads`. Intermediate probe parts land in `.parts/` until `merge`.
|
|
67
|
+
|
|
68
|
+
- **Discriminator detection + overloads** — `mcpgen list` / `probe` emit a stderr advisory
|
|
69
|
+
when a param is shared across ≥2 tools (polymorphic-suspect). Codegen emits one
|
|
70
|
+
`@overload` per discriminator variant (`Literal[<val>]`) plus a union impl.
|
|
71
|
+
|
|
72
|
+
- **`mcpgen list`** — print all tools on a server as JSON `[{name, description}]`.
|
|
73
|
+
Includes discriminator advisory on stderr.
|
|
74
|
+
|
|
75
|
+
- **`mcpgen probe`** — live call(s) → response-shape skeleton. Writes per-probe
|
|
76
|
+
intermediates to `.parts/` for later merge.
|
|
77
|
+
|
|
78
|
+
- **`mcpgen merge`** — consolidate `.parts/` into `<server>.shapes.json`; emits a
|
|
79
|
+
gitignored `verify.json` sidecar with pre-scrub `probed_args`.
|
|
80
|
+
|
|
81
|
+
- **`mcpgen call`** — single live call, raw payload written to disk; useful for
|
|
82
|
+
bootstrapping ids and inspecting raw output.
|
|
83
|
+
|
|
84
|
+
- **`mcpgen discover`** — enumerate MCP servers configured in installed agent hosts
|
|
85
|
+
(reads Claude Code CLI / `~/.claude.json`).
|
|
86
|
+
|
|
87
|
+
- **`mcpgen login`** — browser OAuth login; tokens stored at
|
|
88
|
+
`~/.mcpgen/credentials.json` (chmod 0600) or OS keychain via `--cred-backend keyring`.
|
|
89
|
+
|
|
90
|
+
- **`mcpgen migrate-creds`** — move stored OAuth tokens between `file` / `keyring`
|
|
91
|
+
backends.
|
|
92
|
+
|
|
93
|
+
- **`mcpgen list-creds` / `delete-creds`** — inspect and remove stored credentials.
|
|
94
|
+
|
|
95
|
+
- **Transport flags** — `--stdio` (launch command), `--url` (HTTP), `--bearer` (PAT),
|
|
96
|
+
`--config` (config-file server resolution); shared across `codegen`/`list`/`probe`/`call`/`login`.
|
|
97
|
+
|
|
98
|
+
- **`generate-mcp-wrappers` plugin skill** — agent skill that drives the full
|
|
99
|
+
probe → shape-spec → codegen workflow interactively.
|
|
100
|
+
|
|
101
|
+
- **`generate-mcp-runner` plugin skill** — agent skill for building typed runner
|
|
102
|
+
scripts on top of generated wrappers.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Setup (once)
|
|
9
|
+
uv venv && uv pip install -e .
|
|
10
|
+
|
|
11
|
+
# Tests
|
|
12
|
+
uv run pytest # all tests
|
|
13
|
+
uv run pytest tests/test_codegen.py # single file
|
|
14
|
+
uv run pytest -k "test_name" # single test
|
|
15
|
+
|
|
16
|
+
# Lint / type-check
|
|
17
|
+
uv run ruff check mcpgen/
|
|
18
|
+
uv run mypy
|
|
19
|
+
|
|
20
|
+
# Build dist
|
|
21
|
+
uv build
|
|
22
|
+
|
|
23
|
+
# Run CLI from clone (no install)
|
|
24
|
+
.venv/bin/mcpgen codegen <server> --out <server>.py
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Architecture
|
|
28
|
+
|
|
29
|
+
Four layers, strict one-way dependency:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
CLI (cli.py)
|
|
33
|
+
└── Bridge (_bridge.py) ← auth, transport, live MCP sessions
|
|
34
|
+
└── Codegen (codegen.py) ← pure functions: JSON Schema → Python source
|
|
35
|
+
└── Discovery (discovery.py)← pure enumeration of host-configured servers
|
|
36
|
+
Generated wrappers (.py files)
|
|
37
|
+
└── Seam (seam.py) ← McpCaller Protocol only; no concrete client
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**`seam.py`** — the only thing generated wrappers import from this package. A single `McpCaller` Protocol (`async def call(server, tool, arguments) → Any`). Changing it breaks all generated files.
|
|
41
|
+
|
|
42
|
+
**`codegen.py`** — pure functions (no I/O). `render_module()` takes tool dicts + optional shape-spec and emits Python source. `summarize_shape()` converts a live response to a shape skeleton. `merge_skeletons()` merges multiple probe skeletons into one.
|
|
43
|
+
|
|
44
|
+
**`_bridge.py`** — all async/network code. `McpBridgeCaller` implements `McpCaller`. `session()` is the `asynccontextmanager` for raw MCP sessions. Contains OAuth pre-flight refresh logic (critical — see module docstring; bounded to `mcp<2` because SDK `_initialize` never calls `update_token_expiry`).
|
|
45
|
+
|
|
46
|
+
**`discovery.py`** — sync, no network. Reads Claude Code CLI / `~/.claude.json` to enumerate configured MCP servers.
|
|
47
|
+
|
|
48
|
+
**`cli.py`** — thin argparse wiring over the above. Each `_cmd_*` function maps to a subcommand.
|
|
49
|
+
|
|
50
|
+
## Shape-spec sidecar
|
|
51
|
+
|
|
52
|
+
`<server>.shapes.json` is the hand-editable file that drives typed codegen. Key fields per tool entry: `unwrap` (key path to unwrap), `return_model` (TypedDict name), `fields` (field → Python type), `input_overrides`, `overloads`. The `.parts/` directory holds per-probe intermediates until `mcpgen merge` consolidates them.
|
|
53
|
+
|
|
54
|
+
## Running the skill locally
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
KIT=~/src/mcp-client-kit
|
|
58
|
+
PATH="$KIT/.venv/bin:$PATH" claude --plugin-dir "$KIT"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The skill is `/mcp-client-kit:generate-mcp-wrappers`. It requires `mcpgen` on PATH.
|
|
62
|
+
|
|
63
|
+
## Server config
|
|
64
|
+
|
|
65
|
+
`servers.example.json` → copy to `servers.json` (or set `$MCPGEN_SERVERS`). Config is `{"mcpServers": {"name": {"type": "http", "url": "..."}}}` — same shape as Claude Code's MCP config.
|
|
66
|
+
|
|
67
|
+
## Credentials
|
|
68
|
+
|
|
69
|
+
Stored at `~/.mcpgen/credentials.json` (chmod 0600) or OS keychain via `--cred-backend keyring`. `migrate-creds` moves between backends.
|
|
70
|
+
|
|
71
|
+
## Versioning & release
|
|
72
|
+
|
|
73
|
+
Two artifacts, independent versions — see `doc/DISTRIBUTION.md` for full spec.
|
|
74
|
+
|
|
75
|
+
| Artifact | Tag form | Source of truth | Effect |
|
|
76
|
+
|---|---|---|---|
|
|
77
|
+
| engine (CLI) | `vX.Y.Z` | `pyproject.toml` | triggers PyPI publish |
|
|
78
|
+
| plugin (skill) | `plugin-vX.Y.Z` | `.claude-plugin/plugin.json` + `marketplace.json` | marketplace ref only, no publish |
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Plugin-only release (skill/SKILL.md change, no engine code change)
|
|
82
|
+
git tag plugin-vX.Y.Z origin/main # tag on main after PR merge
|
|
83
|
+
git push origin plugin-vX.Y.Z
|
|
84
|
+
|
|
85
|
+
# Engine release — see doc/DISTRIBUTION.md:108 (fires PyPI publish workflow)
|
|
86
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-client-kit
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.3.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
|
|
@@ -151,8 +151,8 @@ A companion skill, `generate-mcp-runner`, writes a standalone smoke-test `run.py
|
|
|
151
151
|
|
|
152
152
|
| Command | What it does |
|
|
153
153
|
|---------|--------------|
|
|
154
|
-
| `codegen <server>` | Emit typed wrappers; `--shapes` applies the shape-spec, `--probe` records a response shape inline. |
|
|
155
|
-
| `list <server>` | Print a server's tools as JSON. |
|
|
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. |
|
|
155
|
+
| `list <server>` | Print a server's tools as JSON; `--schema` adds raw `inputSchema` per tool. |
|
|
156
156
|
| `probe <server> <tool>` | Live call(s) → response-shape skeleton. |
|
|
157
157
|
| `call <server> <tool> --out <p>` | One live call, raw payload to disk — bootstrap ids or inspect output. |
|
|
158
158
|
| `merge <server>` | Consolidate probe parts into `<server>.shapes.json`. |
|
|
@@ -127,8 +127,8 @@ A companion skill, `generate-mcp-runner`, writes a standalone smoke-test `run.py
|
|
|
127
127
|
|
|
128
128
|
| Command | What it does |
|
|
129
129
|
|---------|--------------|
|
|
130
|
-
| `codegen <server>` | Emit typed wrappers; `--shapes` applies the shape-spec, `--probe` records a response shape inline. |
|
|
131
|
-
| `list <server>` | Print a server's tools as JSON. |
|
|
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. |
|
|
131
|
+
| `list <server>` | Print a server's tools as JSON; `--schema` adds raw `inputSchema` per tool. |
|
|
132
132
|
| `probe <server> <tool>` | Live call(s) → response-shape skeleton. |
|
|
133
133
|
| `call <server> <tool> --out <p>` | One live call, raw payload to disk — bootstrap ids or inspect output. |
|
|
134
134
|
| `merge <server>` | Consolidate probe parts into `<server>.shapes.json`. |
|
|
@@ -74,8 +74,8 @@ uv add mcp-client-kit # or: pip install mcp-client-kit
|
|
|
74
74
|
|
|
75
75
|
| Command | Purpose | Key flags |
|
|
76
76
|
|---------|---------|-----------|
|
|
77
|
-
| `codegen <server>` | Emit typed wrappers | `--out`, `--shapes <path>`, `--probe <tool>` / `--probe-args` |
|
|
78
|
-
| `list <server>` | Tools as JSON `[{name, description}]
|
|
77
|
+
| `codegen <server>` | Emit typed wrappers | `--out`, `--shapes <path>`, `--probe <tool>` / `--probe-args`, `--embed-schema` |
|
|
78
|
+
| `list <server>` | Tools as JSON `[{name, description}]` (add `--schema` for `inputSchema` per tool); discriminator advisory on stderr | `--schema` |
|
|
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>` |
|
|
@@ -420,6 +420,15 @@ Caveats: the discriminator is **always required** (even if the tool schema marks
|
|
|
420
420
|
it optional). An unmodeled discriminator value hits the `int` impl and returns the
|
|
421
421
|
union — it never raises.
|
|
422
422
|
|
|
423
|
+
### Enum params → `Literal[...]`
|
|
424
|
+
|
|
425
|
+
Any input param whose `inputSchema` carries an `enum` array is automatically typed as
|
|
426
|
+
`Literal[<v1>, <v2>, ...]` — no flag required. This applies to all scalar enums
|
|
427
|
+
(string, int, …). Use `mcpgen list <server> --schema` to inspect the raw `enum` arrays
|
|
428
|
+
before generating. Do not widen enum params to `str` in your calling code; if the server
|
|
429
|
+
accepts values outside the declared enum, the appropriate fix is to update the shape-spec
|
|
430
|
+
`input_overrides` for that param rather than loosening the call site.
|
|
431
|
+
|
|
423
432
|
---
|
|
424
433
|
|
|
425
434
|
## Troubleshooting
|
|
@@ -58,7 +58,7 @@ from urllib.parse import parse_qs, urlparse
|
|
|
58
58
|
|
|
59
59
|
import httpx
|
|
60
60
|
from mcp import ClientSession
|
|
61
|
-
from mcp.client.auth import OAuthClientProvider, TokenStorage
|
|
61
|
+
from mcp.client.auth import OAuthClientProvider, OAuthRegistrationError, TokenStorage
|
|
62
62
|
from mcp.client.stdio import StdioServerParameters, stdio_client
|
|
63
63
|
from mcp.client.streamable_http import streamable_http_client
|
|
64
64
|
from mcp.shared._httpx_utils import create_mcp_http_client
|
|
@@ -330,6 +330,44 @@ def _resolve_client_name(server_name: str) -> str:
|
|
|
330
330
|
return _client_names_cache.get(server_name) or f"mcpgen ({server_name})"
|
|
331
331
|
|
|
332
332
|
|
|
333
|
+
def _client_metadata(server_name: str, callback_uri: str, client_name: str | None = None) -> OAuthClientMetadata:
|
|
334
|
+
"""Dynamic-registration metadata (RFC 7591). Shared by both OAuth entry points.
|
|
335
|
+
|
|
336
|
+
`token_endpoint_auth_method="none"` is load-bearing. Omit it and the AS applies
|
|
337
|
+
the RFC 7591 §2 default of `client_secret_basic` and issues a `client_secret`;
|
|
338
|
+
the SDK then sends an `Authorization: Basic` header *and* `client_id` in the form
|
|
339
|
+
body — two client authentication methods in one request, which servers that
|
|
340
|
+
enforce RFC 6749 §2.3 reject with `400 invalid_request`. Registering as a public
|
|
341
|
+
client (RFC 8252 §8.4) is the correct posture for a distributed CLI anyway, and
|
|
342
|
+
the SDK's PKCE (S256, unconditional) is what actually secures the flow.
|
|
343
|
+
"""
|
|
344
|
+
return OAuthClientMetadata(
|
|
345
|
+
client_name=client_name or _resolve_client_name(server_name),
|
|
346
|
+
redirect_uris=[callback_uri], # type: ignore[list-item] # Pydantic coerces str→AnyUrl
|
|
347
|
+
grant_types=["authorization_code", "refresh_token"],
|
|
348
|
+
response_types=["code"],
|
|
349
|
+
token_endpoint_auth_method="none",
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _explain_registration_error(exc: OAuthRegistrationError) -> OAuthRegistrationError:
|
|
354
|
+
"""Annotate `invalid_client_metadata` with its most likely cause.
|
|
355
|
+
|
|
356
|
+
An AS that does not support public clients MUST reject our registration
|
|
357
|
+
(RFC 7591 §2) rather than downgrade it. No such server is known, so rather than
|
|
358
|
+
carry a speculative override flag we make the failure legible if one turns up.
|
|
359
|
+
"""
|
|
360
|
+
if "invalid_client_metadata" not in str(exc):
|
|
361
|
+
return exc
|
|
362
|
+
return OAuthRegistrationError(
|
|
363
|
+
f"{exc}\n\n"
|
|
364
|
+
"Likely cause: mcpgen registers as a public client "
|
|
365
|
+
"(token_endpoint_auth_method=none), and this authorization server appears to "
|
|
366
|
+
"require a client_secret. Please report this at "
|
|
367
|
+
"https://github.com/svd/mcp-client-kit/issues with the message above."
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
333
371
|
# Treat a cached token as expired this many seconds before its real expiry.
|
|
334
372
|
_MARGIN = 120
|
|
335
373
|
|
|
@@ -782,6 +820,12 @@ async def _http_session(
|
|
|
782
820
|
storage = FileTokenStorage(server_name, backend=resolve_cred_backend(cred_backend))
|
|
783
821
|
await _pre_flight_refresh(server_name, storage)
|
|
784
822
|
|
|
823
|
+
# Unlike login(), this does NOT clear a stale confidential-client registration,
|
|
824
|
+
# and must not: a pre-fix credential still refreshes fine here, because
|
|
825
|
+
# _pre_flight_refresh sends client_id + client_secret in the body — that is
|
|
826
|
+
# client_secret_post, a *single* auth method, so it never hit the double-auth
|
|
827
|
+
# bug. Once the refresh token dies, _no_browser raises ReauthenticationRequired
|
|
828
|
+
# → "Run: mcpgen login" → which does clear and re-register as a public client.
|
|
785
829
|
data = storage._load()
|
|
786
830
|
redirect_uris = data.get(server_name, {}).get("client_info", {}).get("redirect_uris", [])
|
|
787
831
|
callback_uri = redirect_uris[0] if redirect_uris else "http://localhost:0/callback"
|
|
@@ -794,12 +838,7 @@ async def _http_session(
|
|
|
794
838
|
|
|
795
839
|
provider = OAuthClientProvider(
|
|
796
840
|
server_url=server_url,
|
|
797
|
-
client_metadata=
|
|
798
|
-
client_name=client_name or _resolve_client_name(server_name),
|
|
799
|
-
redirect_uris=[callback_uri], # type: ignore[list-item] # Pydantic coerces str→AnyUrl
|
|
800
|
-
grant_types=["authorization_code", "refresh_token"],
|
|
801
|
-
response_types=["code"],
|
|
802
|
-
),
|
|
841
|
+
client_metadata=_client_metadata(server_name, callback_uri, client_name),
|
|
803
842
|
storage=storage,
|
|
804
843
|
redirect_handler=_no_browser,
|
|
805
844
|
callback_handler=_no_callback,
|
|
@@ -951,20 +990,61 @@ class McpBridgeCaller:
|
|
|
951
990
|
env=self._env,
|
|
952
991
|
) as s:
|
|
953
992
|
result = await s.call_tool(tool, arguments)
|
|
954
|
-
content = [
|
|
993
|
+
content = [_summarize_content_item(item) for item in result.content]
|
|
955
994
|
return parse(content)
|
|
956
995
|
|
|
957
996
|
|
|
997
|
+
def _summarize_content_item(item: Any) -> dict:
|
|
998
|
+
"""Reduce one MCP content block to a compact, JSON-safe dict.
|
|
999
|
+
|
|
1000
|
+
``text`` blocks keep today's shape ({"type", "text"}). ``image`` / ``resource`` /
|
|
1001
|
+
``resource_link`` blocks carry no useful `.text` — capturing only `.text` (as the
|
|
1002
|
+
old code did) silently degrades them to an empty string, hiding the payload from
|
|
1003
|
+
shape inference entirely. Record presence/metadata instead (never the raw
|
|
1004
|
+
base64/blob bytes, to keep shape-specs small).
|
|
1005
|
+
"""
|
|
1006
|
+
item_type = getattr(item, "type", None)
|
|
1007
|
+
if item_type == "image":
|
|
1008
|
+
return {
|
|
1009
|
+
"type": "image",
|
|
1010
|
+
"mimeType": getattr(item, "mimeType", None),
|
|
1011
|
+
"has_data": bool(getattr(item, "data", None)),
|
|
1012
|
+
}
|
|
1013
|
+
if item_type == "resource":
|
|
1014
|
+
resource = getattr(item, "resource", None)
|
|
1015
|
+
return {
|
|
1016
|
+
"type": "resource",
|
|
1017
|
+
"mimeType": getattr(resource, "mimeType", None),
|
|
1018
|
+
"has_text": bool(getattr(resource, "text", None)),
|
|
1019
|
+
"has_blob": bool(getattr(resource, "blob", None)),
|
|
1020
|
+
}
|
|
1021
|
+
if item_type == "resource_link":
|
|
1022
|
+
uri = getattr(item, "uri", None)
|
|
1023
|
+
return {
|
|
1024
|
+
"type": "resource_link",
|
|
1025
|
+
"uri": str(uri) if uri is not None else None,
|
|
1026
|
+
"name": getattr(item, "name", None),
|
|
1027
|
+
}
|
|
1028
|
+
return {"type": item_type, "text": getattr(item, "text", "")}
|
|
1029
|
+
|
|
1030
|
+
|
|
958
1031
|
def parse(content_items: list) -> Any:
|
|
959
1032
|
"""Extract and JSON-parse the text payload from an MCP tool result.
|
|
960
1033
|
|
|
961
1034
|
Falls back to ``ast.literal_eval`` for Python-repr payloads (e.g. servers
|
|
962
1035
|
that return single-quoted dicts like sqlite), then to a plain string as a
|
|
963
1036
|
last resort so callers can still inspect the response.
|
|
1037
|
+
|
|
1038
|
+
Non-text content blocks (image / resource / resource_link) carry no `.text`
|
|
1039
|
+
to parse — return the block's own summary dict as-is rather than falling
|
|
1040
|
+
through to an empty string, so the shape-spec records something real.
|
|
964
1041
|
"""
|
|
965
1042
|
if not content_items:
|
|
966
1043
|
raise ValueError("MCP tool result has empty content")
|
|
967
1044
|
item = content_items[0]
|
|
1045
|
+
item_type = item.get("type") if isinstance(item, dict) else getattr(item, "type", None)
|
|
1046
|
+
if item_type in ("image", "resource", "resource_link"):
|
|
1047
|
+
return item if isinstance(item, dict) else _summarize_content_item(item)
|
|
968
1048
|
text = item.get("text", "") if isinstance(item, dict) else getattr(item, "text", "")
|
|
969
1049
|
try:
|
|
970
1050
|
return json.loads(text)
|
|
@@ -1067,35 +1147,33 @@ async def login(
|
|
|
1067
1147
|
|
|
1068
1148
|
provider = OAuthClientProvider(
|
|
1069
1149
|
server_url=server_url,
|
|
1070
|
-
client_metadata=
|
|
1071
|
-
client_name=client_name or _resolve_client_name(server_name),
|
|
1072
|
-
redirect_uris=[callback_uri], # type: ignore[list-item] # Pydantic coerces str→AnyUrl
|
|
1073
|
-
grant_types=["authorization_code", "refresh_token"],
|
|
1074
|
-
response_types=["code"],
|
|
1075
|
-
),
|
|
1150
|
+
client_metadata=_client_metadata(server_name, callback_uri, client_name),
|
|
1076
1151
|
storage=storage,
|
|
1077
1152
|
redirect_handler=redirect_handler,
|
|
1078
1153
|
callback_handler=callback_handler,
|
|
1079
1154
|
)
|
|
1080
1155
|
|
|
1081
1156
|
try:
|
|
1082
|
-
|
|
1083
|
-
async with
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1157
|
+
try:
|
|
1158
|
+
async with _open_http(server_url, auth=provider) as (read, write, _):
|
|
1159
|
+
async with ClientSession(read, write) as s:
|
|
1160
|
+
await s.initialize()
|
|
1161
|
+
|
|
1162
|
+
# Persist the token endpoint for later pre-flight refresh. Do this
|
|
1163
|
+
# independently of any tool call — not every server exposes `whoami`.
|
|
1164
|
+
if provider.context.oauth_metadata is not None:
|
|
1165
|
+
endpoint_url = str(provider.context.oauth_metadata.token_endpoint)
|
|
1166
|
+
creds_data = storage._load()
|
|
1167
|
+
creds_data.setdefault(server_name, {})["token_endpoint"] = endpoint_url
|
|
1168
|
+
storage._save(creds_data)
|
|
1169
|
+
|
|
1170
|
+
# Confirm the authenticated session works with a server-agnostic
|
|
1171
|
+
# call. `list_tools` is part of the MCP protocol — every server
|
|
1172
|
+
# supports it, unlike any specific tool name.
|
|
1173
|
+
tools = await s.list_tools()
|
|
1174
|
+
print(f"Login OK ({server_name}); {len(tools.tools)} tool(s) available")
|
|
1175
|
+
except OAuthRegistrationError as e:
|
|
1176
|
+
raise _explain_registration_error(e) from e
|
|
1099
1177
|
finally:
|
|
1100
1178
|
if not callback_future.done():
|
|
1101
1179
|
callback_future.set_result((None, None))
|