Codemod that migrates MCP Python SDK v1 code to v2 (spec 2026-07-28).
Project description
mcp-codemod
Migrates MCP Python SDK v1 code to v2 (the 2026-07-28 spec revision).
The TypeScript and Go SDKs ship a codemod for this migration. Python does not. Python developers get a 2,879-line migration guide and a lot of manual find-and-replace. This tool fills that gap.
Installation
pipx install mcp-codemod
pipx is the recommended way to install a command-line tool. It keeps the tool
in its own environment and puts the command on your PATH.
If you would rather install into an existing virtualenv, pip works too:
pip install mcp-codemod
Note that on Ubuntu 24.04, Debian 12, Fedora, and other distributions that mark
the system Python as externally managed (PEP 668), a bare pip install outside
a virtualenv will be refused. Use pipx there.
Usage
mcp-codemod path/to/your/server # dry run, prints a diff
mcp-codemod path/to/your/server --write # applies the changes
Dry run is the default. Review the diff before writing.
Why not find-and-replace
The v2 models still accept camelCase at construction time. Only attribute access changed:
tool = Tool(name="forecast", inputSchema={"type": "object"}) # valid in v2
print(tool.inputSchema) # broken in v2
Running sed -i 's/inputSchema/input_schema/g' breaks the first line while
fixing the second. This tool handles both correctly because it operates on the
concrete syntax tree, where a keyword argument is a Name node and attribute
access is an Attribute node. Visiting only Attribute nodes cannot reach the
keyword argument.
tool = Tool(name="forecast", inputSchema={"type": "object"})
-print(tool.inputSchema, tool.outputSchema)
+print(tool.input_schema, tool.output_schema)
The implementation uses LibCST rather
than the standard library ast module. A round trip through ast discards
formatting: it reflows the file, strips comments, and normalises quote style.
LibCST preserves every byte it does not deliberately change, so the resulting
diff contains only the migration.
Changes applied automatically
| Change | Example |
|---|---|
FastMCP to MCPServer |
mcp = FastMCP("x") becomes mcp = MCPServer("x") |
mcp.server.fastmcp.* to mcp.server.mcpserver.* |
includes all submodules |
McpError to MCPError |
|
| camelCase to snake_case attribute access | 11 fields including inputSchema, isError, nextCursor, mimeType |
ctx.fastmcp to ctx.mcp_server |
only on parameters annotated Context |
Content to ContentBlock |
|
ResourceReference to ResourceTemplateReference |
|
ClientRequestType to ClientRequest |
and the five other *Type unions |
streamablehttp_client to streamable_http_client |
|
mcp.shared.version to mcp.types.version |
module was removed in v2 |
timedelta timeouts to float seconds |
read_timeout_seconds=timedelta(minutes=2) becomes =120 |
Changes reported for manual review
Some v2 changes depend on runtime types, or on intent that the source code does not express. Applying those automatically would risk introducing bugs, so they are reported and the source is left unmodified.
| Code | Description |
|---|---|
| F001 | model_dump() without by_alias=True |
| F002 | .root access on a union that is no longer a RootModel |
| F003 | httpx or httpx-sse imported, SDK moved to httpx2 |
| F004 | Name removed in v2 with no drop-in replacement (Cursor, AnyFunction, others) |
| F005 | RequestParams.Meta is now a TypedDict, attribute access becomes .get() |
| F006 | Deprecated name (SUPPORTED_PROTOCOL_VERSIONS) |
| F007 | timedelta timeout too dynamic to convert safely |
| F008 | Lowlevel @server.list_tools() decorator, now an on_list_tools= constructor parameter |
| F009 | Transport parameter (host, port, stateless_http, others) still on the MCPServer constructor |
F001 deserves particular attention. In v1, model_dump() emitted camelCase
because the model fields themselves were camelCase. In v2 the same call emits
snake_case, which other MCP implementations will not recognise. The migration
guide describes the consequence directly:
No error is raised; the output is silently in the wrong shape.
This is not corrected automatically because the receiver cannot be statically
proven to be an MCP protocol type. Adding by_alias=True to an unrelated
Pydantic model would corrupt that model's output instead.
F009 is worth reading too. Eleven transport parameters moved off the
MCPServer constructor and onto run() in v2, so this still raises
TypeError at startup even after the class is renamed:
mcp = MCPServer("demo", host="127.0.0.1", port=8788) # crashes in v2
They are not moved automatically because the destination is a different call site, which may be elsewhere in the file, in another module, or absent when the server is mounted as an ASGI app.
Imports that are intentionally left alone
The mcp.types module moved to a standalone mcp-types distribution, but this
is a no-op for projects that depend on mcp. The migration guide is explicit:
mcp.typesis a permanent alias that mirrorsmcp_typesexactly. Keep importing throughmcp, the package you actually depend on, rather than writingimport mcp_types, which would reach past your declared dependency into a transitive one.
Rewriting those imports would introduce an undeclared dependency, so
mcp-codemod never does. There is a test covering this.
Relationship to mcp-migration
mcp-migration detects behavioural
hazards that a codemod cannot rewrite, such as in-memory state mutated inside
tool handlers, session-id dependencies, and live server readiness. It is not a
codemod. This tool is not a hazard detector. The two are complementary:
mcp-codemod . # apply the mechanical changes
mcp-migration scan . # then check for behavioural hazards
Limitations
- Coverage is the migration guide's "changes almost every project hits" table plus the removed-alias set. The full guide is 2,879 lines and this tool does not implement all of it.
- Rewrites are skipped in files that do not import
mcp. A module that handles MCP types without importing frommcpwill not be processed. - Symbol renames are name-based. A local variable named
Contentin a file that also importsmcpwould be renamed. Review the diff. - Import order is left untouched. Use
isortif you need it.
Development
pip install -e ".[dev]"
pytest
Author
Sanjay Keerthan (@8crsk)
License
MIT. See LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mcp_codemod-0.1.2.tar.gz.
File metadata
- Download URL: mcp_codemod-0.1.2.tar.gz
- Upload date:
- Size: 20.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
860de78019ec14d590a19588bfda6107adcf86c5064526ab781c55c7365a142d
|
|
| MD5 |
74641f63bc0481a3cc4ce1d802c4bf3a
|
|
| BLAKE2b-256 |
149d16637b442e45ffe3dba445fae05933e72c0e3f8115dda9094db81c447848
|
Provenance
The following attestation bundles were made for mcp_codemod-0.1.2.tar.gz:
Publisher:
publish.yml on 8crsk/mcp-codemod
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mcp_codemod-0.1.2.tar.gz -
Subject digest:
860de78019ec14d590a19588bfda6107adcf86c5064526ab781c55c7365a142d - Sigstore transparency entry: 2312757087
- Sigstore integration time:
-
Permalink:
8crsk/mcp-codemod@27cabeb1ab1ba2802eafa4cdcc9fb2cbe00ff5b1 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/8crsk
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@27cabeb1ab1ba2802eafa4cdcc9fb2cbe00ff5b1 -
Trigger Event:
release
-
Statement type:
File details
Details for the file mcp_codemod-0.1.2-py3-none-any.whl.
File metadata
- Download URL: mcp_codemod-0.1.2-py3-none-any.whl
- Upload date:
- Size: 17.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
df94dea2032ba22de9aa30e66e4432961f59813c844c8c61760d6403fc7b86b4
|
|
| MD5 |
070b6139e7bdcb6049e5e295027002b1
|
|
| BLAKE2b-256 |
bd72c16367569d7e498cc2f1767eaad177e34a1086736b1a2e6201a45e0883f1
|
Provenance
The following attestation bundles were made for mcp_codemod-0.1.2-py3-none-any.whl:
Publisher:
publish.yml on 8crsk/mcp-codemod
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mcp_codemod-0.1.2-py3-none-any.whl -
Subject digest:
df94dea2032ba22de9aa30e66e4432961f59813c844c8c61760d6403fc7b86b4 - Sigstore transparency entry: 2312757141
- Sigstore integration time:
-
Permalink:
8crsk/mcp-codemod@27cabeb1ab1ba2802eafa4cdcc9fb2cbe00ff5b1 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/8crsk
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@27cabeb1ab1ba2802eafa4cdcc9fb2cbe00ff5b1 -
Trigger Event:
release
-
Statement type: