Skip to content

feat(mcp)!: migrate to MCP SDK 2.0 - #36

Draft
fedorov wants to merge 1 commit into
mainfrom
feat/mcp-sdk-2-migration
Draft

feat(mcp)!: migrate to MCP SDK 2.0#36
fedorov wants to merge 1 commit into
mainfrom
feat/mcp-sdk-2-migration

Conversation

@fedorov

@fedorov fedorov commented Jul 31, 2026

Copy link
Copy Markdown
Member

Draft — parked deliberately. MCP SDK 2.0.0 shipped 2026-07-28 with no patch release yet. The work is done and green; revisit once 2.0.x has a fix or two behind it. Meanwhile #35 caps mcp<2.

Stacked on chore/cap-mcp-major (#35) so the diff reads as "lift the cap, port the code". GitHub will retarget this to main when #35 merges.

The rename

FastMCP                              -> MCPServer (mcp.server.mcpserver)
fastmcp.exceptions.ToolError         -> mcpserver.exceptions.ToolError
stateless_http / json_response /     -> streamable_http_app() kwargs
  transport_security / host / port
mcp._mcp_server.version = ...        -> MCPServer(version=...)
call_tool() -> list[ContentBlock]    -> CallToolResult (snake_case fields)
  or (content, structured) tuple

Unchanged, verified: read_resource() still yields ReadResourceContents with .content; list_tools() / list_resources() identical; ToolError still propagates out of call_tool (the pytest.raises tests hold); templated resources (idc://schema/{table}) still resolve; the SDK still registers a single exact-path route, so the /mcp + /mcp/ aliasing in http_app() is still needed and still works.

Two things this cleans up

The private _mcp_server reach-in for serverInfo.version is gone — 2.0 takes version= on the constructor and plumbs it to the handshake, so the comment explaining the reach-in goes with it. And http_app() no longer reads the endpoint path back off server state; it takes streamable_http_path as an argument, which also let the slash-agnostic test drop its FASTMCP_-env caveat.

The sharp edge

transport_security must be passed to streamable_http_app() explicitly. 2.0 enables DNS-rebinding protection by default; without it every request on a hosted domain answers 421 Invalid Host header. My first pass dropped the kwarg and the Cloud Run path would have been dead on arrival. _transport_security() returns disabled by default here (public, unauthenticated, read-only service), so a missing kwarg silently inverts a security posture. The existing HTTP transport tests catch it.

Verification

Full suite green locally against mcp==2.0.0 on Python 3.12:

74 passed in 5.74s

That includes test_parity.py (REST output == MCP output) and both HTTP transport tests, which drive a real initialize handshake through http_app() on /mcp and /mcp/ and assert HSTS.

User-visible

Behaviour is unchanged — same tools, same resources, same response shapes, both slash forms served directly. The one loss is the SDK's undocumented FASTMCP_HOST / FASTMCP_PORT / FASTMCP_STREAMABLE_HTTP_PATH env vars, which no longer have any effect; --host / --port keep their 127.0.0.1:8000 defaults and are what the deployment has always passed. CHANGELOG entry included.

Dependency surface

New transitives: mcp-types, opentelemetry-api, truststore, and httpx2 / httpcore2 (Pydantic's httpx fork). Floors pydantic at 2.12. Worth a look before this lands on an NCI-deployed service — that's part of why it's parked.

🤖 Generated with Claude Code

SDK 2.0 removes `mcp.server.fastmcp`. Port the adapter to the renamed high-level API:

  FastMCP                             -> MCPServer (mcp.server.mcpserver)
  fastmcp.exceptions.ToolError        -> mcpserver.exceptions.ToolError
  stateless_http/json_response/       -> streamable_http_app() kwargs
    transport_security, host, port
  mcp._mcp_server.version = ...       -> MCPServer(version=...)

Two things this cleans up. The private `_mcp_server` reach-in for serverInfo.version is gone —
2.0 takes `version=` on the constructor and plumbs it to the handshake, so the comment
explaining the reach-in goes with it. And `http_app()` no longer reads the endpoint path back
off server state; it takes `streamable_http_path` as an argument, which is also what let the
slash-agnostic test drop its FASTMCP_-env caveat.

`transport_security` must now be passed to `streamable_http_app()` explicitly: 2.0 enables
DNS-rebinding protection by default, and without it every request on a hosted domain answers
421 "Invalid Host header". Covered by the existing HTTP transport tests.

Behaviour is unchanged — same tools, same resources, same response shapes, both `/mcp` and
`/mcp/` still served directly. The one user-visible loss is the SDK's undocumented FASTMCP_*
environment variables; --host/--port keep their 127.0.0.1:8000 defaults.

New transitive dependencies: mcp-types, opentelemetry-api, truststore, and httpx2/httpcore2
(Pydantic's httpx fork). Floors pydantic at 2.12.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Base automatically changed from chore/cap-mcp-major to main July 31, 2026 18:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant