Skip to content

MCP 2026-07-28 / SDK v2: measured migration state, and the two decisions that need your call #300

Description

@Kayaba-Attribution

MCP protocol revision 2026-07-28 shipped on 2026-07-28, and the TypeScript SDK moved from the monolithic @modelcontextprotocol/sdk (still 1.30.0 on npm) to split v2 packages: @modelcontextprotocol/server, client, core, node, hono, fastify, server-legacy, all at 2.0.0. fastmcp currently pins @modelcontextprotocol/sdk ^1.24.3.

I ran the official codemod against this repo to measure the real cost rather than guess, and I would be glad to do the work. But two of the remaining items are decisions about fastmcp's public API that are yours to make, so I am opening this instead of a PR. Related: #99, #212.

Nothing here is urgent in the sense of a deadline. The SDK's own guide states that v2 speaks the 2025-era protocol unless you explicitly opt in, and the spec now carries a minimum twelve-month deprecation window, so there is room to decide deliberately.

What the codemod does on its own

npx @modelcontextprotocol/codemod@latest v1-to-v2 .

Changes: 86 across 16 file(s)
package.json updated: Removed @modelcontextprotocol/sdk; Added @modelcontextprotocol/client, @modelcontextprotocol/core, @modelcontextprotocol/server
3 location(s) marked with @mcp-codemod-error comments

pnpm install is clean afterwards. vitest run gives 442 passed / 6 failed of 448. src/auth/ is untouched entirely, so the OAuth subsystem is decoupled from the SDK's protocol types. Almost all of it is import consolidation plus setRequestHandler(XSchema, ...) becoming setRequestHandler('x/method', ...).

Also worth knowing: mcp-proxy is not a blocker. Its startHTTPServer takes type ServerLike = { close, connect }, structural typing with no instanceof check, and fastmcp already passes its own FastMCPSession rather than a raw SDK Server. Every mcp-proxy-exercising test passes post-codemod.

The two decisions I cannot make for you

1. tools/call handler signature (src/FastMCP.ts:2176). The one production type error. v2 types this handler as (request, ctx: ServerContext) => InputRequiredResult | ..., so the current plain ContentResult return no longer typechecks. This fails pnpm lint (tsc --noEmit) and the DTS step of pnpm build, so CI is red until it is resolved. Notably this is protocol-revision shape reaching into what is otherwise a mechanical SDK-surface upgrade.

2. Context shape (src/FastMCP.ts:2278). The codemod flagged fastmcp's own Context object literal, because v2 handlers read ctx.mcpReq.* whereas fastmcp's public contract keeps requestId/sessionId at the top level. This is not a mock, it is the public API at src/FastMCP.ts:217-266, so reshaping it is a user-visible change. context.sessionId is documented as coming from the Mcp-Session-Id header, which the new revision removes; FastMCPSession.sessionId/.clientCapabilities/.roots and the connect/disconnect events are in the same position.

The remaining 6 test failures are smaller: two whitebox tests pull handlers out of server._requestHandlers and invoke them with one argument (v2 always passes a second ctx), three assert a doubled "MCP error -32602: MCP error -32602:" prefix that v2 emits once, and one is the custom non-spec notifications/tool/streamContent needing v2's 3-argument setNotificationHandler.

A path that avoids breaking users

Reading the SDK's docs/migration/support-2026-07-28.md, the sanctioned pattern for a project in exactly fastmcp's position (an existing sessionful Streamable HTTP setup) is additive dual-stack rather than an in-place rewrite: leave the current sessionful path untouched, add a separate createMcpHandler entry, and route with isLegacyRequest(request). One caveat I verified in the installed @modelcontextprotocol/server types: createMcpHandler's default legacy: 'stateless' leg builds a fresh instance per request and answers GET/DELETE with 405, so it does not preserve today's session semantics and the routing has to point legacy traffic at a hand-wired handler.

Two things that make this cheaper than it looks: MRTR handlers written once in inputRequired(...) style serve both eras through the SDK's legacy shim, so requestSampling/requestElicitation/roots get rewritten once rather than twice; and ttlMs/cacheScope are emitted automatically by the SDK rather than hand-added per handler.

If you tell me how you want (1) and (2) resolved, I will open the mechanical piece as its own PR with CI green, and keep the architectural piece separate.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions