Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:
run: |
uv build --package mcp
uv build --package mcp-types
uv build --package mcp-codemod

- name: Upload artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand Down
2 changes: 1 addition & 1 deletion README.v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

> **Important: this documents v2 of the SDK, which is in alpha.** Pre-releases are published to PyPI as `2.0.0aN`, and each alpha may contain breaking changes from the previous one.
>
> v2 is a major rework of the SDK, both to support the [2026-07-28 MCP specification release](https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/) and to fix long-standing architectural issues. See the [migration guide](https://py.sdk.modelcontextprotocol.io/v2/migration/) for what's changed. We're targeting a beta on 2026-06-30 and a stable v2 on 2026-07-27, alongside the spec release. Before stable, we plan to add a significant set of backwards compatibility shims so the final upgrade is much smaller than today's diff.
> v2 is a major rework of the SDK, both to support the [2026-07-28 MCP specification release](https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/) and to fix long-standing architectural issues. See the [migration guide](https://py.sdk.modelcontextprotocol.io/v2/migration/) for what's changed; `uvx mcp-codemod v1-to-v2 ./src` automates the mechanical half of it and marks the rest with `# mcp-codemod:` comments. We're targeting a beta on 2026-06-30 and a stable v2 on 2026-07-27, alongside the spec release. Before stable, we plan to add a significant set of backwards compatibility shims so the final upgrade is much smaller than today's diff.
>
> **v1.x is the only stable release line and remains recommended for production.** It is in maintenance mode and continues to receive critical bug fixes and security patches. Installers never select a pre-release unless you opt in (for example `pip install mcp==2.0.0a3`), so existing installs are unaffected. **If your package depends on `mcp`, add a `<2` upper bound to your version constraint (for example `mcp>=1.27,<2`) before the stable release lands.**
>
Expand Down
13 changes: 13 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ This guide covers the breaking changes introduced in v2 of the MCP Python SDK an

Version 2 of the MCP Python SDK introduces several breaking changes to improve the API, align with the MCP specification, and provide better type safety.

## Automated migration

The `mcp-codemod` tool (published from `src/mcp-codemod` in this repository) rewrites every change in this guide whose meaning is unambiguous from the file alone -- the import moves, the symbol renames, the `MCPError` reshape, and the camelCase to snake_case field renames -- and inserts a `# mcp-codemod:` comment above every site it recognized but would not guess at. Run it on a clean branch first, then work through what it marked:

```bash
uvx mcp-codemod v1-to-v2 ./src
grep -rn '# mcp-codemod:' ./src
```

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. Re-running on its own output is a no-op, so it is safe to apply again after a manual fix-up. To preview without writing anything, pass `--dry-run` (add `--diff` to see the full unified diff).

The sections below remain the reference for the changes it cannot make for you: the lowlevel `Server` handler rewrite, relocating transport keyword arguments off the `MCPServer` constructor, and every behavioural change that has no source-level signature.

## Breaking Changes

### `MCPServer.call_tool()` returns `CallToolResult`
Expand Down
16 changes: 14 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ build-constraint-dependencies = [
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",
Expand Down Expand Up @@ -135,6 +138,7 @@ packages = ["src/mcp"]
typeCheckingMode = "strict"
include = [
"src/mcp",
"src/mcp-codemod/mcp_codemod",
"src/mcp-types/mcp_types",
"tests",
"docs_src",
Expand Down Expand Up @@ -213,10 +217,18 @@ 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"]
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" }
Expand Down Expand Up @@ -265,7 +277,7 @@ MD059 = false # descriptive-link-text
branch = true
patch = ["subprocess"]
concurrency = ["multiprocessing", "thread"]
source = ["src", "src/mcp-types/mcp_types", "tests"]
source = ["src", "src/mcp-codemod/mcp_codemod", "src/mcp-types/mcp_types", "tests"]
omit = [
"src/mcp/client/__main__.py",
"src/mcp/server/__main__.py",
Expand Down
72 changes: 72 additions & 0 deletions src/mcp-codemod/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# mcp-codemod

Automated rewrites for migrating code between major versions of the
[MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk).

```bash
uvx mcp-codemod v1-to-v2 ./src
```

It rewrites every 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. After a run, this is the complete list of what is left for a human:

```bash
grep -rn '# mcp-codemod:' ./src
```

Run it on a clean branch, read the diff, and follow the markers into the
[migration guide](https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/migration.md).
Re-running on its own output is a no-op, so it is safe to apply again after a
manual fix-up.

## What it rewrites

- Import paths that moved (`mcp.server.fastmcp` -> `mcp.server.mcpserver`,
`mcp.types` -> `mcp_types`), including `from mcp import types`.
- Renamed symbols (`FastMCP` -> `MCPServer`, `McpError` -> `MCPError`,
`streamablehttp_client` -> `streamable_http_client`), resolved through the
file's imports so an aliased import or an unrelated symbol with the same name
is never touched.
- `McpError(ErrorData(code=..., message=...))` to the flat `MCPError(...)`
constructor, and `e.error.code` / `e.error.message` / `e.error.data` to
`e.code` / `e.message` / `e.data` inside an `except McpError as e:` block.
- camelCase attribute reads on `mcp.types` models to their snake_case v2
spellings (`.inputSchema` -> `.input_schema`), restricted to the field names
the v1 types actually declared. Other camelCase APIs (`logging.getLogger`, a
receiver that resolves to another package) are never considered, and a name
that one of your own classes declares (`inputSchema` on your own model) is
marked for you to split rather than renamed, since your declaration does not
change.
- The `streamable_http_client(...) as (read, write, _)` three-tuple to the v2
two-tuple.

## What it marks instead

Some changes cannot be made safely without information that is not in the file.
The codemod never guesses at these; it leaves them exactly as written and adds a
`# mcp-codemod:` comment explaining what to do:

- Removed APIs that have no drop-in replacement (`create_connected_server_and_client_session`,
the WebSocket transport, `mcp.shared.progress`, `get_context()`).
- The v1 `mcp.types` names with no v2 home (`Cursor`, the `TASK_*` constants, the
type-machinery aliases). `mcp_types` is not a name-superset of v1's `mcp.types`,
so these are marked with their replacement instead of being rewritten into an
import that cannot resolve.
- A `streamablehttp_client(...)` call used anywhere other than directly as a
`with` item (for example through `AsyncExitStack.enter_async_context`): it now
yields two values, not three, and only the inline `as (read, write, _)` form
can be rewritten safely, so every other form is marked.
- Transport keywords on the `MCPServer` constructor (`host=`, `port=`,
`stateless_http=`, ...), which moved to `run()` or one of the app methods. The
right destination depends on how you start the server, so the kwarg is left in
place -- v2 then fails loudly -- rather than silently dropped.
- Lowlevel `@server.call_tool()` decorators, which became `on_call_tool=`
constructor arguments with a different handler signature. Rewriting the
registration also means rewriting the handler body, which is yours to do.
- Renames the codemod applied but cannot prove are right: a camelCase rename
whose receiver could plausibly not be an mcp type gets a `# mcp-codemod: review:`
marker so you look at it instead of trusting it.

`--dry-run` writes nothing, and `--diff` prints a unified diff of every change;
combine the two to preview a run.
23 changes: 23 additions & 0 deletions src/mcp-codemod/mcp_codemod/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Automated rewrites for migrating code between major versions of the MCP Python SDK.

Run it as a tool:

uvx mcp-codemod v1-to-v2 ./src

or call it as a library:

from mcp_codemod import transform

result = transform(source)
print(result.code)

Every rewrite is conservative by construction: names are resolved through the file's
imports rather than matched as text, and anything whose correct rewrite depends on
information that is not in the file gets an inline `# mcp-codemod:` comment instead
of a guess. `grep -rn '# mcp-codemod:'` after a run is the complete list of what is
left for a human.
"""

from mcp_codemod._transformer import MARKER, Diagnostic, Result, transform

__all__ = ["MARKER", "Diagnostic", "Result", "transform"]
Loading
Loading