MCP TypeScript SDK v2 Migration Guide for MCP Apps

MCP TypeScript SDK v2 splits server, client, schema, and runtime adapters while keeping the MCP App view protocol separate.
The MCP TypeScript SDK v2 changes the server code under an MCP App. The single @modelcontextprotocol/sdk package becomes a set of smaller packages, Node.js 20 becomes the minimum, some registration APIs change, and transport imports move.
The UI inside ChatGPT, Claude, or another host uses a separate MCP Apps bridge, so most view code does not need a rewrite. The hard part is keeping the full contract intact while the server changes.
TL;DR: Run the official codemod from the package root, then review its work instead of treating it as a finished migration. Move server imports to @modelcontextprotocol/server, choose the correct Node or web-standard transport package, replace old registration calls with registerTool and registerResource, and keep the MCP App view on @modelcontextprotocol/ext-apps. Upgrade the SDK API first without changing the wire protocol. After that passes, adopt the 2026-07-28 protocol as a separate change.
Why MCP App Migrations Need More Than a Type-Check
A plain MCP server can pass a tool test when tools/list and tools/call work. An MCP App has more linked pieces:
- The tool descriptor must point to the right
ui://resource. - The resource must return HTML with
text/html;profile=mcp-app. - The tool result must contain the data shape the view expects.
- The host must send that input and result through the app bridge.
- App-only tool calls, links, display modes, files, and other host features must still work.
A codemod can update an import. It cannot tell whether a ChatGPT App now opens with a blank iframe or whether a Claude Connector lost its app-only refresh action.
Treat the migration as three contracts:
| Contract | What changes | How to verify it |
|---|---|---|
| TypeScript SDK API | Packages, imports, handlers, schemas, errors | Type-check and unit tests |
| MCP server protocol | Tools, resources, transport, version negotiation | Protocol and conformance tests |
| MCP App runtime | Resource rendering, tool data, host bridge actions | Local host replicas and live smoke tests |
This split also makes failures easier to place. If resources/read returns the wrong MIME type, debug the server contract. If the resource is valid but a display-mode button fails, debug the app-to-host bridge.
Start With a Reversible Baseline
The official v2 migration guide tells you to run the codemod at the package root because it also updates package.json, tests, scripts, and fixtures.
Before that, capture the current behavior:
node --version
pnpm typecheck
pnpm test
git status --short
Use Node.js 20 or newer. Commit the current state or create a named stash, then save responses for:
tools/listresources/list, if the server lists UI resourcesresources/readfor each linkedui://URI- One successful and one failed
tools/callper UI tool - App-only tool discovery and calls
Do not compare raw JSON snapshots without thought. Package upgrades can change harmless ordering or capability details. Assert the fields that form your app contract: names, schemas, _meta.ui, resource MIME types, tool result content, structuredContent, and error state.
Run the Official Codemod
Run this from the package that owns the MCP dependency:
npx @modelcontextprotocol/codemod@latest v1-to-v2 .
Then find every marker and remaining v1 import:
rg '@mcp-codemod-error|@modelcontextprotocol/sdk' .
The codemod handles fixed rewrites, including:
- v1 import paths to the new packages
.tool(),.prompt(), and.resource()to registration methods- raw Zod shapes to schema objects in common cases
- request handler context renames
- many renamed errors and protocol types
- dependency changes in the nearest
package.json
It cannot decide which runtime transport you need, repair custom dependency injection, choose error-handling behavior, or update every monorepo package. Read the diff before installing dependencies.
In a pnpm workspace, check each member that imports MCP packages:
rg -l '@modelcontextprotocol/(sdk|server|client|core|node|express|hono|fastify)' \
--glob 'package.json' \
--glob '*.{ts,tsx,js,jsx,mjs,cjs}'
Each package should declare what its own shipped code imports. Avoid relying on an accidental root-level hoist.
Understand the v2 Package Split
The old package handled server, client, protocol schemas, and runtime adapters. v2 separates those jobs. The MCP Apps packages stay separate from that split:
| Package after migration | Use it for |
|---|---|
@modelcontextprotocol/server | McpServer, server types, web-standard server transport, modern handlers |
@modelcontextprotocol/client | MCP client code and client transports |
@modelcontextprotocol/core | Public Zod protocol schema constants |
@modelcontextprotocol/node | Node HTTP transport and middleware |
@modelcontextprotocol/express | Express integration |
@modelcontextprotocol/hono | Hono integration |
@modelcontextprotocol/fastify | Fastify integration |
@modelcontextprotocol/ext-apps | MCP App view-to-host communication |
@modelcontextprotocol/ext-apps/server | MCP App tool and resource helpers |
A typical Node-based MCP App server changes these imports:
// v1
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
// v2
import { McpServer } from '@modelcontextprotocol/server';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
The MCP App helpers keep their own import:
import {
registerAppResource,
registerAppTool,
RESOURCE_MIME_TYPE,
} from '@modelcontextprotocol/ext-apps/server';
The app view also stays on the MCP Apps package:
import { App } from '@modelcontextprotocol/ext-apps';
// or React hooks from '@modelcontextprotocol/ext-apps/react'
That separation matters. The server SDK speaks MCP between the host and your backend. The app SDK speaks the MCP Apps bridge between the sandboxed view and the host.
Check the MCP Apps Peer Dependency
Do not assume every @modelcontextprotocol/ext-apps release has moved its own internals to SDK v2. Check the version you resolved:
pnpm why @modelcontextprotocol/ext-apps
pnpm why @modelcontextprotocol/sdk
At publication, @modelcontextprotocol/ext-apps 1.7.5 still declares the v1 SDK as a peer and imports it for view protocol code. That can leave @modelcontextprotocol/sdk v1 in the dependency tree after your server imports move to v2. The two generations can coexist because the v2 server uses a different package name.
Do not use --force, delete the peer, or cast away incompatible helper types. If your installed registerAppTool or registerAppResource types do not accept the v2 McpServer, you have two safe options:
- Upgrade to an
ext-appsrelease that declares v2 server support. - Register the tool and resource directly with the v2 server, including
_meta.ui.resourceUri,_meta.ui.visibility,RESOURCE_MIME_TYPE, and any resource metadata, while keepingext-appsin the view bundle.
This is also why rg '@modelcontextprotocol/sdk' . can return valid dependency code after the migration. The result you need to clear is a direct v1 import in your server source, not every transitive copy in node_modules.
Choose the Transport by Runtime
The v2 codemod cannot know where your server runs.
Use NodeStreamableHTTPServerTransport from @modelcontextprotocol/node when the handler receives Node’s IncomingMessage and ServerResponse objects:
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
Use WebStandardStreamableHTTPServerTransport from @modelcontextprotocol/server when the runtime gives you a web-standard Request and expects a Response. That is the usual fit for Cloudflare Workers, Deno, and Bun.
If you use stdio, import StdioServerTransport from @modelcontextprotocol/server/stdio. The v2 package root stays runtime-neutral, so it does not export the stdio transport.
Retest request body parsing, session behavior, CORS, Origin validation, authentication middleware, and close handling. A transport that compiles can still behave differently at the HTTP boundary.
Update Tool and Resource Registration
The old variadic methods are gone. Direct McpServer users should use config objects and Standard Schema-compatible schemas:
import { McpServer } from '@modelcontextprotocol/server';
import { z } from 'zod';
const server = new McpServer({
name: 'weather-app',
version: '2.0.0',
});
server.registerTool(
'show-weather',
{
title: 'Show Weather',
description: 'Show the current weather for a city',
inputSchema: z.object({
city: z.string().min(1),
}),
outputSchema: z.object({
city: z.string(),
temperatureC: z.number(),
}),
_meta: {
ui: {
resourceUri: 'ui://weather/view.html',
},
},
},
async ({ city }) => {
const weather = await getWeather(city);
return {
content: [{ type: 'text', text: `Showing weather for ${city}.` }],
structuredContent: weather,
};
},
);
For MCP Apps, check the details that a broad server migration can miss:
_meta.ui.resourceUristill matches the registered resource exactly._meta.ui.visibilitystill exposes app-only tools to the app and hides them from the model.outputSchemamatchesstructuredContent.contentstill gives non-UI hosts a useful text fallback.- Error results set
isErrorand use a shape the view handles.
If you use registerAppTool and registerAppResource, keep them only when the installed helper version accepts your v2 McpServer. Their job is still MCP App metadata and resource registration, but a type error at this boundary is a compatibility signal, not a reason to add a cast.
Keep the UI Resource Contract Stable
The host learns about an MCP App through tool metadata, then reads the linked resource. That path should not change during an SDK migration:
tools/list
-> _meta.ui.resourceUri
resources/read
-> text/html;profile=mcp-app
tools/call
-> content + structuredContent
host
-> render view and deliver tool result
For every UI tool, add a conformance test that:
- Finds the tool in
tools/list. - Reads
_meta.ui.resourceUri. - Calls
resources/readwith that URI. - Asserts
RESOURCE_MIME_TYPE. - Checks that the HTML includes the built entry script or bundled app.
- Calls the tool and validates its result against the view’s expected type.
This catches the most expensive migration failures before a browser opens.
Separate the SDK Upgrade From the 2026 Protocol
SDK v2 and the 2026-07-28 protocol are related, but they are not the same migration.
The official protocol version guide says a hand-constructed Client, Server, or McpServer keeps the 2025-era behavior by default. Upgrading packages does not automatically switch the wire format.
The modern protocol changes several server assumptions:
server/discoverreplaces the legacy initialization flow.- Request
_metacarries protocol, client, and capability information. - Server-to-client interaction uses multi-round-trip results.
subscriptions/listenreplaces free-floating change notifications.- Closing the response stream handles cancellation over Streamable HTTP.
- Modern HTTP serving uses
createMcpHandler.
Changing both layers at once makes failures hard to place. Use two releases:
- Move to v2 packages while preserving the legacy protocol behavior.
- Add modern protocol support with legacy fallback, then test both eras.
Only reject legacy clients after your supported ChatGPT, Claude, and other host versions have moved. Host updates do not all arrive on the same day.
Test the MCP App Across Three Layers
After the code compiles, test the contracts in order.
1. Server and protocol tests
Assert:
- Tool names, descriptions, input schemas, and output schemas
_meta.ui.resourceUriand visibility- Resource URI, MIME type, and HTML body
- Successful, empty, invalid, cancelled, and error tool results
- Authentication and transport behavior
- Legacy and modern protocol paths, if both are enabled
2. Rendered MCP App tests
Render each tool state in the host runtimes you support. Check:
- Initial tool input before a result arrives
- Loading, success, empty, error, and cancelled views
- Light and dark themes
- Inline and expanded display modes
- App-to-server tool calls
sendMessage,updateModelContext, links, and file actions used by the app- Resize, teardown, and repeated tool calls
The sunpeak MCP App Inspector can run these states against replicated ChatGPT and Claude runtimes without using paid host accounts or model credits:
npx sunpeak inspect --server http://localhost:3000/mcp
For CI, scaffold tests against an existing server:
npx sunpeak test init --server http://localhost:3000/mcp
pnpm test
Use deterministic fixtures for tool input and output so the SDK migration diff does not get mixed with model variance.
3. Live host smoke tests
Keep a small live matrix after local tests pass:
- One ChatGPT tool call that renders and completes an app action
- One Claude tool call that renders the same resource
- Authentication, if the production server requires it
- One host-specific capability the app depends on
Live tests catch rollout and host integration issues. They should confirm the smaller local suite, not replace it.
A Safe Rollout Order
Use this order for an MCP App that already has users:
- Record the v1 server and rendered app behavior.
- Upgrade Node.js and CI images to Node 20.
- Run the codemod and clear every action marker.
- Install only the v2 packages each workspace needs.
- Preserve the legacy protocol path.
- Pass type-checks, unit tests, and protocol conformance tests.
- Pass local ChatGPT and Claude runtime tests.
- Deploy to a staging URL and run live smoke tests.
- Ship the v2 SDK migration.
- Add the 2026-07-28 protocol in a later change with legacy fallback.
The package migration is complete when the app contract still works, not when the last v1 import disappears.
sunpeak keeps that contract visible during the change. Its MCP App testing framework runs protocol, E2E, and visual checks across replicated host states, so you can migrate the server without manually reopening every tool result in every host.
Get Started
npx sunpeak newFurther Reading
- MCP App conformance testing - validate tools, resources, metadata, and fallbacks
- MCP App UI resources - ui:// URIs, MIME types, and resource links
- MCP App lifecycle - connection, tool input, results, and teardown
- End-to-end TypeScript types in MCP Apps
- Cross-host testing for ChatGPT Apps and Claude Connectors
- MCP App framework
- ChatGPT App framework
- Claude Connector framework
- sunpeak MCP App testing framework
- sunpeak MCP App Inspector
- Official MCP TypeScript SDK v2 migration guide
- Official MCP TypeScript SDK protocol version guide
- Official MCP Apps overview and SDK packages
- @modelcontextprotocol/ext-apps 1.7.5 package and peer dependencies
Frequently Asked Questions
How do I migrate an MCP App from the MCP TypeScript SDK v1 to v2?
Upgrade the project to Node.js 20 or newer, commit or stash the current work, run npx @modelcontextprotocol/codemod@latest v1-to-v2 . from the package root, review every @mcp-codemod-error marker, install the split v2 packages, type-check, and run protocol plus rendered UI tests. Migrate the SDK API first while keeping the existing 2025 protocol behavior, then adopt the 2026-07-28 protocol in a separate change.
What replaces @modelcontextprotocol/sdk in v2?
@modelcontextprotocol/server contains the server implementation, @modelcontextprotocol/client contains the client implementation, and @modelcontextprotocol/core contains public protocol schema constants. Runtime and framework adapters live in packages such as @modelcontextprotocol/node, @modelcontextprotocol/express, @modelcontextprotocol/hono, and @modelcontextprotocol/fastify. Install only the packages that each workspace imports.
Does the MCP TypeScript SDK v2 change MCP App view code?
Usually no. The MCP App view still uses @modelcontextprotocol/ext-apps or @modelcontextprotocol/ext-apps/react to communicate with its host through postMessage. The v2 migration mainly changes the MCP server, transport, schemas, and handler APIs. Check the ext-apps peer dependency before removing SDK v1 because some releases still use it internally, then rebuild and render the view.
Does upgrading to MCP TypeScript SDK v2 enable the 2026-07-28 protocol automatically?
No. A hand-built Client, Server, or McpServer keeps the legacy 2025 connection behavior by default. The 2026-07-28 protocol is a separate opt-in that uses modern entry points such as createMcpHandler or serveStdio and changes discovery, request metadata, cancellation, subscriptions, and server-to-client interaction patterns.
Which Streamable HTTP transport should an MCP App server use in SDK v2?
Use NodeStreamableHTTPServerTransport from @modelcontextprotocol/node when your handler receives Node IncomingMessage and ServerResponse objects. Use WebStandardStreamableHTTPServerTransport from @modelcontextprotocol/server when the runtime accepts a web-standard Request and returns a Response, such as Cloudflare Workers, Deno, or Bun. Modern 2026-07-28 endpoints use createMcpHandler instead.
What Node.js version does MCP TypeScript SDK v2 require?
The official v2 migration guide requires Node.js 20 or newer. The packages are ESM-first and also ship CommonJS builds, so both import and require work without the dynamic import workaround that some v1 projects used.
How should I test an MCP App after the SDK v2 migration?
Test tools/list, resources/read, tool calls, content, structuredContent, resource MIME types, _meta.ui.resourceUri, app-only tool visibility, and error results. Then render the app in each supported host runtime and test tool input, tool results, bridge calls, themes, and display modes. Keep one real-host smoke test after local and CI tests pass.