Files
Max Isbey 8900f2527c Add mcp-codemod, an automated v1 to v2 migration tool
A new `mcp-codemod` workspace package (`uvx mcp-codemod v1-to-v2 ./src`)
that rewrites every v1 -> v2 change whose meaning is unambiguous from the
file alone, and inserts a `# mcp-codemod:` comment above every site it
recognized but would not guess at. Built on libCST.

Names are resolved through each file's imports, never matched as text, so
an aliased import or an unrelated symbol that shares a name with an SDK
one is never touched. The camelCase to snake_case rename is restricted to
the field names v1's `mcp.types` actually declared. Anything whose correct
rewrite depends on information that is not in the file -- the lowlevel
decorator to `on_*` relocation, the transport keywords on the `MCPServer`
constructor -- is left exactly as written and marked instead, so the
remaining work is one grep. Re-running on the output is a no-op.

The mapping tables are pinned against the installed v2 package by ratchet
tests so they cannot silently drift: every rename target must resolve,
every removed API must be provably absent, and no flagged constructor
keyword may survive on `MCPServer.__init__`. Measured against the example
files that exist on both `v1.x` and `main` (whose diff is the hand-written
migration), the codemod fully reproduces 13 of the 51 with a real
migration diff, improves 35 more, and makes none worse.

Also adds an "Automated migration" section to docs/migration.md, a mention
of the tool in README.v2.md, and the package to the publish workflow's
build step (the PyPI project and its trusted publisher must exist before a
release is tagged with this in it).
2026-07-01 12:08:22 +00:00

311 lines
10 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]",
# The codemod is a standalone tool, not a dependency of `mcp`; pull it in here
# so the workspace's test environment has it.
"mcp-codemod",
"mcp-example-stories",
"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 = [
"mkdocs>=1.6.1",
"mkdocs-gen-files>=0.5.0",
"mkdocs-glightbox>=0.4.0",
"mkdocs-literate-nav>=0.6.1",
"mkdocs-material[imaging]>=9.5.45",
"mkdocstrings-python>=2.0.1",
]
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'",
"httpx>=0.27.1,<1.0.0",
"httpx-sse>=0.4",
"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",
"pydantic-settings>=2.5.2",
"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/v2/"
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-codemod/mcp_codemod",
"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"]
# Generated by scripts/gen_surface_types.py: raw datamodel-codegen output (TID251 lifts the repo-wide RootModel ban for these generated validators).
"src/mcp-types/mcp_types/v*/__init__.py" = ["D212", "E501", "I001", "TID251", "UP007", "UP037"]
"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-codemod",
"src/mcp-types",
"examples",
"examples/clients/*",
"examples/servers/*",
"examples/snippets",
]
[tool.uv.sources]
mcp = { workspace = true }
mcp-codemod = { 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-codemod/mcp_codemod", "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}"