feat(mcp): port the server to MCP Python SDK v2 (FastMCP → MCPServer) (#2196)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m37s
CI & Build / Build & push image (push) Successful in 31s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m37s
CI & Build / Build & push image (push) Successful in 31s
- 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 <noreply@anthropic.com>
This commit is contained in:
+6
-5
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
+49
-42
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
+1
-1
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user