Skip to content

Latest commit

 

History

History
892 lines (659 loc) · 37.2 KB

File metadata and controls

892 lines (659 loc) · 37.2 KB

Migration Guide: v1 to v2

This guide covers the breaking changes introduced in v2 of the MCP TypeScript SDK and how to update your code.

Overview

Version 2 of the MCP TypeScript SDK introduces several breaking changes to improve modularity, reduce dependency bloat, and provide a cleaner API surface. The biggest change is the split from a single @modelcontextprotocol/sdk package into separate @modelcontextprotocol/core, @modelcontextprotocol/client, and @modelcontextprotocol/server packages.

Breaking Changes

Package split (monorepo)

The single @modelcontextprotocol/sdk package has been split into three packages:

v1 v2
@modelcontextprotocol/sdk @modelcontextprotocol/core (types, protocol, transports)
@modelcontextprotocol/client (client implementation)
@modelcontextprotocol/server (server implementation)

Remove the old package and install only the packages you need:

npm uninstall @modelcontextprotocol/sdk

# If you only need a client
npm install @modelcontextprotocol/client

# If you only need a server
npm install @modelcontextprotocol/server

# Both packages depend on @modelcontextprotocol/core automatically

Update your imports accordingly:

Before (v1):

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

After (v2):

import { Client, StreamableHTTPClientTransport, StdioClientTransport } from '@modelcontextprotocol/client';
import { McpServer, StdioServerTransport, WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server';

// Node.js HTTP server transport is in the @modelcontextprotocol/node package
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';

Note: @modelcontextprotocol/client and @modelcontextprotocol/server both re-export shared types from @modelcontextprotocol/core, so you can import types and error classes from whichever package you already depend on. Do not import from @modelcontextprotocol/core directly — it is an internal package.

Dropped Node.js 18 and CommonJS

v2 requires Node.js 20+ and ships ESM only (no more CommonJS builds).

If your project uses CommonJS (require()), you will need to either:

  • Migrate to ESM (import/export)
  • Use dynamic import() to load the SDK

Server decoupled from HTTP frameworks

The server package no longer depends on Express or Hono. HTTP framework integrations are now separate middleware packages:

v1 v2
Built into @modelcontextprotocol/sdk @modelcontextprotocol/node (Node.js HTTP)
@modelcontextprotocol/express (Express)
@modelcontextprotocol/hono (Hono)

Install the middleware package for your framework:

npm install @modelcontextprotocol/node       # Node.js native http
npm install @modelcontextprotocol/express    # Express
npm install @modelcontextprotocol/hono       # Hono

StreamableHTTPServerTransport renamed

StreamableHTTPServerTransport has been renamed to NodeStreamableHTTPServerTransport and moved to @modelcontextprotocol/node.

Before (v1):

import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';

const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });

After (v2):

import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';

const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });

Server-side SSE transport removed

The SSE transport has been removed from the server. Servers should migrate to Streamable HTTP. The client-side SSE transport remains available for connecting to legacy SSE servers.

WebSocketClientTransport removed

WebSocketClientTransport has been removed. WebSocket is not a spec-defined MCP transport, and keeping it in the SDK encouraged transport proliferation without a conformance baseline.

Use StdioClientTransport for local servers or StreamableHTTPClientTransport for remote servers. If you need WebSocket for a custom deployment, implement the Transport interface directly — it remains exported from @modelcontextprotocol/client.

Before (v1):

import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js';
const transport = new WebSocketClientTransport(new URL('ws://localhost:3000'));

After (v2):

import { StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'));

Server auth removed

Server-side OAuth/auth has been removed entirely from the SDK. This includes mcpAuthRouter, OAuthServerProvider, OAuthTokenVerifier, requireBearerAuth, authenticateClient, ProxyOAuthServerProvider, allowedMethods, and all associated types.

Use a dedicated auth library (e.g., better-auth) or a full Authorization Server instead. See the examples for a working demo with better-auth.

Note: AuthInfo has moved from server/auth/types.ts to the core types and is now re-exported by @modelcontextprotocol/client and @modelcontextprotocol/server.

Headers object instead of plain objects

Transport APIs and RequestInfo.headers now use the Web Standard Headers object instead of plain Record<string, string | string[] | undefined> (IsomorphicHeaders has been removed).

This affects both transport constructors and request handler code that reads headers:

Before (v1):

// Transport headers
const transport = new StreamableHTTPClientTransport(url, {
    requestInit: {
        headers: {
            Authorization: 'Bearer token',
            'X-Custom': 'value'
        }
    }
});

// Reading headers in a request handler
const sessionId = extra.requestInfo?.headers['mcp-session-id'];

After (v2):

// Transport headers
const transport = new StreamableHTTPClientTransport(url, {
    requestInit: {
        headers: new Headers({
            Authorization: 'Bearer token',
            'X-Custom': 'value'
        })
    }
});

// Reading headers in a request handler (ctx.http.req is the standard Web Request object)
const sessionId = ctx.http?.req?.headers.get('mcp-session-id');

// Reading query parameters
const url = new URL(ctx.http!.req!.url);
const debug = url.searchParams.get('debug');

McpServer.tool(), .prompt(), .resource() removed

The deprecated variadic-overload methods have been removed. Use registerTool, registerPrompt, and registerResource instead. These use an explicit config object rather than positional arguments.

Before (v1):

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';

const server = new McpServer({ name: 'demo', version: '1.0.0' });

// Tool with schema
server.tool('greet', { name: z.string() }, async ({ name }) => {
    return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
});

// Tool with description
server.tool('greet', 'Greet a user', { name: z.string() }, async ({ name }) => {
    return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
});

// Prompt
server.prompt('summarize', { text: z.string() }, async ({ text }) => {
    return { messages: [{ role: 'user', content: { type: 'text', text: `Summarize: ${text}` } }] };
});

// Resource
server.resource('config', 'config://app', async uri => {
    return { contents: [{ uri: uri.href, text: '{}' }] };
});

After (v2):

import { McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';

const server = new McpServer({ name: 'demo', version: '1.0.0' });

// Tool with schema
server.registerTool('greet', { inputSchema: z.object({ name: z.string() }) }, async ({ name }) => {
    return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
});

// Tool with description
server.registerTool('greet', { description: 'Greet a user', inputSchema: z.object({ name: z.string() }) }, async ({ name }) => {
    return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
});

// Prompt
server.registerPrompt('summarize', { argsSchema: z.object({ text: z.string() }) }, async ({ text }) => {
    return { messages: [{ role: 'user', content: { type: 'text', text: `Summarize: ${text}` } }] };
});

// Resource
server.registerResource('config', 'config://app', {}, async uri => {
    return { contents: [{ uri: uri.href, text: '{}' }] };
});

Standard Schema objects required (raw shapes no longer supported)

v2 requires schema objects implementing the Standard Schema spec for inputSchema, outputSchema, and argsSchema. Raw object shapes are no longer accepted. Zod v4, ArkType, and Valibot all implement the spec.

Before (v1):

// Raw shape (object with Zod fields) - worked in v1
server.tool('greet', { name: z.string() }, async ({ name }) => { ... });

server.registerTool('greet', {
  inputSchema: { name: z.string() }  // raw shape
}, callback);

After (v2):

import * as z from 'zod/v4';

// Wrap with z.object() (or use any Standard Schema library)
server.registerTool('greet', {
  inputSchema: z.object({ name: z.string() })
}, async ({ name }) => { ... });

// ArkType works too
import { type } from 'arktype';
server.registerTool('greet', {
  inputSchema: type({ name: 'string' })
}, async ({ name }) => { ... });

// Raw JSON Schema via fromJsonSchema (validator defaults to runtime-appropriate choice)
import { fromJsonSchema } from '@modelcontextprotocol/server';
server.registerTool('greet', {
  inputSchema: fromJsonSchema({ type: 'object', properties: { name: { type: 'string' } } })
}, handler);

// For tools with no parameters, use z.object({})
server.registerTool('ping', {
  inputSchema: z.object({})
}, async () => { ... });

This applies to:

  • inputSchema in registerTool()
  • outputSchema in registerTool()
  • argsSchema in registerPrompt()

Removed Zod-specific helpers from @modelcontextprotocol/core (use Standard Schema equivalents):

Removed Replacement
schemaToJson(schema) standardSchemaToJsonSchema(schema)
parseSchemaAsync(schema, data) validateStandardSchema(schema, data)
SchemaInput<T> StandardSchemaWithJSON.InferInput<T>
getSchemaShape, getSchemaDescription, isOptionalSchema, unwrapOptionalSchema No replacement — these are now internal Zod introspection helpers

Host header validation moved

Express-specific middleware (hostHeaderValidation(), localhostHostValidation()) moved from the server package to @modelcontextprotocol/express. The server package now exports framework-agnostic functions instead: validateHostHeader(), localhostAllowedHostnames(), hostHeaderValidationResponse().

Before (v1):

import { hostHeaderValidation } from '@modelcontextprotocol/sdk/server/middleware.js';
app.use(hostHeaderValidation({ allowedHosts: ['example.com'] }));

After (v2):

import { hostHeaderValidation } from '@modelcontextprotocol/express';
app.use(hostHeaderValidation(['example.com']));

Note: the v2 signature takes a plain string[] instead of an options object.

setRequestHandler and setNotificationHandler use method strings

The low-level setRequestHandler and setNotificationHandler methods on Client, Server, and Protocol now take a method string instead of a Zod schema.

Before (v1):

import { Server, InitializeRequestSchema, LoggingMessageNotificationSchema } from '@modelcontextprotocol/sdk/server/index.js';

const server = new Server({ name: 'my-server', version: '1.0.0' });

// Request handler with schema
server.setRequestHandler(InitializeRequestSchema, async request => {
    return { protocolVersion: '...', capabilities: {}, serverInfo: { name: '...', version: '...' } };
});

// Notification handler with schema
server.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
    console.log(notification.params.data);
});

