Skip to content

Commit ec8361b

Browse files
committed
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).
1 parent 3b78f86 commit ec8361b

18 files changed

Lines changed: 4041 additions & 9 deletions

.github/workflows/publish-pypi.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ jobs:
3030
run: |
3131
uv build --package mcp
3232
uv build --package mcp-types
33+
uv build --package mcp-codemod
3334
3435
- name: Upload artifacts
3536
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1

README.v2.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
> **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.
1919
>
20-
> 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.
20+
> 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.
2121
>
2222
> **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.**
2323
>

docs/migration.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@ This guide covers the breaking changes introduced in v2 of the MCP Python SDK an
66

77
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.
88

9+
## Automated migration
10+
11+
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:
12+
13+
```bash
14+
uvx mcp-codemod v1-to-v2 ./src
15+
grep -rn '# mcp-codemod:' ./src
16+
```
17+
18+
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).
19+
20+
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.
21+
922
## Breaking Changes
1023

1124
### `MCPServer.call_tool()` returns `CallToolResult`

pyproject.toml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ build-constraint-dependencies = [
5757
dev = [
5858
# We add mcp[cli] so `uv sync` considers the extras.
5959
"mcp[cli]",
60+
# The codemod is a standalone tool, not a dependency of `mcp`; pull it in here
61+
# so the workspace's test environment has it.
62+
"mcp-codemod",
6063
"mcp-example-stories",
6164
"tomli>=2.0; python_version < '3.11'",
6265
"pyright>=1.1.400",
@@ -135,6 +138,7 @@ packages = ["src/mcp"]
135138
typeCheckingMode = "strict"
136139
include = [
137140
"src/mcp",
141+
"src/mcp-codemod/mcp_codemod",
138142
"src/mcp-types/mcp_types",
139143
"tests",
140144
"docs_src",
@@ -213,10 +217,18 @@ max-returns = 13 # Default is 6
213217
max-statements = 102 # Default is 50
214218

215219
[tool.uv.workspace]
216-
members = ["src/mcp-types", "examples", "examples/clients/*", "examples/servers/*", "examples/snippets"]
220+
members = [
221+
"src/mcp-codemod",
222+
"src/mcp-types",
223+
"examples",
224+
"examples/clients/*",
225+
"examples/servers/*",
226+
"examples/snippets",
227+
]
217228

218229
[tool.uv.sources]
219230
mcp = { workspace = true }
231+
mcp-codemod = { workspace = true }
220232
mcp-example-stories = { workspace = true }
221233
mcp-types = { workspace = true }
222234
strict-no-cover = { git = "https://github.com/pydantic/strict-no-cover" }
@@ -265,7 +277,7 @@ MD059 = false # descriptive-link-text
265277
branch = true
266278
patch = ["subprocess"]
267279
concurrency = ["multiprocessing", "thread"]
268-
source = ["src", "src/mcp-types/mcp_types", "tests"]
280+
source = ["src", "src/mcp-codemod/mcp_codemod", "src/mcp-types/mcp_types", "tests"]
269281
omit = [
270282
"src/mcp/client/__main__.py",
271283
"src/mcp/server/__main__.py",

src/mcp-codemod/README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# mcp-codemod
2+
3+
Automated rewrites for migrating code between major versions of the
4+
[MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk).
5+
6+
```bash
7+
uvx mcp-codemod v1-to-v2 ./src
8+
```
9+
10+
It rewrites every change whose meaning is unambiguous from the file alone, and
11+
inserts a `# mcp-codemod:` comment above every site it recognized but would not
12+
guess at. After a run, this is the complete list of what is left for a human:
13+
14+
```bash
15+
grep -rn '# mcp-codemod:' ./src
16+
```
17+
18+
Run it on a clean branch, read the diff, and follow the markers into the
19+
[migration guide](https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/migration.md).
20+
Re-running on its own output is a no-op, so it is safe to apply again after a
21+
manual fix-up.
22+
23+
## What it rewrites
24+
25+
- Import paths that moved (`mcp.server.fastmcp` -> `mcp.server.mcpserver`,
26+
`mcp.types` -> `mcp_types`), including `from mcp import types`.
27+
- Renamed symbols (`FastMCP` -> `MCPServer`, `McpError` -> `MCPError`,
28+
`streamablehttp_client` -> `streamable_http_client`), resolved through the
29+
file's imports so an aliased import or an unrelated symbol with the same name
30+
is never touched.
31+
- `McpError(ErrorData(code=..., message=...))` to the flat `MCPError(...)`
32+
constructor, and `e.error.code` / `e.error.message` / `e.error.data` to
33+
`e.code` / `e.message` / `e.data` inside an `except McpError as e:` block.
34+
- camelCase attribute reads on `mcp.types` models to their snake_case v2
35+
spellings (`.inputSchema` -> `.input_schema`), restricted to the field names
36+
the v1 types actually declared. Other camelCase APIs (`logging.getLogger`, a
37+
receiver that resolves to another package) are never considered, and a name
38+
that one of your own classes declares (`inputSchema` on your own model) is
39+
marked for you to split rather than renamed, since your declaration does not
40+
change.
41+
- The `streamable_http_client(...) as (read, write, _)` three-tuple to the v2
42+
two-tuple.
43+
44+
## What it marks instead
45+
46+
Some changes cannot be made safely without information that is not in the file.
47+
The codemod never guesses at these; it leaves them exactly as written and adds a
48+
`# mcp-codemod:` comment explaining what to do:
49+
50+
- Removed APIs that have no drop-in replacement (`create_connected_server_and_client_session`,
51+
the WebSocket transport, `mcp.shared.progress`, `get_context()`).
52+
- The v1 `mcp.types` names with no v2 home (`Cursor`, the `TASK_*` constants, the
53+
type-machinery aliases). `mcp_types` is not a name-superset of v1's `mcp.types`,
54+
so these are marked with their replacement instead of being rewritten into an
55+
import that cannot resolve.
56+
- A `streamablehttp_client(...)` call used anywhere other than directly as a
57+
`with` item (for example through `AsyncExitStack.enter_async_context`): it now
58+
yields two values, not three, and only the inline `as (read, write, _)` form
59+
can be rewritten safely, so every other form is marked.
60+
- Transport keywords on the `MCPServer` constructor (`host=`, `port=`,
61+
`stateless_http=`, ...), which moved to `run()` or one of the app methods. The
62+
right destination depends on how you start the server, so the kwarg is left in
63+
place -- v2 then fails loudly -- rather than silently dropped.
64+
- Lowlevel `@server.call_tool()` decorators, which became `on_call_tool=`
65+
constructor arguments with a different handler signature. Rewriting the
66+
registration also means rewriting the handler body, which is yours to do.
67+
- Renames the codemod applied but cannot prove are right: a camelCase rename
68+
whose receiver could plausibly not be an mcp type gets a `# mcp-codemod: review:`
69+
marker so you look at it instead of trusting it.
70+
71+
`--dry-run` writes nothing, and `--diff` prints a unified diff of every change;
72+
combine the two to preview a run.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""Automated rewrites for migrating code between major versions of the MCP Python SDK.
2+
3+
Run it as a tool:
4+
5+
uvx mcp-codemod v1-to-v2 ./src
6+
7+
or call it as a library:
8+
9+
from mcp_codemod import transform
10+
11+
result = transform(source)
12+
print(result.code)
13+
14+
Every rewrite is conservative by construction: names are resolved through the file's
15+
imports rather than matched as text, and anything whose correct rewrite depends on
16+
information that is not in the file gets an inline `# mcp-codemod:` comment instead
17+
of a guess. `grep -rn '# mcp-codemod:'` after a run is the complete list of what is
18+
left for a human.
19+
"""
20+
21+
from mcp_codemod._transformer import MARKER, Diagnostic, Result, transform
22+
23+
__all__ = ["MARKER", "Diagnostic", "Result", "transform"]

0 commit comments

Comments
 (0)