| 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. |
Apply these changes in order: dependencies → imports → API calls → type aliases.
- Node.js 20+ required (v18 dropped)
- ESM only (CJS dropped). If the project uses
require(), convert toimport/exportor use dynamicimport().
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.
Replace all @modelcontextprotocol/sdk/... imports using this table.
| 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 |
| 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) |
| 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.
| v1 symbol | v2 symbol | v2 package |
|---|---|---|
StreamableHTTPServerTransport |
NodeStreamableHTTPServerTransport |
@modelcontextprotocol/node |
| 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.).
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;
}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).
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.
// 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?
// 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?
// 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.
| 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 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) |
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')SSEServerTransport removed entirely. Migrate to NodeStreamableHTTPServerTransport (from @modelcontextprotocol/node). Client-side SSEClientTransport still available for connecting to legacy servers.
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.
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().
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.
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 |
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.
TaskCreationParams.ttl changed from z.union([z.number(), z.null()]).optional() to z.number().optional(). Per the MCP spec, null TTL (unlimited lifetime) is only valid in server responses (Task.ttl), not in client requests. Omit ttl to let the server decide.
| v1 | v2 |
|---|---|
task: { ttl: null } |
task: {} (omit ttl) |
task: { ttl: 60000 } |
task: { ttl: 60000 } (unchanged) |
Type changes in handler context:
| Type | v1 | v2 |
|---|---|---|
TaskContext.requestedTtl |
number | null | undefined |
number | undefined |
CreateTaskServerContext.task.requestedTtl |
number | null | undefined |
number | undefined |
TaskServerContext.task.requestedTtl |
number | null | undefined |
number | undefined |
These task APIs are
@experimentaland may change without notice.
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.
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';
- 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