mcp2cli 3.4.0__tar.gz → 3.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.
- mcp2cli-3.4.0/README.md → mcp2cli-3.5.0/PKG-INFO +34 -1
- mcp2cli-3.4.0/PKG-INFO → mcp2cli-3.5.0/README.md +15 -20
- {mcp2cli-3.4.0 → mcp2cli-3.5.0}/pyproject.toml +2 -2
- {mcp2cli-3.4.0 → mcp2cli-3.5.0}/pyproject.toml.orig +4 -2
- {mcp2cli-3.4.0 → mcp2cli-3.5.0}/src/mcp2cli/__init__.py +177 -39
- {mcp2cli-3.4.0 → mcp2cli-3.5.0}/src/mcp2cli/__main__.py +0 -0
- {mcp2cli-3.4.0 → mcp2cli-3.5.0}/src/mcp2cli/py.typed +0 -0
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp2cli
|
|
3
|
+
Version: 3.5.0
|
|
4
|
+
Summary: Turn any MCP server or OpenAPI spec into a CLI
|
|
5
|
+
Author: Stephan Fitzpatrick
|
|
6
|
+
Author-email: Stephan Fitzpatrick <stephan@knowsuchagency.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Dist: httpx
|
|
9
|
+
Requires-Dist: mcp>=1.26,<3
|
|
10
|
+
Requires-Dist: pyyaml
|
|
11
|
+
Requires-Dist: pytest ; extra == 'test'
|
|
12
|
+
Requires-Dist: pytest-asyncio ; extra == 'test'
|
|
13
|
+
Requires-Dist: tiktoken ; extra == 'test'
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Project-URL: Homepage, https://github.com/knowsuchagency/mcp2cli
|
|
16
|
+
Project-URL: Repository, https://github.com/knowsuchagency/mcp2cli
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
1
20
|
<p align="center">
|
|
2
21
|
<img src="https://raw.githubusercontent.com/knowsuchagency/mcp2cli/main/assets/hero.png" alt="mcp2cli — one CLI for every API" width="700">
|
|
3
22
|
</p>
|
|
@@ -364,13 +383,27 @@ Subcommands and their flags are generated dynamically from the spec or MCP serve
|
|
|
364
383
|
# Install with test + MCP deps
|
|
365
384
|
uv sync --extra test
|
|
366
385
|
|
|
367
|
-
# Run tests
|
|
386
|
+
# Run tests
|
|
368
387
|
uv run pytest tests/ -v
|
|
369
388
|
|
|
370
389
|
# Run just the token savings tests
|
|
371
390
|
uv run pytest tests/test_token_savings.py -v -s
|
|
372
391
|
```
|
|
373
392
|
|
|
393
|
+
### MCP SDK compatibility
|
|
394
|
+
|
|
395
|
+
mcp2cli works with **both major versions** of the MCP Python SDK (`mcp>=1.26,<3`),
|
|
396
|
+
so it never forces a resolver conflict with other tools in the same environment.
|
|
397
|
+
CI runs the suite against the declared floor, the latest 1.x, and the latest 2.x.
|
|
398
|
+
|
|
399
|
+
The two majors differ in ways that matter to a client — v2 renamed model fields
|
|
400
|
+
to snake_case, replaced `streamablehttp_client`, moved to `httpx2`, and dropped
|
|
401
|
+
the session-id element from the transport tuple. Those differences are confined
|
|
402
|
+
to a handful of helpers (`_mcp_attr`, `_mcp_dump`, `_streamable_streams`,
|
|
403
|
+
`_list_tools_page`, `_resource_uri`, `_authorization_code_result`); the rest of
|
|
404
|
+
the codebase is version-agnostic. The test fixtures speak the JSON-RPC wire
|
|
405
|
+
protocol directly and import no SDK, so they hold across majors.
|
|
406
|
+
|
|
374
407
|
---
|
|
375
408
|
|
|
376
409
|
## License
|
|
@@ -1,22 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: mcp2cli
|
|
3
|
-
Version: 3.4.0
|
|
4
|
-
Summary: Turn any MCP server or OpenAPI spec into a CLI
|
|
5
|
-
Author: Stephan Fitzpatrick
|
|
6
|
-
Author-email: Stephan Fitzpatrick <stephan@knowsuchagency.com>
|
|
7
|
-
License-Expression: MIT
|
|
8
|
-
Requires-Dist: httpx
|
|
9
|
-
Requires-Dist: mcp>=1.0,<2
|
|
10
|
-
Requires-Dist: pyyaml
|
|
11
|
-
Requires-Dist: pytest ; extra == 'test'
|
|
12
|
-
Requires-Dist: pytest-asyncio ; extra == 'test'
|
|
13
|
-
Requires-Dist: tiktoken ; extra == 'test'
|
|
14
|
-
Requires-Python: >=3.10
|
|
15
|
-
Project-URL: Homepage, https://github.com/knowsuchagency/mcp2cli
|
|
16
|
-
Project-URL: Repository, https://github.com/knowsuchagency/mcp2cli
|
|
17
|
-
Provides-Extra: test
|
|
18
|
-
Description-Content-Type: text/markdown
|
|
19
|
-
|
|
20
1
|
<p align="center">
|
|
21
2
|
<img src="https://raw.githubusercontent.com/knowsuchagency/mcp2cli/main/assets/hero.png" alt="mcp2cli — one CLI for every API" width="700">
|
|
22
3
|
</p>
|
|
@@ -383,13 +364,27 @@ Subcommands and their flags are generated dynamically from the spec or MCP serve
|
|
|
383
364
|
# Install with test + MCP deps
|
|
384
365
|
uv sync --extra test
|
|
385
366
|
|
|
386
|
-
# Run tests
|
|
367
|
+
# Run tests
|
|
387
368
|
uv run pytest tests/ -v
|
|
388
369
|
|
|
389
370
|
# Run just the token savings tests
|
|
390
371
|
uv run pytest tests/test_token_savings.py -v -s
|
|
391
372
|
```
|
|
392
373
|
|
|
374
|
+
### MCP SDK compatibility
|
|
375
|
+
|
|
376
|
+
mcp2cli works with **both major versions** of the MCP Python SDK (`mcp>=1.26,<3`),
|
|
377
|
+
so it never forces a resolver conflict with other tools in the same environment.
|
|
378
|
+
CI runs the suite against the declared floor, the latest 1.x, and the latest 2.x.
|
|
379
|
+
|
|
380
|
+
The two majors differ in ways that matter to a client — v2 renamed model fields
|
|
381
|
+
to snake_case, replaced `streamablehttp_client`, moved to `httpx2`, and dropped
|
|
382
|
+
the session-id element from the transport tuple. Those differences are confined
|
|
383
|
+
to a handful of helpers (`_mcp_attr`, `_mcp_dump`, `_streamable_streams`,
|
|
384
|
+
`_list_tools_page`, `_resource_uri`, `_authorization_code_result`); the rest of
|
|
385
|
+
the codebase is version-agnostic. The test fixtures speak the JSON-RPC wire
|
|
386
|
+
protocol directly and import no SDK, so they hold across majors.
|
|
387
|
+
|
|
393
388
|
---
|
|
394
389
|
|
|
395
390
|
## License
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mcp2cli"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.5.0"
|
|
4
4
|
description = "Turn any MCP server or OpenAPI spec into a CLI"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
7
7
|
requires-python = ">=3.10"
|
|
8
8
|
dependencies = [
|
|
9
9
|
"httpx",
|
|
10
|
-
"mcp>=1.
|
|
10
|
+
"mcp>=1.26,<3",
|
|
11
11
|
"pyyaml",
|
|
12
12
|
]
|
|
13
13
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mcp2cli"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.5.0"
|
|
4
4
|
description = "Turn any MCP server or OpenAPI spec into a CLI"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -10,7 +10,9 @@ authors = [
|
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
dependencies = [
|
|
12
12
|
"httpx",
|
|
13
|
-
|
|
13
|
+
# Works on both SDK majors; 1.26 is the floor we verify in CI, and the one
|
|
14
|
+
# where `streamable_http_client` + `create_mcp_http_client` are both present.
|
|
15
|
+
"mcp>=1.26,<3",
|
|
14
16
|
"pyyaml",
|
|
15
17
|
]
|
|
16
18
|
|
|
@@ -23,6 +23,7 @@ import sys
|
|
|
23
23
|
import threading
|
|
24
24
|
import time
|
|
25
25
|
import webbrowser
|
|
26
|
+
from contextlib import asynccontextmanager
|
|
26
27
|
from dataclasses import dataclass, field
|
|
27
28
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
28
29
|
from pathlib import Path
|
|
@@ -281,6 +282,125 @@ def _toon_encode(json_str: str) -> str | None:
|
|
|
281
282
|
return None
|
|
282
283
|
|
|
283
284
|
|
|
285
|
+
# ---------------------------------------------------------------------------
|
|
286
|
+
# MCP SDK compatibility (v1 and v2)
|
|
287
|
+
# ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
# SDK 2.0 renamed model fields from camelCase to snake_case, keeping camelCase
|
|
290
|
+
# only as serialization aliases -- so attribute access has to use the new name
|
|
291
|
+
# while the wire format is unchanged. A name missing from this map is an
|
|
292
|
+
# unhandled rename and raises KeyError rather than silently reading None.
|
|
293
|
+
_MCP_RENAMED_FIELDS = {
|
|
294
|
+
"inputSchema": "input_schema",
|
|
295
|
+
"outputSchema": "output_schema",
|
|
296
|
+
"nextCursor": "next_cursor",
|
|
297
|
+
"resourceTemplates": "resource_templates",
|
|
298
|
+
"uriTemplate": "uri_template",
|
|
299
|
+
"mimeType": "mime_type",
|
|
300
|
+
"structuredContent": "structured_content",
|
|
301
|
+
"isError": "is_error",
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _mcp_attr(obj, name: str):
|
|
306
|
+
"""Read an SDK model field across the v1/v2 camelCase -> snake_case rename."""
|
|
307
|
+
try:
|
|
308
|
+
return getattr(obj, name)
|
|
309
|
+
except AttributeError:
|
|
310
|
+
return getattr(obj, _MCP_RENAMED_FIELDS[name])
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _demote_meta_alias(node: dict) -> None:
|
|
314
|
+
"""Emit the SDK's ``_meta`` alias as ``meta``, the spelling mcp2cli ships."""
|
|
315
|
+
if "_meta" in node:
|
|
316
|
+
node["meta"] = node.pop("_meta")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _mcp_dump(model) -> dict:
|
|
320
|
+
"""Serialize an SDK model using the camelCase wire names on either major.
|
|
321
|
+
|
|
322
|
+
v2 renamed model attributes to snake_case, so a plain ``model_dump()``
|
|
323
|
+
would silently change mcp2cli's ``--json`` envelope from ``isError`` to
|
|
324
|
+
``is_error`` depending on which SDK happened to be installed. ``by_alias``
|
|
325
|
+
pins the wire spelling on both.
|
|
326
|
+
|
|
327
|
+
``meta`` is aliased to ``_meta`` in both majors, and mcp2cli has always
|
|
328
|
+
emitted it as ``meta``, so it is mapped back -- but only on the envelope
|
|
329
|
+
and its content items, which the SDK owns. ``structuredContent`` is the
|
|
330
|
+
tool's own payload and is never rewritten, so a tool that legitimately
|
|
331
|
+
returns a ``_meta`` key keeps it.
|
|
332
|
+
"""
|
|
333
|
+
data = model.model_dump(mode="json", by_alias=True)
|
|
334
|
+
_demote_meta_alias(data)
|
|
335
|
+
for item in data.get("content") or ():
|
|
336
|
+
if isinstance(item, dict):
|
|
337
|
+
_demote_meta_alias(item)
|
|
338
|
+
return data
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _resource_uri(uri: str):
|
|
342
|
+
"""Coerce a resource URI to what ``resources/read`` expects.
|
|
343
|
+
|
|
344
|
+
v1 types the request param as a pydantic ``AnyUrl``; v2 takes a plain
|
|
345
|
+
string and rejects an ``AnyUrl``.
|
|
346
|
+
"""
|
|
347
|
+
from mcp.types import ReadResourceRequestParams
|
|
348
|
+
|
|
349
|
+
if ReadResourceRequestParams.model_fields["uri"].annotation is str:
|
|
350
|
+
return uri
|
|
351
|
+
from pydantic import AnyUrl
|
|
352
|
+
|
|
353
|
+
return AnyUrl(uri)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@asynccontextmanager
|
|
357
|
+
async def _streamable_streams(url: str, headers=None, auth=None):
|
|
358
|
+
"""Open a streamable-http transport, yielding ``(read, write)``.
|
|
359
|
+
|
|
360
|
+
Three things differ across SDK majors here, which is why this is the only
|
|
361
|
+
place that talks to that transport (issues #68, #74):
|
|
362
|
+
|
|
363
|
+
* v2 dropped the ``streamablehttp_client`` alias, keeping only
|
|
364
|
+
``streamable_http_client`` -- the original break.
|
|
365
|
+
* that surviving function takes a pre-built ``http_client`` instead of
|
|
366
|
+
``headers``/``auth``, and v2 is built on **httpx2**, not httpx, so the
|
|
367
|
+
client has to come from the SDK's own factory to be the right flavour.
|
|
368
|
+
* v1 yields a third element (a get-session-id callback) that v2 dropped.
|
|
369
|
+
mcp2cli never used it, so both shapes collapse to ``(read, write)``.
|
|
370
|
+
"""
|
|
371
|
+
from mcp.client.streamable_http import streamable_http_client
|
|
372
|
+
from mcp.shared._httpx_utils import create_mcp_http_client
|
|
373
|
+
|
|
374
|
+
async with create_mcp_http_client(headers=headers, auth=auth) as client:
|
|
375
|
+
async with streamable_http_client(url, http_client=client) as streams:
|
|
376
|
+
yield streams[0], streams[1]
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
async def _list_tools_page(session, cursor: str | None):
|
|
380
|
+
"""Request one page of ``tools/list``.
|
|
381
|
+
|
|
382
|
+
Both majors accept ``params``; v1's ``cursor=`` shorthand is deprecated
|
|
383
|
+
there and gone in v2, so this is the one spelling that works on both.
|
|
384
|
+
"""
|
|
385
|
+
from mcp.types import PaginatedRequestParams
|
|
386
|
+
|
|
387
|
+
params = PaginatedRequestParams(cursor=cursor) if cursor else None
|
|
388
|
+
return await session.list_tools(params=params)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _authorization_code_result(code: str, state: str | None):
|
|
392
|
+
"""Wrap a callback result in whatever ``callback_handler`` must return.
|
|
393
|
+
|
|
394
|
+
v1 expects a plain ``(code, state)`` tuple; v2 expects an
|
|
395
|
+
``AuthorizationCodeResult`` model.
|
|
396
|
+
"""
|
|
397
|
+
try:
|
|
398
|
+
from mcp.shared.auth import AuthorizationCodeResult
|
|
399
|
+
except ImportError:
|
|
400
|
+
return (code, state)
|
|
401
|
+
return AuthorizationCodeResult(code=code, state=state)
|
|
402
|
+
|
|
403
|
+
|
|
284
404
|
def _ensure_utf8_output() -> None:
|
|
285
405
|
"""Make non-ASCII output safe on consoles that cannot encode it.
|
|
286
406
|
|
|
@@ -877,12 +997,21 @@ def build_oauth_provider(
|
|
|
877
997
|
await super()._initialize()
|
|
878
998
|
_restore_token_expiry_from_sidecar(self.context)
|
|
879
999
|
|
|
1000
|
+
# v2 renamed the `scopes` argument to `scope`.
|
|
1001
|
+
import inspect
|
|
1002
|
+
|
|
1003
|
+
scope_kwarg = (
|
|
1004
|
+
"scope"
|
|
1005
|
+
if "scope"
|
|
1006
|
+
in inspect.signature(ClientCredentialsOAuthProvider.__init__).parameters
|
|
1007
|
+
else "scopes"
|
|
1008
|
+
)
|
|
880
1009
|
return _RobustClientCredentialsProvider(
|
|
881
1010
|
server_url=server_url,
|
|
882
1011
|
storage=storage,
|
|
883
1012
|
client_id=client_id,
|
|
884
1013
|
client_secret=client_secret,
|
|
885
|
-
|
|
1014
|
+
**{scope_kwarg: scope},
|
|
886
1015
|
)
|
|
887
1016
|
|
|
888
1017
|
from mcp.client.auth.oauth2 import OAuthClientProvider
|
|
@@ -1105,8 +1234,9 @@ def build_oauth_provider(
|
|
|
1105
1234
|
file=sys.stderr,
|
|
1106
1235
|
)
|
|
1107
1236
|
|
|
1108
|
-
async def callback_handler()
|
|
1109
|
-
|
|
1237
|
+
async def callback_handler():
|
|
1238
|
+
code, state = await anyio.to_thread.run_sync(_prompt_oauth_callback)
|
|
1239
|
+
return _authorization_code_result(code, state)
|
|
1110
1240
|
else:
|
|
1111
1241
|
# Reset callback handler state
|
|
1112
1242
|
_CallbackHandler.auth_code = None
|
|
@@ -1129,7 +1259,7 @@ def build_oauth_provider(
|
|
|
1129
1259
|
print(f"If browser doesn't open, visit: {auth_url}", file=sys.stderr)
|
|
1130
1260
|
webbrowser.open(auth_url)
|
|
1131
1261
|
|
|
1132
|
-
async def callback_handler()
|
|
1262
|
+
async def callback_handler():
|
|
1133
1263
|
# Run the HTTP server in a thread, wait for the callback
|
|
1134
1264
|
thread = threading.Thread(target=server.handle_request, daemon=True)
|
|
1135
1265
|
thread.start()
|
|
@@ -1142,7 +1272,9 @@ def build_oauth_provider(
|
|
|
1142
1272
|
raise RuntimeError(f"OAuth error: {_CallbackHandler.error}")
|
|
1143
1273
|
if not _CallbackHandler.auth_code:
|
|
1144
1274
|
raise RuntimeError("No authorization code received")
|
|
1145
|
-
return (
|
|
1275
|
+
return _authorization_code_result(
|
|
1276
|
+
_CallbackHandler.auth_code, _CallbackHandler.state
|
|
1277
|
+
)
|
|
1146
1278
|
|
|
1147
1279
|
return _RobustOAuthClientProvider(
|
|
1148
1280
|
server_url=server_url,
|
|
@@ -2681,11 +2813,9 @@ def run_mcp_http(
|
|
|
2681
2813
|
headers = dict(auth_headers) if auth_headers else None
|
|
2682
2814
|
|
|
2683
2815
|
async def _with_streamable():
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
async with streamablehttp_client(
|
|
2816
|
+
async with _streamable_streams(
|
|
2687
2817
|
url, headers=headers, auth=oauth_provider
|
|
2688
|
-
) as (read, write
|
|
2818
|
+
) as (read, write):
|
|
2689
2819
|
async with ClientSession(read, write) as session:
|
|
2690
2820
|
await session.initialize()
|
|
2691
2821
|
return await _mcp_session(
|
|
@@ -2863,7 +2993,7 @@ async def _mcp_session(
|
|
|
2863
2993
|
{
|
|
2864
2994
|
"name": t.name,
|
|
2865
2995
|
"description": t.description or "",
|
|
2866
|
-
"inputSchema": t
|
|
2996
|
+
"inputSchema": _mcp_attr(t, "inputSchema") or {},
|
|
2867
2997
|
}
|
|
2868
2998
|
for t in all_tools
|
|
2869
2999
|
]
|
|
@@ -2895,8 +3025,9 @@ async def _mcp_session(
|
|
|
2895
3025
|
|
|
2896
3026
|
if json_output:
|
|
2897
3027
|
# Emit the full MCP CallToolResult envelope (content, structuredContent,
|
|
2898
|
-
# isError)
|
|
2899
|
-
|
|
3028
|
+
# isError) with the camelCase wire names, so the envelope does not
|
|
3029
|
+
# change shape with the installed SDK major.
|
|
3030
|
+
output_result(_mcp_dump(result), pretty=pretty, head=head, json_output=True)
|
|
2900
3031
|
return
|
|
2901
3032
|
|
|
2902
3033
|
text = _extract_content_parts(result.content)
|
|
@@ -2926,7 +3057,7 @@ async def _handle_resources(
|
|
|
2926
3057
|
"name": r.name,
|
|
2927
3058
|
"uri": str(r.uri),
|
|
2928
3059
|
"description": r.description or "",
|
|
2929
|
-
"mimeType": r
|
|
3060
|
+
"mimeType": _mcp_attr(r, "mimeType") or "",
|
|
2930
3061
|
}
|
|
2931
3062
|
for r in result.resources
|
|
2932
3063
|
]
|
|
@@ -2936,17 +3067,15 @@ async def _handle_resources(
|
|
|
2936
3067
|
data = [
|
|
2937
3068
|
{
|
|
2938
3069
|
"name": t.name,
|
|
2939
|
-
"uriTemplate": str(t
|
|
3070
|
+
"uriTemplate": str(_mcp_attr(t, "uriTemplate")),
|
|
2940
3071
|
"description": t.description or "",
|
|
2941
|
-
"mimeType": t
|
|
3072
|
+
"mimeType": _mcp_attr(t, "mimeType") or "",
|
|
2942
3073
|
}
|
|
2943
|
-
for t in result
|
|
3074
|
+
for t in _mcp_attr(result, "resourceTemplates")
|
|
2944
3075
|
]
|
|
2945
3076
|
output_result(data, **_out)
|
|
2946
3077
|
elif action == "read":
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
result = await session.read_resource(AnyUrl(uri))
|
|
3078
|
+
result = await session.read_resource(_resource_uri(uri))
|
|
2950
3079
|
parts = []
|
|
2951
3080
|
for content in result.contents:
|
|
2952
3081
|
if hasattr(content, "text"):
|
|
@@ -3001,7 +3130,7 @@ async def _handle_prompts(
|
|
|
3001
3130
|
messages.append({"role": msg.role, "content": content.text})
|
|
3002
3131
|
else:
|
|
3003
3132
|
messages.append(
|
|
3004
|
-
{"role": msg.role, "content": json.dumps(content
|
|
3133
|
+
{"role": msg.role, "content": json.dumps(_mcp_dump(content))}
|
|
3005
3134
|
)
|
|
3006
3135
|
data = {"description": result.description or "", "messages": messages}
|
|
3007
3136
|
output_result(data, **_out)
|
|
@@ -3170,9 +3299,9 @@ async def _list_all_tools(session):
|
|
|
3170
3299
|
tools = []
|
|
3171
3300
|
cursor = None
|
|
3172
3301
|
while True:
|
|
3173
|
-
result = await session
|
|
3302
|
+
result = await _list_tools_page(session, cursor)
|
|
3174
3303
|
tools.extend(result.tools)
|
|
3175
|
-
cursor = result
|
|
3304
|
+
cursor = _mcp_attr(result, "nextCursor")
|
|
3176
3305
|
if not cursor:
|
|
3177
3306
|
return tools
|
|
3178
3307
|
|
|
@@ -3180,7 +3309,11 @@ async def _list_all_tools(session):
|
|
|
3180
3309
|
async def _dispatch_list_tools(session, params):
|
|
3181
3310
|
tools = await _list_all_tools(session)
|
|
3182
3311
|
return [
|
|
3183
|
-
{
|
|
3312
|
+
{
|
|
3313
|
+
"name": t.name,
|
|
3314
|
+
"description": t.description or "",
|
|
3315
|
+
"inputSchema": _mcp_attr(t, "inputSchema") or {},
|
|
3316
|
+
}
|
|
3184
3317
|
for t in tools
|
|
3185
3318
|
]
|
|
3186
3319
|
|
|
@@ -3193,23 +3326,31 @@ async def _dispatch_call_tool(session, params):
|
|
|
3193
3326
|
async def _dispatch_list_resources(session, params):
|
|
3194
3327
|
result = await session.list_resources()
|
|
3195
3328
|
return [
|
|
3196
|
-
{
|
|
3329
|
+
{
|
|
3330
|
+
"name": r.name,
|
|
3331
|
+
"uri": str(r.uri),
|
|
3332
|
+
"description": r.description or "",
|
|
3333
|
+
"mimeType": _mcp_attr(r, "mimeType") or "",
|
|
3334
|
+
}
|
|
3197
3335
|
for r in result.resources
|
|
3198
3336
|
]
|
|
3199
3337
|
|
|
3200
3338
|
|
|
3201
3339
|
async def _dispatch_read_resource(session, params):
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
result = await session.read_resource(AnyUrl(params["uri"]))
|
|
3340
|
+
result = await session.read_resource(_resource_uri(params["uri"]))
|
|
3205
3341
|
return _extract_content_parts(result.contents, attrs=("text", "blob"))
|
|
3206
3342
|
|
|
3207
3343
|
|
|
3208
3344
|
async def _dispatch_list_resource_templates(session, params):
|
|
3209
3345
|
result = await session.list_resource_templates()
|
|
3210
3346
|
return [
|
|
3211
|
-
{
|
|
3212
|
-
|
|
3347
|
+
{
|
|
3348
|
+
"name": t.name,
|
|
3349
|
+
"uriTemplate": str(_mcp_attr(t, "uriTemplate")),
|
|
3350
|
+
"description": t.description or "",
|
|
3351
|
+
"mimeType": _mcp_attr(t, "mimeType") or "",
|
|
3352
|
+
}
|
|
3353
|
+
for t in _mcp_attr(result, "resourceTemplates")
|
|
3213
3354
|
]
|
|
3214
3355
|
|
|
3215
3356
|
|
|
@@ -3236,7 +3377,9 @@ async def _dispatch_get_prompt(session, params):
|
|
|
3236
3377
|
if hasattr(content, "text"):
|
|
3237
3378
|
messages.append({"role": msg.role, "content": content.text})
|
|
3238
3379
|
else:
|
|
3239
|
-
messages.append(
|
|
3380
|
+
messages.append(
|
|
3381
|
+
{"role": msg.role, "content": json.dumps(_mcp_dump(content))}
|
|
3382
|
+
)
|
|
3240
3383
|
return {"description": result.description or "", "messages": messages}
|
|
3241
3384
|
|
|
3242
3385
|
|
|
@@ -3386,12 +3529,9 @@ def _run_session_daemon(config_json: str):
|
|
|
3386
3529
|
headers = dict(auth_headers) if auth_headers else None
|
|
3387
3530
|
|
|
3388
3531
|
async def _via_streamable():
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
async with streamablehttp_client(source, headers=headers) as (
|
|
3532
|
+
async with _streamable_streams(source, headers=headers) as (
|
|
3392
3533
|
read,
|
|
3393
3534
|
write,
|
|
3394
|
-
_,
|
|
3395
3535
|
):
|
|
3396
3536
|
async with ClientSession(read, write) as session:
|
|
3397
3537
|
await _run_with_session(session)
|
|
@@ -3668,7 +3808,7 @@ def _fetch_mcp_tools(
|
|
|
3668
3808
|
{
|
|
3669
3809
|
"name": t.name,
|
|
3670
3810
|
"description": t.description or "",
|
|
3671
|
-
"inputSchema": t
|
|
3811
|
+
"inputSchema": _mcp_attr(t, "inputSchema") or {},
|
|
3672
3812
|
}
|
|
3673
3813
|
for t in all_tools
|
|
3674
3814
|
)
|
|
@@ -3693,11 +3833,9 @@ def _fetch_mcp_tools(
|
|
|
3693
3833
|
headers = dict(auth_headers) if auth_headers else None
|
|
3694
3834
|
|
|
3695
3835
|
async def _via_streamable():
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
async with streamablehttp_client(
|
|
3836
|
+
async with _streamable_streams(
|
|
3699
3837
|
source, headers=headers, auth=oauth_provider
|
|
3700
|
-
) as (read, write
|
|
3838
|
+
) as (read, write):
|
|
3701
3839
|
async with ClientSession(read, write) as session:
|
|
3702
3840
|
await session.initialize()
|
|
3703
3841
|
await _extract_tools(session)
|
|
File without changes
|
|
File without changes
|