Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions backend/opencheck/mcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,24 @@
from typing import Any

from .. import __version__
from .server import TOOL_NAMES, mcp
from .server import _TRANSPORT_SECURITY, TOOL_NAMES, mcp

_PUBLIC_BASE = "https://api.opencheck.world"


def asgi_app():
"""Build the streamable-HTTP ASGI app (also creates ``mcp.session_manager``)."""
return mcp.streamable_http_app()
"""Build the streamable-HTTP ASGI app (also creates ``mcp.session_manager``).

SDK v2 moved the transport settings here from the ``MCPServer`` constructor.
``host`` is set to the public hostname so the SDK does not auto-enable its
localhost DNS-rebinding allowlist (we pass explicit settings anyway).
"""
return mcp.streamable_http_app(
streamable_http_path="/mcp",
stateless_http=True,
transport_security=_TRANSPORT_SECURITY,
host="api.opencheck.world",
)


def descriptor() -> dict[str, Any]:
Expand Down
20 changes: 13 additions & 7 deletions backend/opencheck/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
diverge from the REST path. Responses are flattened by ``shaping`` into compact,
agent-readable structures; licence notices are preserved end to end.

Transport is streamable HTTP (``stateless_http=True``) so the server mounts onto
the existing FastAPI app and is reachable remotely at ``/mcp``.
Transport is streamable HTTP (``stateless_http=True``, configured on the ASGI
factory in ``__init__``) so the server mounts onto the existing FastAPI app and
is reachable remotely at ``/mcp``.
"""

from __future__ import annotations
Expand All @@ -20,7 +21,8 @@
from typing import Any

from fastapi import HTTPException
from mcp.server.fastmcp import FastMCP
from mcp.server.caching import CacheHint
from mcp.server.mcpserver import MCPServer
from mcp.server.transport_security import TransportSecuritySettings

from . import shaping
Expand All @@ -46,12 +48,16 @@
# (see app.py) rather than mounted under a prefix — a ``Mount`` at ``/mcp`` would
# 307-redirect a bare ``POST /mcp`` to ``/mcp/``, and many MCP clients don't
# replay the POST across that redirect, so the connector silently fails.
mcp = FastMCP(
#
# SDK v2 (spec 2026-07-28): transport settings moved off the constructor onto
# ``streamable_http_app()`` — see ``asgi_app()`` in ``__init__``. ``cache_hints``
# (SEP-2549) marks ``tools/list`` as long-lived and public: the tool inventory
# only changes on deploy, so clients may cache it for 24h and share it across
# users.
mcp = MCPServer(
"opencheck",
instructions=_INSTRUCTIONS,
stateless_http=True,
streamable_http_path="/mcp",
transport_security=_TRANSPORT_SECURITY,
cache_hints={"tools/list": CacheHint(ttl_ms=24 * 60 * 60 * 1000, scope="public")},
)


Expand Down
2 changes: 1 addition & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ dependencies = [
# Pinned <2: SDK v2 (MCP spec 2026-07-28) renames FastMCP→MCPServer and
# splits mcp.types out — the import would fail and app.py's defensive mount
# would silently drop /mcp. Migration tracked on the "Update MCP" ticket.
"mcp>=1.28,<2",
"mcp>=2,<3",
"slowapi>=0.1.10",
]

Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ async def test_registered_tools_match_declared_names() -> None:
tools = await mcp_server.mcp.list_tools()
assert sorted(t.name for t in tools) == sorted(TOOL_NAMES)
# Every tool exposes an input schema for agents.
assert all(t.inputSchema for t in tools)
assert all(t.input_schema for t in tools)


# --------------------------------------------------------------------------
Expand Down
82 changes: 68 additions & 14 deletions backend/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading