MCP: mcp 2.3 (MCPServer, transport arguments on streamable_http_app, bearer read in middleware); tests for the 2026-07-28 revision and the resource check
This commit is contained in:
@@ -20,7 +20,7 @@ from collections.abc import AsyncIterator, Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from mcp.types import TextContent
|
||||
from mcp.types import CallToolResult, TextContent
|
||||
|
||||
from fluksio.ai import client
|
||||
from fluksio.ai.config import AIConfig
|
||||
@@ -68,7 +68,7 @@ async def tool_specs() -> list[dict[str, Any]]:
|
||||
"function": {
|
||||
"name": tool.name,
|
||||
"description": tool.description or "",
|
||||
"parameters": tool.inputSchema,
|
||||
"parameters": tool.input_schema,
|
||||
},
|
||||
}
|
||||
for tool in await mcp.list_tools()
|
||||
@@ -108,10 +108,9 @@ async def _call_tool(name: str, arguments: dict[str, Any]) -> str:
|
||||
result = await mcp.call_tool(name, arguments)
|
||||
except Exception as exc: # noqa: BLE001 - an unknown tool, or refused arguments
|
||||
return json.dumps({"error": str(exc)})
|
||||
if isinstance(result, dict):
|
||||
text = json.dumps(result)
|
||||
else:
|
||||
text = "".join(c.text for c in result if isinstance(c, TextContent))
|
||||
if not isinstance(result, CallToolResult):
|
||||
return json.dumps({"error": "The tool asked for more input than a chat gives."})
|
||||
text = "".join(c.text for c in result.content if isinstance(c, TextContent))
|
||||
if len(text) > _RESULT_LIMIT:
|
||||
text = text[:_RESULT_LIMIT] + "… (truncated)"
|
||||
return text
|
||||
|
||||
@@ -167,18 +167,21 @@ class _ThroughThePortal:
|
||||
|
||||
def build_http_app(app: object) -> ASGIApp:
|
||||
"""The MCP sub-app, wired to this app's API and its OAuth server."""
|
||||
server.mcp.settings.stateless_http = True
|
||||
server.mcp.settings.json_response = True
|
||||
server.mcp.settings.auth = AuthSettings(
|
||||
issuer_url=AnyHttpUrl(settings.oauth_issuer),
|
||||
resource_server_url=AnyHttpUrl(settings.mcp_resource),
|
||||
required_scopes=None,
|
||||
validate_token_resource=True,
|
||||
)
|
||||
server.mcp.settings.transport_security = _transport_security()
|
||||
# The SDK only takes a verifier through its constructor, and the instance is
|
||||
# module-level so the tools can be declared at import time.
|
||||
server.mcp._token_verifier = _JWTVerifier() # noqa: SLF001
|
||||
|
||||
server.connect(app)
|
||||
return _ThroughThePortal(server.mcp.streamable_http_app())
|
||||
return _ThroughThePortal(
|
||||
server.mcp.streamable_http_app(
|
||||
stateless_http=True,
|
||||
json_response=True,
|
||||
transport_security=_transport_security(),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -13,24 +13,46 @@ identity rather than some service account.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
from contextvars import ContextVar
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from mcp.server.lowlevel.server import request_ctx
|
||||
from mcp.server.context import CallNext, HandlerResult, ServerRequestContext
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
from fluksio.sdk.client import seg as _seg
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# FastMCP's own settings model leaves `lifespan` a forward reference, which
|
||||
# pydantic-settings warns about on every start. Nothing reads that field from
|
||||
# the environment, so the warning says nothing about this app.
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", message="Field 'lifespan' has an incomplete")
|
||||
mcp = FastMCP("fluksio")
|
||||
#: The ``Authorization`` value tool calls carry onto the REST API. The endpoint's
|
||||
#: middleware sets it from each MCP request; the assistant sets it to the token of
|
||||
#: the person chatting, for the length of their turn, since no MCP request is
|
||||
#: involved there.
|
||||
caller_authorization: ContextVar[str | None] = ContextVar(
|
||||
"caller_authorization", default=None
|
||||
)
|
||||
|
||||
|
||||
async def _carry_authorization(
|
||||
ctx: ServerRequestContext[Any, Any], call_next: CallNext
|
||||
) -> HandlerResult:
|
||||
"""Make the bearer token of an MCP request the caller's for its tool calls.
|
||||
|
||||
The transport attaches the originating HTTP request to every JSON-RPC
|
||||
message, so the token is read per message rather than per connection.
|
||||
"""
|
||||
headers = getattr(ctx.request, "headers", None)
|
||||
authorization = headers.get("authorization") if headers else None
|
||||
if not authorization:
|
||||
return await call_next(ctx)
|
||||
token = caller_authorization.set(authorization)
|
||||
try:
|
||||
return await call_next(ctx)
|
||||
finally:
|
||||
caller_authorization.reset(token)
|
||||
|
||||
|
||||
mcp = MCPServer("fluksio", middleware=[_carry_authorization])
|
||||
|
||||
#: The tool calls never leave the process, so the host is a label, not a route.
|
||||
_INTERNAL_BASE = "http://fluksio-mcp.internal"
|
||||
@@ -39,13 +61,6 @@ _INTERNAL_BASE = "http://fluksio-mcp.internal"
|
||||
#: hop. Built once by :func:`connect`, for the endpoint and the assistant alike.
|
||||
_client: httpx.AsyncClient | None = None
|
||||
|
||||
#: The ``Authorization`` value for tool calls made from inside this process,
|
||||
#: where no MCP request carries one: the assistant sets it to the token of the
|
||||
#: person chatting, for the length of their turn.
|
||||
caller_authorization: ContextVar[str | None] = ContextVar(
|
||||
"caller_authorization", default=None
|
||||
)
|
||||
|
||||
|
||||
def set_client(client: httpx.AsyncClient | None) -> None:
|
||||
global _client
|
||||
@@ -77,18 +92,10 @@ async def aclose() -> None:
|
||||
async def forward_caller_auth(request: httpx.Request) -> None:
|
||||
"""Carry the caller's bearer token onto the REST call.
|
||||
|
||||
Over the endpoint, the transport attaches the originating HTTP request to
|
||||
every JSON-RPC message, and a tool handler runs inside that request's
|
||||
context, so the token is read per message rather than per connection. A
|
||||
call from inside the process has no such request and uses what
|
||||
:data:`caller_authorization` holds.
|
||||
A tool handler runs inside the context :data:`caller_authorization` was set
|
||||
in, by the endpoint's middleware or by the assistant.
|
||||
"""
|
||||
authorization = caller_authorization.get()
|
||||
try:
|
||||
source = request_ctx.get().request
|
||||
except LookupError:
|
||||
source = None
|
||||
authorization = getattr(source, "headers", {}).get("authorization") or authorization
|
||||
if authorization:
|
||||
request.headers["authorization"] = authorization
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ dependencies = [
|
||||
# The `inject` node's cron. Imported where it is used, behind a message
|
||||
# that cannot fire while this line is here.
|
||||
"croniter>=1.3.0",
|
||||
"mcp>=1.30,<2",
|
||||
"mcp>=2.3,<3",
|
||||
"fluksio-worker>=0.1,<0.2",
|
||||
# The Modules screen installs node code's packages with it, into a venv of
|
||||
# the user's own. Present in the image; a pip install would otherwise have
|
||||
|
||||
@@ -51,9 +51,6 @@ def over_mcp(monkeypatch: pytest.MonkeyPatch):
|
||||
from fluksio.mcp import http as mcp_http
|
||||
from fluksio.mcp import server as mcp_server
|
||||
|
||||
# A FastMCP instance enters its session manager once, and another test in
|
||||
# the run may have built one already.
|
||||
monkeypatch.setattr(mcp_server.mcp, "_session_manager", None, raising=False)
|
||||
mcp_app = mcp_http.build_http_app(app)
|
||||
|
||||
def run(block: Callable[[httpx.AsyncClient], Awaitable[Any]]) -> Any:
|
||||
@@ -194,6 +191,34 @@ def test_an_agent_can_list_and_call_tools(
|
||||
assert called.json()["result"]["isError"] is False
|
||||
|
||||
|
||||
def test_a_token_for_another_resource_is_refused(
|
||||
over_mcp,
|
||||
client: TestClient,
|
||||
superuser_token_headers: dict[str, str],
|
||||
agent: uuid.UUID,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""The SDK compares the resource the verifier reports with its own URL."""
|
||||
from fluksio.mcp import http as mcp_http
|
||||
|
||||
verify = mcp_http._JWTVerifier.verify_token
|
||||
|
||||
async def for_elsewhere(self: Any, token: str) -> Any:
|
||||
accepted = await verify(self, token)
|
||||
return accepted.model_copy(update={"resource": "https://other.example/mcp"})
|
||||
|
||||
monkeypatch.setattr(mcp_http._JWTVerifier, "verify_token", for_elsewhere)
|
||||
me = client.get(
|
||||
f"{settings.API_V1_STR}/users/me", headers=superuser_token_headers
|
||||
).json()
|
||||
token = mcp_token(uuid.UUID(me["id"]), agent)
|
||||
|
||||
async def block(http: httpx.AsyncClient) -> httpx.Response:
|
||||
return await http.post("/mcp", **rpc(token, "tools/list"))
|
||||
|
||||
assert over_mcp(block).status_code == 401
|
||||
|
||||
|
||||
def test_revoking_an_agent_stops_the_token_it_already_holds(
|
||||
over_mcp,
|
||||
client: TestClient,
|
||||
@@ -278,7 +303,7 @@ def test_a_real_client_can_connect_and_call_a_tool(
|
||||
) as http_client:
|
||||
async with streamable_http_client(
|
||||
f"{settings.oauth_issuer}/mcp", http_client=http_client
|
||||
) as (read, write, _):
|
||||
) as (read, write):
|
||||
async with ClientSession(read, write) as session:
|
||||
initialized = await session.initialize()
|
||||
tools = await session.list_tools()
|
||||
@@ -286,9 +311,13 @@ def test_a_real_client_can_connect_and_call_a_tool(
|
||||
return initialized, tools, called
|
||||
|
||||
initialized, tools, called = over_mcp(block)
|
||||
assert initialized.serverInfo.name == "fluksio"
|
||||
assert initialized.server_info.name == "fluksio"
|
||||
assert {"list_flows", "run_flow"} <= {tool.name for tool in tools.tools}
|
||||
assert called.isError is False
|
||||
assert called.is_error is False
|
||||
# The tool reached the API as the agent's person: refused calls come back
|
||||
# as an error object in the text, not as a failed call.
|
||||
answer = json.loads(called.content[0].text)
|
||||
assert not (isinstance(answer, dict) and "error" in answer), answer
|
||||
|
||||
|
||||
@pytest.mark.parametrize("revision", ["2025-03-26", "2025-06-18", "2025-11-25"])
|
||||
@@ -326,3 +355,36 @@ def test_clients_of_older_protocol_revisions_still_connect(
|
||||
assert initialized.json()["result"]["protocolVersion"] == revision
|
||||
assert listed.status_code == 200
|
||||
assert "list_flows" in {tool["name"] for tool in listed.json()["result"]["tools"]}
|
||||
|
||||
|
||||
def test_a_request_of_the_2026_07_28_revision_is_served(
|
||||
over_mcp,
|
||||
client: TestClient,
|
||||
superuser_token_headers: dict[str, str],
|
||||
agent: uuid.UUID,
|
||||
) -> None:
|
||||
"""Its requests carry their own protocol envelope and no handshake."""
|
||||
me = client.get(
|
||||
f"{settings.API_V1_STR}/users/me", headers=superuser_token_headers
|
||||
).json()
|
||||
token = mcp_token(uuid.UUID(me["id"]), agent)
|
||||
meta = {
|
||||
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
|
||||
"io.modelcontextprotocol/clientCapabilities": {},
|
||||
}
|
||||
call = rpc(token, "tools/call", _meta=meta, name="list_flows", arguments={})
|
||||
call["headers"] |= {
|
||||
"MCP-Protocol-Version": "2026-07-28",
|
||||
"Mcp-Method": "tools/call",
|
||||
"Mcp-Name": "list_flows",
|
||||
}
|
||||
|
||||
async def block(http: httpx.AsyncClient) -> httpx.Response:
|
||||
return await http.post("/mcp", **call)
|
||||
|
||||
answer = over_mcp(block)
|
||||
assert answer.status_code == 200
|
||||
result = answer.json()["result"]
|
||||
assert result["isError"] is False
|
||||
# Carrying the agent's token on to the API, or the call is refused there.
|
||||
assert "error" not in json.loads(result["content"][0]["text"])
|
||||
|
||||
@@ -39,3 +39,36 @@ def test_a_name_cannot_climb_out_of_its_route() -> None:
|
||||
"DELETE /api/v1/flows/%2E%2E",
|
||||
"GET /api/v1/flows/%2E%2E/nodes/%2E%2E%2Fsource/source",
|
||||
]
|
||||
|
||||
|
||||
def test_the_assistant_calls_a_tool_as_the_person_chatting() -> None:
|
||||
"""The in-process path: no MCP request, so the person's token is set by hand."""
|
||||
from fluksio.ai import agent
|
||||
|
||||
seen: list[str | None] = []
|
||||
|
||||
def record(request: httpx.Request) -> httpx.Response:
|
||||
seen.append(request.headers.get("authorization"))
|
||||
return httpx.Response(200, json=[{"name": "home"}])
|
||||
|
||||
async def calls() -> tuple[list[dict], str]:
|
||||
previous = server._client
|
||||
mock = httpx.AsyncClient(
|
||||
transport=httpx.MockTransport(record),
|
||||
base_url="http://test",
|
||||
event_hooks={"request": [server.forward_caller_auth]},
|
||||
)
|
||||
server.set_client(mock)
|
||||
try:
|
||||
specs = await agent.tool_specs()
|
||||
answer = await agent.mcp_caller("Bearer person")("list_flows", {})
|
||||
finally:
|
||||
server.set_client(previous)
|
||||
await mock.aclose()
|
||||
return specs, answer
|
||||
|
||||
specs, answer = asyncio.run(calls())
|
||||
assert {"list_flows", "run_flow"} <= {s["function"]["name"] for s in specs}
|
||||
assert all(s["function"]["parameters"]["type"] == "object" for s in specs)
|
||||
assert seen == ["Bearer person"]
|
||||
assert "home" in answer
|
||||
|
||||
+2
-1
@@ -14,7 +14,8 @@ Configure your MCP client with an HTTP endpoint:
|
||||
- Deployment: `https://<your-domain>/mcp`
|
||||
|
||||
The endpoint has to be the address in `FRONTEND_HOST`, which is also the
|
||||
issuer of the client's tokens.
|
||||
issuer of the client's tokens. It speaks the streamable HTTP transport without
|
||||
sessions and answers clients of the MCP revisions 2025-03-26 to 2026-07-28.
|
||||
|
||||
The client registers, opens browser authorization, and receives an OAuth
|
||||
token after you approve access. The issuer and browser redirects use
|
||||
|
||||
@@ -866,7 +866,7 @@ requires-dist = [
|
||||
{ name = "httpx", specifier = ">=0.25.1,<1.0.0" },
|
||||
{ name = "influxdb-client", extras = ["async"], marker = "extra == 'influx'", specifier = ">=1.40.0" },
|
||||
{ name = "jinja2", specifier = ">=3.1.4,<4.0.0" },
|
||||
{ name = "mcp", specifier = ">=1.30,<2" },
|
||||
{ name = "mcp", specifier = ">=2.3,<3" },
|
||||
{ name = "numpy", marker = "extra == 'ml'", specifier = ">=2.2.6" },
|
||||
{ name = "orjson", specifier = ">=3.10" },
|
||||
{ name = "pwdlib", extras = ["argon2", "bcrypt"], specifier = ">=0.3.0" },
|
||||
@@ -1061,6 +1061,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.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "h11" },
|
||||
{ name = "truststore" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c0/61/afa9d6b7452f9eca73f73d45324eb0bb52f098fd5e6049bf6b8263be9b4e/httpcore2-2.16.0.tar.gz", hash = "sha256:c72cd0413563b17e1cd8354677dff887466f8ec2551679759e93126534428643", size = 69886, upload-time = "2026-10-21T07:09:45.31Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/af/c2/ce417c397afa5181f0e3c6ff01a4d01639414e1adef08730d97571297151/httpcore2-2.16.0-py3-none-any.whl", hash = "sha256:5c4358319f2602d984ad93182c9fee5160f22e9da7babdda870a5e7c4e2dc34f", size = 85070, upload-time = "2026-10-21T07:09:42.802Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httptools"
|
||||
version = "0.7.1"
|
||||
@@ -1106,12 +1119,29 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx-sse"
|
||||
version = "0.4.3"
|
||||
name = "httpx2"
|
||||
version = "2.16.0"
|
||||
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'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/73/5ca2cedf5e4e50423d1c258877865a6b62793633b561c58499f8aeb33b7e/httpx2-2.16.0.tar.gz", hash = "sha256:0d54c98128e8f2ff7fbbde0fca8c359e5f032edeaaaac443cdb7f31f685bcd7f", size = 103116, upload-time = "2026-10-21T07:09:46.348Z" }
|
||||
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/7b/3d/2e4bc4fd1ef9770871669adb348d612a6e4786b303a23f9935b81c48dd60/httpx2-2.16.0-py3-none-any.whl", hash = "sha256:fd2c5d7909e8d86fe92d53020c310ca0e4e431b838c207c5754b9bbf0308d49a", size = 97880, upload-time = "2026-10-21T07:09:44.01Z" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
@@ -1457,15 +1487,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "mcp"
|
||||
version = "1.30.0"
|
||||
version = "2.3.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'" },
|
||||
@@ -1475,9 +1505,22 @@ dependencies = [
|
||||
{ name = "typing-inspection" },
|
||||
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/93/0142dc84a666daf8ad51a34268f34c12fd6fda4f3810c4be2504eecc8212/mcp-1.30.0.tar.gz", hash = "sha256:445414625fce5c295faa505bb11bacece661ab6f4028d57c935db57820b7a3e4", size = 680511, upload-time = "2026-09-07T14:34:15.845Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/8d/e0d339616f4810e9051d4aba6887afab289ab1f81875fe908b606cdfd0e3/mcp-2.3.0.tar.gz", hash = "sha256:8b147a50441cf059dc88c684e0aeed3687f0aa0f39c6cde7b90330effd2b34d8", size = 4171955, upload-time = "2026-10-02T22:06:56.091Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/f4/e58bc33317c92a0203664daaf00bf6f41166cc0149e5d6870a03f7cd004a/mcp-1.30.0-py3-none-any.whl", hash = "sha256:666edb5009503e1047c9d60346a756f94b261f05cc2625f23d41c728ffc484d0", size = 234581, upload-time = "2026-09-07T14:34:14.266Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/d7/4a2878f128df729db3ca04e71bfce26405cc2e0c9ebe19c592feeb4c61b9/mcp-2.3.0-py3-none-any.whl", hash = "sha256:dd0c44c089d16453e8ae31a3877a0054d7a2314caaa81f5e0541b9b1734b2377", size = 368664, upload-time = "2026-10-02T22:06:52.119Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp-types"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/2d/7c251e34207f6c51000312fc8839111ac45cfe02023f90b44e7f1051dd8e/mcp_types-2.3.0.tar.gz", hash = "sha256:d1e46549edb35ee19a94940fcee6d1addd7e589ab7ea92dda83f5d84781fc362", size = 65898, upload-time = "2026-10-02T22:06:57.863Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/48/32583066b155bb5d4507e7887dc88ee23118cc4de8e2a12870c49719e2a8/mcp_types-2.3.0-py3-none-any.whl", hash = "sha256:968efdbdaedfab06adae40d378a34395f1090c5921d4be3c9cde283aaf76d91d", size = 69108, upload-time = "2026-10-02T22:06:54.099Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1751,6 +1794,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-api"
|
||||
version = "1.45.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2e/02/6e0ae9cc61bd3169d401077b507b3ebc344745171e1051ab430be012dcd9/opentelemetry_api-1.45.1.tar.gz", hash = "sha256:aa38ed19bcc084ba42782a73255b3582283eced7ad6dddbd6695189e69adfb75", size = 72804, upload-time = "2026-10-06T17:32:58.133Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/41/f7dcf80b81ee8e71c1a2b59f14208bc723edbd89ed027a73b175abf6348e/opentelemetry_api-1.45.1-py3-none-any.whl", hash = "sha256:b31553efa588ae44bc306f863c785c5333a9ecc091248c6ee68b4b6c87fdedfb", size = 60256, upload-time = "2026-10-06T17:32:33.506Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "orjson"
|
||||
version = "3.12.0"
|
||||
@@ -2624,6 +2679,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/3f/f88a53f60a472b46f4023f56d204dd7de33d34c5d2acbfa0d70a674e639e/threadpoolctl-3.7.0-py3-none-any.whl", hash = "sha256:cd8b60b5641b45c67bbf73c64c843235fc2d8a480c87389f52f5dbee893b86be", size = 26362, upload-time = "2026-09-15T15:46:19.168Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truststore"
|
||||
version = "0.10.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/9f/c5201d42a484c061e528825fc8e2d565f5abd50a4ced6fb7d29c4ec99b2b/truststore-0.10.5.tar.gz", hash = "sha256:30d36967ccaded5cbb38d602c433f53600036c79d502f4533a49b60a03bbefcd", size = 28091, upload-time = "2026-10-12T22:27:31.808Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/e9/3a7820be2bb0fe53b6bc9c3be26d3d1158004e4c3ab953aa6840b955b1e9/truststore-0.10.5-py3-none-any.whl", hash = "sha256:9aaaedaefaf06d8b206278cf8b5012bc897f485a874503501e12d776df78951c", size = 19017, upload-time = "2026-10-12T22:27:30.377Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.21.1"
|
||||
|
||||
Reference in New Issue
Block a user