Skip to content

sdk

_sdk

One import site for the MCP SDK, so a renamed module is a one-line fix.

mcp 2.0.0 removed mcp.server.fastmcp. The server class moved to mcp.server.mcpserver and was renamed FastMCP -> MCPServer. Every tool, resource and prompt in this package imported Context from the old path, so a fresh pip install resolving mcp 2.x produced a server that could not import at all, with 26 import sites to chase.

The public API this package uses is unchanged between the two: MCPServer(name, instructions), the tool()/prompt()/resource(uri) decorators, and Context. So supporting both is an import shim rather than a port, and the dependency does not need pinning to a major version.

Functions:

Name Description
build_server

Construct the SDK server and give it a version, on either major.

forbid_unknown_arguments

Make tool name refuse arguments it does not declare.

report_progress

Route SDK 1 progress onto the call's POST stream, not a session GET.

run_server

Run the server; on SDK 2, HTTP without protocol sessions.

unknown_argument_message

The refusal for unknown arguments: what each might have meant, and what exists.

Functions:

build_server

build_server(*, name: str, instructions: str, lifespan, version: str)

Construct the SDK server and give it a version, on either major.

The version is why this is a function rather than a bare class re-export. mcp 1.x has no public way to set it, so the only option was poking server._mcp_server.version. mcp 2.0 accepts version on the constructor and has no _mcp_server at all, so the poke raised AttributeError at import time -- which is a broken server, not a missing version string.

forbid_unknown_arguments

forbid_unknown_arguments(server, name: str) -> None

Make tool name refuse arguments it does not declare.

The SDK's argument model ignores extra keys, so a misspelled filter (node_typ="geo") was dropped and the call answered as if unfiltered, which reads as a filter that matched everything. With extra="forbid" the call fails, and the advertised schema says additionalProperties: false. _tool_manager and fn_metadata are private but identical on 1.x and 2.x, which is why this lives here.

pydantic's own refusal ("Extra inputs are not permitted") names the key but not the argument meant: create_node(node_name=...) left the caller to find name in the schema. A before-validator on a subclass of the tool's model refuses first, with the closest accepted names and the full list (unknown_argument_message).

report_progress async

report_progress(
    ctx, progress: float, total: float | None, message: str
) -> None

Route SDK 1 progress onto the call's POST stream, not a session GET.

Early 1.x (1.21.1) sends it without related_request_id; 1.29.0 already passes it.

run_server

run_server(server, *, transport: str) -> None

Run the server; on SDK 2, HTTP without protocol sessions.

Stateless, a client keeps no Mcp-Session-Id between requests (2026-07-28 clients never had one: SDK 2 serves them statelessly either way). SDK 2 enters the lifespan once per session manager, so the Houdini bridge lives as long as the process. SDK 1 enters it once per stateless request: a new bridge and port scan for every call, and connect_houdini forgotten as soon as it answers. SDK 1 stays stateful.

unknown_argument_message

unknown_argument_message(
    tool: str, unknown: list[str], accepted: set[str]
) -> str

The refusal for unknown arguments: what each might have meant, and what exists.

A name that contains an accepted one comes first (node_name -> name), then the near spellings (node_typ -> node_type).