diff --git a/.changeset/migration-doc-overhaul.md b/.changeset/migration-doc-overhaul.md new file mode 100644 index 000000000..4c9a2848b --- /dev/null +++ b/.changeset/migration-doc-overhaul.md @@ -0,0 +1,7 @@ +--- +'@modelcontextprotocol/server': patch +'@modelcontextprotocol/client': patch +--- + +Overhaul v1→v2 migration docs: lead with the "just bump `@modelcontextprotocol/sdk` to ^2" path, add prerequisite callouts (zod ^4.2.0, `moduleResolution: bundler|nodenext`, bun cache), and fill mapping-table gaps (`ResourceTemplateType`, `OAuthError.code`, `parseJSONRPCMessage`, +`specTypeSchema`, `ZodRawShapeCompat`, `roots/list`/`tasks/*`/`completion/complete` method strings, `InMemoryTransport`, `callToolStream`, custom-method handlers, transitive-v1-dep guidance). diff --git a/docs/migration-SKILL.md b/docs/migration-SKILL.md index a37b5e206..68b517ead 100644 --- a/docs/migration-SKILL.md +++ b/docs/migration-SKILL.md @@ -1,20 +1,25 @@ --- 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. +description: Migrate MCP TypeScript SDK code from v1 (@modelcontextprotocol/sdk) to v2 (@modelcontextprotocol/sdk@^2, or the split /client and /server packages). 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. +**Shortest path for most servers:** bump `@modelcontextprotocol/sdk` to `^2.0.0` and stop. v1 import paths and APIs continue to work as deprecated aliases that warn once and forward to the new implementation. If the user only wants to "get on v2", do that, run the build, and +report any deprecation warnings as optional follow-ups. + +Apply the rest of this guide when the user wants a **full migration** to the new split packages with no deprecation warnings. Order: environment → dependencies → imports → API calls → type aliases. ## 1. Environment - Node.js 20+ required (v18 dropped) - ESM only (CJS dropped). If the project uses `require()`, convert to `import`/`export` or use dynamic `import()`. +- `tsconfig.json` must use `"moduleResolution": "bundler"`, `"nodenext"`, or `"node16"`. Legacy `"node"` / `"node10"` cannot resolve v2's `exports`-only packages and fails with `TS2307`. Do **not** add `main`/`types` fields to work around this — fix the tsconfig. +- **Zod must be `^4.2.0`** (if used). zod 3.x and 4.0–4.1 lack `~standard.jsonSchema` and crash at runtime on `tools/list` even though typecheck passes. Import from `'zod'` or `'zod/v4'`. ## 2. Dependencies -Remove the old package and install only what you need: +Either keep `@modelcontextprotocol/sdk@^2` (meta-package, simplest), **or** remove it and install the split packages: ```bash npm uninstall @modelcontextprotocol/sdk @@ -28,7 +33,7 @@ npm uninstall @modelcontextprotocol/sdk | Server + Express | `npm install @modelcontextprotocol/server @modelcontextprotocol/express` | | Server + Hono | `npm install @modelcontextprotocol/server @modelcontextprotocol/hono` | -`@modelcontextprotocol/core` is installed automatically as a dependency. +`@modelcontextprotocol/core` is an internal package — never install or import it directly. Its contents are bundled into `@modelcontextprotocol/client` and `@modelcontextprotocol/server`, which re-export everything you need. ## 3. Import Mapping @@ -36,13 +41,13 @@ 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` | +| 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` | REMOVED (use Streamable HTTP or stdio; implement `Transport` for custom needs) | ### Server imports @@ -53,24 +58,27 @@ Replace all `@modelcontextprotocol/sdk/...` imports using this table. | `@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/sse.js` | REMOVED (migrate to Streamable HTTP; deprecated copy at `@modelcontextprotocol/node/sse` for proxy/bridge use) | +| `@modelcontextprotocol/sdk/server/auth/*` | REMOVED (use external auth library; `requireBearerAuth`/`mcpAuthMetadataRouter` available in `@modelcontextprotocol/express`) | +| `@modelcontextprotocol/sdk/server/auth/errors.js` | `@modelcontextprotocol/client` or `/server` (see OAuth error consolidation in section 5) | | `@modelcontextprotocol/sdk/server/middleware.js` | `@modelcontextprotocol/express` (signature changed, see section 8) | +| `@modelcontextprotocol/sdk/server/express.js` | `@modelcontextprotocol/express` | ### Types / shared imports -| v1 import path | v2 package | -| ------------------------------------------------- | ---------------------------- | +| 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` | +| `@modelcontextprotocol/sdk/inMemory.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | Notes: -- `@modelcontextprotocol/client` and `@modelcontextprotocol/server` both re-export shared types from `@modelcontextprotocol/core`, so import from whichever package you already depend on. Do not import from `@modelcontextprotocol/core` directly — it is an internal package. +- `@modelcontextprotocol/client` and `@modelcontextprotocol/server` both re-export the shared types, so import from whichever package you already depend on. Do not import from `@modelcontextprotocol/core` directly — 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 @@ -81,24 +89,33 @@ Notes: ## 5. Removed / Renamed Type Aliases and Symbols -| v1 (removed) | v2 (replacement) | -| ---------------------------------------- | -------------------------------------------------------- | -| `JSONRPCError` | `JSONRPCErrorResponse` | -| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` | -| `isJSONRPCError` | `isJSONRPCErrorResponse` | -| `isJSONRPCResponse` (deprecated in v1) | `isJSONRPCResultResponse` (**not** v2's new `isJSONRPCResponse`, which correctly matches both result and error) | -| `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*`) | -| `WebSocketClientTransport` | REMOVED (use `StreamableHTTPClientTransport` or `StdioClientTransport`) | - -All other **type** symbols from `@modelcontextprotocol/sdk/types.js` retain their original names. **Zod schemas** (e.g., `CallToolResultSchema`, `ListToolsResultSchema`) are no longer part of the public API — they are internal to the SDK. For runtime validation, use type guard functions like `isCallToolResult` instead of `CallToolResultSchema.safeParse()`. +| v1 (removed) | v2 (replacement) | +| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `JSONRPCError` | `JSONRPCErrorResponse` | +| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` | +| `isJSONRPCError` | `isJSONRPCErrorResponse` | +| `isJSONRPCResponse` (deprecated in v1) | `isJSONRPCResultResponse` (**not** v2's new `isJSONRPCResponse`, which correctly matches both result and error) | +| `ResourceReference` | `ResourceTemplateReference` | +| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` | +| `ResourceTemplate` (the protocol _type_) | `ResourceTemplateType` (`ResourceTemplate` is now the server template _class_) | +| `IsomorphicHeaders` | REMOVED (use Web Standard `Headers`) | +| `RequestInfo` (custom SDK type) | REMOVED (use Web Standard `Request` via `ctx.http?.req`) | +| `ZodRawShapeCompat` | `StandardSchemaWithJSON` | +| `AuthInfo` (from `server/auth/types.js`) | `AuthInfo` (now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`) | +| `OAuthProtectedResourceMetadata` (from `shared/auth.js`) | `OAuthProtectedResourceMetadata` (re-exported by `@modelcontextprotocol/client` / `server`) | +| `OAuthError#errorCode` (instance prop) | `OAuthError#code` | +| `JSONRPCMessageSchema.parse(raw)` | `parseJSONRPCMessage(raw)` | +| `Schema.parse(v)` (any spec type) | `specTypeSchema('').parse(v)` / `isSpecType('', v)` | +| `McpError` | `ProtocolError` | +| `ErrorCode` | `ProtocolErrorCode` | +| `ErrorCode.RequestTimeout` | `SdkErrorCode.RequestTimeout` | +| `ErrorCode.ConnectionClosed` | `SdkErrorCode.ConnectionClosed` | +| `StreamableHTTPError` | REMOVED (use `SdkError` with `SdkErrorCode.ClientHttp*`) | +| `WebSocketClientTransport` | REMOVED (use `StreamableHTTPClientTransport` or `StdioClientTransport`) | + +All other **type** symbols from `@modelcontextprotocol/sdk/types.js` retain their original names. **Zod schemas** (e.g., `CallToolResultSchema`, `ListToolsResultSchema`, `OAuthTokensSchema`) are no longer part of the public API. For runtime validation: use +`parseJSONRPCMessage(raw)` for transport framing, `isCallToolResult(v)` for the common case, or `specTypeSchema('')` / `isSpecType('', v)` for any other spec type. The raw Zod constants remain available at `@modelcontextprotocol/server/zod-schemas` as a compatibility +escape hatch. ### Error class changes @@ -204,13 +221,14 @@ 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). +protocol **type** definitions, all callback return types. Note: `callTool()` and `request()` signatures changed (schema parameter removed, see section 11). Zod **schema constants** are no longer exported (see section 5). ## 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](https://standardschema.dev/) — 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)` from `@modelcontextprotocol/server` (validator defaults automatically; pass an explicit validator for custom configurations). Applies to `inputSchema`, `outputSchema`, and `argsSchema`. +**IMPORTANT**: v2 requires schema objects implementing [Standard Schema](https://standardschema.dev/) — 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)` from `@modelcontextprotocol/server` (validator defaults automatically; pass an explicit validator for custom configurations). Applies to `inputSchema`, `outputSchema`, and `argsSchema`. ### Tools @@ -280,20 +298,20 @@ 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() })` | +| 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` | `StandardSchemaWithJSON.InferInput` | +| Removed from `@modelcontextprotocol/core` | Replacement | +| ------------------------------------------------------------------------------------ | ----------------------------------------- | +| `schemaToJson(schema)` | `standardSchemaToJsonSchema(schema)` | +| `parseSchemaAsync(schema, data)` | `validateStandardSchema(schema, data)` | +| `SchemaInput` | `StandardSchemaWithJSON.InferInput` | | `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema` | none (internal Zod introspection helpers) | ## 7. Headers API @@ -373,10 +391,22 @@ Schema to method string mapping: | `PromptListChangedNotificationSchema` | `'notifications/prompts/list_changed'` | | `ProgressNotificationSchema` | `'notifications/progress'` | | `CancelledNotificationSchema` | `'notifications/cancelled'` | +| `RootsListChangedNotificationSchema` | `'notifications/roots/list_changed'` | +| `ListRootsRequestSchema` | `'roots/list'` | +| `CompleteRequestSchema` | `'completion/complete'` | +| `SubscribeRequestSchema` | `'resources/subscribe'` | +| `UnsubscribeRequestSchema` | `'resources/unsubscribe'` | +| `ListResourceTemplatesRequestSchema` | `'resources/templates/list'` | +| `GetTaskRequestSchema` | `'tasks/get'` | +| `GetTaskPayloadRequestSchema` | `'tasks/result'` | +| `ElicitationCompleteNotificationSchema` | `'notifications/elicitation/complete'` | | `InitializedNotificationSchema` | `'notifications/initialized'` | Request/notification params remain fully typed. Remove unused schema imports after migration. +For **vendor-prefixed custom methods**, pass an explicit schema as the second argument: `server.setNotificationHandler('x-myorg/heartbeat', z.object({...}), handler)`. Do **not** add custom params to a spec method name — v2 validates against the spec schema and strips unknown +fields. + ## 10. Request Handler Context Types `RequestHandlerExtra` → structured context types with nested groups. Rename `extra` → `ctx` in all handler callbacks. @@ -407,9 +437,9 @@ Request/notification params remain fully typed. Remove unused schema imports aft | `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()` +## 11. Schema parameter removed from `request()`, `send()`, `callTool()`, and `callToolStream()` -`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. +`Protocol.request()`, `BaseContext.mcpReq.send()`, `Client.callTool()`, and `client.experimental.tasks.callToolStream()` no longer take a Zod result schema argument. The SDK resolves the schema internally from the method name. ```typescript // v1: schema required @@ -424,40 +454,41 @@ 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)` | +| v1 call | v2 call | +| -------------------------------------------------------------------------- | ------------------------------------------------------------ | +| `client.request(req, ResultSchema)` | `client.request(req)` | +| `client.request(req, ResultSchema, options)` | `client.request(req, options)` | +| `client.experimental.tasks.callToolStream(params, ResultSchema, options?)` | `client.experimental.tasks.callToolStream(params, 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. If `CallToolResultSchema` was used for **runtime validation** (not just as a `request()` argument), replace with the `isCallToolResult` type guard: -| v1 pattern | v2 replacement | -| --------------------------------------------------- | -------------------------- | -| `CallToolResultSchema.safeParse(value).success` | `isCallToolResult(value)` | -| `CallToolResultSchema.parse(value)` | Use `isCallToolResult(value)` then cast, or use `CallToolResult` type | +| v1 pattern | v2 replacement | +| ----------------------------------------------- | --------------------------------------------------------------------- | +| `CallToolResultSchema.safeParse(value).success` | `isCallToolResult(value)` | +| `CallToolResultSchema.parse(value)` | Use `isCallToolResult(value)` then cast, or use `CallToolResult` type | ## 12. Experimental: `TaskCreationParams.ttl` no longer accepts `null` `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) | +| 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` | +| 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` | +| `TaskServerContext.task.requestedTtl` | `number \| null \| undefined` | `number \| undefined` | > These task APIs are `@experimental` and may change without notice. @@ -487,13 +518,11 @@ new McpServer( new McpServer({ name: 'server', version: '1.0.0' }, {}); ``` -Access validators explicitly: -- Runtime-aware default: `import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims';` -- AJV (Node.js): `import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server';` -- CF Worker: `import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server/validators/cf-worker';` +Access validators via `_shims` export: `import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims';` ## 15. Migration Steps (apply in this order) +0. **Shortest path:** bump `@modelcontextprotocol/sdk` to `^2.0.0`, ensure `zod@^4.2.0` and `moduleResolution: bundler|nodenext`, run the build. If it passes, you are on v2; deprecation warnings are optional follow-ups. Stop here unless a full migration is requested. 1. Update `package.json`: `npm uninstall @modelcontextprotocol/sdk`, install the appropriate v2 packages 2. Replace all imports from `@modelcontextprotocol/sdk/...` using the import mapping tables (sections 3-4), including `StreamableHTTPServerTransport` → `NodeStreamableHTTPServerTransport` 3. Replace removed type aliases (`JSONRPCError` → `JSONRPCErrorResponse`, etc.) per section 5 diff --git a/docs/migration.md b/docs/migration.md index 7cb7d58f6..6dc9330be 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -2,38 +2,110 @@ This guide covers the breaking changes introduced in v2 of the MCP TypeScript SDK and how to update your code. +## TL;DR — most servers just bump the version + +For **most MCP servers**, upgrading to v2 is a one-line change: + +```diff +- "@modelcontextprotocol/sdk": "^1.0.0" ++ "@modelcontextprotocol/sdk": "^2.0.0" +``` + +In v2, `@modelcontextprotocol/sdk` is a meta-package that re-exports the new split packages at the v1 import paths. v1 APIs (deep-import paths like `@modelcontextprotocol/sdk/server/mcp.js`, variadic `server.tool()`, `McpError`, `RequestHandlerExtra`, etc.) continue to work as +**deprecated aliases** that emit a one-time console warning pointing at the new API. You can ship on v2 immediately and migrate the deprecation warnings at your own pace. + +Read on if you want to: + +- Migrate to the new split packages (`@modelcontextprotocol/server`, `@modelcontextprotocol/client`) for smaller bundles +- Adopt the new API surface (no deprecation warnings) +- Understand a specific breaking change + +> **Clients and frameworks** (host applications, custom transports, proxies) typically need a few more changes than servers — see the [Runtime validation](#zod-schemas-are-no-longer-exported-runtime-validation), [Custom methods](#custom-method-names-and-extending-spec-methods), +> and [Server auth](#server-auth-removed) sections. + +## Prerequisites + +Before upgrading, check these three environment requirements. They are the most common source of confusing errors during migration. + +### Zod must be `^4.2.0` + +If you use Zod for tool/prompt schemas, you must be on **`zod@^4.2.0` or later**, and import from the `zod` (or `zod/v4`) entry point. + +Older Zod versions (3.x, or 4.0.0–4.1.x) do **not** implement the `~standard.jsonSchema` property the SDK uses to render `inputSchema` for `tools/list`. This passes type-checking but **crashes at runtime** when a client calls `tools/list`. + +```jsonc +// package.json +"dependencies": { + "zod": "^4.2.0" +} +``` + +```typescript +// Either of these works on zod@^4.2.0: +import * as z from 'zod'; +import * as z from 'zod/v4'; + +// This does NOT work (zod 3.x has no Standard Schema support): +import * as z from 'zod/v3'; +``` + +### TypeScript `moduleResolution` must be `bundler`, `nodenext`, or `node16` + +v2 packages ship ESM-only with an `exports` map and no top-level `main`/`types` fields. TypeScript's legacy `"moduleResolution": "node"` (or `"node10"`) cannot resolve them and fails with `TS2307: Cannot find module '@modelcontextprotocol/server'`. + +Update your `tsconfig.json`: + +```jsonc +{ + "compilerOptions": { + "moduleResolution": "bundler" // or "nodenext" / "node16" + } +} +``` + +> We deliberately do **not** ship `main`/`types` fallback fields — testing showed it suppresses TypeScript's helpful "consider updating moduleResolution" diagnostic and replaces it with dozens of misleading transitive errors. + +### Testing locally with bun: clear the cache + +If you are testing a local v2 build via `file:` tarballs, note that **bun caches `file:` dependencies by filename** and ignores content changes. Re-packing the SDK without bumping the version installs a stale tarball. + +```bash +bun pm cache rm && bun install --force +``` + +npm and pnpm do not have this issue. + ## 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. +Version 2 of the MCP TypeScript SDK introduces several changes to improve modularity, reduce dependency bloat, and provide a cleaner API surface. The biggest structural change is the split into separate `@modelcontextprotocol/client` and `@modelcontextprotocol/server` packages, +with `@modelcontextprotocol/sdk` remaining as a meta-package that re-exports both. ## 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) | +The single `@modelcontextprotocol/sdk` package has been split. `@modelcontextprotocol/sdk` still exists as a **meta-package** (the recommended default — just bump the version), but you can now also install only the half you need: -Remove the old package and install only the packages you need: +| v1 | v2 | +| --------------------------- | ----------------------------------------------------------------------------- | +| `@modelcontextprotocol/sdk` | `@modelcontextprotocol/sdk` (meta-package, re-exports everything below) | +| | `@modelcontextprotocol/client` (client implementation) | +| | `@modelcontextprotocol/server` (server implementation) | +| | `@modelcontextprotocol/node` / `express` / `hono` / `fastify` (HTTP adapters) | ```bash -npm uninstall @modelcontextprotocol/sdk +# Easiest: keep the meta-package +npm install @modelcontextprotocol/sdk@^2 -# If you only need a client +# Or install only what you need 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: +> `@modelcontextprotocol/core` is an **internal** package — never install or import it directly. Its contents are **bundled into** (not depended on by) `@modelcontextprotocol/client` and `@modelcontextprotocol/server`, so it never appears in your `node_modules`. Everything you +> need is re-exported from the client/server packages. + +If you switch to the split packages, update your imports accordingly: **Before (v1):** @@ -57,7 +129,7 @@ import { McpServer, StdioServerTransport, WebStandardStreamableHTTPServerTranspo 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. +Note: `@modelcontextprotocol/client` and `@modelcontextprotocol/server` both re-export the shared types, 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 @@ -110,6 +182,40 @@ const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () 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. +**Before (v1):** + +```typescript +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; + +let transport: SSEServerTransport; + +app.get('/sse', async (req, res) => { + transport = new SSEServerTransport('/messages', res); + await server.connect(transport); +}); +app.post('/messages', async (req, res) => { + await transport.handlePostMessage(req, res); +}); +``` + +**After (v2, stateless):** + +```typescript +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; + +app.all('/mcp', async (req, res) => { + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined }); + await server.connect(transport); + await transport.handleRequest(req, res); +}); +``` + +With `sessionIdGenerator: undefined` the transport runs in stateless mode, so creating a fresh instance per request is correct. For stateful sessions, the transport must be created once per session and stored in a `Map` keyed by session +ID — see `examples/server/src/simpleStreamableHttp.ts` for the full pattern. + +> **Proxy/bridge use case:** If you were using `SSEServerTransport` to bridge a stdio server to a browser (the inspector pattern), a deprecated copy is available at `@modelcontextprotocol/node/sse` for transitional use. New code should expose Streamable HTTP and let the browser +> client open the SSE stream itself. + ### `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. @@ -294,11 +400,11 @@ This applies to: **Removed Zod-specific helpers** from `@modelcontextprotocol/core` (use Standard Schema equivalents): -| Removed | Replacement | -|---|---| -| `schemaToJson(schema)` | `standardSchemaToJsonSchema(schema)` | -| `parseSchemaAsync(schema, data)` | `validateStandardSchema(schema, data)` | -| `SchemaInput` | `StandardSchemaWithJSON.InferInput` | +| Removed | Replacement | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------- | +| `schemaToJson(schema)` | `standardSchemaToJsonSchema(schema)` | +| `parseSchemaAsync(schema, data)` | `validateStandardSchema(schema, data)` | +| `SchemaInput` | `StandardSchemaWithJSON.InferInput` | | `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema` | No replacement — these are now internal Zod introspection helpers | ### Host header validation moved @@ -381,11 +487,21 @@ Common method string replacements: | `ToolListChangedNotificationSchema` | `'notifications/tools/list_changed'` | | `ResourceListChangedNotificationSchema` | `'notifications/resources/list_changed'` | | `PromptListChangedNotificationSchema` | `'notifications/prompts/list_changed'` | +| `RootsListChangedNotificationSchema` | `'notifications/roots/list_changed'` | +| `ListRootsRequestSchema` | `'roots/list'` | +| `CompleteRequestSchema` | `'completion/complete'` | +| `SubscribeRequestSchema` | `'resources/subscribe'` | +| `UnsubscribeRequestSchema` | `'resources/unsubscribe'` | +| `ListResourceTemplatesRequestSchema` | `'resources/templates/list'` | +| `GetTaskRequestSchema` | `'tasks/get'` | +| `GetTaskPayloadRequestSchema` | `'tasks/result'` | +| `ElicitationCompleteNotificationSchema` | `'notifications/elicitation/complete'` | +| `InitializedNotificationSchema` | `'notifications/initialized'` | ### `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. +The public `Protocol.request()`, `BaseContext.mcpReq.send()`, `Client.callTool()`, and `client.experimental.tasks.callToolStream()` 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):** @@ -440,6 +556,24 @@ const result = await client.callTool({ name: 'my-tool', arguments: {} }, Compati const result = await client.callTool({ name: 'my-tool', arguments: {} }); ``` +**`client.experimental.tasks.callToolStream()` — Before (v1):** + +```typescript +import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; + +for await (const event of client.experimental.tasks.callToolStream({ name: 'my-tool', arguments: {} }, CallToolResultSchema)) { + // ... +} +``` + +**After (v2):** + +```typescript +for await (const event of client.experimental.tasks.callToolStream({ 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`. If you were using `CallToolResultSchema` for **runtime validation** (not just in `request()`/`callTool()` calls), use the new `isCallToolResult` type guard instead: @@ -447,13 +581,59 @@ If you were using `CallToolResultSchema` for **runtime validation** (not just in ```typescript // v1: runtime validation with Zod schema import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; -if (CallToolResultSchema.safeParse(value).success) { /* ... */ } +if (CallToolResultSchema.safeParse(value).success) { + /* ... */ +} // v2: use the type guard import { isCallToolResult } from '@modelcontextprotocol/client'; -if (isCallToolResult(value)) { /* ... */ } +if (isCallToolResult(value)) { + /* ... */ +} +``` + +### Zod schemas are no longer exported (runtime validation) + +v1 exported every protocol Zod schema (`CallToolResultSchema`, `JSONRPCMessageSchema`, `OAuthTokensSchema`, etc.) as part of the public API. v2 exports only the **TypeScript types** — the Zod schemas are now internal implementation details. This affects code that did runtime +validation at trust boundaries (custom transports, OAuth response parsing, error-data inspection). + +| v1 pattern | v2 replacement | +| ------------------------------------------------------------------- | ------------------------------------------------ | +| `JSONRPCMessageSchema.parse(raw)` | `parseJSONRPCMessage(raw)` | +| `CallToolResultSchema.safeParse(v).success` | `isCallToolResult(v)` | +| `Schema.safeParse(v).success` (any spec type) | `isSpecType('', v)` | +| `Schema.parse(v)` (any spec type) | `specTypeSchema('').parse(v)` | +| `OAuthTokensSchema.parse(body)` / `OAuthMetadataSchema.parse(body)` | `specTypeSchema('OAuthTokens').parse(body)` etc. | + +`parseJSONRPCMessage`, `isSpecType`, and `specTypeSchema` are exported from both `@modelcontextprotocol/client` and `@modelcontextprotocol/server`. + +If you need direct access to the Zod constants (e.g., to extend or compose them), they remain available at the `@modelcontextprotocol/server/zod-schemas` subpath as a compatibility escape hatch. + +### Custom method names and extending spec methods + +v1 let you register a handler for **any** method string by passing a custom Zod schema. In v2, `setRequestHandler('method', handler)` and `setNotificationHandler('method', handler)` are typed against the closed set of spec method names. + +To register a handler for a **vendor-prefixed custom method**, pass an explicit schema as the second argument: + +```typescript +server.setNotificationHandler('x-myorg/heartbeat', z.object({ ts: z.number() }), notification => { + // notification.params is typed as { ts: number } +}); ``` +> **Warning:** Do **not** add custom fields to a _spec_ method's params (e.g., adding `{ tabId, image }` to `'notifications/message'`). v2 validates incoming spec messages against the spec schema, so unknown fields are stripped before your handler sees them. Use a vendor-prefixed +> method name instead. + +### Transitive dependencies still on v1 + +If a package you depend on (e.g., a shared MCP utilities library or framework adapter) still vends types from `@modelcontextprotocol/sdk@^1`, you will see structural type errors where v1 and v2 types meet (`Server` is not assignable to `Server`, `Transport` not assignable to +`Transport`). + +There is no SDK-side fix for this. Either: + +- Upgrade the transitive dependency to v2 first, or +- Add a type assertion (`as unknown as Transport`) at the boundary until it is upgraded. + ### 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. @@ -469,40 +649,45 @@ const client = new Client( ); ``` -### `InMemoryTransport` removed from public API +### `InMemoryTransport` import path -`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: +`InMemoryTransport` is used for testing client/server interactions within a single process. It is re-exported from both `@modelcontextprotocol/server` and `@modelcontextprotocol/client`. ```typescript // v1 import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; -// v2 (testing only — @modelcontextprotocol/core is internal, not for production use) -import { InMemoryTransport } from '@modelcontextprotocol/core'; +// v2 +import { InMemoryTransport } from '@modelcontextprotocol/server'; ``` -For **production in-process connections**, use `StreamableHTTPClientTransport` with a local server URL, or connect client and server via paired streams. +For **production in-process connections**, prefer `StreamableHTTPClientTransport` with a local server URL. ### 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` (see note below) | -| `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`. - -> **Note on `isJSONRPCResponse`:** v1's `isJSONRPCResponse` was a deprecated alias that only checked for *result* responses (it was equivalent to `isJSONRPCResultResponse`). v2 removes the deprecated alias and introduces a **new** `isJSONRPCResponse` with corrected semantics — it checks for *any* response (either result or error). If you are migrating v1 code that used `isJSONRPCResponse`, rename it to `isJSONRPCResultResponse` to preserve the original behavior. Use the new `isJSONRPCResponse` only when you want to match both result and error responses. +| Removed | Replacement | +| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| `JSONRPCError` | `JSONRPCErrorResponse` | +| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` | +| `isJSONRPCError` | `isJSONRPCErrorResponse` | +| `isJSONRPCResponse` | `isJSONRPCResultResponse` (see note below) | +| `ResourceReferenceSchema` | `ResourceTemplateReferenceSchema` | +| `ResourceReference` | `ResourceTemplateReference` | +| `ResourceTemplate` (the protocol _type_) | `ResourceTemplateType` (the name `ResourceTemplate` is now the server-side template _class_) | +| `IsomorphicHeaders` | Use Web Standard `Headers` | +| `RequestInfo` (custom SDK type) | Use Web Standard `Request` (via `ctx.http?.req`) | +| `FetchLike` | `FetchLike` (unchanged, but now imported from `@modelcontextprotocol/client` / `server`) | +| `ZodRawShapeCompat` | `StandardSchemaWithJSON` (the constraint type for `inputSchema`/`argsSchema`) | +| `AuthInfo` (from `server/auth/types.js`) | `AuthInfo` (now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`) | +| `OAuthProtectedResourceMetadata` (from `shared/auth.js`) | `OAuthProtectedResourceMetadata` (now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`) | + +All other **types** exported from `@modelcontextprotocol/sdk/types.js` retain their original names — import them from `@modelcontextprotocol/client` or `@modelcontextprotocol/server`. **Zod schemas** (`*Schema` constants) are no longer part of the public API — see +[Zod schemas are no longer exported](#zod-schemas-are-no-longer-exported-runtime-validation). + +> **Note on `isJSONRPCResponse`:** v1's `isJSONRPCResponse` was a deprecated alias that only checked for _result_ responses (it was equivalent to `isJSONRPCResultResponse`). v2 removes the deprecated alias and introduces a **new** `isJSONRPCResponse` with corrected semantics — it +> checks for _any_ response (either result or error). If you are migrating v1 code that used `isJSONRPCResponse`, rename it to `isJSONRPCResultResponse` to preserve the original behavior. Use the new `isJSONRPCResponse` only when you want to match both result and error responses. **Before (v1):** @@ -530,7 +715,7 @@ The `RequestHandlerExtra` type has been replaced with a structured context type | `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.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` | @@ -726,6 +911,20 @@ The new design: The OAuth error classes have been consolidated into a single `OAuthError` class with an `OAuthErrorCode` enum. +#### `errorCode` property renamed to `code` + +The `OAuthError` instance property `errorCode` has been renamed to `code` for consistency with `ProtocolError` and `SdkError`: + +```typescript +// v1 +if (err instanceof OAuthError && err.errorCode === 'invalid_token') { ... } + +// v2 +if (err instanceof OAuthError && err.code === OAuthErrorCode.InvalidToken) { ... } +``` + +A deprecated `errorCode` getter remains for backwards compatibility and emits a one-time warning. + #### Removed classes The following individual error classes have been removed in favor of `OAuthError` with the appropriate code: @@ -797,7 +996,8 @@ try { ### 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. +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`. @@ -876,10 +1076,8 @@ const server = new McpServer( You can still explicitly override the validator if needed: ```typescript -// Runtime-aware default (auto-selects AjvJsonSchemaValidator or CfWorkerJsonSchemaValidator) import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims'; - -// Specific validators +// or import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server'; import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server/validators/cf-worker'; ``` @@ -893,7 +1091,7 @@ The following APIs are unchanged between v1 and v2 (only the import paths change - `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) +- All protocol **type definitions** from `types.ts` (except the aliases listed above). Zod **schema constants** are no longer exported — see [Zod schemas are no longer exported](#zod-schemas-are-no-longer-exported-runtime-validation). - Tool, prompt, and resource callback return types ## Using an LLM to migrate your code