After (v2):

import { Server } from '@modelcontextprotocol/server';

const server = new Server({ name: 'my-server', version: '1.0.0' });

// Request handler with method string
server.setRequestHandler('initialize', async request => {
    return { protocolVersion: '...', capabilities: {}, serverInfo: { name: '...', version: '...' } };
});

// Notification handler with method string
server.setNotificationHandler('notifications/message', notification => {
    console.log(notification.params.data);
});

The request and notification parameters remain fully typed via RequestTypeMap and NotificationTypeMap. You no longer need to import the individual *RequestSchema or *NotificationSchema constants for handler registration.

Common method string replacements:

Schema (v1) Method string (v2)
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'
LoggingMessageNotificationSchema 'notifications/message'
ToolListChangedNotificationSchema 'notifications/tools/list_changed'
ResourceListChangedNotificationSchema 'notifications/resources/list_changed'
PromptListChangedNotificationSchema 'notifications/prompts/list_changed'

Protocol.request(), ctx.mcpReq.send(), and Client.callTool() no longer take a schema parameter

The public Protocol.request(), BaseContext.mcpReq.send(), and Client.callTool() methods no longer accept a Zod result schema argument. The SDK now resolves the correct result schema internally based on the method name. This means you no longer need to import result schemas like CallToolResultSchema or ElicitResultSchema when making requests.

client.request() — Before (v1):

import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';

const result = await client.request({ method: 'tools/call', params: { name: 'my-tool', arguments: {} } }, CallToolResultSchema);

After (v2):

const result = await client.request({ method: 'tools/call', params: { name: 'my-tool', arguments: {} } });

ctx.mcpReq.send() — Before (v1):

import { CreateMessageResultSchema } from '@modelcontextprotocol/sdk/types.js';

server.setRequestHandler('tools/call', async (request, ctx) => {
    const samplingResult = await ctx.mcpReq.send(
        { method: 'sampling/createMessage', params: { messages: [...], maxTokens: 100 } },
        CreateMessageResultSchema
    );
    return { content: [{ type: 'text', text: 'done' }] };
});

