You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Cut import and startup cost with deferred model builds and lazy imports
Every import path now pays only for what it uses, with no public API
added or removed:
- The protocol models (mcp.types / mcp_types, incl. the JSON-RPC
envelopes and the generated per-version wire packages) build their
pydantic validators on first use instead of at import (defer_build),
through one shared private base class. First-use builds are
serialised behind a single process-wide lock, since released pydantic
does not make concurrent first use of a deferred model thread-safe;
this also fixes a pre-existing concurrent-first-use failure that
reproduces on main.
- `import mcp` binds the client/server names lazily on first attribute
access (PEP 562) instead of importing both stacks eagerly, and the
client no longer imports the server, so client entry points stop
loading the server, the web stack, httpx2 and cryptography.
- The web application stack (starlette's app machinery, sse_starlette,
uvicorn) loads with the app builders that use it, and each protocol
version's wire package loads on the first message parsed for that
version rather than both loading at import.
On the fresh-interpreter harness `import mcp` is ~0.4x of v1 (main is
~1.6x), the client entry points ~0.6x of v1, `import mcp.server.mcpserver`
~0.7x, and time-to-ready / stdio cold start land at parity with v1. RSS
after `import mcp` is 19 MiB (v1 43.5, main 57). Steady-state per-call
latency is unchanged.
Observable-but-incidental differences (removed incidental namespace
bindings, deeper submodules no longer imported as a side effect of a
bare `import mcp`, get_type_hints needing localns= for a documented set
of callables, pre-first-use introspection) are catalogued in
docs/migration.md; ratchet tests pin the import footprints and the
concurrent-first-use safety.
Delete any shim that accepted or synthesized null-id error responses. Code that assumed `error.id` was always a `str | int` must now handle `None`, and tests that pinned v1's rejection of `"id": null` now fail because validation succeeds.
642
662
663
+
### Protocol models build their validators on first use
664
+
665
+
The protocol models (`mcp.types` / `mcp_types`, including the JSON-RPC envelopes) now build
666
+
their pydantic validators on a model's first use in the process instead of at import
667
+
(`defer_build`), which is most of the SDK's startup cost. Validation, serialization, JSON
668
+
schemas, and everything after a model's first use are unchanged (one wrinkle: `inspect.signature`
669
+
on the `model_rebuild` / `model_json_schema` classmethods shows the SDK's evaluated annotations
670
+
rather than pydantic's stringised type aliases — parameter names, kinds and defaults are
671
+
identical). Two things are observable *before* a model's first use:
672
+
673
+
*`inspect.signature(Model)` / `help(Model)` show pydantic's generic `(**data)` initializer
674
+
and `Model.__pydantic_complete__` is `False`. A few models (`CallToolRequest`,
behaved this way; it now applies to all of them until first use. Use the model once, or call
677
+
`Model.model_rebuild()`, when you need the resolved signature earlier.
678
+
* Every model's MRO gains one private base (`mcp_types._wire_base.DeferredModel`) between it
679
+
and `pydantic.BaseModel`, visible only to code that walks `__mro__`.
680
+
* The module-level parse adapters (`client_request_adapter`, ..., `jsonrpc_message_adapter`)
681
+
are instances of a private `TypeAdapter` subclass (`mcp_types._wire_base.DeferredAdapter`) so
682
+
their first-use build takes the same lock; `isinstance(adapter, TypeAdapter)` and every
683
+
documented `TypeAdapter` operation are unchanged.
684
+
685
+
The one-time build cost moves from `import` to each model's first use — a few milliseconds
686
+
for the first message a connection parses; see [Startup cost](advanced/startup.md).
687
+
688
+
The per-version wire types behind the `mcp_types.methods` surface maps
689
+
(`CLIENT_REQUESTS`, `SERVER_RESULTS`, ...) go further: a version's wire types load on the
690
+
first row read for that version — in practice with the first message a connection parses —
691
+
rather than at `import mcp_types.methods`, so a process loads only the protocol version it
692
+
negotiates. Every documented map operation is unchanged (lookup, `in`, iteration, `len`,
693
+
`get`, `==`, spreading into an extension map, `repr`); the whole-map reads among them
694
+
load both versions at that moment. Three obscurities are observable: the maps'
695
+
non-`Mapping` dict extras are gone (`.copy()`, `|`, `reversed()`, and `keys()`/`values()`/
696
+
`items()` return view objects), the internal `mcp_types.methods.v2025`/`v2026` attributes
697
+
are import-free stand-ins rather than the wire-package modules, and the wire types are no
698
+
longer bound in `mcp.server.elicitation`. Import the generated packages
699
+
(`mcp_types._v2025_11_25`, `mcp_types._v2026_07_28`) directly if you need them, though the
700
+
version-free `mcp.types` models remain the supported surface.
701
+
643
702
## MCPServer (formerly FastMCP)
644
703
645
704
### `FastMCP` renamed to `MCPServer`
@@ -877,6 +936,27 @@ Beyond the constructor parameters that moved to `run()`/`streamable_http_app()`
877
936
878
937
Only private attributes moved: `mcp._mcp_server` is now `mcp._lowlevel_server` (see [Registering lowlevel handlers from `MCPServer`](#registering-lowlevel-handlers-from-mcpserver)), and `_session_manager` now lives on that lowlevel `Server`. Prefer the public `mcp.session_manager` property to either.
879
938
939
+
### The server modules no longer import the HTTP stack
940
+
941
+
`mcp.server.lowlevel.server` and `mcp.server.mcpserver.server` used to import the Streamable
942
+
HTTP / SSE stack at module top, so any server — including a stdio one — loaded starlette,
943
+
`sse_starlette`, and `uvicorn` at import. That stack now loads inside `streamable_http_app()`,
944
+
`sse_app()`, and `custom_route()`, their only users; a stdio server never pays for it. Two
945
+
things follow:
946
+
947
+
* The HTTP names that were only incidentally reachable as attributes of those two modules
*`typing.get_type_hints()` on the HTTP-app methods (`streamable_http_app`, `sse_app`,
955
+
`run_sse_async`, `run_streamable_http_async`, and the `session_manager` properties) raises
956
+
`NameError`, because their annotations name types those modules import for type checkers only;
957
+
pass them yourself as `localns={...}` if you evaluate the hints at runtime. See
958
+
[Startup cost](advanced/startup.md).
959
+
880
960
### `MCPServer.get_context()` removed
881
961
882
962
`MCPServer.get_context()` has been removed. Context is now injected by the framework and passed explicitly — there is no ambient ContextVar to read from.
@@ -2056,6 +2136,21 @@ result = await client.call_tool("long_running_task", {}, progress_callback=on_pr
2056
2136
2057
2137
Also drop `execution=ToolExecution(taskSupport=types.TASK_REQUIRED)` from tool definitions: the `TASK_REQUIRED` / `TASK_OPTIONAL` / `TASK_FORBIDDEN` constants are gone from `mcp.types` (`ToolExecution.task_support` takes the plain `"required"` / `"optional"` / `"forbidden"` literal), and no v2 client or server reads the field.
2058
2138
2139
+
### `mcp.client.client` no longer imports the server stack
2140
+
2141
+
The client module no longer imports the server, so names that were only incidentally
2142
+
reachable as attributes of `mcp.client.client` (`Server`, `MCPServer`, `modern_on_request`,
2143
+
`InMemoryTransport`, `streamable_http_client`) are no longer bound there. Import and
2144
+
`mock.patch` them at their own modules: `mcp.server.Server`, `mcp.server.mcpserver.MCPServer`,
Server-side transport entry points (`stdio_server()`, `SseServerTransport`, `StreamableHTTPSessionManager`) keep their v1 import paths and signatures (see [Lowlevel `Server`: what did not change](#lowlevel-server-what-did-not-change)), so the sections below are client-side apart from [`stdio_server` keeps the protocol streams on private descriptors](#stdio_server-keeps-the-protocol-streams-on-private-descriptors); the other server-side transport changes ([lifespan entered once](#streamable-http-lifespan-now-entered-once-at-manager-startup), the [4 MiB request-body limit](#streamable-http-request-bodies-are-limited-to-4-mib)) sit under MCPServer.
0 commit comments