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.
Cuts import and startup cost across the SDK so that each entry point pays only for what it uses, without adding or removing any public API. It is a much smaller take on the ground #3220 explored: the same big wins from about 440 added / 105 removed lines of product code instead of ~3700 changed, so it can be reverted cleanly if anything shakes loose.
Three things account for essentially all of it:
The type re-exports on mcp and mcp.types are deliberately kept eager, so import mcp remains a real types namespace (~180 ms) rather than an empty shell.
Motivation and Context
On main, import mcp costs about 1.6× what it did on v1, and every deeper import path pays the same, because mcp/__init__.py eagerly binds the whole client and server stack and both per-version wire packages load with the method maps. Around 90 % of the regression is pydantic building model classes at import; the rest is module-graph growth. Stdio servers pay this on every host session start, and libraries pay it just to import a handful of types.
Numbers
Paired geomean ratios from the same fresh-interpreter harness used for the v1-vs-v2 comparison (wheels installed into otherwise-identical CPython 3.14 venvs, round-interleaved arms with an A/A twin per arm; the box is a shared host under load, so the ratios with their 95 % CIs are the finding and the absolute milliseconds are load-inflated; single A/A-gated session per row).
A replay of the module-scope import statements of 26 real consumers (fastmcp, langchain-mcp-adapters, google-adk, openai-agents, litellm, mcpo, an official-servers pattern, …) still resolves all ~450 statements, and every profile is faster than main (0.25×–0.61× depending on how much of the SDK it imports); none is slower.
Where the deferred work goes
Nothing is deleted, only moved off import. Each bill is paid once per process: the first message parsed for a protocol version imports that version's wire package (~50 ms once); the first HTTP app build loads the web stack (~60 ms once); a server's first elicitation-schema render pays the older wire package (~50 ms once); and a warm host's first tools/call carries ~15 ms of deferred validator builds. Steady-state per-message work is unchanged (no new imports, model builds or cache misses on the request path after warm-up).
How Has This Been Tested?
Breaking Changes
No public API is added, removed or renamed; __all__, object identity, subclassing, pickling and warning behaviour are unchanged. The observable-but-incidental differences, all in docs/migration.md:
Types of changes
Checklist
Additional context
AI Disclaimer