After (v2):

server.setRequestHandler('tools/call', async (request, ctx) => {
    const samplingResult = await ctx.mcpReq.send(
        { method: 'sampling/createMessage', params: { messages: [...], maxTokens: 100 } }
    );
    return { content: [{ type: 'text', text: 'done' }] };
});

client.callTool() — Before (v1):

import { CompatibilityCallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';

const result = await client.callTool({ name: 'my-tool', arguments: {} }, CompatibilityCallToolResultSchema);

After (v2):

const result = await client.callTool({ name: 'my-tool', arguments: {} });

The return type is now inferred from the method name via ResultTypeMap. For example, client.request({ method: 'tools/call', ... }) returns Promise<CallToolResult | CreateTaskResult>.

Client list methods return empty results for missing capabilities

Client.listPrompts(), listResources(), listResourceTemplates(), and listTools() now return empty results when the server didn't advertise the corresponding capability, instead of sending the request. This respects the MCP spec's capability negotiation.

To restore v1 behavior (throw an error when capabilities are missing), set enforceStrictCapabilities: true:

const client = new Client(
    { name: 'my-client', version: '1.0.0' },
    {
        enforceStrictCapabilities: true
    }
);

InMemoryTransport removed from public API

InMemoryTransport has been removed from the public API surface. It was previously used for in-process client-server connections and testing.

For testing, import it directly from the internal core package:

// v1
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';

// v2 (testing only — @modelcontextprotocol/core is internal, not for production use)
import { InMemoryTransport } from '@modelcontextprotocol/core';

For production in-process connections, use StreamableHTTPClientTransport with a local server URL, or connect client and server via paired streams.

Removed type aliases and deprecated exports

The following deprecated type aliases have been removed from @modelcontextprotocol/core:

Removed Replacement
JSONRPCError JSONRPCErrorResponse
JSONRPCErrorSchema JSONRPCErrorResponseSchema
isJSONRPCError isJSONRPCErrorResponse
isJSONRPCResponse isJSONRPCResultResponse
ResourceReferenceSchema ResourceTemplateReferenceSchema
ResourceReference ResourceTemplateReference
IsomorphicHeaders Use Web Standard Headers
AuthInfo (from server/auth/types.js) AuthInfo (now re-exported by @modelcontextprotocol/client and @modelcontextprotocol/server)

All other types and schemas exported from @modelcontextprotocol/sdk/types.js retain their original names — import them from @modelcontextprotocol/client or @modelcontextprotocol/server.

Before (v1):

import { JSONRPCError, ResourceReference, isJSONRPCError } from '@modelcontextprotocol/sdk/types.js';

After (v2):

import { JSONRPCErrorResponse, ResourceTemplateReference, isJSONRPCErrorResponse } from '@modelcontextprotocol/server';

Request handler context types

The RequestHandlerExtra type has been replaced with a structured context type hierarchy using nested groups:

v1 v2
RequestHandlerExtra (flat, all fields) ServerContext (server handlers) or ClientContext (client handlers)
extra parameter name ctx parameter name
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.requestInfo ctx.http?.req (standard Web Request, only on ServerContext)
extra.closeSSEStream ctx.http?.closeSSE (only on ServerContext)
extra.closeStandaloneSSEStream ctx.http?.closeStandaloneSSE (only on ServerContext)
extra.sessionId ctx.sessionId
extra.taskStore ctx.task?.store
extra.taskId ctx.task?.id
extra.taskRequestedTtl ctx.task?.requestedTtl

Before (v1):

server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
    const headers = extra.requestInfo?.headers;
    const taskStore = extra.taskStore;
    await extra.sendNotification({ method: 'notifications/progress', params: { progressToken: 'abc', progress: 50, total: 100 } });
    return { content: [{ type: 'text', text: 'result' }] };
});

After (v2):

