Skip to main content
All posts

MCP TypeScript SDK v2 Migration Guide for MCP Apps

Abe Wheeler
MCP AppsMCP App FrameworkMCP App TestingChatGPT AppsChatGPT App FrameworkChatGPT App TestingClaude ConnectorsClaude Connector FrameworkMCP TypeScript SDKMCP SDK v2TypeScriptMigration
MCP TypeScript SDK v2 splits server, client, schema, and runtime adapters while keeping the MCP App view protocol separate.

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:

  1. The tool descriptor must point to the right ui:// resource.
  2. The resource must return HTML with text/html;profile=mcp-app.
  3. The tool result must contain the data shape the view expects.
  4. The host must send that input and result through the app bridge.
  5. 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:

ContractWhat changesHow to verify it
TypeScript SDK APIPackages, imports, handlers, schemas, errorsType-check and unit tests
MCP server protocolTools, resources, transport, version negotiationProtocol and conformance tests
MCP App runtimeResource rendering, tool data, host bridge actionsLocal 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/list
  • resources/list, if the server lists UI resources
  • resources/read for each linked ui:// URI
  • One successful and one failed tools/call per 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 migrationUse it for
@modelcontextprotocol/serverMcpServer, server types, web-standard server transport, modern handlers
@modelcontextprotocol/clientMCP client code and client transports
@modelcontextprotocol/corePublic Zod protocol schema constants
@modelcontextprotocol/nodeNode HTTP transport and middleware
@modelcontextprotocol/expressExpress integration
@modelcontextprotocol/honoHono integration
@modelcontextprotocol/fastifyFastify integration
@modelcontextprotocol/ext-appsMCP App view-to-host communication
@modelcontextprotocol/ext-apps/serverMCP 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-apps release 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 keeping ext-apps in 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.resourceUri still matches the registered resource exactly.
  • _meta.ui.visibility still exposes app-only tools to the app and hides them from the model.
  • outputSchema matches structuredContent.
  • content still gives non-UI hosts a useful text fallback.
  • Error results set isError and 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:

  1. Finds the tool in tools/list.
  2. Reads _meta.ui.resourceUri.
  3. Calls resources/read with that URI.
  4. Asserts RESOURCE_MIME_TYPE.
  5. Checks that the HTML includes the built entry script or bundled app.
  6. 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/discover replaces the legacy initialization flow.
  • Request _meta carries protocol, client, and capability information.
  • Server-to-client interaction uses multi-round-trip results.
  • subscriptions/listen replaces 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:

  1. Move to v2 packages while preserving the legacy protocol behavior.
  2. 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.resourceUri and 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:

  1. Record the v1 server and rendered app behavior.
  2. Upgrade Node.js and CI images to Node 20.
  3. Run the codemod and clear every action marker.
  4. Install only the v2 packages each workspace needs.
  5. Preserve the legacy protocol path.
  6. Pass type-checks, unit tests, and protocol conformance tests.
  7. Pass local ChatGPT and Claude runtime tests.
  8. Deploy to a staging URL and run live smoke tests.
  9. Ship the v2 SDK migration.
  10. 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

Documentation →
npx sunpeak new

Further Reading

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.