diff --git a/nemo_gym/mcp_auto_exposure.py b/nemo_gym/mcp_auto_exposure.py index a8c32c623dd..82f22832c88 100644 --- a/nemo_gym/mcp_auto_exposure.py +++ b/nemo_gym/mcp_auto_exposure.py @@ -71,12 +71,14 @@ import json import logging import re +from collections.abc import Awaitable from contextlib import asynccontextmanager from dataclasses import dataclass from types import UnionType from typing import Any, Callable, Optional, Union, get_args, get_origin, get_type_hints from uuid import uuid4 +import jsonschema import mcp.types as types from aiohttp import ClientResponseError from fastapi import FastAPI, Request @@ -756,13 +758,57 @@ def _parse_session_token( return payload["sid"], None if allowed is None else frozenset(allowed) -def _to_result(payload: Any): - """Per the SDK's call_tool contract, a ``(content, structuredContent)`` tuple populates both - CallToolResult fields while a bare content list leaves structuredContent unset — and only a JSON - object maps to structuredContent.""" +def _to_result(payload: Any, *, output_schema: Optional[dict] = None) -> types.CallToolResult: + """Preserve text content and object-only structured content on both SDK versions.""" + structured = payload if isinstance(payload, dict) else None + if output_schema is not None: + if structured is None: + raise ValueError("Output validation error: outputSchema defined but no structured output returned") + try: + jsonschema.validate(instance=structured, schema=output_schema) + except jsonschema.ValidationError as exc: + raise ValueError(f"Output validation error: {exc.message}") from exc text = payload if isinstance(payload, str) else JSONResponse(payload).body.decode("utf-8") - content = [types.TextContent(type="text", text=text)] - return (content, payload) if isinstance(payload, dict) else content + return types.CallToolResult( + content=[types.TextContent(type="text", text=text)], structuredContent=structured, isError=False + ) + + +def _make_mcp_server( + name: str, + list_tools: Callable[[Optional[Request]], Awaitable[list[types.Tool]]], + call_tool: Callable[[Optional[Request], str, dict], Awaitable[types.CallToolResult]], +) -> _LowLevelMCPServer: + """Adapt only SDK registration and request context; Gym dispatch stays version independent.""" + if "on_list_tools" in inspect.signature(_LowLevelMCPServer).parameters: + from mcp.server.context import ServerRequestContext + + async def on_list_tools( + ctx: ServerRequestContext, params: Optional[types.PaginatedRequestParams] + ) -> types.ListToolsResult: + return types.ListToolsResult(tools=await list_tools(ctx.request)) + + async def on_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult: + try: + return await call_tool(ctx.request, params.name, params.arguments or {}) + except Exception as exc: + # MCP 1's call_tool decorator performs this translation. MCP 2's low-level + # callbacks otherwise turn tool failures into JSON-RPC protocol errors. + return types.CallToolResult(content=[types.TextContent(type="text", text=str(exc))], isError=True) + + return _LowLevelMCPServer(name, on_list_tools=on_list_tools, on_call_tool=on_call_tool) + + mcp_server = _LowLevelMCPServer(name) + + @mcp_server.list_tools() + async def legacy_list_tools() -> list[types.Tool]: + return await list_tools(mcp_server.request_context.request) + + @mcp_server.call_tool(validate_input=False) + async def legacy_call_tool(name: str, arguments: dict) -> types.CallToolResult: + return await call_tool(mcp_server.request_context.request, name, arguments) + + return mcp_server def session_token_serializer(server: Any) -> URLSafeSerializer: @@ -805,22 +851,15 @@ def install_auto_exposure(server: Any, app: FastAPI) -> dict[str, MCPTool]: _wrap_seed_session(app, mint_metadata) _wrap_verify(app, server) - mcp_server = _LowLevelMCPServer(server.config.name or type(server).__name__) - - # mcp_server.request_context is a contextvar the SDK populates only while one of the handlers - # below is on the stack — exactly when session_claims runs, which is why it stays local. - def session_claims(required: bool = True) -> tuple[Optional[str], Optional[frozenset]]: - ctx_request = mcp_server.request_context.request # the POST /mcp starlette Request + def session_claims(ctx_request: Optional[Request], *, required: bool) -> tuple[Optional[str], Optional[frozenset]]: token = ctx_request.headers.get(NEMO_GYM_MCP_SESSION_TOKEN_HEADER) if ctx_request is not None else None return _parse_session_token(serializer, token, required) - @mcp_server.list_tools() - async def list_tools() -> list[types.Tool]: - _, allowed = session_claims(required=False) + async def list_tools(ctx_request: Optional[Request]) -> list[types.Tool]: + _, allowed = session_claims(ctx_request, required=False) return [t.tool for t in tools.values() if allowed is None or t.name in allowed] - @mcp_server.call_tool(validate_input=False) - async def call_tool(name: str, arguments: dict): + async def call_tool(ctx_request: Optional[Request], name: str, arguments: dict) -> types.CallToolResult: tool = tools.get(name) if tool is None: # Models hallucinate tool names, so answer the miss cheaply with a plain isError result @@ -832,15 +871,16 @@ async def call_tool(name: str, arguments: dict): ], isError=True, ) - session_id, allowed = session_claims(required=True) + session_id, allowed = session_claims(ctx_request, required=True) if allowed is not None and name not in allowed: raise ValueError(f"Tool {name!r} is not allowed for this session.") try: payload = await call_direct(app, tool.binding, session_id, arguments, path_value=tool.path_value) except DirectDispatchError as exc: raise ValueError(f"HTTP {exc.status} from POST /{name}: {exc.detail}") - return _to_result(payload) + return _to_result(payload, output_schema=tool.tool.model_dump(by_alias=True).get("outputSchema")) + mcp_server = _make_mcp_server(server.config.name or type(server).__name__, list_tools, call_tool) manager = StreamableHTTPSessionManager( app=mcp_server, event_store=None, diff --git a/pyproject.toml b/pyproject.toml index 72c5da886e1..7033a6838b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -133,9 +133,9 @@ dependencies = [ "fastapi!=0.142.*", # MCP Python SDK: Used by Gym-owned Streamable HTTP MCP resources servers. - # Updated Tue Jul 29, 2026 with mcp>=1.28.1,<2 + # Updated Tue Oct 13, 2026: MCP 1.30.0 and 2.3.0 compatibility validated. # License: MIT https://github.com/modelcontextprotocol/python-sdk/blob/main/LICENSE - "mcp>=1.28.1,<2", + "mcp>=1.28.1,<3", # anyio: transitive dependency of mcp/fastapi/starlette; pinned directly since # mcp's own floor (>=4.10.0) resolves to a version with a known CVE. @@ -508,10 +508,10 @@ exclude-dependencies = [ # (missing #include in src/args.cc). Excluded as a transitive dep # of unbabel-comet; wmt_translation does not import fasttext at runtime. "fasttext-wheel", + # cryptography must remain available: MCP 2 imports it at startup via pyjwt[crypto]. # Exclude unwanted dependencies from mlflow "alembic", "blinker", - "cryptography", "flask", "flask-cors", "graphene", diff --git a/resources_servers/gdpval/tests/test_tools.py b/resources_servers/gdpval/tests/test_tools.py index 35711d79721..bbd6c902fcc 100644 --- a/resources_servers/gdpval/tests/test_tools.py +++ b/resources_servers/gdpval/tests/test_tools.py @@ -63,7 +63,7 @@ def test_benchmark_tools_are_the_tools_the_server_serves(server): "type": "function", "name": t.name, "description": t.tool.description, - "parameters": t.tool.inputSchema, + "parameters": t.tool.model_dump(by_alias=True)["inputSchema"], "strict": None, } for t in tools.values() diff --git a/resources_servers/newton_bench/requirements.txt b/resources_servers/newton_bench/requirements.txt index 364f3f166d3..e5540f2ff8f 100644 --- a/resources_servers/newton_bench/requirements.txt +++ b/resources_servers/newton_bench/requirements.txt @@ -4,3 +4,5 @@ scipy>=1.7 sympy>=1.10 fix-busted-json>=0.0.18 pandas>=1.5.3 +# NewtonBench imports dotenv directly; do not rely on optional transitive dependencies. +python-dotenv>=1.0.0 diff --git a/resources_servers/ns_tools/requirements.txt b/resources_servers/ns_tools/requirements.txt index 1a920f13ff3..12c99e45930 100644 --- a/resources_servers/ns_tools/requirements.txt +++ b/resources_servers/ns_tools/requirements.txt @@ -1,3 +1,5 @@ -e nemo-gym[dev] @ ../../ ## NeMo-Skills tools subpackage as of June 12, 2026 nemo-skills-tools @ git+https://github.com/NVIDIA-NeMo/Skills.git@da85a881d972e6fec847b90cf553a0bf9bf10638#subdirectory=tools +# The pinned NeMo-Skills client imports the MCP 1 Streamable HTTP API. +mcp>=1.28.1,<2 diff --git a/responses_api_agents/hermes_agent/requirements.txt b/responses_api_agents/hermes_agent/requirements.txt index 8c1a80c3c1e..af075f62a3c 100644 --- a/responses_api_agents/hermes_agent/requirements.txt +++ b/responses_api_agents/hermes_agent/requirements.txt @@ -1,2 +1,4 @@ -e nemo-gym[dev,sandbox] @ ../../ hermes-agent @ git+https://github.com/cmunley1/hermes-agent@26bb847a +# The pinned Hermes client requires the MCP 1 Streamable HTTP API. +mcp>=1.28.1,<2 diff --git a/responses_api_agents/hermes_agent/sandbox.py b/responses_api_agents/hermes_agent/sandbox.py index 0550b331e09..88a21c924de 100644 --- a/responses_api_agents/hermes_agent/sandbox.py +++ b/responses_api_agents/hermes_agent/sandbox.py @@ -100,7 +100,7 @@ async def _runtime_installed(self) -> bool: session in this sandbox is reused. """ check = await self.session.sandbox.exec( - f"{quote(_SANDBOX_PYTHON)} -c 'import run_agent, mcp'", + f"{quote(_SANDBOX_PYTHON)} -c 'import run_agent; from mcp.client.streamable_http import streamablehttp_client'", cwd=self.session.workdir, timeout_s=120, ) @@ -114,7 +114,7 @@ async def _install_runtime(self, timeout: float, uv_cache_dir: str) -> None: install = await self.session.sandbox.exec( f"chmod 755 {quote(_SANDBOX_UV)} && rm -rf {venv} && " f"{quote(_SANDBOX_UV)} venv {venv} --python 3.13 && " - f"{quote(_SANDBOX_UV)} pip install --python {quote(_SANDBOX_PYTHON)} {quote(_HERMES_REQUIREMENT)}", + f"{quote(_SANDBOX_UV)} pip install --python {quote(_SANDBOX_PYTHON)} {quote(_HERMES_REQUIREMENT)} 'mcp>=1.28.1,<2'", cwd=self.session.workdir, timeout_s=timeout, ) diff --git a/responses_api_agents/hermes_agent/tests/test_app.py b/responses_api_agents/hermes_agent/tests/test_app.py index ee64d65cfec..2eec647f513 100644 --- a/responses_api_agents/hermes_agent/tests/test_app.py +++ b/responses_api_agents/hermes_agent/tests/test_app.py @@ -224,6 +224,7 @@ async def exec_(command, **_kwargs): commands = [call.args[0] for call in sandbox.exec.await_args_list] install = [command for command in commands if "pip install" in command] assert len(install) == 1 and "rm -rf" in install[0] + assert "mcp>=1.28.1,<2" in install[0] # The import is checked before installing and again after. assert sum("import run_agent" in command for command in commands) == 2 assert sandbox.upload.await_args_list[0].args[0] == Path("/usr/bin/uv") diff --git a/responses_api_agents/osworld_agent/uv.toml b/responses_api_agents/osworld_agent/uv.toml index e22593d10e2..5db127e6ab7 100644 --- a/responses_api_agents/osworld_agent/uv.toml +++ b/responses_api_agents/osworld_agent/uv.toml @@ -33,7 +33,6 @@ exclude-dependencies = [ "fasttext-wheel", "alembic", "blinker", - "cryptography", "flask", "flask-cors", "graphene", diff --git a/tests/unit_tests/test_mcp_auto_exposure.py b/tests/unit_tests/test_mcp_auto_exposure.py index e261518be6c..cacaa59915a 100644 --- a/tests/unit_tests/test_mcp_auto_exposure.py +++ b/tests/unit_tests/test_mcp_auto_exposure.py @@ -26,7 +26,8 @@ import logging import subprocess import sys -from contextlib import contextmanager +from concurrent.futures import ThreadPoolExecutor +from contextlib import asynccontextmanager, contextmanager from types import SimpleNamespace from typing import Any, Optional from unittest.mock import MagicMock @@ -704,7 +705,7 @@ async def gated(body: EchoBody, ok: bool = Depends(lambda: True)): return app def mcp_tools(self, harvested, catchall): - harvested_schemas.update({t.name: t.tool.inputSchema for t in harvested}) + harvested_schemas.update({t.name: t.tool.model_dump(by_alias=True)["inputSchema"] for t in harvested}) return [t for t in harvested if t.name not in ("end_session", "gated")] server = _server(DroppedTyped, "excl") @@ -956,3 +957,161 @@ def test_normalize_tool_name_without_server_name_strips_first_namespace(): # with a server name, only that server's prefix is stripped assert normalize_tool_name("mcp__store__append", "store") == "append" assert normalize_tool_name("mcp__other__append", "store") == "mcp__other__append" + + +@pytest.mark.parametrize("payload", [{"answer": 42}, [1, 2], "hello", 7, None]) +def test_result_content_and_structured_content(payload): + class Results(Store): + def setup_webserver(self): + app = super().setup_webserver() + + @app.post("/result") + async def result(): + return payload + + return app + + with _mcp(Results) as (client, token): + result = _call(client, "result", {}, token) + assert result.get("isError") is not True + text = result["content"][0]["text"] + assert (text if isinstance(payload, str) else json.loads(text)) == payload + assert result.get("structuredContent") == (payload if isinstance(payload, dict) else None) + + +@pytest.mark.parametrize("payload", [{"answer": 42}, {"answer": "wrong"}, "not an object"]) +def test_output_schema_validation_is_preserved(payload): + class OutputSchema(Store): + def setup_webserver(self): + app = super().setup_webserver() + + @app.post("/result") + async def result(): + return payload + + return app + + def mcp_tools(self, harvested, catchall): + tools = super().mcp_tools(harvested, catchall) + for tool in tools: + if tool.name == "result": + data = tool.tool.model_dump(by_alias=True) + data["outputSchema"] = { + "type": "object", + "properties": {"answer": {"type": "integer"}}, + "required": ["answer"], + } + tool.tool = type(tool.tool).model_validate(data) + return tools + + with _mcp(OutputSchema) as (client, token): + result = _call(client, "result", {}, token) + if payload == {"answer": 42}: + assert result.get("isError") is not True + assert result["structuredContent"] == payload + else: + assert result["isError"] is True + assert "Output validation error:" in result["content"][0]["text"] + + +def test_omitted_arguments_and_pydantic_coercion(): + class Coercing(Store): + def setup_webserver(self): + app = super().setup_webserver() + + @app.post("/coerce") + async def coerce(body: CountBody): + return {"count": body.count} + + return app + + with _mcp(Coercing) as (client, token): + result = _rpc(client, "tools/call", {"name": "raw_step"}, token)["result"] + assert result["structuredContent"] == {"echo": {}} + # Gym delegates input validation/coercion to Pydantic, not SDK JSON Schema validation. + result = _call(client, "coerce", {"count": "42"}, token) + assert result["structuredContent"] == {"count": 42} + + +class CountBody(BaseModel): + count: int + + +def test_concurrent_calls_use_each_requests_signed_session(): + with _mcp(SessionScoped) as (client, _): + tokens = [ + client.post("/seed_session", json={"allowed_tools": names}).json()["mcp"]["headers"][TOKEN_HEADER] + for names in (["append"], ["raw_step"]) + ] + + def run(index): + token = tokens[index % 2] + expected = "append" if index % 2 == 0 else "raw_step" + assert [tool["name"] for tool in _list(client, token)] == [expected] + allowed = _call(client, expected, {"value": str(index)}, token) + assert allowed.get("isError") is not True + denied = _call(client, "raw_step" if expected == "append" else "append", {}, token) + assert denied["isError"] is True + assert "not allowed" in denied["content"][0]["text"] + + with ThreadPoolExecutor(max_workers=4) as pool: + list(pool.map(run, range(12))) + + +def test_original_lifespan_state_and_shutdown_are_preserved(): + events = [] + server = _server() + app = server.setup_webserver() + + @asynccontextmanager + async def lifespan(app): + events.append("start") + try: + yield {"marker": "original lifespan"} + finally: + events.append("stop") + + app.router.lifespan_context = lifespan + maybe_auto_expose(server, app) + with TestClient(app) as client: + assert client.app_state["marker"] == "original lifespan" + assert events == ["start"] + token = _seed(client) + _handshake(client) + assert _call(client, "raw_step", {}, token)["structuredContent"] == {"echo": {}} + assert events == ["start", "stop"] + + +async def test_sdk_client_discovers_and_calls_over_streamable_http(): + from importlib.metadata import version + + from mcp import ClientSession + from mcp.client.streamable_http import streamable_http_client + + # Exercise each SDK's real HTTP transport against the ASGI app without a network listener. + # MCP 2 uses httpx2; MCP 1 uses httpx. These clients are test-only. + if version("mcp").startswith("2."): + import httpx2 as http_client_module + else: + import httpx as http_client_module + + server = _server(SessionScoped) + app = server.setup_webserver() + maybe_auto_expose(server, app) + async with app.router.lifespan_context(app): + async with http_client_module.AsyncClient( + transport=http_client_module.ASGITransport(app=app), base_url="http://testserver" + ) as http_client: + seeded = await http_client.post("/seed_session", json={"allowed_tools": ["append"]}) + http_client.headers[TOKEN_HEADER] = seeded.json()["mcp"]["headers"][TOKEN_HEADER] + async with streamable_http_client("http://testserver/mcp", http_client=http_client) as streams: + async with ClientSession(streams[0], streams[1]) as session: + await session.initialize() + listed = await session.list_tools() + assert [tool.name for tool in listed.tools] == ["append"] + result = (await session.call_tool("append", {"value": "sdk"})).model_dump(by_alias=True) + assert result["structuredContent"] == {"values": ["sdk"]} + assert result["isError"] is False + denied = (await session.call_tool("raw_step", {})).model_dump(by_alias=True) + assert denied["isError"] is True + assert "not allowed" in denied["content"][0]["text"] diff --git a/uv.lock b/uv.lock index 060c488391d..1633f7831fc 100644 --- a/uv.lock +++ b/uv.lock @@ -30,7 +30,6 @@ overrides = [ excludes = [ "alembic", "blinker", - "cryptography", "diskcache", "fasttext-wheel", "flask", @@ -481,7 +480,7 @@ name = "cffi" version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } wheels = [ @@ -767,6 +766,69 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/b6/fff6609354deba9aeec466e4bcaeb9d1ed3e5d60b14b57df2a36fb2273f2/coverage-7.10.5-py3-none-any.whl", hash = "sha256:0be24d35e4db1d23d0db5c0f6a74a962e2ec83c426b5cac09f4234aadef38e4a", size = 208736, upload-time = "2025-08-23T14:42:43.145Z" }, ] +[[package]] +name = "cryptography" +version = "50.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/af/182eb91b0df3fe75c4d9f26fe70684569566745f6ba7e5c9c73a862c5252/cryptography-50.0.2.tar.gz", hash = "sha256:7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5", size = 880623, upload-time = "2026-09-30T15:30:04.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/56/d194340cc4a57535e82e1bee9e89667ac4b7c13b5d3f59686deae3094dd5/cryptography-50.0.2-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:fa8f5efb344d6908a1ce62f4a24e2e5780f825d6f53f5f50ec5ffacac72936cb", size = 3914904, upload-time = "2026-09-30T14:43:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/d9/69/c9bd862c3bf43d6399c433caf002df16e2dffd4be49bdf515cda38038711/cryptography-50.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:79def8d059362e7831389ed3be0ecdf58a89386e1271e35dd9f5af84e81bffd0", size = 4731146, upload-time = "2026-09-30T14:43:47.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/69/64cef1f702bf6657e0cc186ed1a2891d50d29fb41586b254e1c07adea261/cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2", size = 4719841, upload-time = "2026-09-30T14:43:49.01Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61a3f8d8c5e1e49a6cddccafc4015cc1c0021360ab0acb4080e7a423644a/cryptography-50.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480", size = 4738340, upload-time = "2026-09-30T14:43:50.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2e/7212ca32fd43dc91f2f41db20160b268098874b4c9a0e7be94d6835f5b2e/cryptography-50.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a582ab2ae1d34f67112cadc86702774c9ea4374df6bca6afe672817203c99134", size = 5367029, upload-time = "2026-09-30T14:43:52.911Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f1/b474e930c4d910328780e3940da76f5aa5cbc48ce1fc14e44d239d9ea9db/cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856", size = 4753050, upload-time = "2026-09-30T14:43:55.272Z" }, + { url = "https://files.pythonhosted.org/packages/7c/52/9af10e80ac16b0fcc2123f9cbd5e7afbd0fd5075bb7a607c592258a39cda/cryptography-50.0.2-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:ac9ed99d81760c62fe89d5f0815cdfa1ba9a35141cf30f1c2d044f04b4803d2e", size = 4376724, upload-time = "2026-09-30T14:43:57.24Z" }, + { url = "https://files.pythonhosted.org/packages/71/37/6202e488cc1eb625ea110c292c6bda92823176e023f427d8d5660ce8d632/cryptography-50.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:87e9ce85beb6b328ba370cc6e6aea483c92617b4c95b1d33a49297eb662bfb04", size = 4737859, upload-time = "2026-09-30T14:43:59.541Z" }, + { url = "https://files.pythonhosted.org/packages/8f/30/e86d7d518489b0ae2497091a35287abcb1a2ce4037837a34afbe9b1d6964/cryptography-50.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f265528741e048bce55c3463ed721fb0aa45a5888d8add8cfeccb3035451bbdc", size = 5324103, upload-time = "2026-09-30T14:44:01.901Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/2c833a049475e0a3444e94c7d0aca0aa51d166374a449b09e92ac98138de/cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079", size = 4752576, upload-time = "2026-09-30T14:44:04.545Z" }, + { url = "https://files.pythonhosted.org/packages/6c/5d/906970b83bbfc1f5bbfb677a143c181f2801f23b6a7204a3b47c42c97e65/cryptography-50.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25784ce8b9621c90c643efb9e1e2162ab3b0224cae446ad5e70e7fcb1ce18b51", size = 4870819, upload-time = "2026-09-30T14:44:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/68/e3/f2298d3bb55e0c4a91841ec4d01b3f020ba8c5fbf15ccdcc6dcf03f97025/cryptography-50.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85d0d9a31b9098e98534226d5686b47264b95e62ce459dc2e62fdfc809f9fe93", size = 5030152, upload-time = "2026-09-30T14:44:09.443Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4f/adfc442765721292fff86d314ce385d3249d22db42295c0dd057727b60f3/cryptography-50.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:7afa5a6602a9f29af1f3a2965f831bae7c9d5d597b7cbb716d41ab3b7d89879c", size = 3824692, upload-time = "2026-09-30T14:44:11.671Z" }, + { url = "https://files.pythonhosted.org/packages/ce/cb/52eb3770c0d0be2702a98c6e96065ddc0a2877cf0845aa9c23397c142cd4/cryptography-50.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f785f6161f202ab04d8ca194158968798e480ca058943907972da5f12e2881e8", size = 3892731, upload-time = "2026-09-30T14:44:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/19/8e/aa1fc533d4546b127b45de8aa024eb5933d23eff9debfe25931e56861095/cryptography-50.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ecbc5652bdb6fc9eaf89a7d196e20941adfe812f43bc4ca05d9150496821047", size = 4710431, upload-time = "2026-09-30T14:44:15.427Z" }, + { url = "https://files.pythonhosted.org/packages/6a/64/72bc3f75176e7e406b748a3e3830432b8c51297b38368713df04dc04898a/cryptography-50.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ab50ee449bf968271e820086f10a33d101dd060370abc10bcd22279be2656539", size = 4694824, upload-time = "2026-09-30T14:44:17.69Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c6/62c77550edfa5ca3f14bf44a1e6739b9fa09d6e998a11d97ed8213bccc98/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a9f7355e6fab51f6c369b86fb7571cffa05edee2c2121e0380a37fb9ac1cd5c1", size = 4716967, upload-time = "2026-09-30T14:44:19.661Z" }, + { url = "https://files.pythonhosted.org/packages/f4/37/cce70f150c432914460157a6ecc161752e053aa5ec0ef3b3f7dc6e31039a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:94e5e9f108ee10471288214d3d233fbfbb492840a8457eb85178d643ddeb32c7", size = 5328676, upload-time = "2026-09-30T14:44:21.744Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9a/6f2f0304d634ceafdeaf23e84537336664ac419b5d07611675c2ad3f6b7a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:241449bf940a5d27309bd317e6f9a2af6932113818bb2b8f5c59ddc7ef16da18", size = 4727698, upload-time = "2026-09-30T14:44:24.178Z" }, + { url = "https://files.pythonhosted.org/packages/1d/de/66bcf9244d118663b2e1aaded8990f4640e3d7b7411870a5765f252074d2/cryptography-50.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d8947001be83df1394050758ce0e745dd74fb134eef0a4b5124208dfc3a68c37", size = 4354821, upload-time = "2026-09-30T14:44:26.263Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e6/db28a28c7b6c676addce89136de3d8db49ea825a8c863472e36e42ead4ad/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4a20ce1e5cb4284a86692fdcba7cb8754185c6b2e5c56fcef3751cf451d3cdc2", size = 4716748, upload-time = "2026-09-30T14:44:28.447Z" }, + { url = "https://files.pythonhosted.org/packages/30/96/01546c7f69ea0e2ab790a2e4f0934a4052fb9b388147fbf83c2fd72f1e57/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:84f964e537f916e2cc85199e5a88742e964939b575ac8598b3f9d6cc416cdaf1", size = 5285085, upload-time = "2026-09-30T14:44:30.704Z" }, + { url = "https://files.pythonhosted.org/packages/6c/01/03263395f74d50b071e9e66daace3f8bef80493e5d410726f2ba8554736b/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:828d49b0ff5a0e3975865571c5d91dbbdd0d38d8289b249a163e9425413a5e05", size = 4727268, upload-time = "2026-09-30T14:44:32.92Z" }, + { url = "https://files.pythonhosted.org/packages/eb/94/2bfe8f29ec0cc9c0d99359c4161adf32858e4934b72c6d100d2ac0bbe962/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:deb9fde5c60e437ee4821bc9bc39ff31b42135c27e1dc61ef0a629389c1de62e", size = 4849503, upload-time = "2026-09-30T14:44:34.969Z" }, + { url = "https://files.pythonhosted.org/packages/54/44/e80651ecbf0e42b62e2bb5f5768916e07eea72e1297338956a61df361f88/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8c71ba2cd31fc93748c38e1b613200ff1c2665cbfd5341fe3a61cfde35a1430e", size = 5004057, upload-time = "2026-09-30T14:44:37.064Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cc/1d33befb3cd7ea7e77d2d73f43f2066471da1b21f24a6156efcaabf6d2e8/cryptography-50.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:78198641e5be9521beea5aa782bb551a58068d10e6eb04c9c680c1b69f2e7d45", size = 3795868, upload-time = "2026-09-30T14:44:39.71Z" }, + { url = "https://files.pythonhosted.org/packages/2d/49/93f6a6e7a87c9aa68d44d3e1cdb5fe8f60c90d5d2f46acae9a56892816b8/cryptography-50.0.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:edc3342adf8f697fc5f59c887a304356f147b397809440ed64e2fa6af2f50f37", size = 4133708, upload-time = "2026-09-30T14:44:41.807Z" }, + { url = "https://files.pythonhosted.org/packages/8c/75/32ac2a56243d778805c16ca6a32b8f74fb757df7e28d7ecb560afafb59cf/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d370b8d1dfcdf7130178137f6fbee6140774a1acc6cacefc4b42643ec11d0a3a", size = 4956267, upload-time = "2026-09-30T14:44:43.693Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/2c8d734e43d97f0842ee9f1b7b4bfb3d0cf5e19edebf43c2afe6675c2320/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67", size = 4966465, upload-time = "2026-09-30T14:44:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/c2/58/ee288c829a6f41f6235ae9dd33d82fd19b45442b65b4c8a3da36963d9f7a/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_aarch64.whl", hash = "sha256:e275096ea1e60cc595cda2836fd4a6c725d1125108b868be17f53684d164e2cc", size = 4959356, upload-time = "2026-09-30T14:44:48.211Z" }, + { url = "https://files.pythonhosted.org/packages/92/20/9ded6d51ddd9897f6b6e81fb9ebea7951d7cc5d6c890b0ed8abf77a51a80/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_ppc64le.whl", hash = "sha256:b13478603dcd0a2479ff8e87e2c19a7d525734686fe3c49542472293a204212d", size = 5548822, upload-time = "2026-09-30T14:44:50.86Z" }, + { url = "https://files.pythonhosted.org/packages/02/a8/8df951850d6b31d2a00218f19e2b3f999523437ed7a819df7fa427942fca/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl", hash = "sha256:58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7", size = 5001199, upload-time = "2026-09-30T14:44:53.379Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f9/36b3022218ce75b7cdf068fb95f809f9bd0d820e4955ef43b90c255cc7ac/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_31_armv7l.whl", hash = "sha256:d38cdff612d06fa6a32840d5e1b1f7a27cee4a349aa9085d94a67789d6bfd408", size = 4629333, upload-time = "2026-09-30T14:44:55.635Z" }, + { url = "https://files.pythonhosted.org/packages/8c/72/20f99a219f6af47cdd1cbd978c243b92d71496e168a746138af44ded4f29/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_aarch64.whl", hash = "sha256:fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b", size = 4958822, upload-time = "2026-09-30T14:44:59.639Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/196f112617fb08eb4d608a2a6c422373d46f9cc2857f38fc0667033c0899/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_ppc64le.whl", hash = "sha256:cbc8738fd8526d80f35cb3a40d41f41a2e7030bb3b18b09a6778ef63d291c2fd", size = 5506351, upload-time = "2026-09-30T14:45:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/83378121ef3eaaaf71d4b781577ff794acb39b9e1b87a3f156898c8497ed/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl", hash = "sha256:e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c", size = 5000859, upload-time = "2026-09-30T14:45:05.009Z" }, + { url = "https://files.pythonhosted.org/packages/22/f7/70fd7ae4d1dbfa7ba29b02e1b9068771519a86027756510b700ce81086a8/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6f8700550aa1474a91e5dc07049c46f98b423b5b1ddd0483e0b51362eeeaf5be", size = 5092151, upload-time = "2026-09-30T15:29:15.932Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/688367b74de86984bd58d8efacfc7c9e68b89a6a22ced0fb4f38db50254a/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:c71be1cbfa5cd9a41ee452acf1eccd82b2c05950358b106ec8ceb83411d1a020", size = 5286120, upload-time = "2026-09-30T15:29:18.309Z" }, + { url = "https://files.pythonhosted.org/packages/39/d1/55f8a3f2ef5d1529e16835ef10cf0fe3d559ce237b46dddc440c0bba3649/cryptography-50.0.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:c423ab384a46c4dff7217b2ea5ba2e11cffdeab6441acd04cf65a369caf0366c", size = 4111557, upload-time = "2026-09-30T15:29:20.155Z" }, + { url = "https://files.pythonhosted.org/packages/23/ad/ac987755d00e1e64273760228d2635ae38dae2be83e3c6e0d3289d91dec3/cryptography-50.0.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0ec5f09541743261e66e291b4a0cbf0fb2997aeaab6d9e9c740b9dba1b58d1c2", size = 3943588, upload-time = "2026-09-30T15:29:22.265Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8d/6d585339bedf85d45044c85d8412dac53f2bb6f918e8b7777efba1787844/cryptography-50.0.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5e67125c7dca78d199ec4e116aa93dbb83494808ecbb8211a2cb09b1bf41dbd", size = 4756166, upload-time = "2026-09-30T15:29:24.58Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f1/1c1f6874e8550cfddd4b688ceb38cefb6ed15ceed224d56f133f3d88c214/cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767", size = 4749145, upload-time = "2026-09-30T15:29:26.807Z" }, + { url = "https://files.pythonhosted.org/packages/c1/63/61b15dc1a8de03fe0adbe3fd7608b3ad5c73bf50993bbcb1faaa930afe33/cryptography-50.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfe9763530994147d9af1def057a5b9658b00e8f8fe8743d144d1e0911c2e454", size = 4763638, upload-time = "2026-09-30T15:29:28.588Z" }, + { url = "https://files.pythonhosted.org/packages/fc/35/b345bdfa40c9126df1a9d33236aa98418367931b8725f84fc3ae2b98dc59/cryptography-50.0.2-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:58ddb5a8e3179d12f19e4ea34d2d32e9d63a4baa142c875c1eb59f41b7243acd", size = 5382217, upload-time = "2026-09-30T15:29:30.589Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/ef344a9e616871f2519c22d6afcda79ddd5d35e9592d95eb6e677608d055/cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5", size = 4781387, upload-time = "2026-09-30T15:29:32.605Z" }, + { url = "https://files.pythonhosted.org/packages/90/5b/f2fdb13cd0b96f6f932c8627bb292a45f11c64d21620a8e120aee9a3b848/cryptography-50.0.2-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:9c8402a82ea0dc4ceeab793db05f0fafa8ca139ca34fcde5df0f596103c74107", size = 4403790, upload-time = "2026-09-30T15:29:34.374Z" }, + { url = "https://files.pythonhosted.org/packages/bc/ce/7e4f662b1e3c393513569e402cfc85ac7da0bd3d5435e122a3140219eb2d/cryptography-50.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:0ddc924c04591c2811ca024d62ecad4f7f6f08af8939c211438f48a16bd23602", size = 4764319, upload-time = "2026-09-30T15:29:36.149Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3f/86ff33ce34cc0de6847fb96e035a1a760d81652e38643f617c02ad32ef7a/cryptography-50.0.2-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a6557e5f38e065ca9fbdaf7cfc7435ecb1d113aa81a022d1b51921ee7432e227", size = 5338560, upload-time = "2026-09-30T15:29:39.053Z" }, + { url = "https://files.pythonhosted.org/packages/40/cf/6b5c8e2fd9202d98988ab7cb5cc5c991704c4ad55f492ff408e4969f83f1/cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c", size = 4780973, upload-time = "2026-09-30T15:29:41.251Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/8d6ebc7dded797bd0f0160d52188021211f011a2b164ef0ae1dac4587465/cryptography-50.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a8701d6b584d76e909e3d305b7d126b41439876a5aaf76cddc67fc230eafa2e", size = 4897738, upload-time = "2026-09-30T15:29:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/d4/aa/f3f6e0de7e6253b8baa8b2d8fb9d50924fa75cee3d4624bd4bc1208ee923/cryptography-50.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce47f66801c20ec6c6632453bb5960fe38939e9306970b48b3a5a26de7745d94", size = 5058280, upload-time = "2026-09-30T15:29:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b6/a1faf3a27ae9405fb34b1713cc73b2d8a26b04d5c561578fa2e6ef3e5bb9/cryptography-50.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:4e81d95e5bafc2d6e34e4bed780e53e4d5b9a2f928573428aa4d35fbec1eb0de", size = 3854095, upload-time = "2026-09-30T15:29:46.782Z" }, +] + [[package]] name = "cuda-bindings" version = "13.3.1" @@ -2271,7 +2333,7 @@ dependencies = [ { name = "jsonschema" }, { name = "pydantic" }, { name = "pydantic-settings" }, - { name = "pyjwt" }, + { name = "pyjwt", extra = ["crypto"] }, { name = "python-multipart" }, { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "sse-starlette" }, @@ -2355,6 +2417,7 @@ version = "3.16.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, + { name = "cryptography" }, { name = "docker" }, { name = "matplotlib" }, { name = "mlflow-skinny" }, @@ -2812,7 +2875,7 @@ requires-dist = [ { name = "itsdangerous" }, { name = "jinja2", marker = "extra == 'ray'", specifier = ">=3.1" }, { name = "jsonschema", specifier = ">=4.18,<5" }, - { name = "mcp", specifier = ">=1.28.1,<2" }, + { name = "mcp", specifier = ">=1.28.1,<3" }, { name = "mlflow", marker = "extra == 'mlflow'", specifier = ">=3.16.1" }, { name = "mlflow-skinny", marker = "extra == 'mlflow'", specifier = ">=3.16.1" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8.0" }, @@ -4466,6 +4529,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/50/ca/44de4e75f8aadc457f0634be3b542815078ded46dca30efb960edeecad6e/pyjwt-2.15.1-py3-none-any.whl", hash = "sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193", size = 33860, upload-time = "2026-09-28T18:40:41.429Z" }, ] +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + [[package]] name = "pynvvideocodec" version = "2.0.4"