| name | description |
|---|---|
migrate-v1-to-v2 |
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/core |
@modelcontextprotocol/sdk/shared/protocol.js |
@modelcontextprotocol/core |
@modelcontextprotocol/sdk/shared/transport.js |
@modelcontextprotocol/core |
@modelcontextprotocol/sdk/shared/stdio.js |
@modelcontextprotocol/core |
@modelcontextprotocol/sdk/shared/uriTemplate.js |
@modelcontextprotocol/core |
@modelcontextprotocol/sdk/shared/auth.js |
@modelcontextprotocol/core |
Notes:
@modelcontextprotocol/clientand@modelcontextprotocol/serverboth re-export everything from@modelcontextprotocol/core, so you can import types from whichever package you already depend on.- When multiple v1 imports map to the same v2 package, consolidate them into a single import statement.
- If code imports from
sdk/client/..., install@modelcontextprotocol/client. If fromsdk/server/..., install@modelcontextprotocol/server. If fromsdk/types.jsorsdk/shared/...only, install@modelcontextprotocol/core.
| 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 in @modelcontextprotocol/core) |
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 auth flow | 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/core';
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/core';
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/core';
if (error instanceof InvalidClientError) { ... }
// v2
import { OAuthError, OAuthErrorCode } from '@modelcontextprotocol/core';
if (error instanceof OAuthError && error.code === OAuthErrorCode.InvalidClient) { ... }Unchanged APIs (only import paths changed): Client constructor and methods, McpServer constructor, server.connect(), server.close(), all client transports (StreamableHTTPClientTransport, SSEClientTransport, StdioClientTransport), StdioServerTransport, all Zod schemas, all callback return types.
The variadic .tool(), .prompt(), .resource() methods are removed. Use the register* methods with a config object.
IMPORTANT: v2 requires full Zod schemas — raw shapes like { name: z.string() } are no longer supported. You must wrap with z.object(). This 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 (Zod schema) |
|---|---|
{ 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 |
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 |
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