server.setRequestHandler('tools/call', async (request, ctx) => {
    const headers = ctx.http?.req?.headers; // standard Web Request object
    const taskStore = ctx.task?.store;
    await ctx.mcpReq.notify({ method: 'notifications/progress', params: { progressToken: 'abc', progress: 50, total: 100 } });
    return { content: [{ type: 'text', text: 'result' }] };
});

Context fields are organized into 4 groups:

  • mcpReq — request-level concerns: id, method, _meta, signal, send(), notify(), plus server-only log(), elicitInput(), and requestSampling()
  • http? — HTTP transport concerns (undefined for stdio): authInfo, plus server-only req, closeSSE, closeStandaloneSSE
  • task? — task lifecycle: id, store, requestedTtl

BaseContext is the common base type shared by both ServerContext and ClientContext. ServerContext extends each group with server-specific additions via type intersection.

ServerContext also provides convenience methods for common server→client operations:

server.setRequestHandler('tools/call', async (request, ctx) => {
    // Send a log message (respects client's log level filter)
    await ctx.mcpReq.log('info', 'Processing tool call', 'my-logger');

    // Request client to sample an LLM
    const samplingResult = await ctx.mcpReq.requestSampling({
        messages: [{ role: 'user', content: { type: 'text', text: 'Hello' } }],
        maxTokens: 100
    });

    // Elicit user input via a form
    const elicitResult = await ctx.mcpReq.elicitInput({
        message: 'Please provide details',
        requestedSchema: { type: 'object', properties: { name: { type: 'string' } } }
    });

    return { content: [{ type: 'text', text: 'done' }] };
});

These replace the pattern of calling server.sendLoggingMessage(), server.createMessage(), and server.elicitInput() from within handlers.

Error hierarchy refactoring

The SDK now distinguishes between two types of errors:

  1. ProtocolError (renamed from McpError): Protocol errors that cross the wire as JSON-RPC error responses
  2. SdkError: Local SDK errors that never cross the wire (timeouts, connection issues, capability checks)

Renamed exports

v1 v2
McpError ProtocolError
ErrorCode ProtocolErrorCode
ErrorCode.RequestTimeout SdkErrorCode.RequestTimeout
ErrorCode.ConnectionClosed SdkErrorCode.ConnectionClosed

Before (v1):

import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';

try {
    await client.callTool({ name: 'test', arguments: {} });
} catch (error) {
    if (error instanceof McpError && error.code === ErrorCode.RequestTimeout) {
        console.log('Request timed out');
    }
    if (error instanceof McpError && error.code === ErrorCode.InvalidParams) {
        console.log('Invalid parameters');
    }
}

After (v2):

import { ProtocolError, ProtocolErrorCode, SdkError, SdkErrorCode } from '@modelcontextprotocol/client';

try {
    await client.callTool({ name: 'test', arguments: {} });
} catch (error) {
    // Local timeout/connection errors are now SdkError
    if (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) {
        console.log('Request timed out');
    }
    // Protocol errors from the server are still ProtocolError
    if (error instanceof ProtocolError && error.code === ProtocolErrorCode.InvalidParams) {
        console.log('Invalid parameters');
    }
}

New SdkErrorCode enum

The new SdkErrorCode enum contains string-valued codes for local SDK errors:

Code Description
SdkErrorCode.NotConnected Transport is not connected
SdkErrorCode.AlreadyConnected Transport is already connected
SdkErrorCode.NotInitialized Protocol is not initialized
SdkErrorCode.CapabilityNotSupported Required capability is not supported
SdkErrorCode.RequestTimeout Request timed out waiting for response
SdkErrorCode.ConnectionClosed Connection was closed
SdkErrorCode.SendFailed Failed to send message
SdkErrorCode.ClientHttpNotImplemented HTTP POST request failed
SdkErrorCode.ClientHttpAuthentication Server returned 401 after re-authentication
SdkErrorCode.ClientHttpForbidden Server returned 403 after trying upscoping
SdkErrorCode.ClientHttpUnexpectedContent Unexpected content type in HTTP response
SdkErrorCode.ClientHttpFailedToOpenStream Failed to open SSE stream
SdkErrorCode.ClientHttpFailedToTerminateSession Failed to terminate session

