[do not merge] Add mcp-codemod, an automated v1 to v2 migration tool#3011
Draft
maxisbey wants to merge 1 commit into
Draft
[do not merge] Add mcp-codemod, an automated v1 to v2 migration tool#3011maxisbey wants to merge 1 commit into
maxisbey wants to merge 1 commit into
Conversation
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Important
DO NOT MERGE. Opening as a draft for design review. The codemod is self-contained
(a new workspace package; nothing in
mcpdepends on it), but the scope, the mappingtables, and the publishing prerequisite below deserve eyes before any of it is real.
Adds
mcp-codemod, a libCST-based tool that automates the mechanical half of the v1 to v2 migration: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, so theremaining work is one grep. Re-running on its own output is a no-op, and
--dry-run(optionally with
--diff) previews a run without writing anything.Motivation and Context
docs/migration.mdis ~1,600 lines, and most of what it asks for is tedium rather thanjudgment: import moves, symbol renames, and the camelCase to snake_case field renames that
touch nearly every file. More prose cannot reduce tedium. A codemod removes exactly that
half, so a reader's (or an agent's) attention goes to the changes that actually need it.
The TypeScript SDK already ships
@modelcontextprotocol/codemodas step 1 of its upgradeguide; this is the Python counterpart.
What it rewrites (each gated on resolving a name through the file's imports, never on
matching text, so an aliased import or an unrelated symbol with the same name is never
touched):
mcp.server.fastmcp->mcp.server.mcpserver,mcp.types->mcp_types(including thefrom mcp import typesform, which needs awhole-statement rewrite),
mcp.shared.version->mcp_types.version.FastMCP->MCPServer,McpError->MCPError,FastMCPError->MCPServerError,streamablehttp_client->streamable_http_client,and the removed
Content/ResourceReferencealiases.McpError(ErrorData(code=..., message=...))to the flatMCPError(...)constructor, ande.error.codetoe.codeinside anexcept McpError as e:block (only the fullthree-part chain -- a bare
e.errormay be a wholeErrorDataand is never collapsed).mcp.typesmodels, restricted to the 40 field names v1actually declared. This matters: in real v1 code most camelCase attribute accesses are
logging.getLogger,.basicConfig, or the user's own attributes, so anything broaderthan an allowlist is unusable.
streamable_http_client(...) as (read, write, _)three-tuple to the v2 two-tuple.What it deliberately only marks (the design rule is: never guess at a change that
depends on information not in the file):
mcp.typesnames with no v2 home (Cursor, theTASK_*constants, thev1 type-machinery aliases).
mcp_typesis not a name-superset of v1'smcp.types,so each of these is marked with its replacement at the import and at every use --
the alternative is rewriting them into an import that cannot resolve, silently. A
test pins, against the installed package, that every public name v1's
mcp.typesdefined either exists on
mcp_typesor is explicitly accounted for, so this listcannot rot as v2 evolves.
streamablehttp_client(...)used anywhere other than directly as awithitem(
enter_async_context(...)is the common form): its result shape changed and onlythe inline
as (read, write, _)form can be rewritten rather than flagged.instructions; v2's istitle, so renaming the call alone would silently send theinstructions as the title), and a camelCase name that one of the file's own classes
declares (renaming its uses would break that class, whose declaration does not change).
@server.call_tool()decorators. They are syntactically identical to thehigh-level
@mcp.tool()ones -- only what the receiver is bound to distinguishes them --and migrating one also means reordering statements and rewriting the handler signature.
bump-pydanticwas eventually archived as "incomplete" largely because it attempted itsequivalent of this and got it wrong often enough to lose trust; the marker names the
exact
on_*=keyword instead.MCPServerconstructor (host=,stateless_http=, ...). Theright destination (
run()/sse_app()/streamable_http_app()) depends on how theserver is started and may be in another file, so the kwarg is left in place -- v2 then
fails loudly -- rather than deleted, which would silently lose configuration.
How Has This Been Tested?
(
./scripts/testis green for the whole tree), strict pyright, ruff.not touch and making code worse, so that is what most of the suite pins, with exact
reproductions: a file that never imports the SDK is never modified even when it spells
tempting names (a local variable named
mcp,getattr(row, "createdAt")on an ORMrow,
self.get_context()in a Django view); nothing is ever rewritten into a silentNameError(import mcpplusmcp.types.Xis marked, not half-rewritten); nothingthat works on v2 is broken (
e.error.message = ...is a write to a still-mutablefield and is left alone; only reads collapse to the new properties) or wrongly marked
(
ctx.request_contextis a live v2 idiom, so that name is deliberately NOT matched);and a re-run of the codemod over its own output is byte-for-byte identical, including
for a marker on the first statement of a module, which libCST parses back into the
module header rather than the statement.
installed v2 surface by construction: a "removed attribute" name may not be spelled by
any living public v2 API (the
request_contextlesson, now a test), and every publicname v1's
mcp.typesdefined must exist onmcp_typesor be explicitly accounted for.they cannot silently drift as v2 evolves: every rename target is
exec'd and mustresolve, every removed API must be provably absent, and no flagged constructor keyword
may survive on
MCPServer.__init__. That last one is not theoretical -- it existsbecause
debug,log_level, anddependenciesall looked removed at one point and areactually still accepted, and a marker on a keyword that works is a lie the user cannot
reconcile.
v1.xandmainwere migrated by hand, so their diff is the correct migration. Running the codemod on
the v1 side and diffing against the human result (all sides normalized through the
repo's own ruff config): of the 51 files with a real migration diff, 13 are reproduced
exactly, 35 partially, and 0 are made worse. The lowlevel examples get ~0% help by
design. Reproduce with
scratch/codemod-spike/eval_production.pyon this branch.Breaking Changes
None. The package is additive;
mcpdoes not depend on it.Types of changes
Checklist
Additional context
Needs an owner before this merges: the PR adds
mcp-codemodto the publishworkflow's build step, so the PyPI project must be registered and a trusted publisher
configured for it (the same dance
mcp-typesneeded) before this lands. Theordering matters: if a release is tagged first, the upload job dies at the unregistered
mcp-codemodwheel aftermcpis uploaded and beforemcp-types, andmcp'sexact pin on
mcp-typesmakes that half-published release uninstallable until the jobis re-run. (Without the workflow change the documented
uvx mcp-codemodfails for everyreader instead, so the docs and the publishing have to land together either way.)
Deliberately not in this PR (each is a clean follow-up, and none should gate the
design review):
mcp.{client,server,shared}.experimentalTasks namespace,
mcp.client.websocket,mcp.shared.progress). Today a removedsymbol is marked wherever it appears, but
import mcp.shared.progressalone is not.docs/migration.mdcodemod-first into the two-journey split theTypeScript guide uses, with the mapping tables linked as the source of truth.
their type-check results, like the TypeScript codemod's
batch-test.AI Disclaimer