From c26b7f248ef84e4ebc56077f3405122621c396d3 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 24 Sep 2026 07:21:27 -0400 Subject: [PATCH] =?UTF-8?q?feat(mcp):=20port=20the=20server=20to=20MCP=20P?= =?UTF-8?q?ython=20SDK=20v2=20(FastMCP=20=E2=86=92=20MCPServer)=20(#2196)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - server.py: `mcp.server.mcpserver.MCPServer`; StrictArgsFastMCP becomes StrictArgsMCPServer, whose call_tool takes and forwards v2's `context` and raises ToolError (the SDK logs anything else as an unexpected crash; the message reaches the caller either way). - stateless_http and transport_security moved from the constructor to `streamable_http_app(...)` in mount_mcp, with their reasons. - Per-request user identity is unchanged: the contextvar set around the ASGI call reaches the handler on both v2 paths (legacy stateless spawns from the request task; the 2026-07-28 modern path opens its task group inside the request). - pyproject: mcp[cli]>=2.2, no ceiling (installs are --locked). uv.lock regenerated with --upgrade-package mcp in the ci-python image: only mcp and its own dependencies moved (106 → 110 packages). Co-Authored-By: Claude Opus 5.5 --- pyproject.toml | 11 ++-- src/scribe/mcp/_context.py | 2 +- src/scribe/mcp/server.py | 91 ++++++++++++++------------- src/scribe/mcp/tools/__init__.py | 4 +- src/scribe/mcp/tools/tasks.py | 2 +- tests/helpers.py | 2 +- tests/test_mcp_endpoint.py | 12 ++-- tests/test_mcp_strict_args.py | 19 +++--- uv.lock | 105 ++++++++++++++++++++++--------- 9 files changed, 153 insertions(+), 95 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 02da167..ccb3309 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,11 +19,12 @@ dependencies = [ "caldav>=1.3", "icalendar>=5.0", "APScheduler>=3.10,<4.0", - # Capped below 2.0: that release removed `mcp.server.fastmcp`, which - # src/scribe/mcp/server.py imports to build the whole tool surface. The - # ceiling is a real incompatibility, not caution — lift it in the same - # change that ports server.py to the 2.x API. - "mcp[cli]>=1.0,<2", + # Floor 2.2: server.py uses the 2.x `mcp.server.mcpserver.MCPServer` API + # (transport settings on `streamable_http_app`, `call_tool(..., context)`, + # `ToolError`), ported and read against 2.2.0 (#2196). No ceiling: CI + # installs with `uv sync --locked`, so a new major arrives only through a + # deliberate `uv lock`, which is where its breakage should be diagnosed. + "mcp[cli]>=2.2", "fastembed>=0.4", "pgvector>=0.3", ] diff --git a/src/scribe/mcp/_context.py b/src/scribe/mcp/_context.py index d503800..a0eb84c 100644 --- a/src/scribe/mcp/_context.py +++ b/src/scribe/mcp/_context.py @@ -1,7 +1,7 @@ """Per-request MCP context. The ASGI middleware (mcp/server.py) populates `_user_id_ctx` from -scope['scribe_user_id'] before dispatching to FastMCP tool handlers. +scope['scribe_user_id'] before dispatching to MCPServer tool handlers. Tool functions then call `current_user_id()` to retrieve it. Tools must never fall back to a default user — `current_user_id()` raises diff --git a/src/scribe/mcp/server.py b/src/scribe/mcp/server.py index 6a07d7c..5ffca8b 100644 --- a/src/scribe/mcp/server.py +++ b/src/scribe/mcp/server.py @@ -1,9 +1,10 @@ -"""FastMCP instance + Quart mount-point. Tools are registered in mcp/tools/.""" +"""MCPServer instance + Quart mount-point. Tools are registered in mcp/tools/.""" from __future__ import annotations import difflib -from mcp.server.fastmcp import FastMCP +from mcp.server.mcpserver import MCPServer +from mcp.server.mcpserver.exceptions import ToolError from mcp.server.transport_security import TransportSecuritySettings from quart import Quart @@ -256,10 +257,10 @@ def _body_calls_write_tool(body: bytes) -> bool: return False -class StrictArgsFastMCP(FastMCP): - """A FastMCP that REJECTS tool calls carrying undeclared arguments. +class StrictArgsMCPServer(MCPServer): + """An MCPServer that REJECTS tool calls carrying undeclared arguments. - FastMCP validates arguments with a pydantic model built from the tool + The SDK validates arguments with a pydantic model built from the tool signature, and pydantic's default extra-field policy is "ignore" — so a misnamed argument simply vanishes and the tool runs with that field's default. On the create/update tools the default is "", which turns a @@ -274,7 +275,10 @@ class StrictArgsFastMCP(FastMCP): declared cannot have been meant to be dropped. """ - async def call_tool(self, name, arguments): + # ToolError, not ValueError: the SDK returns either one's text to the + # caller as an error result, but logs anything that is not a ToolError as + # an unexpected crash. A misnamed argument is the caller's mistake. + async def call_tool(self, name, arguments, context=None): try: tool = self._tool_manager.get_tool(name) except Exception: @@ -288,56 +292,35 @@ class StrictArgsFastMCP(FastMCP): close = difflib.get_close_matches(arg, sorted(declared), n=1) suggestion = f" (did you mean '{close[0]}'?)" if close else "" hints.append(f"'{arg}'{suggestion}") - raise ValueError( + raise ToolError( f"{name} does not accept argument(s) {', '.join(hints)}. " f"It accepts: {', '.join(sorted(declared))}. Nothing was " "created or changed — retry with the declared names." ) - return await super().call_tool(name, arguments) + return await super().call_tool(name, arguments, context) -def build_mcp_server() -> FastMCP: - """Build the FastMCP instance with all tools registered. +def build_mcp_server() -> MCPServer: + """Build the MCPServer instance with all tools registered. - DNS-rebinding protection is disabled: FastMCP's default allow-list is - just localhost variants, which means any deployment behind a reverse - proxy (Traefik with a hostname like devassistant.traefik.internal, - Cloudflare, nginx, etc.) gets 421 Misdirected Request. The threat - model that protection addresses — a malicious browser page rebinding - DNS to hit a localhost MCP — doesn't apply here: this is HTTP transport - behind a reverse proxy with bearer-token auth as the real security - boundary. + Transport settings (statelessness, DNS-rebinding protection) are not + here: since SDK v2 they belong to the ASGI app, built in `mount_mcp`. """ - # stateless_http=True: don't hand the client a persistent Mcp-Session-Id. - # The stateful default strands Claude Code after a container redeploy — - # it reconnects with the now-unknown session id, the server returns 404, - # and the client won't re-initialize on a 404 (Claude Code issue #60949), - # so the connection stays dead until a manual /mcp retry. Stateless makes - # every request self-contained (bearer-auth only), so a post-deploy - # reconnect just works. Trade-off: no server-pushed list_changed stream, - # which we don't use — tools are re-fetched on reconnect anyway. - mcp = StrictArgsFastMCP( - "scribe", - instructions=_INSTRUCTIONS.strip(), - stateless_http=True, - transport_security=TransportSecuritySettings( - enable_dns_rebinding_protection=False, - ), - ) + mcp = StrictArgsMCPServer("scribe", instructions=_INSTRUCTIONS.strip()) from scribe.mcp.tools import register_all register_all(mcp) return mcp def mount_mcp(app: Quart) -> None: - """Mount the FastMCP streamable-HTTP ASGI sub-app at /mcp on the Quart app. + """Mount the MCPServer streamable-HTTP ASGI sub-app at /mcp on the Quart app. - A small ASGI middleware between Quart and the FastMCP sub-app validates the + A small ASGI middleware between Quart and the MCPServer sub-app validates the Bearer token against the api_keys table. Authenticated requests have their user_id attached to the ASGI scope under "scribe_user_id" for tool handlers to read. - FastMCP's streamable_http session manager owns a task group that must be + The SDK's streamable_http session manager owns a task group that must be running before it can serve requests. In a stand-alone Starlette deployment that would happen via the Starlette `lifespan` parameter. Since we're hosted inside Quart, we hook the session manager's `run()` async context manager @@ -346,7 +329,31 @@ def mount_mcp(app: Quart) -> None: from scribe.mcp.auth import resolve_bearer mcp = build_mcp_server() - mcp_asgi = mcp.streamable_http_app() + # stateless_http=True: don't hand the client a persistent Mcp-Session-Id. + # The stateful default strands Claude Code after a container redeploy — + # it reconnects with the now-unknown session id, the server returns 404, + # and the client won't re-initialize on a 404 (Claude Code issue #60949), + # so the connection stays dead until a manual /mcp retry. Stateless makes + # every request self-contained (bearer-auth only), so a post-deploy + # reconnect just works. Trade-off: no server-pushed list_changed stream, + # which we don't use — tools are re-fetched on reconnect anyway. (Clients + # on the 2026-07-28 revision are stateless by protocol and take the SDK's + # modern path regardless; this governs the 2025-era handshake path.) + # + # DNS-rebinding protection is disabled: the SDK's default allow-list is + # just localhost variants, which means any deployment behind a reverse + # proxy (Traefik with a hostname like devassistant.traefik.internal, + # Cloudflare, nginx, etc.) gets 421 Misdirected Request. The threat + # model that protection addresses — a malicious browser page rebinding + # DNS to hit a localhost MCP — doesn't apply here: this is HTTP transport + # behind a reverse proxy with bearer-token auth as the real security + # boundary. + mcp_asgi = mcp.streamable_http_app( + stateless_http=True, + transport_security=TransportSecuritySettings( + enable_dns_rebinding_protection=False, + ), + ) app.mcp_instance = mcp @app.before_serving @@ -416,11 +423,11 @@ def mount_mcp(app: Quart) -> None: if scope["type"] == "http": path = scope.get("path", "") if path == "/mcp" or path.startswith("/mcp/"): - # Don't rewrite the path: FastMCP's streamable_http_app mounts + # Don't rewrite the path: the SDK's streamable_http_app mounts # its handler at /mcp by default. If we strip the prefix to "/", - # FastMCP's internal routing returns 404 because there's no - # handler at "/" — only at "/mcp". Pass the scope through - # untouched and let FastMCP's own routing match. + # its internal routing returns 404 because there's no handler + # at "/" — only at "/mcp". Pass the scope through untouched and + # let the SDK's own routing match. return await auth_wrapped(scope, receive, send) return await original_asgi(scope, receive, send) diff --git a/src/scribe/mcp/tools/__init__.py b/src/scribe/mcp/tools/__init__.py index e892aef..59aeea1 100644 --- a/src/scribe/mcp/tools/__init__.py +++ b/src/scribe/mcp/tools/__init__.py @@ -1,7 +1,7 @@ """MCP tool implementations. Each tool module exposes a `register(mcp)` function that attaches its tools -to a FastMCP instance. `register_all(mcp)` is the single entry point called +to an MCPServer instance. `register_all(mcp)` is the single entry point called from `mcp.server.build_mcp_server`. """ from scribe.mcp.tools import ( @@ -13,7 +13,7 @@ from scribe.mcp.tools import ( def register_all(mcp) -> None: - """Register every tool module's tools on the given FastMCP instance.""" + """Register every tool module's tools on the given MCPServer instance.""" search.register(mcp) retrieval_tuning.register(mcp) wide_net.register(mcp) diff --git a/src/scribe/mcp/tools/tasks.py b/src/scribe/mcp/tools/tasks.py index 98dd9ee..daaf9ff 100644 --- a/src/scribe/mcp/tools/tasks.py +++ b/src/scribe/mcp/tools/tasks.py @@ -537,7 +537,7 @@ _ITEM_KEYS = {"title", "body", "type", "status", "priority", "kind", "tags", "sy def _batch_items(records: list[dict], *, what: str = "record") -> list[batch_svc.BatchItem]: """Parse the door's plain dicts into BatchItems, refusing unknown keys. - Strict for the same reason StrictArgsFastMCP is (#2709): a misspelt key + Strict for the same reason StrictArgsMCPServer is (#2709): a misspelt key silently dropped — `milestone` for a per-record milestone, `desc` for body — creates a record that looks right and is missing what the caller sent. """ diff --git a/tests/helpers.py b/tests/helpers.py index 88a964d..f844fe7 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -321,7 +321,7 @@ def plain_rule_detail(): class FakeMCP: - """Stand-in for the FastMCP server a tool module's ``register(mcp)`` is + """Stand-in for the MCPServer a tool module's ``register(mcp)`` is handed: records the ``name=`` of every ``@mcp.tool(...)`` registration in ``names`` and leaves the function untouched, so a test can assert which tools a module exposes.""" diff --git a/tests/test_mcp_endpoint.py b/tests/test_mcp_endpoint.py index 42b9293..dc99de8 100644 --- a/tests/test_mcp_endpoint.py +++ b/tests/test_mcp_endpoint.py @@ -3,7 +3,7 @@ These exercise the ASGI dispatch + auth middleware by driving the app's ASGI callable directly. We avoid Quart's test_client() because it expects Quart's request pipeline to set `app._preserved_context` as a side effect, but our -ASGI middleware forwards /mcp requests to FastMCP without touching the Quart +ASGI middleware forwards /mcp requests to MCPServer without touching the Quart pipeline (correct production behavior), which causes test_client to fail on teardown. @@ -85,12 +85,12 @@ async def test_mcp_endpoint_invalid_token_returns_401(): @pytest.mark.asyncio async def test_mcp_endpoint_valid_token_passes_auth(): - """With a valid Bearer, the request must successfully reach FastMCP's + """With a valid Bearer, the request must successfully reach MCPServer's initialize handler. Asserting `!= 401` is too weak: it lets a 404 from - a path-mismatch (the original bug) through. FastMCP responds 200 to a + a path-mismatch (the original bug) through. MCPServer responds 200 to a well-formed initialize handshake. - FastMCP's session manager normally starts via Quart's @before_serving + MCPServer's session manager normally starts via Quart's @before_serving hook in production. This raw-ASGI test doesn't go through Quart's serving lifecycle, so we manually enter the session manager.""" fake_key = MagicMock() @@ -113,7 +113,7 @@ async def test_mcp_endpoint_valid_token_passes_auth(): status, _ = await _send_request( app, "POST", "/mcp", headers={ - # FastMCP's transport_security module enforces a Host + # MCPServer's transport_security module enforces a Host # header (DNS-rebinding protection); without it the # request gets a 421 Misdirected Request. "Host": "testserver", @@ -123,7 +123,7 @@ async def test_mcp_endpoint_valid_token_passes_auth(): }, body=initialize_body, ) - assert status == 200, f"expected 200 from FastMCP initialize, got {status}" + assert status == 200, f"expected 200 from MCPServer initialize, got {status}" # Note: there's no explicit "non-/mcp paths bypass the middleware" test here diff --git a/tests/test_mcp_strict_args.py b/tests/test_mcp_strict_args.py index 3333f9f..f68e86c 100644 --- a/tests/test_mcp_strict_args.py +++ b/tests/test_mcp_strict_args.py @@ -1,23 +1,24 @@ """Unknown tool arguments are rejected, never silently dropped (#2709). -The failure this pins: FastMCP validates tool arguments with a pydantic model +The failure this pins: MCPServer validates tool arguments with a pydantic model whose extra-field policy is "ignore", so `create_note(content=...)` — a plausible near-miss for `body=`, primed by add_task_log's `content` — ran successfully, stored `body: ""`, and left a record embedding/search cannot see. The call REPORTED SUCCESS. Two real notes were persisted body-less before the pattern was noticed. -The fix is at the dispatch seam, not per-tool: StrictArgsFastMCP rejects any +The fix is at the dispatch seam, not per-tool: StrictArgsMCPServer rejects any call carrying arguments the tool does not declare, with a did-you-mean when one is close. An error the caller sees once beats data half-written forever. """ import pytest +from mcp.server.mcpserver.exceptions import ToolError -from scribe.mcp.server import StrictArgsFastMCP +from scribe.mcp.server import StrictArgsMCPServer -def _echo_server() -> StrictArgsFastMCP: - mcp = StrictArgsFastMCP("strict-test") +def _echo_server() -> StrictArgsMCPServer: + mcp = StrictArgsMCPServer("strict-test") @mcp.tool() def echo(text: str = "") -> str: @@ -36,7 +37,7 @@ async def test_unknown_argument_is_an_error_not_a_silent_drop(): """The load-bearing property: the call must FAIL, because succeeding is what turned a typo into data loss.""" mcp = _echo_server() - with pytest.raises(ValueError) as exc: + with pytest.raises(ToolError) as exc: await mcp.call_tool("echo", {"txt": "hi"}) msg = str(exc.value) assert "'txt'" in msg @@ -51,7 +52,7 @@ async def test_empty_arguments_pass(): async def test_unknown_tool_keeps_the_upstream_error(): """The gate must not swallow or reshape 'no such tool' — that error path - belongs to FastMCP and clients already understand it.""" + belongs to MCPServer and clients already understand it.""" mcp = _echo_server() with pytest.raises(Exception) as exc: await mcp.call_tool("no_such_tool", {"text": "hi"}) @@ -65,8 +66,8 @@ async def test_the_original_regression_create_note_with_content(): from scribe.mcp.server import build_mcp_server mcp = build_mcp_server() - assert isinstance(mcp, StrictArgsFastMCP) # the guard is actually mounted - with pytest.raises(ValueError) as exc: + assert isinstance(mcp, StrictArgsMCPServer) # the guard is actually mounted + with pytest.raises(ToolError) as exc: await mcp.call_tool( "create_note", {"title": "t", "content": "the body text"} ) diff --git a/uv.lock b/uv.lock index 67bed32..a28edb6 100644 --- a/uv.lock +++ b/uv.lock @@ -513,6 +513,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] +[[package]] +name = "httpcore2" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/f3/1db7aa2bc2524062192bb0e0323969492d1883152a232fe36eea65f4e35c/httpcore2-2.13.1.tar.gz", hash = "sha256:e0aa977abe17e69a3b820a24542a6fa88702676d83880b8d194dcd18408e5103", size = 68071, upload-time = "2026-09-23T07:47:22.372Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/ba/a4568248771ce81957bfb7cc600264a40fbcda092391ee1c415c50be4bea/httpcore2-2.13.1-py3-none-any.whl", hash = "sha256:e1e05d4f25f7d7d496bfb96748f6f4b67657b03da069b3a68c36069f3db73d0a", size = 83423, upload-time = "2026-09-23T07:47:19.365Z" }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -529,12 +542,28 @@ wheels = [ ] [[package]] -name = "httpx-sse" -version = "0.4.3" +name = "httpx2" +version = "2.13.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +dependencies = [ + { name = "anyio", marker = "sys_platform != 'emscripten'" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, + { name = "httpx2-jsfetch", marker = "sys_platform == 'emscripten'" }, + { name = "idna" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/44/474bef2a0e9d90f1715d32cb98b0738695ca17ba324095fb2497ed7fbd59/httpx2-2.13.1.tar.gz", hash = "sha256:e48744a19e3af5ee48313d0ce5fe941d5422fae5705ea922a4aabf94d7800dfa", size = 100405, upload-time = "2026-09-23T07:47:23.052Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, + { url = "https://files.pythonhosted.org/packages/d8/9c/6fe8931fd9f381042a9e4c7d5a7b4cbf7016b252bec0c99a49fce42c3326/httpx2-2.13.1-py3-none-any.whl", hash = "sha256:6dff50fabc270ee5fd25d845d0b078ed20564579744d6d962850975996d2f9a4", size = 95597, upload-time = "2026-09-23T07:47:20.995Z" }, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, ] [[package]] @@ -609,11 +638,11 @@ wheels = [ [[package]] name = "idna" -version = "3.11" +version = "3.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, ] [[package]] @@ -825,15 +854,15 @@ wheels = [ [[package]] name = "mcp" -version = "1.27.2" +version = "2.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, + { name = "httpx2" }, { name = "jsonschema" }, + { name = "mcp-types" }, + { name = "opentelemetry-api" }, { name = "pydantic" }, - { name = "pydantic-settings" }, { name = "pyjwt", extra = ["crypto"] }, { name = "python-multipart" }, { name = "pywin32", marker = "sys_platform == 'win32'" }, @@ -843,9 +872,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/3c/347cf965d313f5d41764e7d46bea6ffe7d9ef13b983cc429b0340962a082/mcp-1.27.2.tar.gz", hash = "sha256:8e02db104096d1c25b28e64bde29a5c32b31bc241710213e12fd4d84985bdfef", size = 621116, upload-time = "2026-05-29T17:16:04.039Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/31/ac54fb0fdd5b37de704486e288bba4fbbb463f24cfcfedbede407b854513/mcp-2.2.0.tar.gz", hash = "sha256:2dc37ecb1974becdcebdbf7561e7c15a07dbbf20ba21ba16c3593b3038b3afbd", size = 4084129, upload-time = "2026-09-07T16:06:23.439Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/11/252c6f971dc4f16af1d98a1c469d8ba523aab00d1bb76b4d3bc1ff32eacc/mcp-1.27.2-py3-none-any.whl", hash = "sha256:d6ff5160c6ca65d93013626efb3fc249de683c30b2d8570755ceddd490344de5", size = 220498, upload-time = "2026-05-29T17:16:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ff/8e7eade68b8a28f7da0ed1085544341b51f9c935dbf6b95c76b7edfea6a0/mcp-2.2.0-py3-none-any.whl", hash = "sha256:bde982589473a060ae145e3406e9a5333fe538c97229ba841f5a7f92be004f81", size = 365656, upload-time = "2026-09-07T16:06:19.711Z" }, ] [package.optional-dependencies] @@ -854,6 +883,19 @@ cli = [ { name = "typer" }, ] +[[package]] +name = "mcp-types" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/91/762d7755d971aff8a28d75f7961656148edf27875c8026e6385aaab08ae7/mcp_types-2.2.0.tar.gz", hash = "sha256:d3ed53703ddd10d9c6399f29d322bb66f3f67ab41348ac8556ba23e07fedefad", size = 65892, upload-time = "2026-09-07T16:06:25.187Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/6ffba5d8cd5dd9b8a19478875c50e04945314ba5074e84d749283f27f62d/mcp_types-2.2.0-py3-none-any.whl", hash = "sha256:ea476b73ee86709ab5abc9452385ed36cc05907e582355622e294595c9a04f13", size = 69106, upload-time = "2026-09-07T16:06:21.461Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -981,6 +1023,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6c/1d/1666dc64e78d8587d168fec4e3b7922b92eb286a2ddeebcf6acb55c7dc82/onnxruntime-1.24.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1cc6a518255f012134bc791975a6294806be9a3b20c4a54cca25194c90cf731", size = 17247021, upload-time = "2026-03-17T22:04:52.377Z" }, ] +[[package]] +name = "opentelemetry-api" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, +] + [[package]] name = "packaging" version = "26.0" @@ -1146,20 +1200,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, ] -[[package]] -name = "pydantic-settings" -version = "2.14.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -1497,7 +1537,7 @@ requires-dist = [ { name = "httpx", specifier = ">=0.27" }, { name = "hypercorn", specifier = ">=0.17" }, { name = "icalendar", specifier = ">=5.0" }, - { name = "mcp", extras = ["cli"], specifier = ">=1.0,<2" }, + { name = "mcp", extras = ["cli"], specifier = ">=2.2" }, { name = "pgvector", specifier = ">=0.3" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23" }, @@ -1631,6 +1671,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "typer" version = "0.24.1"