StreamableHTTPError removed

The StreamableHTTPError class has been removed. HTTP transport errors are now thrown as SdkError with specific SdkErrorCode values that provide more granular error information:

Before (v1):

import { StreamableHTTPError } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

try {
    await transport.send(message);
} catch (error) {
    if (error instanceof StreamableHTTPError) {
        console.log('HTTP error:', error.code); // HTTP status code
    }
}

After (v2):

import { SdkError, SdkErrorCode } from '@modelcontextprotocol/client';

try {
    await transport.send(message);
} catch (error) {
    if (error instanceof SdkError) {
        switch (error.code) {
            case SdkErrorCode.ClientHttpAuthentication:
                console.log('Auth failed — server rejected token after re-auth');
                break;
            case SdkErrorCode.ClientHttpForbidden:
                console.log('Forbidden after upscoping attempt');
                break;
            case SdkErrorCode.ClientHttpFailedToOpenStream:
                console.log('Failed to open SSE stream');
                break;
            case SdkErrorCode.ClientHttpNotImplemented:
                console.log('HTTP request failed');
                break;
        }
        // Access HTTP status code from error.data if needed
        const httpStatus = (error.data as { status?: number })?.status;
    }
}

Why this change?

Previously, ErrorCode.RequestTimeout (-32001) and ErrorCode.ConnectionClosed (-32000) were used for local timeout/connection errors. However, these errors never cross the wire as JSON-RPC responses - they are rejected locally. Using protocol error codes for local errors was semantically inconsistent.

The new design:

  • ProtocolError with ProtocolErrorCode: For errors that are serialized and sent as JSON-RPC error responses
  • SdkError with SdkErrorCode: For local errors that are thrown/rejected locally and never leave the SDK

OAuth error refactoring

The OAuth error classes have been consolidated into a single OAuthError class with an OAuthErrorCode enum.

Removed classes

The following individual error classes have been removed in favor of OAuthError with the appropriate code:

v1 Class v2 Equivalent
InvalidRequestError new OAuthError(OAuthErrorCode.InvalidRequest, message)
InvalidClientError new OAuthError(OAuthErrorCode.InvalidClient, message)
InvalidGrantError new OAuthError(OAuthErrorCode.InvalidGrant, message)
UnauthorizedClientError new OAuthError(OAuthErrorCode.UnauthorizedClient, message)
UnsupportedGrantTypeError new OAuthError(OAuthErrorCode.UnsupportedGrantType, message)
InvalidScopeError new OAuthError(OAuthErrorCode.InvalidScope, message)
AccessDeniedError new OAuthError(OAuthErrorCode.AccessDenied, message)
ServerError new OAuthError(OAuthErrorCode.ServerError, message)
TemporarilyUnavailableError new OAuthError(OAuthErrorCode.TemporarilyUnavailable, message)
UnsupportedResponseTypeError new OAuthError(OAuthErrorCode.UnsupportedResponseType, message)
UnsupportedTokenTypeError new OAuthError(OAuthErrorCode.UnsupportedTokenType, message)
InvalidTokenError new OAuthError(OAuthErrorCode.InvalidToken, message)
MethodNotAllowedError new OAuthError(OAuthErrorCode.MethodNotAllowed, message)
TooManyRequestsError new OAuthError(OAuthErrorCode.TooManyRequests, message)
InvalidClientMetadataError new OAuthError(OAuthErrorCode.InvalidClientMetadata, message)
InsufficientScopeError new OAuthError(OAuthErrorCode.InsufficientScope, message)
InvalidTargetError new OAuthError(OAuthErrorCode.InvalidTarget, message)
CustomOAuthError new OAuthError(customCode, message)

The OAUTH_ERRORS constant has also been removed.

Before (v1):

import { InvalidClientError, InvalidGrantError, ServerError } from '@modelcontextprotocol/client';

