name: migrate-v1-to-v2 description: Migrate MCP TypeScript SDK code from v1 (@modelcontextprotocol/sdk) to v2 (@modelcontextprotocol/core, /client, /server). Use when a user asks to migrate, upgrade, or port their MCP TypeScript code from v1 to v2.
MCP TypeScript SDK: v1 → v2 Migration
Apply these changes in order: dependencies → imports → API calls → type aliases.
1. Environment
- Node.js 20+ required (v18 dropped)
- ESM only (CJS dropped). If the project uses
require(), convert toimport/exportor use dynamicimport().
2. Dependencies
Remove the old package and install only what you need:
npm uninstall @modelcontextprotocol/sdk
| You need | Install |
|---|---|
| Client only | npm install @modelcontextprotocol/client |
| Server only | npm install @modelcontextprotocol/server |
| Server + Node.js HTTP | npm install @modelcontextprotocol/server @modelcontextprotocol/node |
| Server + Express | npm install @modelcontextprotocol/server @modelcontextprotocol/express |
| Server + Hono | npm install @modelcontextprotocol/server @modelcontextprotocol/hono |
@modelcontextprotocol/core is installed automatically as a dependency.
3. Import Mapping
Replace all @modelcontextprotocol/sdk/... imports using this table.
Client imports
| v1 import path | v2 package |
|---|---|
@modelcontextprotocol/sdk/client/index.js | @modelcontextprotocol/client |
@modelcontextprotocol/sdk/client/auth.js | @modelcontextprotocol/client |
@modelcontextprotocol/sdk/client/streamableHttp.js | @modelcontextprotocol/client |
@modelcontextprotocol/sdk/client/sse.js | @modelcontextprotocol/client |
@modelcontextprotocol/sdk/client/stdio.js | @modelcontextprotocol/client |
@modelcontextprotocol/sdk/client/websocket.js | @modelcontextprotocol/client |
Server imports
| v1 import path | v2 package |
|---|---|
@modelcontextprotocol/sdk/server/mcp.js | @modelcontextprotocol/server |
@modelcontextprotocol/sdk/server/index.js | @modelcontextprotocol/server |
@modelcontextprotocol/sdk/server/stdio.js | @modelcontextprotocol/server |
@modelcontextprotocol/sdk/server/streamableHttp.js | @modelcontextprotocol/node (class renamed to NodeStreamableHTTPServerTransport) OR @modelcontextprotocol/server (web-standard WebStandardStreamableHTTPServerTransport for Cloudflare Workers, Deno, etc.) |
@modelcontextprotocol/sdk/server/sse.js | REMOVED (migrate to Streamable HTTP) |
@modelcontextprotocol/sdk/server/auth/* | REMOVED (use external auth library) |
@modelcontextprotocol/sdk/server/middleware.js | @modelcontextprotocol/express (signature changed, see section 8) |
Types / shared imports
| v1 import path | v2 package |
|---|---|
@modelcontextprotocol/sdk/types.js | @modelcontextprotocol/client or @modelcontextprotocol/server |
@modelcontextprotocol/sdk/shared/protocol.js | @modelcontextprotocol/client or @modelcontextprotocol/server |
@modelcontextprotocol/sdk/shared/transport.js | @modelcontextprotocol/client or @modelcontextprotocol/server |
@modelcontextprotocol/sdk/shared/uriTemplate.js | @modelcontextprotocol/client or @modelcontextprotocol/server |
@modelcontextprotocol/sdk/shared/auth.js | @modelcontextprotocol/client or @modelcontextprotocol/server |
@modelcontextprotocol/sdk/shared/stdio.js | @modelcontextprotocol/client or @modelcontextprotocol/server |
Notes:
@modelcontextprotocol/clientand@modelcontextprotocol/serverboth re-export shared types from@modelcontextprotocol/core, so import from whichever package you already depend on. Do not import from@modelcontextprotocol/coredirectly — it is an internal package.- When multiple v1 imports map to the same v2 package, consolidate them into a single import statement.
4. Renamed Symbols
| v1 symbol | v2 symbol | v2 package |
|---|---|---|
StreamableHTTPServerTransport | NodeStreamableHTTPServerTransport | @modelcontextprotocol/node |
5. Removed / Renamed Type Aliases and Symbols
| v1 (removed) | v2 (replacement) |
|---|---|
JSONRPCError | JSONRPCErrorResponse |
JSONRPCErrorSchema | JSONRPCErrorResponseSchema |
isJSONRPCError | isJSONRPCErrorResponse |
isJSONRPCResponse | isJSONRPCResultResponse |
ResourceReference | ResourceTemplateReference |
ResourceReferenceSchema | ResourceTemplateReferenceSchema |
IsomorphicHeaders | REMOVED (use Web Standard Headers) |
AuthInfo (from server/auth/types.js) | AuthInfo (now re-exported by @modelcontextprotocol/client and @modelcontextprotocol/server) |
McpError | ProtocolError |
ErrorCode | ProtocolErrorCode |
ErrorCode.RequestTimeout | SdkErrorCode.RequestTimeout |
ErrorCode.ConnectionClosed | SdkErrorCode.ConnectionClosed |
StreamableHTTPError | REMOVED (use SdkError with SdkErrorCode.ClientHttp*) |
All other symbols from @modelcontextprotocol/sdk/types.js retain their original names (e.g., CallToolResultSchema, ListToolsResultSchema, etc.).
Error class changes
Two error classes now exist:
ProtocolError(renamed fromMcpError): Protocol errors that cross the wire as JSON-RPC responsesSdkError(new): Local SDK errors that never cross the wire
| Error scenario | v1 type | v2 type |
|---|---|---|
| Request timeout | McpError with ErrorCode.RequestTimeout | SdkError with SdkErrorCode.RequestTimeout |
| Connection closed | McpError with ErrorCode.ConnectionClosed | SdkError with SdkErrorCode.ConnectionClosed |
| Capability not supported | new Error(...) | SdkError with SdkErrorCode.CapabilityNotSupported |
| Not connected | new Error('Not connected') | SdkError with SdkErrorCode.NotConnected |
| Invalid params (server response) | McpError with ErrorCode.InvalidParams | ProtocolError with ProtocolErrorCode.InvalidParams |
| HTTP transport error | StreamableHTTPError | SdkError with SdkErrorCode.ClientHttp* |
| Failed to open SSE stream | StreamableHTTPError | SdkError with SdkErrorCode.ClientHttpFailedToOpenStream |
| 401 after re-auth (circuit break) | StreamableHTTPError | SdkError with SdkErrorCode.ClientHttpAuthentication |
| 403 after upscoping | StreamableHTTPError | SdkError with SdkErrorCode.ClientHttpForbidden |
| Unexpected content type | StreamableHTTPError | SdkError with SdkErrorCode.ClientHttpUnexpectedContent |
| Session termination failed | StreamableHTTPError | SdkError with SdkErrorCode.ClientHttpFailedToTerminateSession |
New SdkErrorCode enum values:
SdkErrorCode.NotConnected='NOT_CONNECTED'SdkErrorCode.AlreadyConnected='ALREADY_CONNECTED'SdkErrorCode.NotInitialized='NOT_INITIALIZED'SdkErrorCode.CapabilityNotSupported='CAPABILITY_NOT_SUPPORTED'SdkErrorCode.RequestTimeout='REQUEST_TIMEOUT'SdkErrorCode.ConnectionClosed='CONNECTION_CLOSED'SdkErrorCode.SendFailed='SEND_FAILED'SdkErrorCode.ClientHttpNotImplemented='CLIENT_HTTP_NOT_IMPLEMENTED'SdkErrorCode.ClientHttpAuthentication='CLIENT_HTTP_AUTHENTICATION'SdkErrorCode.ClientHttpForbidden='CLIENT_HTTP_FORBIDDEN'SdkErrorCode.ClientHttpUnexpectedContent='CLIENT_HTTP_UNEXPECTED_CONTENT'SdkErrorCode.ClientHttpFailedToOpenStream='CLIENT_HTTP_FAILED_TO_OPEN_STREAM'SdkErrorCode.ClientHttpFailedToTerminateSession='CLIENT_HTTP_FAILED_TO_TERMINATE_SESSION'
Update error handling:
// v1
if (error instanceof McpError && error.code === ErrorCode.RequestTimeout) { ... }
// v2
import { SdkError, SdkErrorCode } from '@modelcontextprotocol/client';
if (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) { ... }
Update HTTP transport error handling:
// v1
import { StreamableHTTPError } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
if (error instanceof StreamableHTTPError) {
console.log('HTTP status:', error.code);
}
// v2
import { SdkError, SdkErrorCode } from '@modelcontextprotocol/client';
if (error instanceof SdkError && error.code === SdkErrorCode.ClientHttpFailedToOpenStream) {
const status = (error.data as { status?: number })?.status;
}
OAuth error consolidation
Individual OAuth error classes replaced with single OAuthError class and OAuthErrorCode enum:
| v1 Class | v2 Equivalent |
|---|---|
InvalidRequestError | OAuthError with OAuthErrorCode.InvalidRequest |
InvalidClientError | OAuthError with OAuthErrorCode.InvalidClient |
InvalidGrantError | OAuthError with OAuthErrorCode.InvalidGrant |
UnauthorizedClientError | OAuthError with OAuthErrorCode.UnauthorizedClient |
UnsupportedGrantTypeError | OAuthError with OAuthErrorCode.UnsupportedGrantType |
InvalidScopeError | OAuthError with OAuthErrorCode.InvalidScope |
AccessDeniedError | OAuthError with OAuthErrorCode.AccessDenied |
ServerError | OAuthError with OAuthErrorCode.ServerError |
TemporarilyUnavailableError | OAuthError with OAuthErrorCode.TemporarilyUnavailable |
UnsupportedResponseTypeError | OAuthError with OAuthErrorCode.UnsupportedResponseType |
UnsupportedTokenTypeError | OAuthError with OAuthErrorCode.UnsupportedTokenType |
InvalidTokenError | OAuthError with OAuthErrorCode.InvalidToken |
MethodNotAllowedError | OAuthError with OAuthErrorCode.MethodNotAllowed |
TooManyRequestsError | OAuthError with OAuthErrorCode.TooManyRequests |
InvalidClientMetadataError | OAuthError with OAuthErrorCode.InvalidClientMetadata |
InsufficientScopeError | OAuthError with OAuthErrorCode.InsufficientScope |
InvalidTargetError | OAuthError with OAuthErrorCode.InvalidTarget |
CustomOAuthError | new OAuthError(customCode, message) |
Removed: OAUTH_ERRORS constant.
Update OAuth error handling:
// v1
import { InvalidClientError, InvalidGrantError } from '@modelcontextprotocol/client';
if (error instanceof InvalidClientError) { ... }
// v2
import { OAuthError, OAuthErrorCode } from '@modelcontextprotocol/client';
if (error instanceof OAuthError && error.code === OAuthErrorCode.InvalidClient) { ... }
Unchanged APIs (only import paths changed): Client constructor and most methods, McpServer constructor, server.connect(), server.close(), all client transports (StreamableHTTPClientTransport, SSEClientTransport, StdioClientTransport), StdioServerTransport, all
Zod schemas, all callback return types. Note: callTool() and request() signatures changed (schema parameter removed, see section 11).
6. McpServer API Changes
The variadic .tool(), .prompt(), .resource() methods are removed. Use the register* methods with a config object.
IMPORTANT: v2 requires schema objects implementing Standard Schema — raw shapes like { name: z.string() } are no longer supported. Wrap with z.object() (Zod v4), or use ArkType's type({...}), or Valibot. For raw JSON Schema, wrap with fromJsonSchema(schema, validator) from @modelcontextprotocol/server. Applies to inputSchema, outputSchema, and argsSchema.
Tools
// v1: server.tool(name, schema, callback) - raw shape worked
server.tool('greet', { name: z.string() }, async ({ name }) => {
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
});
// v1: server.tool(name, description, schema, callback)
server.tool('greet', 'Greet a user', { name: z.string() }, async ({ name }) => {
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
});
// v2: server.registerTool(name, config, callback)
server.registerTool(
'greet',
{
description: 'Greet a user',
inputSchema: z.object({ name: z.string() })
},
async ({ name }) => {
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
}
);
Config object fields: title?, description?, inputSchema?, outputSchema?, annotations?, _meta?
Prompts
// v1: server.prompt(name, schema, callback) - raw shape worked
server.prompt('summarize', { text: z.string() }, async ({ text }) => {
return { messages: [{ role: 'user', content: { type: 'text', text } }] };
});
// v2: server.registerPrompt(name, config, callback)
server.registerPrompt(
'summarize',
{
argsSchema: z.object({ text: z.string() })
},
async ({ text }) => {
return { messages: [{ role: 'user', content: { type: 'text', text } }] };
}
);
Config object fields: title?, description?, argsSchema?
Resources
// v1: server.resource(name, uri, callback)
server.resource('config', 'config://app', async uri => {
return { contents: [{ uri: uri.href, text: '{}' }] };
});
// v2: server.registerResource(name, uri, metadata, callback)
server.registerResource('config', 'config://app', {}, async uri => {
return { contents: [{ uri: uri.href, text: '{}' }] };
});
Note: the third argument (metadata) is required — pass {} if no metadata.
Schema Migration Quick Reference
| v1 (raw shape) | v2 (Standard Schema object) |
|---|---|
{ name: z.string() } | z.object({ name: z.string() }) |
{ count: z.number().optional() } | z.object({ count: z.number().optional() }) |
{} (empty) | z.object({}) |
undefined (no schema) | undefined or omit the field |
Removed core exports
Removed from @modelcontextprotocol/core | Replacement |
|---|---|
schemaToJson(schema) | standardSchemaToJsonSchema(schema) |
parseSchemaAsync(schema, data) | validateStandardSchema(schema, data) |
SchemaInput<T> | StandardSchemaWithJSON.InferInput<T> |
getSchemaShape, getSchemaDescription, isOptionalSchema, unwrapOptionalSchema | none (internal Zod introspection helpers) |
7. Headers API
Transport constructors and RequestInfo.headers now use the Web Standard Headers object instead of plain objects.
// v1: plain object, bracket access
headers: { 'Authorization': 'Bearer token' }
extra.requestInfo?.headers['mcp-session-id']
// v2: Headers object, .get() access
headers: new Headers({ 'Authorization': 'Bearer token' })
ctx.http?.req?.headers.get('mcp-session-id')
8. Removed Server Features
SSE server transport
SSEServerTransport removed entirely. Migrate to NodeStreamableHTTPServerTransport (from @modelcontextprotocol/node). Client-side SSEClientTransport still available for connecting to legacy servers.
Server-side auth
All server OAuth exports removed: mcpAuthRouter, OAuthServerProvider, OAuthTokenVerifier, requireBearerAuth, authenticateClient, ProxyOAuthServerProvider, allowedMethods, and associated types. Use an external auth library (e.g., better-auth). See
examples/server/src/ for demos.
Host header validation (Express)
hostHeaderValidation() and localhostHostValidation() moved from server package to @modelcontextprotocol/express. Signature changed: takes string[] instead of options object.
// v1
import { hostHeaderValidation } from '@modelcontextprotocol/sdk/server/middleware.js';
app.use(hostHeaderValidation({ allowedHosts: ['example.com'] }));
// v2
import { hostHeaderValidation } from '@modelcontextprotocol/express';
app.use(hostHeaderValidation(['example.com']));
The server package now exports framework-agnostic alternatives: validateHostHeader(), localhostAllowedHostnames(), hostHeaderValidationResponse().
9. setRequestHandler / setNotificationHandler API
The low-level handler registration methods now take a method string instead of a Zod schema.
// v1: schema-based
server.setRequestHandler(InitializeRequestSchema, async (request) => { ... });
server.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => { ... });
// v2: method string
server.setRequestHandler('initialize', async (request) => { ... });
server.setNotificationHandler('notifications/message', (notification) => { ... });
Schema to method string mapping:
| v1 Schema | v2 Method String |
|---|---|
InitializeRequestSchema | 'initialize' |
CallToolRequestSchema | 'tools/call' |
ListToolsRequestSchema | 'tools/list' |
ListPromptsRequestSchema | 'prompts/list' |
GetPromptRequestSchema | 'prompts/get' |
ListResourcesRequestSchema | 'resources/list' |
ReadResourceRequestSchema | 'resources/read' |
CreateMessageRequestSchema | 'sampling/createMessage' |
ElicitRequestSchema | 'elicitation/create' |
SetLevelRequestSchema | 'logging/setLevel' |
PingRequestSchema | 'ping' |
LoggingMessageNotificationSchema | 'notifications/message' |
ToolListChangedNotificationSchema | 'notifications/tools/list_changed' |
ResourceListChangedNotificationSchema | 'notifications/resources/list_changed' |
PromptListChangedNotificationSchema | 'notifications/prompts/list_changed' |
ProgressNotificationSchema | 'notifications/progress' |
CancelledNotificationSchema | 'notifications/cancelled' |
InitializedNotificationSchema | 'notifications/initialized' |
Request/notification params remain fully typed. Remove unused schema imports after migration.
10. Request Handler Context Types
RequestHandlerExtra → structured context types with nested groups. Rename extra → ctx in all handler callbacks.
| v1 | v2 |
|---|---|
RequestHandlerExtra | ServerContext (server) / ClientContext (client) / BaseContext (base) |
extra (param name) | ctx |
extra.signal | ctx.mcpReq.signal |
extra.requestId | ctx.mcpReq.id |
extra._meta | ctx.mcpReq._meta |
extra.sendRequest(...) | ctx.mcpReq.send(...) |
extra.sendNotification(...) | ctx.mcpReq.notify(...) |
extra.authInfo | ctx.http?.authInfo |
extra.sessionId | ctx.sessionId |
extra.requestInfo | ctx.http?.req (only ServerContext) |
extra.closeSSEStream | ctx.http?.closeSSE (only ServerContext) |
extra.closeStandaloneSSEStream | ctx.http?.closeStandaloneSSE (only ServerContext) |
extra.taskStore | ctx.task?.store |
extra.taskId | ctx.task?.id |
extra.taskRequestedTtl | ctx.task?.requestedTtl |
ServerContext convenience methods (new in v2, no v1 equivalent):
| Method | Description | Replaces |
|---|---|---|
ctx.mcpReq.log(level, data, logger?) | Send log notification (respects client's level filter) | server.sendLoggingMessage(...) from within handler |
ctx.mcpReq.elicitInput(params, options?) | Elicit user input (form or URL) | server.elicitInput(...) from within handler |
ctx.mcpReq.requestSampling(params, options?) | Request LLM sampling from client | server.createMessage(...) from within handler |
11. Schema parameter removed from request(), send(), and callTool()
Protocol.request(), BaseContext.mcpReq.send(), and Client.callTool() no longer take a Zod result schema argument. The SDK resolves the schema internally from the method name.
// v1: schema required
import { CallToolResultSchema, ElicitResultSchema } from '@modelcontextprotocol/sdk/types.js';
const result = await client.request({ method: 'tools/call', params: { ... } }, CallToolResultSchema);
const elicit = await ctx.mcpReq.send({ method: 'elicitation/create', params: { ... } }, ElicitResultSchema);
const tool = await client.callTool({ name: 'my-tool', arguments: {} }, CompatibilityCallToolResultSchema);
// v2: no schema argument
const result = await client.request({ method: 'tools/call', params: { ... } });
const elicit = await ctx.mcpReq.send({ method: 'elicitation/create', params: { ... } });
const tool = await client.callTool({ name: 'my-tool', arguments: {} });
| v1 call | v2 call |
|---|---|
client.request(req, ResultSchema) | client.request(req) |
client.request(req, ResultSchema, options) | client.request(req, options) |
ctx.mcpReq.send(req, ResultSchema) | ctx.mcpReq.send(req) |
ctx.mcpReq.send(req, ResultSchema, options) | ctx.mcpReq.send(req, options) |
client.callTool(params, CompatibilityCallToolResultSchema) | client.callTool(params) |
client.callTool(params, schema, options) | client.callTool(params, options) |
Remove unused schema imports: CallToolResultSchema, CompatibilityCallToolResultSchema, ElicitResultSchema, CreateMessageResultSchema, etc., when they were only used in request()/send()/callTool() calls.
12. Client Behavioral Changes
Client.listPrompts(), listResources(), listResourceTemplates(), listTools() now return empty results when the server lacks the corresponding capability (instead of sending the request). Set enforceStrictCapabilities: true in ClientOptions to throw an error instead.
13. Runtime-Specific JSON Schema Validators (Enhancement)
The SDK now auto-selects the appropriate JSON Schema validator based on runtime:
- Node.js →
AjvJsonSchemaValidator(no change from v1) - Cloudflare Workers (workerd) →
CfWorkerJsonSchemaValidator(previously required manual config)
No action required for most users. Cloudflare Workers users can remove explicit jsonSchemaValidator configuration:
// v1 (Cloudflare Workers): Required explicit validator
new McpServer(
{ name: 'server', version: '1.0.0' },
{
jsonSchemaValidator: new CfWorkerJsonSchemaValidator()
}
);
// v2 (Cloudflare Workers): Auto-selected, explicit config optional
new McpServer({ name: 'server', version: '1.0.0' }, {});
Access validators via _shims export: import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims';
14. Migration Steps (apply in this order)
- Update
package.json:npm uninstall @modelcontextprotocol/sdk, install the appropriate v2 packages - Replace all imports from
@modelcontextprotocol/sdk/...using the import mapping tables (sections 3-4), includingStreamableHTTPServerTransport→NodeStreamableHTTPServerTransport - Replace removed type aliases (
JSONRPCError→JSONRPCErrorResponse, etc.) per section 5 - Replace
.tool()/.prompt()/.resource()calls withregisterTool/registerPrompt/registerResourceper section 6 - Wrap all raw Zod shapes with
z.object(): ChangeinputSchema: { name: z.string() }→inputSchema: z.object({ name: z.string() }). Same foroutputSchemain tools andargsSchemain prompts. - Replace plain header objects with
new Headers({...})and bracket access (headers['x']) with.get()calls per section 7 - If using
hostHeaderValidationfrom server, update import and signature per section 8 - If using server SSE transport, migrate to Streamable HTTP
- If using server auth from the SDK, migrate to an external auth library
- If relying on
listTools()/listPrompts()/etc. throwing on missing capabilities, setenforceStrictCapabilities: true - Verify: build with
tsc/ run tests