Files
Max Isbey ec5b2258c9 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.
2026-08-03 15:06:11 +00:00

321 lines
12 KiB
TOML

[project]
name = "mcp"
dynamic = ["version", "dependencies"]
description = "Model Context Protocol SDK"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
maintainers = [
{ name = "David Soria Parra", email = "davidsp@anthropic.com" },
{ name = "Marcelo Trylesinski", email = "marcelotryle@gmail.com" },
{ name = "Max Isbey", email = "maxisbey@anthropic.com" },
{ name = "Felix Weinberger", email = "fweinberger@anthropic.com" },
]
keywords = ["mcp", "llm", "automation"]
license = { text = "MIT" }
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
[project.optional-dependencies]
rich = ["rich>=13.9.4"]
cli = ["typer>=0.16.0", "python-dotenv>=1.0.0"]
[project.scripts]
mcp = "mcp.cli:app [cli]"
[tool.uv]
default-groups = ["dev", "docs"]
required-version = ">=0.9.5"
# PEP 517 build isolation fetches [build-system].requires (and transitives) at
# floating-latest with no hash check on every fresh sync; uv does not lock them
# (astral-sh/uv#5190). Pinning here narrows that to known-good versions. Covers
# the workspace builds (hatchling + uv-dynamic-versioning) and the legacy
# setuptools fallback used by the strict-no-cover git dep.
build-constraint-dependencies = [
"hatchling==1.29.0",
"uv-dynamic-versioning==0.14.0",
"dunamai==1.26.1",
"jinja2==3.1.6",
"markupsafe==3.0.3",
"packaging==26.1",
"pathspec==1.0.4",
"pluggy==1.6.0",
"tomlkit==0.14.0",
"trove-classifiers==2026.1.14.14",
"setuptools==82.0.1",
]
[dependency-groups]
dev = [
# We add mcp[cli] so `uv sync` considers the extras.
"mcp[cli]",
"mcp-example-stories",
# pydantic-settings is used only by examples (simple-auth, mcpserver/text_me);
# keep it reachable for pyright, which covers examples/servers.
"pydantic-settings>=2.5.2",
"tomli>=2.0; python_version < '3.11'",
"pyright>=1.1.400",
"pytest>=8.4.0",
"ruff>=0.8.5",
"trio>=0.26.2",
"pytest-flakefinder>=1.1.0",
"pytest-xdist>=3.6.1",
"pytest-examples>=0.0.14",
"pytest-pretty>=1.2.0",
"inline-snapshot>=0.23.0",
"dirty-equals>=0.9.0",
"coverage[toml]>=7.10.7,<=7.13",
"pillow>=12.0",
"strict-no-cover",
"logfire>=3.0.0",
"opentelemetry-sdk>=1.39.1",
]
docs = [
# Zensical is the Material team's successor to MkDocs; it natively
# re-implements search, glightbox and mkdocstrings but runs no arbitrary
# MkDocs plugins or hooks, so the API reference (formerly gen-files +
# literate-nav) and llms.txt (formerly a hook) are generated by the
# standalone scripts under scripts/docs/. See scripts/docs/build.sh.
# 0.0.48 fixed relative/scoped cross-references for mkdocstrings-python
# (which the mkdocstrings config in mkdocs.yml relies on) but broke
# search; 0.0.50 fixes it. The toolchain is pinned exactly: Zensical is
# pre-1.0 and the build guards key on its rendering behavior, so bumps
# should be deliberate.
"zensical==0.0.50",
# Zensical's mkdocstrings compatibility layer targets the mkdocstrings 1.x /
# mkdocstrings-python 2.0.5+ API (griffe 2 / griffelib); the older
# mkdocstrings 0.30 / python 2.0.1 line renders API pages with an
# unregistered-autorefs KeyError under Zensical.
"mkdocstrings==1.0.4",
"mkdocstrings-python==2.0.5",
# scripts/docs/build_config.py and llms_txt.py read mkdocs.yml directly.
"pyyaml>=6.0.2",
# gen_ref_pages.py imports griffe directly. griffelib is not a typo: it is
# griffe's successor distribution (same author) and still imports as
# `griffe`; the old `griffe` distribution is the incompatible 1.x line.
"griffelib==2.1.0",
]
codegen = ["datamodel-code-generator==0.57.0"]
[build-system]
requires = ["hatchling", "uv-dynamic-versioning"]
build-backend = "hatchling.build"
[tool.hatch.version]
source = "uv-dynamic-versioning"
[tool.uv-dynamic-versioning]
vcs = "git"
style = "pep440"
bump = true
[tool.hatch.metadata.hooks.uv-dynamic-versioning]
dependencies = [
# anyio < 4.10 triggers a compile-time SyntaxWarning on Python 3.14 (PEP 765,
# "'return' in a 'finally' block"); for stdio servers it lands on the child's
# stderr (agronholm/anyio#816, fixed in 4.10).
"anyio>=4.10; python_version >= '3.14'",
"anyio>=4.9; python_version < '3.14'",
"httpx2>=2.5.0",
"mcp-types=={{ version }}",
"pydantic>=2.12.0",
"starlette>=0.48.0; python_version >= '3.14'",
"starlette>=0.27; python_version < '3.14'",
"python-multipart>=0.0.9",
"sse-starlette>=3.0.0",
"uvicorn>=0.31.1; sys_platform != 'emscripten'",
"jsonschema>=4.20.0",
"pywin32>=311; sys_platform == 'win32'",
"pyjwt[crypto]>=2.10.1",
"typing-extensions>=4.13.0",
"typing-inspection>=0.4.1",
"opentelemetry-api>=1.28.0",
]
[project.urls]
Homepage = "https://modelcontextprotocol.io"
Documentation = "https://py.sdk.modelcontextprotocol.io/"
Repository = "https://github.com/modelcontextprotocol/python-sdk"
Issues = "https://github.com/modelcontextprotocol/python-sdk/issues"
[tool.hatch.build.targets.wheel]
packages = ["src/mcp"]
[tool.pyright]
typeCheckingMode = "strict"
include = [
"src/mcp",
"src/mcp-types/mcp_types",
"tests",
"docs_src",
"examples/stories",
"examples/servers",
"examples/snippets",
"examples/clients",
]
venvPath = "."
venv = ".venv"
# `stories` is a workspace package rooted at examples/; the IDE language server
# does not always pick up the editable-install .pth, so resolve it statically.
extraPaths = ["examples"]
# The FastAPI style of using decorators in tests gives a `reportUnusedFunction` error.
# See https://github.com/microsoft/pyright/issues/7771 for more details.
# TODO(Marcelo): We should remove `reportPrivateUsage = false`. The idea is that we should test the workflow that uses
# those private functions instead of testing the private functions directly. It makes it easier to maintain the code source
# and refactor code that is not public.
executionEnvironments = [
{ root = "tests", extraPaths = [
".",
"examples",
], reportUnusedFunction = false, reportPrivateUsage = false },
{ root = "examples/stories", extraPaths = [
"examples",
], reportUnusedFunction = false },
# The `mcp-example-stories` editable install puts `examples/` on sys.path,
# which defeats pyright's auto-detection of `simple-auth/` as a package
# root (it's the one server example that imports itself by absolute name).
{ root = "examples/servers", extraPaths = [
"examples/servers/simple-auth",
], reportUnusedFunction = false },
# docs_src/ holds the complete, runnable code examples included into docs/*.md.
# Decorated (@mcp.tool/...) module-level functions are never called by name.
{ root = "docs_src", reportUnusedFunction = false },
]
[tool.ruff]
line-length = 120
target-version = "py310"
[tool.ruff.lint]
select = [
"C4", # flake8-comprehensions
"C90", # mccabe
"D212", # pydocstyle: multi-line docstring summary should start at the first line
"E", # pycodestyle
"F", # pyflakes
"I", # isort
"PERF", # Perflint
"UP", # pyupgrade
"TID251", # https://docs.astral.sh/ruff/rules/banned-api/
]
ignore = ["PERF203"]
[tool.ruff.lint.flake8-tidy-imports.banned-api]
"pydantic.RootModel".msg = "Use `pydantic.TypeAdapter` instead."
[tool.ruff.lint.mccabe]
max-complexity = 24 # Default is 10
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
# The mcp.types package is an alias that mirrors mcp_types namespaces by design.
"src/mcp/types/*.py" = ["F403"]
# Generated by scripts/gen_surface_types.py: raw datamodel-codegen output (TID251 lifts the repo-wide RootModel ban for these generated validators
# and their shared `WireRootModel` base).
"src/mcp-types/mcp_types/_v*/__init__.py" = ["D212", "E501", "I001", "TID251", "UP007", "UP037"]
"src/mcp-types/mcp_types/_wire_base.py" = ["TID251"]
"tests/server/mcpserver/test_func_metadata.py" = ["E501"]
"tests/shared/test_progress_notifications.py" = ["PLW0603"]
[tool.ruff.lint.pylint]
allow-magic-value-types = ["bytes", "float", "int", "str"]
max-args = 23 # Default is 5
max-branches = 23 # Default is 12
max-returns = 13 # Default is 6
max-statements = 102 # Default is 50
[tool.uv.workspace]
members = ["src/mcp-types", "examples", "examples/clients/*", "examples/servers/*", "examples/snippets"]
[tool.uv.sources]
mcp = { workspace = true }
mcp-example-stories = { workspace = true }
mcp-types = { workspace = true }
strict-no-cover = { git = "https://github.com/pydantic/strict-no-cover" }
[tool.pytest.ini_options]
log_cli = true
xfail_strict = true
markers = [
"requirement(id): links a test to the entry in tests/interaction/_requirements.py it exercises",
]
addopts = """
--color=yes
--capture=fd
-p anyio
-p examples
"""
filterwarnings = [
"error",
# pywin32 internal deprecation warning
"ignore:getargs.*The 'u' format is deprecated:DeprecationWarning",
# SEP-2577 deprecates the roots/sampling/logging methods; the SDK still calls
# them internally (e.g. `ctx.debug` -> `log` -> `send_log_message`), so the
# advisory warning is silenced. Tests asserting it opt back in with pytest.warns.
"ignore:.*is deprecated as of 2026-07-28 \\(SEP-2577\\).:mcp.MCPDeprecationWarning",
# 2026-07-28 restricts progress to server->client; the client send path is
# advisory-deprecated and a handful of tests still exercise it.
"ignore:Client-to-server progress is deprecated as of 2026-07-28.*:mcp.MCPDeprecationWarning",
# 2026-07-28 drops ping; Client.send_ping() is advisory-deprecated and the
# legacy interaction/transport tests still drive it.
"ignore:ping is removed as of 2026-07-28.*:mcp.MCPDeprecationWarning",
]
[tool.markdown.lint]
default = true
MD004 = false # ul-style - Unordered list style
MD007.indent = 2 # ul-indent - Unordered list indentation
MD013 = false # line-length - Line length
MD029 = false # ol-prefix - Ordered list item prefix
MD033 = false # no-inline-html Inline HTML
MD041 = false # first-line-heading/first-line-h1
MD046 = false # indented-code-blocks
MD059 = false # descriptive-link-text
# https://coverage.readthedocs.io/en/latest/config.html#run
[tool.coverage.run]
branch = true
patch = ["subprocess"]
concurrency = ["multiprocessing", "thread"]
source = ["src", "src/mcp-types/mcp_types", "tests"]
omit = [
"src/mcp/client/__main__.py",
"src/mcp/server/__main__.py",
"src/mcp/os/posix/utilities.py",
"src/mcp/os/win32/utilities.py",
]
# https://coverage.readthedocs.io/en/latest/config.html#report
[tool.coverage.report]
fail_under = 100
skip_covered = true
show_missing = true
ignore_errors = true
precision = 2
exclude_also = [
"pragma: lax no cover",
"@overload",
"raise NotImplementedError",
]
# https://coverage.readthedocs.io/en/latest/config.html#paths
[tool.coverage.paths]
source = [
"src/",
"/home/runner/work/python-sdk/python-sdk/src/",
'D:\a\python-sdk\python-sdk\src',
]
[tool.inline-snapshot]
default-flags = ["disable"]
format-command = "ruff format --stdin-filename {filename}"