try {
    await refreshToken();
} catch (error) {
    if (error instanceof InvalidClientError) {
        // Handle invalid client
    } else if (error instanceof InvalidGrantError) {
        // Handle invalid grant
    } else if (error instanceof ServerError) {
        // Handle server error
    }
}

After (v2):

import { OAuthError, OAuthErrorCode } from '@modelcontextprotocol/client';

try {
    await refreshToken();
} catch (error) {
    if (error instanceof OAuthError) {
        switch (error.code) {
            case OAuthErrorCode.InvalidClient:
                // Handle invalid client
                break;
            case OAuthErrorCode.InvalidGrant:
                // Handle invalid grant
                break;
            case OAuthErrorCode.ServerError:
                // Handle server error
                break;
        }
    }
}

Experimental: TaskCreationParams.ttl no longer accepts null

The ttl field in TaskCreationParams (used when requesting the server to create a task) no longer accepts null. Per the MCP spec, null TTL (meaning unlimited lifetime) is only valid in server responses (Task.ttl), not in client requests. Clients should omit ttl to let the server decide the lifetime.

This also narrows the type of requestedTtl in TaskContext, CreateTaskServerContext, and TaskServerContext from number | null | undefined to number | undefined.

Before (v1):

// Requesting unlimited lifetime by passing null
const result = await client.callTool({
    name: 'long-task',
    arguments: {},
    task: { ttl: null }
});

// Handler context had number | null | undefined
server.setRequestHandler('tools/call', async (request, ctx) => {
    const ttl: number | null | undefined = ctx.task?.requestedTtl;
});

After (v2):

// Omit ttl to let the server decide (server may return null for unlimited)
const result = await client.callTool({
    name: 'long-task',
    arguments: {},
    task: {}
});

// Handler context is now number | undefined
server.setRequestHandler('tools/call', async (request, ctx) => {
    const ttl: number | undefined = ctx.task?.requestedTtl;
});

Note: These task APIs are marked @experimental and may change without notice.

Enhancements

Automatic JSON Schema validator selection by runtime

The SDK now automatically selects the appropriate JSON Schema validator based on your runtime environment:

  • Node.js: Uses AjvJsonSchemaValidator (same as v1 default)
  • Cloudflare Workers: Uses CfWorkerJsonSchemaValidator (previously required manual configuration)

This means Cloudflare Workers users no longer need to explicitly pass the validator:

Before (v1) - Cloudflare Workers required explicit configuration:

import { McpServer, CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server';

const server = new McpServer(
    { name: 'my-server', version: '1.0.0' },
    {
        capabilities: { tools: {} },
        jsonSchemaValidator: new CfWorkerJsonSchemaValidator() // Required in v1
    }
);

After (v2) - Works automatically:

import { McpServer } from '@modelcontextprotocol/server';

const server = new McpServer(
    { name: 'my-server', version: '1.0.0' },
    { capabilities: { tools: {} } }
    // Validator auto-selected based on runtime
);

You can still explicitly override the validator if needed. The validators are available via the _shims export:

import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims';
// or
import { AjvJsonSchemaValidator, CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server';

Unchanged APIs

The following APIs are unchanged between v1 and v2 (only the import paths changed):

  • Client constructor and most client methods (connect, listTools, listPrompts, listResources, readResource, etc.) — note: callTool() signature changed (schema parameter removed)
  • McpServer constructor, server.connect(transport), server.close()
  • Server (low-level) constructor and all methods
  • StreamableHTTPClientTransport, SSEClientTransport, StdioClientTransport constructors and options
  • StdioServerTransport constructor and options
  • All Zod schemas and type definitions from types.ts (except the aliases listed above)
  • Tool, prompt, and resource callback return types

Using an LLM to migrate your code

An LLM-optimized version of this guide is available at docs/migration-SKILL.md. It contains dense mapping tables designed for tools like Claude Code to mechanically apply all the changes described above. You can paste it into your LLM context or load it as a skill.

Need Help?

If you encounter issues during migration:

  1. Check the FAQ for common questions about v2 changes
  2. Review the examples for updated usage patterns
  3. Open an issue on GitHub if you find a bug or need further assistance