mcp-client-kit 0.4.0__tar.gz → 0.5.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.5.0}/CHANGELOG.md +35 -1
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/PKG-INFO +1 -1
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/doc/USAGE.md +23 -2
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/mcpgen/__init__.py +2 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/mcpgen/_bridge.py +23 -3
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/mcpgen/cli.py +53 -2
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/pyproject.toml +1 -1
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/tests/test_bridge.py +165 -2
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/uv.lock +1 -1
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/.claude/skills/releasing-a-version/SKILL.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/.claude-plugin/marketplace.json +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/.claude-plugin/plugin.json +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/.github/workflows/ci.yml +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/.github/workflows/publish.yml +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/.gitignore +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/CLAUDE.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/LICENSE +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/README.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/doc/CODEGEN_SKILL_IDEA.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/doc/DISTRIBUTION.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/doc/EXTRACTION_ANALYSIS.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/doc/LANDSCAPE.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/doc/MARKET_RESEARCH.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/doc/RUNNING_LOCALLY.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/doc/VERDICT.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/mcpgen/codegen.py +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/mcpgen/discovery.py +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/mcpgen/seam.py +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/servers.example.json +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/skills/generate-mcp-runner/SKILL.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/skills/generate-mcp-runner/runner_templates/http_bearer.py +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/skills/generate-mcp-runner/runner_templates/http_oauth.py +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/skills/generate-mcp-runner/runner_templates/http_public.py +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/skills/generate-mcp-runner/runner_templates/stdio.py +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/skills/generate-mcp-wrappers/SKILL.md +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/tests/test_cli_call.py +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/tests/test_cli_env.py +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/tests/test_cli_list.py +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/tests/test_cli_merge.py +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/tests/test_cli_probe.py +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/tests/test_codegen.py +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/tests/test_config.py +0 -0
- {mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/tests/test_discovery.py +0 -0
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [Unreleased] — 0.
|
|
3
|
+
## [Unreleased] — 0.6.0
|
|
4
|
+
|
|
5
|
+
## [0.5.0] — 2026-08-09
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **A caller-supplied credentials path was silently ignored once past login, so tokens were
|
|
10
|
+
written to one file and read from another.** `login()` and `ensure_login()` accepted
|
|
11
|
+
`creds_path` and honoured it, but the call path never did: `_http_session()` built its
|
|
12
|
+
`FileTokenStorage` on the default `~/.mcpgen/credentials.json`, and neither `session()` nor
|
|
13
|
+
`McpBridgeCaller` even accepted the parameter, so there was no way to route a call to the file
|
|
14
|
+
the login had just written. Anyone keeping tokens outside the shared store — a project-local
|
|
15
|
+
credentials file, two identities side by side, a test fixture — logged in successfully and then
|
|
16
|
+
got `ReauthenticationRequired` on the very next call, in a loop, with both files on disk looking
|
|
17
|
+
plausible. `creds_path` now flows through `_http_session()`, `session()`, and
|
|
18
|
+
`McpBridgeCaller`, defaulting to `DEFAULT_CREDS_PATH` when omitted, so existing behaviour is
|
|
19
|
+
unchanged. The stdio, bearer, static-header, and raw-URL transports do not take it: they store
|
|
20
|
+
no credentials at all, and a parameter that silently does nothing is worse than its absence.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **`--creds PATH`** on every command that touches stored credentials — `codegen`, `list`,
|
|
25
|
+
`probe`, `call`, `login`, and the management trio `list-creds`, `delete-creds`,
|
|
26
|
+
`migrate-creds` — the CLI surface for the fix above. `mcpgen login` previously dropped the
|
|
27
|
+
path entirely, having no flag to drop it from; it now logs in to exactly the file the
|
|
28
|
+
subsequent calls will read. The management commands were the other half of the same gap: all
|
|
29
|
+
three already took a `credentials_path` internally but exposed no way to set it, so after a
|
|
30
|
+
`--creds` login `list-creds` reported an empty store, `delete-creds` deleted nothing while
|
|
31
|
+
saying so, and `migrate-creds` migrated the wrong file. A flag that exists on half the
|
|
32
|
+
commands that read a file is worse than no flag at all — it makes the other half look broken.
|
|
33
|
+
|
|
34
|
+
- **`DEFAULT_CREDS_PATH` is exported from `mcpgen`** — consumers that thread a credentials path
|
|
35
|
+
through their own CLI need the default as a value. Re-deriving
|
|
36
|
+
`Path.home() / ".mcpgen" / "credentials.json"` downstream would silently drift the day mcpgen
|
|
37
|
+
changes it.
|
|
4
38
|
|
|
5
39
|
## [0.4.0] — 2026-08-09
|
|
6
40
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-client-kit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -80,13 +80,17 @@ uv add mcp-client-kit # or: pip install mcp-client-kit
|
|
|
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
82
|
| `login <server>` | Browser OAuth login | `--headless` / `--no-headless`, `--callback-timeout <seconds>`, connection flags below |
|
|
83
|
-
| `migrate-creds` | Copy stored OAuth tokens between `file`/`keyring` backends | `--from`, `--to`, `--servers`, `--purge`, `--set-default
|
|
83
|
+
| `migrate-creds` | Copy stored OAuth tokens between `file`/`keyring` backends | `--from`, `--to`, `--servers`, `--purge`, `--set-default`, `--creds <path>` |
|
|
84
84
|
| `discover` | List servers from agent hosts | `--host <id>` (repeatable), `--json` |
|
|
85
85
|
|
|
86
86
|
Connection flags shared by `codegen`/`list`/`probe`/`call`/`login`: `--url`,
|
|
87
|
-
`--bearer`, `--stdio`, `--config`, `--client-name`, `--cred-backend`
|
|
87
|
+
`--bearer`, `--stdio`, `--config`, `--client-name`, `--cred-backend`, `--creds`
|
|
88
88
|
(see [§ Authenticate](#authenticate)).
|
|
89
89
|
|
|
90
|
+
`--creds` is also accepted by the credential-management commands — `list-creds`,
|
|
91
|
+
`delete-creds`, `migrate-creds` — so a non-default store stays addressable from every
|
|
92
|
+
command that reads or writes it.
|
|
93
|
+
|
|
90
94
|
> **PII:** `call` writes the raw, unscrubbed payload. Name the file `*.probe-raw.json`
|
|
91
95
|
> (gitignored) and never commit it.
|
|
92
96
|
|
|
@@ -210,6 +214,23 @@ by `--cred-backend`, then `$MCPGEN_CRED_BACKEND`, then `~/.mcpgen/config.json`
|
|
|
210
214
|
mcpgen login myserver --cred-backend keyring
|
|
211
215
|
```
|
|
212
216
|
|
|
217
|
+
**Credential file location:** `--creds PATH` moves the `file` backend off
|
|
218
|
+
`~/.mcpgen/credentials.json` — useful to keep one project's tokens out of the shared
|
|
219
|
+
store, or to run two identities side by side. Pass the same path to every command
|
|
220
|
+
that touches those tokens; a login written to one file is invisible to a call reading
|
|
221
|
+
another:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
mcpgen login myserver --creds ./.mcp-creds.json
|
|
225
|
+
mcpgen call myserver whoami --creds ./.mcp-creds.json --out out.json
|
|
226
|
+
mcpgen list-creds --creds ./.mcp-creds.json
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The flag has no effect with `--bearer` or `--stdio`, which store no credentials. In
|
|
230
|
+
code the same value is `creds_path=` on `login()`, `ensure_login()`,
|
|
231
|
+
`ensure_login_all()`, `session()`, and `McpBridgeCaller(...)`; `mcpgen.DEFAULT_CREDS_PATH`
|
|
232
|
+
is exported as the default.
|
|
233
|
+
|
|
213
234
|
**Validate keyring storage** — confirm the token landed in the OS keystore, not the
|
|
214
235
|
fallback file (service `mcpgen`, username `credentials`):
|
|
215
236
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""mcpgen: typed Python wrapper generator for MCP servers."""
|
|
2
2
|
|
|
3
3
|
from mcpgen._bridge import (
|
|
4
|
+
DEFAULT_CREDS_PATH,
|
|
4
5
|
McpBridgeCaller,
|
|
5
6
|
ReauthenticationRequired,
|
|
6
7
|
delete_cred,
|
|
@@ -13,6 +14,7 @@ from mcpgen._bridge import (
|
|
|
13
14
|
from mcpgen.seam import McpCaller
|
|
14
15
|
|
|
15
16
|
__all__ = [
|
|
17
|
+
"DEFAULT_CREDS_PATH",
|
|
16
18
|
"McpBridgeCaller",
|
|
17
19
|
"McpCaller",
|
|
18
20
|
"ReauthenticationRequired",
|
|
@@ -824,9 +824,16 @@ async def _http_session(
|
|
|
824
824
|
*,
|
|
825
825
|
client_name: str | None = None,
|
|
826
826
|
cred_backend: str | None = None,
|
|
827
|
+
creds_path: Path | None = None,
|
|
827
828
|
):
|
|
828
|
-
"""OAuth-authenticated HTTP MCP session. Pre-flight refresh before connecting.
|
|
829
|
-
|
|
829
|
+
"""OAuth-authenticated HTTP MCP session. Pre-flight refresh before connecting.
|
|
830
|
+
|
|
831
|
+
creds_path: read tokens from this file instead of DEFAULT_CREDS_PATH. Must match
|
|
832
|
+
whatever login() wrote to, or the session reads an empty store.
|
|
833
|
+
"""
|
|
834
|
+
storage = FileTokenStorage(
|
|
835
|
+
server_name, creds_path or DEFAULT_CREDS_PATH, backend=resolve_cred_backend(cred_backend)
|
|
836
|
+
)
|
|
830
837
|
await _pre_flight_refresh(server_name, storage)
|
|
831
838
|
|
|
832
839
|
# Unlike login(), this does NOT clear a stale confidential-client registration,
|
|
@@ -913,6 +920,7 @@ async def session(
|
|
|
913
920
|
client_name: str | None = None,
|
|
914
921
|
config_path: str | Path | None = None,
|
|
915
922
|
cred_backend: str | None = None,
|
|
923
|
+
creds_path: Path | None = None,
|
|
916
924
|
env: dict[str, str] | None = None,
|
|
917
925
|
):
|
|
918
926
|
"""Yield an initialized MCP ClientSession.
|
|
@@ -924,6 +932,9 @@ async def session(
|
|
|
924
932
|
url: inline server URL — routes through HTTP + OAuth keyed by `server` name,
|
|
925
933
|
overriding config. client_name: inline OAuth client_name override.
|
|
926
934
|
config_path: read the server registry from this file instead of the default search.
|
|
935
|
+
creds_path: read OAuth tokens from this file instead of DEFAULT_CREDS_PATH. Only
|
|
936
|
+
meaningful on the OAuth path — the stdio, bearer, static-header, and raw-URL
|
|
937
|
+
transports store no credentials.
|
|
927
938
|
server: a configured name (servers()) → HTTP + OAuth; otherwise a raw URL.
|
|
928
939
|
env: extra env vars forwarded to the stdio subprocess (merged over the SDK's
|
|
929
940
|
safe allowlist). Keys NOT in ``get_default_environment()`` (e.g. API keys)
|
|
@@ -953,7 +964,13 @@ async def session(
|
|
|
953
964
|
async with _static_headers_session(resolved_url, _headers_cache[server]) as s:
|
|
954
965
|
yield s
|
|
955
966
|
elif resolved_url is not None:
|
|
956
|
-
async with _http_session(
|
|
967
|
+
async with _http_session(
|
|
968
|
+
server,
|
|
969
|
+
resolved_url,
|
|
970
|
+
client_name=client_name,
|
|
971
|
+
cred_backend=cred_backend,
|
|
972
|
+
creds_path=creds_path,
|
|
973
|
+
) as s:
|
|
957
974
|
yield s
|
|
958
975
|
elif "://" not in server:
|
|
959
976
|
raise ValueError(f"server {server!r} not found in config and is not a URL")
|
|
@@ -977,6 +994,7 @@ class McpBridgeCaller:
|
|
|
977
994
|
client_name: str | None = None,
|
|
978
995
|
config_path: str | Path | None = None,
|
|
979
996
|
cred_backend: str | None = None,
|
|
997
|
+
creds_path: Path | None = None,
|
|
980
998
|
env: dict[str, str] | None = None,
|
|
981
999
|
) -> None:
|
|
982
1000
|
self._cmd = cmd
|
|
@@ -985,6 +1003,7 @@ class McpBridgeCaller:
|
|
|
985
1003
|
self._client_name = client_name
|
|
986
1004
|
self._config_path = config_path
|
|
987
1005
|
self._cred_backend = cred_backend
|
|
1006
|
+
self._creds_path = creds_path
|
|
988
1007
|
self._env = env
|
|
989
1008
|
|
|
990
1009
|
async def call(self, server: str, tool: str, arguments: dict) -> Any:
|
|
@@ -996,6 +1015,7 @@ class McpBridgeCaller:
|
|
|
996
1015
|
client_name=self._client_name,
|
|
997
1016
|
config_path=self._config_path,
|
|
998
1017
|
cred_backend=self._cred_backend,
|
|
1018
|
+
creds_path=self._creds_path,
|
|
999
1019
|
env=self._env,
|
|
1000
1020
|
) as s:
|
|
1001
1021
|
result = await s.call_tool(tool, arguments)
|
|
@@ -33,6 +33,7 @@ async def _list_tools(
|
|
|
33
33
|
client_name: str | None = None,
|
|
34
34
|
config_path: str | None = None,
|
|
35
35
|
cred_backend: str | None = None,
|
|
36
|
+
creds_path: Path | None = None,
|
|
36
37
|
env: dict[str, str] | None = None,
|
|
37
38
|
) -> list[dict]:
|
|
38
39
|
async with _bridge.session(
|
|
@@ -43,6 +44,7 @@ async def _list_tools(
|
|
|
43
44
|
client_name=client_name,
|
|
44
45
|
config_path=config_path,
|
|
45
46
|
cred_backend=cred_backend,
|
|
47
|
+
creds_path=creds_path,
|
|
46
48
|
env=env,
|
|
47
49
|
) as s:
|
|
48
50
|
result = await s.list_tools()
|
|
@@ -72,6 +74,7 @@ async def _probe(
|
|
|
72
74
|
client_name: str | None = None,
|
|
73
75
|
config_path: str | None = None,
|
|
74
76
|
cred_backend: str | None = None,
|
|
77
|
+
creds_path: Path | None = None,
|
|
75
78
|
env: dict[str, str] | None = None,
|
|
76
79
|
) -> tuple[Any, int]:
|
|
77
80
|
"""Return `(observed_shape, observed_byte_size)` for one live probe call."""
|
|
@@ -82,6 +85,7 @@ async def _probe(
|
|
|
82
85
|
client_name=client_name,
|
|
83
86
|
config_path=config_path,
|
|
84
87
|
cred_backend=cred_backend,
|
|
88
|
+
creds_path=creds_path,
|
|
85
89
|
env=env,
|
|
86
90
|
)
|
|
87
91
|
raw = await caller.call(server, tool, args)
|
|
@@ -100,6 +104,7 @@ async def _call(
|
|
|
100
104
|
client_name: str | None = None,
|
|
101
105
|
config_path: str | None = None,
|
|
102
106
|
cred_backend: str | None = None,
|
|
107
|
+
creds_path: Path | None = None,
|
|
103
108
|
env: dict[str, str] | None = None,
|
|
104
109
|
) -> Any:
|
|
105
110
|
caller = _bridge.McpBridgeCaller(
|
|
@@ -109,6 +114,7 @@ async def _call(
|
|
|
109
114
|
client_name=client_name,
|
|
110
115
|
config_path=config_path,
|
|
111
116
|
cred_backend=cred_backend,
|
|
117
|
+
creds_path=creds_path,
|
|
112
118
|
env=env,
|
|
113
119
|
)
|
|
114
120
|
return await caller.call(server, tool, args)
|
|
@@ -272,6 +278,7 @@ def _cmd_codegen(ns: argparse.Namespace) -> int:
|
|
|
272
278
|
client_name=ns.client_name,
|
|
273
279
|
config_path=ns.config,
|
|
274
280
|
cred_backend=ns.cred_backend,
|
|
281
|
+
creds_path=_creds_path(ns),
|
|
275
282
|
env=_parse_env(ns),
|
|
276
283
|
)
|
|
277
284
|
try:
|
|
@@ -329,6 +336,7 @@ def _cmd_probe(ns: argparse.Namespace) -> int:
|
|
|
329
336
|
client_name=ns.client_name,
|
|
330
337
|
config_path=ns.config,
|
|
331
338
|
cred_backend=ns.cred_backend,
|
|
339
|
+
creds_path=_creds_path(ns),
|
|
332
340
|
env=_parse_env(ns),
|
|
333
341
|
)
|
|
334
342
|
shapes = []
|
|
@@ -400,6 +408,7 @@ def _cmd_call(ns: argparse.Namespace) -> int:
|
|
|
400
408
|
client_name=ns.client_name,
|
|
401
409
|
config_path=ns.config,
|
|
402
410
|
cred_backend=ns.cred_backend,
|
|
411
|
+
creds_path=_creds_path(ns),
|
|
403
412
|
env=_parse_env(ns),
|
|
404
413
|
)
|
|
405
414
|
|
|
@@ -583,6 +592,7 @@ def _cmd_list(ns: argparse.Namespace) -> int:
|
|
|
583
592
|
client_name=ns.client_name,
|
|
584
593
|
config_path=ns.config,
|
|
585
594
|
cred_backend=ns.cred_backend,
|
|
595
|
+
creds_path=_creds_path(ns),
|
|
586
596
|
env=_parse_env(ns),
|
|
587
597
|
)
|
|
588
598
|
try:
|
|
@@ -620,6 +630,7 @@ def _cmd_login(ns: argparse.Namespace) -> int:
|
|
|
620
630
|
asyncio.run(
|
|
621
631
|
_bridge.login(
|
|
622
632
|
ns.server,
|
|
633
|
+
_creds_path(ns) or _bridge.DEFAULT_CREDS_PATH,
|
|
623
634
|
url=ns.url,
|
|
624
635
|
client_name=ns.client_name,
|
|
625
636
|
config_path=ns.config,
|
|
@@ -642,6 +653,7 @@ def _cmd_migrate_creds(ns: argparse.Namespace) -> int:
|
|
|
642
653
|
ns.from_backend,
|
|
643
654
|
ns.to_backend,
|
|
644
655
|
servers=parsed_servers,
|
|
656
|
+
credentials_path=_creds_path(ns) or _bridge.DEFAULT_CREDS_PATH,
|
|
645
657
|
purge=ns.purge,
|
|
646
658
|
set_default=ns.set_default,
|
|
647
659
|
)
|
|
@@ -661,7 +673,11 @@ def _cmd_migrate_creds(ns: argparse.Namespace) -> int:
|
|
|
661
673
|
|
|
662
674
|
def _cmd_list_creds(ns: argparse.Namespace) -> int:
|
|
663
675
|
"""Print stored credentials as a human table (default) or JSON (--json)."""
|
|
664
|
-
rows = _bridge.list_creds(
|
|
676
|
+
rows = _bridge.list_creds(
|
|
677
|
+
backend=ns.cred_backend,
|
|
678
|
+
credentials_path=_creds_path(ns) or _bridge.DEFAULT_CREDS_PATH,
|
|
679
|
+
expired_only=ns.expired,
|
|
680
|
+
)
|
|
665
681
|
|
|
666
682
|
if ns.json:
|
|
667
683
|
sys.stdout.write(json.dumps(rows, indent=2) + "\n")
|
|
@@ -708,7 +724,11 @@ def _cmd_delete_creds(ns: argparse.Namespace) -> int:
|
|
|
708
724
|
print("[delete-creds] aborted", file=sys.stderr)
|
|
709
725
|
return 0
|
|
710
726
|
|
|
711
|
-
existed = _bridge.delete_cred(
|
|
727
|
+
existed = _bridge.delete_cred(
|
|
728
|
+
ns.server,
|
|
729
|
+
backend=ns.cred_backend,
|
|
730
|
+
credentials_path=_creds_path(ns) or _bridge.DEFAULT_CREDS_PATH,
|
|
731
|
+
)
|
|
712
732
|
if existed:
|
|
713
733
|
print(f"[delete-creds] deleted {ns.server!r}", file=sys.stderr)
|
|
714
734
|
else:
|
|
@@ -729,6 +749,33 @@ def _add_headless_flag(p: argparse.ArgumentParser) -> None:
|
|
|
729
749
|
)
|
|
730
750
|
|
|
731
751
|
|
|
752
|
+
def _add_creds_flag(p: argparse.ArgumentParser, *, transport_note: bool = True) -> None:
|
|
753
|
+
"""Add --creds PATH — the file backend's credentials store.
|
|
754
|
+
|
|
755
|
+
Shared by the connection commands and the credential-management ones, so a
|
|
756
|
+
non-default store stays addressable end to end: login writes it, calls read
|
|
757
|
+
it, list/delete/migrate operate on it.
|
|
758
|
+
"""
|
|
759
|
+
note = (
|
|
760
|
+
" No effect with --bearer or --stdio, which store no credentials."
|
|
761
|
+
if transport_note
|
|
762
|
+
else " Only the file backend has a path; the keyring backend ignores it."
|
|
763
|
+
)
|
|
764
|
+
p.add_argument(
|
|
765
|
+
"--creds",
|
|
766
|
+
dest="creds",
|
|
767
|
+
metavar="PATH",
|
|
768
|
+
help=f"OAuth credentials file for both storing and reading tokens "
|
|
769
|
+
f"(default: {_bridge.DEFAULT_CREDS_PATH}).{note}",
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def _creds_path(ns: argparse.Namespace) -> Path | None:
|
|
774
|
+
"""--creds as a Path, or None to let _bridge use DEFAULT_CREDS_PATH."""
|
|
775
|
+
value = getattr(ns, "creds", None)
|
|
776
|
+
return Path(value) if value else None
|
|
777
|
+
|
|
778
|
+
|
|
732
779
|
def _callback_timeout(value: str) -> float:
|
|
733
780
|
"""argparse type for --callback-timeout: a non-negative number of seconds."""
|
|
734
781
|
try:
|
|
@@ -756,6 +803,7 @@ def _add_conn_args(p: argparse.ArgumentParser) -> None:
|
|
|
756
803
|
p.add_argument(
|
|
757
804
|
"--config", dest="config", help="servers config path; overrides $MCPGEN_SERVERS and the default search"
|
|
758
805
|
)
|
|
806
|
+
_add_creds_flag(p)
|
|
759
807
|
p.add_argument(
|
|
760
808
|
"--cred-backend",
|
|
761
809
|
dest="cred_backend",
|
|
@@ -907,6 +955,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
907
955
|
help="write cred_backend=<to> into ~/.mcpgen/config.json so future "
|
|
908
956
|
"commands default to the target backend (default: leave config untouched)",
|
|
909
957
|
)
|
|
958
|
+
_add_creds_flag(mc, transport_note=False)
|
|
910
959
|
mc.set_defaults(func=_cmd_migrate_creds)
|
|
911
960
|
|
|
912
961
|
lc = sub.add_parser("list-creds", help="list stored credentials (flags expired)")
|
|
@@ -926,6 +975,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
926
975
|
choices=["file", "keyring", "auto"],
|
|
927
976
|
help="credential storage backend (default: resolved from env/config, else file)",
|
|
928
977
|
)
|
|
978
|
+
_add_creds_flag(lc, transport_note=False)
|
|
929
979
|
lc.set_defaults(func=_cmd_list_creds)
|
|
930
980
|
|
|
931
981
|
dl = sub.add_parser("delete-creds", help="delete the stored credential for one server")
|
|
@@ -942,6 +992,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
942
992
|
choices=["file", "keyring", "auto"],
|
|
943
993
|
help="credential storage backend (default: resolved from env/config, else file)",
|
|
944
994
|
)
|
|
995
|
+
_add_creds_flag(dl, transport_note=False)
|
|
945
996
|
dl.set_defaults(func=_cmd_delete_creds)
|
|
946
997
|
|
|
947
998
|
dc = sub.add_parser("discover", help="list MCP servers from installed agent hosts")
|
|
@@ -18,6 +18,7 @@ import stat
|
|
|
18
18
|
import time
|
|
19
19
|
import warnings
|
|
20
20
|
from contextlib import asynccontextmanager
|
|
21
|
+
from pathlib import Path
|
|
21
22
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
22
23
|
|
|
23
24
|
import pytest
|
|
@@ -113,7 +114,7 @@ def test_session_routes_bearer_not_oauth_when_bearer_provided():
|
|
|
113
114
|
yield _make_mock_session()
|
|
114
115
|
|
|
115
116
|
@asynccontextmanager
|
|
116
|
-
async def fake_oauth(name, url, *, client_name=None, cred_backend=None):
|
|
117
|
+
async def fake_oauth(name, url, *, client_name=None, cred_backend=None, creds_path=None):
|
|
117
118
|
oauth_calls.append(name)
|
|
118
119
|
yield _make_mock_session()
|
|
119
120
|
|
|
@@ -161,7 +162,7 @@ def test_session_oauth_path_unchanged_without_bearer():
|
|
|
161
162
|
oauth_calls: list = []
|
|
162
163
|
|
|
163
164
|
@asynccontextmanager
|
|
164
|
-
async def fake_oauth(name, url, *, client_name=None, cred_backend=None):
|
|
165
|
+
async def fake_oauth(name, url, *, client_name=None, cred_backend=None, creds_path=None):
|
|
165
166
|
oauth_calls.append(name)
|
|
166
167
|
yield _make_mock_session()
|
|
167
168
|
|
|
@@ -1806,3 +1807,165 @@ def test_cli_login_callback_timeout_rejects_invalid(bad, capsys):
|
|
|
1806
1807
|
main(["login", "acme", "--callback-timeout", bad])
|
|
1807
1808
|
assert exc.value.code == 2
|
|
1808
1809
|
assert "--callback-timeout" in capsys.readouterr().err
|
|
1810
|
+
|
|
1811
|
+
|
|
1812
|
+
# ---------------------------------------------------------------------------
|
|
1813
|
+
# creds_path on the CALL path (not just login)
|
|
1814
|
+
# ---------------------------------------------------------------------------
|
|
1815
|
+
|
|
1816
|
+
|
|
1817
|
+
def _recording_storage_factory(recorded: dict):
|
|
1818
|
+
"""Stand-in for FileTokenStorage that records how it was constructed."""
|
|
1819
|
+
|
|
1820
|
+
def factory(server_name, credentials_path=_bridge.DEFAULT_CREDS_PATH, *, backend=None):
|
|
1821
|
+
recorded["server_name"] = server_name
|
|
1822
|
+
recorded["credentials_path"] = credentials_path
|
|
1823
|
+
recorded["backend"] = backend
|
|
1824
|
+
storage = MagicMock()
|
|
1825
|
+
storage._load.return_value = {}
|
|
1826
|
+
return storage
|
|
1827
|
+
|
|
1828
|
+
return factory
|
|
1829
|
+
|
|
1830
|
+
|
|
1831
|
+
def _run_failing_call(recorded: dict, **caller_kwargs):
|
|
1832
|
+
"""Drive McpBridgeCaller.call() far enough to construct the token storage."""
|
|
1833
|
+
|
|
1834
|
+
async def run():
|
|
1835
|
+
with (
|
|
1836
|
+
patch("mcpgen._bridge.FileTokenStorage", _recording_storage_factory(recorded)),
|
|
1837
|
+
patch("mcpgen._bridge._pre_flight_refresh", AsyncMock()),
|
|
1838
|
+
patch("mcpgen._bridge.OAuthClientProvider", MagicMock()),
|
|
1839
|
+
patch("mcpgen._bridge._open_http", _fake_http_fail),
|
|
1840
|
+
):
|
|
1841
|
+
caller = _bridge.McpBridgeCaller(url="https://acme.example.com/mcp", **caller_kwargs)
|
|
1842
|
+
with pytest.raises(RuntimeError, match="network error"):
|
|
1843
|
+
await caller.call("acme", "whoami", {})
|
|
1844
|
+
|
|
1845
|
+
asyncio.run(run())
|
|
1846
|
+
|
|
1847
|
+
|
|
1848
|
+
def test_call_path_uses_custom_creds_path(tmp_path):
|
|
1849
|
+
"""A caller-supplied creds_path must reach the token storage on the call path.
|
|
1850
|
+
|
|
1851
|
+
login() honoured creds_path but the session did not, so tokens were written
|
|
1852
|
+
to one file and read from another.
|
|
1853
|
+
"""
|
|
1854
|
+
creds = tmp_path / "alt-credentials.json"
|
|
1855
|
+
recorded: dict = {}
|
|
1856
|
+
_run_failing_call(recorded, creds_path=creds)
|
|
1857
|
+
assert recorded["credentials_path"] == creds
|
|
1858
|
+
|
|
1859
|
+
|
|
1860
|
+
def test_call_path_defaults_to_default_creds_path():
|
|
1861
|
+
"""No creds_path → DEFAULT_CREDS_PATH; existing behaviour is unchanged."""
|
|
1862
|
+
recorded: dict = {}
|
|
1863
|
+
_run_failing_call(recorded)
|
|
1864
|
+
assert recorded["credentials_path"] == _bridge.DEFAULT_CREDS_PATH
|
|
1865
|
+
|
|
1866
|
+
|
|
1867
|
+
def test_session_forwards_creds_path_to_http_session(tmp_path):
|
|
1868
|
+
"""session() is a public entry point too — it must forward the path."""
|
|
1869
|
+
creds = tmp_path / "alt-credentials.json"
|
|
1870
|
+
recorded: dict = {}
|
|
1871
|
+
|
|
1872
|
+
async def run():
|
|
1873
|
+
with (
|
|
1874
|
+
patch("mcpgen._bridge.FileTokenStorage", _recording_storage_factory(recorded)),
|
|
1875
|
+
patch("mcpgen._bridge._pre_flight_refresh", AsyncMock()),
|
|
1876
|
+
patch("mcpgen._bridge.OAuthClientProvider", MagicMock()),
|
|
1877
|
+
patch("mcpgen._bridge._open_http", _fake_http_fail),
|
|
1878
|
+
):
|
|
1879
|
+
with pytest.raises(RuntimeError, match="network error"):
|
|
1880
|
+
async with _bridge.session("acme", url="https://acme.example.com/mcp", creds_path=creds):
|
|
1881
|
+
pass
|
|
1882
|
+
|
|
1883
|
+
asyncio.run(run())
|
|
1884
|
+
assert recorded["credentials_path"] == creds
|
|
1885
|
+
|
|
1886
|
+
|
|
1887
|
+
def test_default_creds_path_is_exported():
|
|
1888
|
+
"""Consumers need the default as a value instead of re-deriving the path."""
|
|
1889
|
+
import mcpgen
|
|
1890
|
+
|
|
1891
|
+
assert mcpgen.DEFAULT_CREDS_PATH is _bridge.DEFAULT_CREDS_PATH
|
|
1892
|
+
assert "DEFAULT_CREDS_PATH" in mcpgen.__all__
|
|
1893
|
+
|
|
1894
|
+
|
|
1895
|
+
def _fake_session_factory(captured: dict):
|
|
1896
|
+
@asynccontextmanager
|
|
1897
|
+
async def fake_session(*args, **kwargs):
|
|
1898
|
+
captured.update(kwargs)
|
|
1899
|
+
s = MagicMock()
|
|
1900
|
+
s.list_tools = AsyncMock(return_value=MagicMock(tools=[]))
|
|
1901
|
+
yield s
|
|
1902
|
+
|
|
1903
|
+
return fake_session
|
|
1904
|
+
|
|
1905
|
+
|
|
1906
|
+
@pytest.mark.parametrize(
|
|
1907
|
+
("argv_flag", "expected"),
|
|
1908
|
+
[(["--creds", "/tmp/alt-creds.json"], Path("/tmp/alt-creds.json")), ([], None)],
|
|
1909
|
+
)
|
|
1910
|
+
def test_cli_creds_flag_reaches_session(argv_flag, expected):
|
|
1911
|
+
"""`--creds` on a session-opening command arrives at _bridge.session."""
|
|
1912
|
+
from mcpgen.cli import main
|
|
1913
|
+
|
|
1914
|
+
captured: dict = {}
|
|
1915
|
+
with patch("mcpgen._bridge.session", _fake_session_factory(captured)):
|
|
1916
|
+
assert main(["list", "acme", "--url", "https://acme.example.com/mcp", *argv_flag]) == 0
|
|
1917
|
+
assert captured["creds_path"] == expected
|
|
1918
|
+
|
|
1919
|
+
|
|
1920
|
+
@pytest.mark.parametrize(
|
|
1921
|
+
("argv_flag", "expected"),
|
|
1922
|
+
[(["--creds", "/tmp/alt-creds.json"], Path("/tmp/alt-creds.json")), ([], _bridge.DEFAULT_CREDS_PATH)],
|
|
1923
|
+
)
|
|
1924
|
+
def test_cli_login_passes_creds_path(argv_flag, expected):
|
|
1925
|
+
"""`mcpgen login` previously dropped --creds entirely — it must log in to that file."""
|
|
1926
|
+
from mcpgen.cli import main
|
|
1927
|
+
|
|
1928
|
+
login_mock = AsyncMock()
|
|
1929
|
+
with patch("mcpgen._bridge.login", login_mock):
|
|
1930
|
+
assert main(["login", "acme", *argv_flag]) == 0
|
|
1931
|
+
assert login_mock.await_args.args[1] == expected
|
|
1932
|
+
|
|
1933
|
+
|
|
1934
|
+
@pytest.mark.parametrize(
|
|
1935
|
+
("argv_flag", "expected"),
|
|
1936
|
+
[(["--creds", "/tmp/alt-creds.json"], Path("/tmp/alt-creds.json")), ([], _bridge.DEFAULT_CREDS_PATH)],
|
|
1937
|
+
)
|
|
1938
|
+
def test_cli_list_creds_passes_creds_path(argv_flag, expected):
|
|
1939
|
+
"""`list-creds` must inspect the same store `login --creds` wrote to."""
|
|
1940
|
+
from mcpgen.cli import main
|
|
1941
|
+
|
|
1942
|
+
with patch("mcpgen._bridge.list_creds", return_value=[]) as list_mock:
|
|
1943
|
+
assert main(["list-creds", *argv_flag]) == 0
|
|
1944
|
+
assert list_mock.call_args.kwargs["credentials_path"] == expected
|
|
1945
|
+
|
|
1946
|
+
|
|
1947
|
+
@pytest.mark.parametrize(
|
|
1948
|
+
("argv_flag", "expected"),
|
|
1949
|
+
[(["--creds", "/tmp/alt-creds.json"], Path("/tmp/alt-creds.json")), ([], _bridge.DEFAULT_CREDS_PATH)],
|
|
1950
|
+
)
|
|
1951
|
+
def test_cli_delete_creds_passes_creds_path(argv_flag, expected):
|
|
1952
|
+
"""Deleting from the default store when the user named another would be a silent no-op."""
|
|
1953
|
+
from mcpgen.cli import main
|
|
1954
|
+
|
|
1955
|
+
with patch("mcpgen._bridge.delete_cred", return_value=True) as delete_mock:
|
|
1956
|
+
assert main(["delete-creds", "acme", "--yes", *argv_flag]) == 0
|
|
1957
|
+
assert delete_mock.call_args.kwargs["credentials_path"] == expected
|
|
1958
|
+
|
|
1959
|
+
|
|
1960
|
+
@pytest.mark.parametrize(
|
|
1961
|
+
("argv_flag", "expected"),
|
|
1962
|
+
[(["--creds", "/tmp/alt-creds.json"], Path("/tmp/alt-creds.json")), ([], _bridge.DEFAULT_CREDS_PATH)],
|
|
1963
|
+
)
|
|
1964
|
+
def test_cli_migrate_creds_passes_creds_path(argv_flag, expected):
|
|
1965
|
+
"""migrate-creds reads and writes the file backend — it needs the path too."""
|
|
1966
|
+
from mcpgen.cli import main
|
|
1967
|
+
|
|
1968
|
+
result = {"from": "file", "to": "keyring", "migrated": 0, "overwritten": 0, "purged": False, "set_default": False}
|
|
1969
|
+
with patch("mcpgen._bridge.migrate_creds", return_value=result) as migrate_mock:
|
|
1970
|
+
assert main(["migrate-creds", "--from", "file", "--to", "keyring", *argv_flag]) == 0
|
|
1971
|
+
assert migrate_mock.call_args.kwargs["credentials_path"] == expected
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_client_kit-0.4.0 → mcp_client_kit-0.5.0}/skills/generate-mcp-runner/runner_templates/stdio.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|