docs(migration): close guide gaps surfaced by real v1-to-v2 migrations (#2382)
This commit is contained in:
parent
9f8ba61279
commit
b96a94c6b1
5 changed files with 412 additions and 184 deletions
4
.changeset/migration-doc-findings.md
Normal file
4
.changeset/migration-doc-findings.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
---
|
||||
|
||||
Docs-only: migration-guide corrections from real v1-to-v2 migrations. No package changes.
|
||||
|
|
@ -1,3 +1,10 @@
|
|||
---
|
||||
title: Migration Guides
|
||||
children:
|
||||
- ./upgrade-to-v2.md
|
||||
- ./support-2026-07-28.md
|
||||
---
|
||||
|
||||
# MCP TypeScript SDK — Migration Guides
|
||||
|
||||
Pick the guide for your starting point.
|
||||
|
|
@ -12,9 +19,12 @@ v2 packages (`@modelcontextprotocol/client`, `@modelcontextprotocol/server`, …
|
|||
Start by running the codemod:
|
||||
|
||||
```bash
|
||||
npx @modelcontextprotocol/codemod@alpha v1-to-v2 ./src
|
||||
npx @modelcontextprotocol/codemod@alpha v1-to-v2 .
|
||||
```
|
||||
|
||||
Run it at the package root (`.`) — real projects import the SDK from `test/`,
|
||||
`scripts/`, and fixtures too, and those rewrites are missed when you point it at `./src`.
|
||||
|
||||
The codemod handles most mechanical renames. The guide covers what it can't. The
|
||||
codemod handles the v1→v2 SDK surface upgrade only — adopting the 2026-07-28 protocol
|
||||
revision (`createMcpHandler`, multi-round-trip requests, `versionNegotiation`) is
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
---
|
||||
title: Supporting protocol revision 2026-07-28
|
||||
---
|
||||
|
||||
# Supporting protocol revision 2026-07-28
|
||||
|
||||
This guide is for code **already on the v2 packages** that wants to speak the 2026-07-28
|
||||
|
|
@ -5,6 +9,11 @@ protocol revision — and for code written against an earlier **v2 alpha** that
|
|||
wire-only members directly. If you are on `@modelcontextprotocol/sdk` (v1.x), start with
|
||||
[upgrade-to-v2.md](./upgrade-to-v2.md) instead.
|
||||
|
||||
> **Schema artifact:** until the revision is finalized, the spec repository publishes
|
||||
> the 2026-07-28 schema under `schema/draft/` — there is no `schema/2026-07-28/`
|
||||
> directory yet. Tooling that vendors per-revision schema artifacts should track
|
||||
> `draft/` and note the divergence.
|
||||
|
||||
Nothing in v2 puts a 2026-07-28 byte on the wire by default: a hand-constructed
|
||||
`Client` / `Server` / `McpServer` keeps speaking the 2025-era protocol it was written
|
||||
for. Serving or speaking 2026-07-28 is always an explicit opt-in via one of the entries
|
||||
|
|
@ -49,6 +58,14 @@ client.getProtocolEra(); // 'modern' | 'legacy'
|
|||
- **`mode: { pin: '2026-07-28' }`** — modern only; no fallback, `connect()` rejects with
|
||||
`SdkError(EraNegotiationFailed)` against a 2025-only server.
|
||||
|
||||
`ProtocolOptions.supportedProtocolVersions` — the same option that pins what the legacy
|
||||
`initialize` handshake offers (see
|
||||
[upgrade-to-v2.md › Client connection & dispatch](./upgrade-to-v2.md#client-connection--dispatch))
|
||||
— shapes `'auto'`: the modern candidates are the option's modern entries (when it lists
|
||||
any; otherwise the SDK's default modern set), and legacy fallback is available only if
|
||||
the list has a pre-2026 entry. A `{ pin }` is honored as given — it must name a modern
|
||||
revision but is not checked against the list.
|
||||
|
||||
#### Probe policy
|
||||
|
||||
Failure semantics under `'auto'` are deliberately conservative but never silent about
|
||||
|
|
@ -78,8 +95,19 @@ versionNegotiation: {
|
|||
continuation — select-and-continue with a mutual version — is a separate negotiation step
|
||||
and is never counted against it).
|
||||
|
||||
Once a modern era is negotiated the client auto-attaches the per-request `_meta`
|
||||
envelope to every outgoing request and notification. A gateway/worker fleet can skip the
|
||||
**Who should not default to `'auto'`:** spawn-per-invocation CLI and debugging tools.
|
||||
On stdio, a legacy server that never answers unknown pre-`initialize` requests stalls
|
||||
`connect()` for the full probe timeout before falling back; and the probe round trip
|
||||
changes recorded transcripts/raw logs, which matters for tools whose value is
|
||||
byte-stable observation. Such tools should keep the default and expose `'auto'` /
|
||||
a pin as an explicit flag.
|
||||
|
||||
The probe request itself already carries the per-request `_meta` envelope
|
||||
(`io.modelcontextprotocol/protocolVersion`, `clientInfo`, `clientCapabilities`) —
|
||||
**before** the era is known. Once a modern era is negotiated the client auto-attaches
|
||||
the envelope to every outgoing request and notification. Tooling that classifies
|
||||
traffic must not treat "saw an envelope" as "modern era negotiated": the legacy-fallback
|
||||
path also begins with one enveloped probe. A gateway/worker fleet can skip the
|
||||
probe entirely with `client.connect(transport, { prior: persistedDiscoverResult })`.
|
||||
|
||||
### Server over HTTP: `createMcpHandler`
|
||||
|
|
@ -151,6 +179,22 @@ A client whose connection negotiated a modern era drops inbound server→client
|
|||
requests (the 2026 era has no such channel) instead of answering them; legacy-era
|
||||
connections are unchanged.
|
||||
|
||||
### In-process testing
|
||||
|
||||
There is no in-memory serving entry — `InMemoryTransport.createLinkedPair()` connects
|
||||
2025-era instances only. To exercise 2026-07-28 behavior in tests without sockets,
|
||||
drive `createMcpHandler` directly through its fetch function:
|
||||
|
||||
```typescript
|
||||
const handler = createMcpHandler(buildServer);
|
||||
const transport = new StreamableHTTPClientTransport(new URL('http://test.local/mcp'), {
|
||||
fetch: (url, init) => handler.fetch(new Request(url, init))
|
||||
});
|
||||
```
|
||||
|
||||
The URL is never dialed — `handler.fetch` serves the request in-process. For stdio-era
|
||||
coverage, spawn `serveStdio` as a child process.
|
||||
|
||||
### Client cancellation on Streamable HTTP
|
||||
|
||||
On a 2026-07-28 Streamable HTTP connection, aborting an in-flight client request
|
||||
|
|
@ -227,12 +271,12 @@ toward a 2025-era peer, or any `tasks/*` method toward a 2026-era peer) throws
|
|||
|
||||
If you were on a v2 alpha and consumed wire schemas directly:
|
||||
|
||||
| v2-alpha pattern | Mechanical fix |
|
||||
| --- | --- |
|
||||
| parsing wire bytes with `EmptyResultSchema` that may carry `resultType` | strip `resultType` first (the schema now rejects it as an unknown key) |
|
||||
| `specTypeSchemas` / `SpecTypeName` references to task message types or `RequestMetaEnvelope` | remove — these validators left the public set (the **types** remain importable) |
|
||||
| `ClientRequest` / `ServerResult` / … aggregate types expected to include task members | use the individual deprecated `Task*` types — role aggregates are now the neutral (task-free) sets |
|
||||
| relying on `isCallToolResult` to reject wire-only members | guards validate neutral shapes (loose passthrough); validate raw wire traffic with a transport-level parse |
|
||||
| v2-alpha pattern | Mechanical fix |
|
||||
| -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| parsing wire bytes with `EmptyResultSchema` that may carry `resultType` | strip `resultType` first (the schema now rejects it as an unknown key) |
|
||||
| `specTypeSchemas` / `SpecTypeName` references to task message types or `RequestMetaEnvelope` | remove — these validators left the public set (the **types** remain importable) |
|
||||
| `ClientRequest` / `ServerResult` / … aggregate types expected to include task members | use the individual deprecated `Task*` types — role aggregates are now the neutral (task-free) sets |
|
||||
| relying on `isCallToolResult` to reject wire-only members | guards validate neutral shapes (loose passthrough); validate raw wire traffic with a transport-level parse |
|
||||
|
||||
The `resultType` / `EmptyResultSchema` / `specTypeSchemas` rules above have **no v1.x
|
||||
impact** — these members did not exist before 2026-07-28. The neutral-model wire
|
||||
|
|
@ -258,6 +302,10 @@ and the multi-round-trip retry fields (`inputResponses`, `requestState`).
|
|||
- **`resultType` is gone from every public result type** (`Result`, `CallToolResult`,
|
||||
`GetPromptResult`, …). The wire schemas keep parsing it, and the protocol layer
|
||||
consumes it before results reach your code.
|
||||
- **`DiscoverResult` strips its cache fields too.** `ttlMs` / `cacheScope` on
|
||||
`server/discover` are wire-only — consumed by the client's response-cache layer and
|
||||
absent from the public `DiscoverResult` type returned by `getDiscoverResult()`.
|
||||
Tooling that displays the server's advertised cache policy must parse raw frames.
|
||||
- **High-level methods return the named public types** (`client.callTool()` →
|
||||
`Promise<CallToolResult>`, etc.). Handler return positions are unaffected.
|
||||
- **Reserved envelope keys and retry fields appear in no public params/result type.**
|
||||
|
|
@ -287,11 +335,11 @@ The protocol layer enforces the same boundary at runtime:
|
|||
|
||||
**If you were on a v2 alpha** and read the wire shape directly:
|
||||
|
||||
| Pattern | Mechanical fix |
|
||||
| --- | --- |
|
||||
| `result.resultType` (typed read) | delete the read — the SDK consumes the field; results are complete when delivered |
|
||||
| `Result['resultType']` type reference | remove; the member is no longer declared |
|
||||
| return-type capture of `callTool` etc. | use the named public types (`CallToolResult`, `ListToolsResult`, …) |
|
||||
| Pattern | Mechanical fix |
|
||||
| -------------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| `result.resultType` (typed read) | delete the read — the SDK consumes the field; results are complete when delivered |
|
||||
| `Result['resultType']` type reference | remove; the member is no longer declared |
|
||||
| return-type capture of `callTool` etc. | use the named public types (`CallToolResult`, `ListToolsResult`, …) |
|
||||
|
||||
`MessageExtraInfo.classification` is an optional carrier (`{ era, revision?, envelope? }`)
|
||||
for transports that classify inbound messages at the edge; dispatch validates it against
|
||||
|
|
@ -306,11 +354,11 @@ obtain client input (elicitation, sampling, roots) **in-band** by returning
|
|||
`inputRequired(...)` from a `tools/call` / `prompts/get` / `resources/read` handler; the
|
||||
client retries the original call with the responses.
|
||||
|
||||
| Handler serving 2026-07-28 requests | Mechanical fix |
|
||||
| --- | --- |
|
||||
| Handler serving 2026-07-28 requests | Mechanical fix |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `await ctx.mcpReq.elicitInput({…})` / `requestSampling({…})` | `return inputRequired({ inputRequests: { id: inputRequired.elicit({…}) } })`; read `acceptedContent(ctx.mcpReq.inputResponses, 'id')` on re-entry |
|
||||
| `throw new UrlElicitationRequiredError([…])` | `return inputRequired({ inputRequests: { id: inputRequired.elicitUrl({…}) } })` |
|
||||
| handler shared across both eras | branch on the served era: keep the push-style call toward 2025-era requests, return `inputRequired(...)` toward 2026-07-28 requests |
|
||||
| `throw new UrlElicitationRequiredError([…])` | `return inputRequired({ inputRequests: { id: inputRequired.elicitUrl({…}) } })` |
|
||||
| handler shared across both eras | branch on the served era: keep the push-style call toward 2025-era requests, return `inputRequired(...)` toward 2026-07-28 requests |
|
||||
|
||||
`inputRequired` / `acceptedContent` / `InputRequiredSpec` are exported from
|
||||
`@modelcontextprotocol/server`. On 2026-era requests the push-style APIs
|
||||
|
|
@ -429,15 +477,15 @@ The experimental tasks **interception** layer is removed entirely — see
|
|||
|
||||
## Appendix: 2025-era vs 2026-era behavior matrix
|
||||
|
||||
| Axis | 2025-era (2024-10-07 … 2025-11-25) | 2026-07-28 |
|
||||
| --- | --- | --- |
|
||||
| Server HTTP entry | `*StreamableHTTPServerTransport` | `createMcpHandler` (`legacy: 'stateless'` also serves 2025) |
|
||||
| Server stdio entry | `server.connect(new StdioServerTransport())` | `serveStdio(factory)` (also serves 2025 unless `legacy: 'reject'`) |
|
||||
| Client connect | `initialize` handshake | `server/discover` probe (`versionNegotiation`) |
|
||||
| Client identity | `getClientCapabilities()` / `getClientVersion()` (initialize-scoped) | `ctx.mcpReq.envelope` (per request) |
|
||||
| Server→client requests | `ctx.mcpReq.elicitInput` / `requestSampling`, instance `createMessage()` etc. | `return inputRequired(...)` from handler |
|
||||
| Change notifications | unsolicited `list_changed` / `resources/updated` | `subscriptions/listen` stream |
|
||||
| Client cancellation (Streamable HTTP) | POST `notifications/cancelled` | close the request's SSE response stream |
|
||||
| `ctx.mcpReq.log()` level filter | session-scoped `logging/setLevel` | per-request `_meta.logLevel` envelope key (absent = opt-out) |
|
||||
| `400` JSON-RPC error body | `SdkHttpError` | `ProtocolError` (in-band) |
|
||||
| Era-mismatched spec method (outbound) | n/a | `SdkError(MethodNotSupportedByProtocolVersion)` |
|
||||
| Axis | 2025-era (2024-10-07 … 2025-11-25) | 2026-07-28 |
|
||||
| ------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| Server HTTP entry | `*StreamableHTTPServerTransport` | `createMcpHandler` (`legacy: 'stateless'` also serves 2025) |
|
||||
| Server stdio entry | `server.connect(new StdioServerTransport())` | `serveStdio(factory)` (also serves 2025 unless `legacy: 'reject'`) |
|
||||
| Client connect | `initialize` handshake | `server/discover` probe (`versionNegotiation`) |
|
||||
| Client identity | `getClientCapabilities()` / `getClientVersion()` (initialize-scoped) | `ctx.mcpReq.envelope` (per request) |
|
||||
| Server→client requests | `ctx.mcpReq.elicitInput` / `requestSampling`, instance `createMessage()` etc. | `return inputRequired(...)` from handler |
|
||||
| Change notifications | unsolicited `list_changed` / `resources/updated` | `subscriptions/listen` stream |
|
||||
| Client cancellation (Streamable HTTP) | POST `notifications/cancelled` | close the request's SSE response stream |
|
||||
| `ctx.mcpReq.log()` level filter | session-scoped `logging/setLevel` | per-request `_meta.logLevel` envelope key (absent = opt-out) |
|
||||
| `400` JSON-RPC error body | `SdkHttpError` | `ProtocolError` (in-band) |
|
||||
| Era-mismatched spec method (outbound) | n/a | `SdkError(MethodNotSupportedByProtocolVersion)` |
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
---
|
||||
title: Upgrading from v1.x to v2
|
||||
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.
|
||||
---
|
||||
|
|
@ -17,14 +18,16 @@ If you are already on v2 and want to adopt the **2026-07-28 protocol revision**,
|
|||
1. **Prerequisites.** Node.js 20+ and ESM (`"type": "module"` or `.mts`). v2 ships ESM
|
||||
only; CommonJS callers must use dynamic `import()`.
|
||||
2. **Run the codemod.**
|
||||
```bash
|
||||
npx @modelcontextprotocol/codemod@alpha v1-to-v2 ./src
|
||||
```
|
||||
```bash
|
||||
npx @modelcontextprotocol/codemod@alpha v1-to-v2 .
|
||||
```
|
||||
Run it at the **package root** (`.`), not `./src` — it also rewrites `package.json`,
|
||||
and real projects import the SDK from `test/`, `scripts/`, and fixtures too.
|
||||
3. **Grep for markers.** Anything the codemod recognized but could not safely rewrite is
|
||||
marked in place:
|
||||
```bash
|
||||
grep -rn '@mcp-codemod-error' .
|
||||
```
|
||||
```bash
|
||||
grep -rn '@mcp-codemod-error' .
|
||||
```
|
||||
4. **Type-check.** `tsc --noEmit` (or your build). Remaining errors map to the
|
||||
[manual sections](#manual-changes-what-the-codemod-does-not-handle) below.
|
||||
5. **Format.** The codemod rewrites the AST without reformatting — run your formatter on
|
||||
|
|
@ -58,12 +61,12 @@ The codemod ([`@modelcontextprotocol/codemod`](../../packages/codemod/README.md)
|
|||
mechanically applies every rename whose mapping is fixed. The mappings are the
|
||||
**source of truth** — they live in the codemod package and are not reproduced here:
|
||||
|
||||
| Mapping | Source file |
|
||||
| --- | --- |
|
||||
| `@modelcontextprotocol/sdk/...` import paths → v2 packages | [`mappings/importMap.ts`](../../packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts) |
|
||||
| Symbol renames (`McpError` → `ProtocolError`, `JSONRPCError` → `JSONRPCErrorResponse`, …) | [`mappings/symbolMap.ts`](../../packages/codemod/src/migrations/v1-to-v2/mappings/symbolMap.ts) |
|
||||
| `setRequestHandler(Schema, …)` → `setRequestHandler('method/string', …)` | [`mappings/schemaToMethodMap.ts`](../../packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts) |
|
||||
| `extra.*` → `ctx.mcpReq.*` / `ctx.http?.*` property remap | [`mappings/contextPropertyMap.ts`](../../packages/codemod/src/migrations/v1-to-v2/mappings/contextPropertyMap.ts) |
|
||||
| Mapping | Source file |
|
||||
| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| `@modelcontextprotocol/sdk/...` import paths → v2 packages | [`mappings/importMap.ts`](../../packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts) |
|
||||
| Symbol renames (`McpError` → `ProtocolError`, `JSONRPCError` → `JSONRPCErrorResponse`, …) | [`mappings/symbolMap.ts`](../../packages/codemod/src/migrations/v1-to-v2/mappings/symbolMap.ts) |
|
||||
| `setRequestHandler(Schema, …)` → `setRequestHandler('method/string', …)` | [`mappings/schemaToMethodMap.ts`](../../packages/codemod/src/migrations/v1-to-v2/mappings/schemaToMethodMap.ts) |
|
||||
| `extra.*` → `ctx.mcpReq.*` / `ctx.http?.*` property remap | [`mappings/contextPropertyMap.ts`](../../packages/codemod/src/migrations/v1-to-v2/mappings/contextPropertyMap.ts) |
|
||||
|
||||
In addition the codemod:
|
||||
|
||||
|
|
@ -129,12 +132,12 @@ recognized but could not safely rewrite with an `@mcp-codemod-error` comment.
|
|||
|
||||
The single `@modelcontextprotocol/sdk` package is split:
|
||||
|
||||
| v1 | v2 |
|
||||
| --- | --- |
|
||||
| `@modelcontextprotocol/sdk` | `@modelcontextprotocol/client` (client implementation) |
|
||||
| | `@modelcontextprotocol/server` (server implementation) |
|
||||
| | `@modelcontextprotocol/core` (public Zod `*Schema` constants) |
|
||||
| | `@modelcontextprotocol/core-internal` (internal — never import directly) |
|
||||
| v1 | v2 |
|
||||
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `@modelcontextprotocol/sdk` | `@modelcontextprotocol/client` (client implementation) |
|
||||
| | `@modelcontextprotocol/server` (server implementation) |
|
||||
| | `@modelcontextprotocol/core` (public Zod `*Schema` constants) |
|
||||
| | `@modelcontextprotocol/core-internal` (internal — never import directly) |
|
||||
| Built-in HTTP framework support | `@modelcontextprotocol/node` / `@modelcontextprotocol/express` / `@modelcontextprotocol/hono` / `@modelcontextprotocol/fastify` |
|
||||
|
||||
`@modelcontextprotocol/client` and `@modelcontextprotocol/server` both re-export shared
|
||||
|
|
@ -144,6 +147,13 @@ whichever package you already depend on. `@modelcontextprotocol/core-internal` i
|
|||
`@modelcontextprotocol/core` is the public Zod-schema package (raw `*Schema` constants
|
||||
only); see [Zod `*Schema` constants moved to `@modelcontextprotocol/core`](#zod-schema-constants-moved-to-modelcontextprotocolcore) below.
|
||||
|
||||
After the codemod runs, verify the dependencies in `package.json`: the swap rewrites
|
||||
the **nearest** manifest found walking up from the target directory — one manifest
|
||||
total, so workspace-member manifests in a monorepo are not visited (remove the v1
|
||||
dependency from those by hand once nothing imports it). On already-migrated sources
|
||||
the codemod still removes the v1 dependency but may not add the v2 packages you need
|
||||
— check both directions.
|
||||
|
||||
The framework adapter packages declare their framework as a **peer dependency**
|
||||
(`express`, `hono`, `fastify`); v1 shipped them as direct deps. The codemod adds the
|
||||
`@modelcontextprotocol/*` packages your imports use, but does not add the framework
|
||||
|
|
@ -179,12 +189,12 @@ A few transports need a decision the codemod can't make:
|
|||
`import { CallToolResult, CallToolResultSchema } from '…/types.js'` is split by the
|
||||
codemod — see [Types & schemas](#types--schemas).
|
||||
|
||||
```typescript
|
||||
// v1
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
// v2
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
|
||||
```
|
||||
```typescript
|
||||
// v1
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
// v2
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
|
||||
```
|
||||
|
||||
- **`SSEServerTransport`** is removed. Migrate to Streamable HTTP. A frozen v1 copy is
|
||||
available from `@modelcontextprotocol/server-legacy/sse` as a temporary bridge.
|
||||
|
|
@ -195,12 +205,12 @@ A few transports need a decision the codemod can't make:
|
|||
- **`InMemoryTransport`** is now exported from `@modelcontextprotocol/client` and
|
||||
`@modelcontextprotocol/server` (both re-export it):
|
||||
|
||||
```typescript
|
||||
// v1
|
||||
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
||||
// v2
|
||||
import { InMemoryTransport } from '@modelcontextprotocol/server'; // or /client
|
||||
```
|
||||
```typescript
|
||||
// v1
|
||||
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
||||
// v2
|
||||
import { InMemoryTransport } from '@modelcontextprotocol/server'; // or /client
|
||||
```
|
||||
|
||||
- **`EventStore`, `StreamId`, `EventId`** are exported from `@modelcontextprotocol/server`
|
||||
only (v1 re-exported them alongside the transport from `sdk/server/streamableHttp.js`;
|
||||
|
|
@ -215,14 +225,14 @@ A few transports need a decision the codemod can't make:
|
|||
(deprecated, frozen v1 copy); migrate AS to a dedicated IdP/OAuth library. `AuthInfo`
|
||||
is now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`.
|
||||
|
||||
The codemod's [`importMap.ts`](../../packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts)
|
||||
routes every `…/server/auth/**` deep path (including
|
||||
`…/server/auth/middleware/{bearerAuth,allowedMethods,clientAuth}.js`,
|
||||
`…/server/auth/handlers/*.js`, `…/server/auth/providers/proxyProvider.js`) to
|
||||
`@modelcontextprotocol/server-legacy/auth`, and `…/server/express.js` /
|
||||
`…/server/middleware/hostHeaderValidation.js` to `@modelcontextprotocol/express`. The
|
||||
AS→`server-legacy` routing is conservative — re-point RS-only call sites
|
||||
(`requireBearerAuth`, `mcpAuthMetadataRouter`) at `@modelcontextprotocol/express` by hand.
|
||||
The codemod's [`importMap.ts`](../../packages/codemod/src/migrations/v1-to-v2/mappings/importMap.ts)
|
||||
routes every `…/server/auth/**` deep path (including
|
||||
`…/server/auth/middleware/{bearerAuth,allowedMethods,clientAuth}.js`,
|
||||
`…/server/auth/handlers/*.js`, `…/server/auth/providers/proxyProvider.js`) to
|
||||
`@modelcontextprotocol/server-legacy/auth`, and `…/server/express.js` /
|
||||
`…/server/middleware/hostHeaderValidation.js` to `@modelcontextprotocol/express`. The
|
||||
AS→`server-legacy` routing is conservative — re-point RS-only call sites
|
||||
(`requireBearerAuth`, `mcpAuthMetadataRouter`) at `@modelcontextprotocol/express` by hand.
|
||||
|
||||
### Low-level protocol & handler context (`ctx`)
|
||||
|
||||
|
|
@ -235,29 +245,29 @@ The codemod renames the parameter and remaps property access via
|
|||
A few mappings need optional-chaining adjustment (the `http` group is `undefined` on
|
||||
stdio):
|
||||
|
||||
| v1 (`extra.*`) | v2 (`ctx.*`) | Note |
|
||||
| --- | --- | --- |
|
||||
| `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.sessionId` | `ctx.sessionId` | |
|
||||
| `extra.authInfo` | `ctx.http?.authInfo` | optional — `undefined` on stdio |
|
||||
| `extra.requestInfo` | `ctx.http?.req` | a standard Web `Request`; `ServerContext` only |
|
||||
| `extra.closeSSEStream` | `ctx.http?.closeSSE` | `ServerContext` only |
|
||||
| `extra.closeStandaloneSSEStream` | `ctx.http?.closeStandaloneSSE` | `ServerContext` only |
|
||||
| `extra.taskStore` / `taskId` / `taskRequestedTtl` | _removed_ | see [Experimental tasks](#experimental-tasks-interception-removed) |
|
||||
| v1 (`extra.*`) | v2 (`ctx.*`) | Note |
|
||||
| ------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------ |
|
||||
| `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.sessionId` | `ctx.sessionId` | |
|
||||
| `extra.authInfo` | `ctx.http?.authInfo` | optional — `undefined` on stdio |
|
||||
| `extra.requestInfo` | `ctx.http?.req` | a standard Web `Request`; `ServerContext` only |
|
||||
| `extra.closeSSEStream` | `ctx.http?.closeSSE` | `ServerContext` only |
|
||||
| `extra.closeStandaloneSSEStream` | `ctx.http?.closeStandaloneSSE` | `ServerContext` only |
|
||||
| `extra.taskStore` / `taskId` / `taskRequestedTtl` | _removed_ | see [Experimental tasks](#experimental-tasks-interception-removed) |
|
||||
|
||||
`BaseContext` is the common base; `ServerContext` and `ClientContext` extend it.
|
||||
`ServerContext.mcpReq` adds convenience methods that replace calling `server.*` from
|
||||
inside a handler:
|
||||
|
||||
| `ctx.mcpReq.*` (new) | Replaces (inside a handler) |
|
||||
| --- | --- |
|
||||
| `ctx.mcpReq.log(level, data, logger?)` | `server.sendLoggingMessage(...)` — ⚠ **`@deprecated`**, see [§Deprecated in v2](#deprecated-in-v2-sep-2577) |
|
||||
| `ctx.mcpReq.elicitInput(params, options?)` | `server.elicitInput(...)` |
|
||||
| `ctx.mcpReq.requestSampling(params, options?)` | `server.createMessage(...)` — ⚠ **`@deprecated`**, see [§Deprecated in v2](#deprecated-in-v2-sep-2577) |
|
||||
| `ctx.mcpReq.*` (new) | Replaces (inside a handler) |
|
||||
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| `ctx.mcpReq.log(level, data, logger?)` | `server.sendLoggingMessage(...)` — ⚠ **`@deprecated`**, see [§Deprecated in v2](#deprecated-in-v2-sep-2577) |
|
||||
| `ctx.mcpReq.elicitInput(params, options?)` | `server.elicitInput(...)` |
|
||||
| `ctx.mcpReq.requestSampling(params, options?)` | `server.createMessage(...)` — ⚠ **`@deprecated`**, see [§Deprecated in v2](#deprecated-in-v2-sep-2577) |
|
||||
|
||||
#### Deprecated in v2 (SEP-2577)
|
||||
|
||||
|
|
@ -330,6 +340,24 @@ For **custom (non-spec)** methods, keep the result-schema argument:
|
|||
`await client.request({ method: 'acme/search', params }, SearchResult)` — only drop the
|
||||
schema when calling a spec method.
|
||||
|
||||
**Forwarding arbitrary methods (gateways / proxies).** Dropping the schema changes
|
||||
semantics, not just the signature: a schema-less spec-method call now **enforces** the
|
||||
spec result schema (a non-conforming upstream result is rejected locally with
|
||||
`SdkError(SdkErrorCode.InvalidResult)` and a conforming one is re-serialized in schema
|
||||
key order), and a schema-less call for a **non-spec** method throws a `TypeError` at
|
||||
the call site (`'…' is not a spec method; pass a result schema`).
|
||||
A relay that forwards `{ method, params }` it does not understand must keep passing an
|
||||
explicit result schema. The v1 idiom survives with an import-path change:
|
||||
|
||||
```typescript
|
||||
import { ResultSchema } from '@modelcontextprotocol/core';
|
||||
const result = await upstream.request({ method, params }, ResultSchema); // v1-identical passthrough
|
||||
```
|
||||
|
||||
For byte-exact forwarding (member order preserved), pass your own accept-anything
|
||||
Standard Schema instead. Check call sites whose `method` is **not a literal** — the
|
||||
codemod may have dropped the schema argument there; restore it.
|
||||
|
||||
The return type is inferred from the method name via `ResultTypeMap` (e.g.
|
||||
`client.request({ method: 'tools/call', ... })` returns `Promise<CallToolResult>`).
|
||||
|
||||
|
|
@ -347,13 +375,9 @@ server.tool('greet', 'Greet a user', { name: z.string() }, async ({ name }) => {
|
|||
});
|
||||
|
||||
// v2 — config object, Standard Schema
|
||||
server.registerTool(
|
||||
'greet',
|
||||
{ description: 'Greet a user', inputSchema: z.object({ name: z.string() }) },
|
||||
async ({ name }) => {
|
||||
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
|
||||
}
|
||||
);
|
||||
server.registerTool('greet', { description: 'Greet a user', inputSchema: z.object({ name: z.string() }) }, async ({ name }) => {
|
||||
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
|
||||
});
|
||||
```
|
||||
|
||||
`registerResource` requires a `metadata` argument — pass `{}` if you have none.
|
||||
|
|
@ -366,10 +390,27 @@ are still **accepted via `@deprecated` overloads** on `registerTool`/`registerPr
|
|||
(auto-wrapped with `z.object()`), and `completable()` accepts any `StandardSchemaV1`;
|
||||
prefer wrapping explicitly. Zod v4, ArkType, and Valibot all implement the spec.
|
||||
|
||||
**Zod v3 is no longer supported** (v1 peer was `^3.25 || ^4.0`). Passing a Zod v3 schema
|
||||
hard-errors with a pointer at `fromJsonSchema()`; Zod 4.0–4.1 schemas (which lack
|
||||
`~standard.jsonSchema`) work via a bundled fallback with a one-time console warning.
|
||||
Upgrade to `zod ^4.2.0` or use another Standard Schema library.
|
||||
**Zod v3 is no longer supported** (v1 peer was `^3.25 || ^4.0`). Check the **declared
|
||||
range** in your `package.json`, not just the installed version: a zod-3 range that
|
||||
satisfied the v1 peer installs and typechecks cleanly under v2 and only fails at
|
||||
runtime, when the first registration throws — under a spawning harness that surfaces
|
||||
as an opaque child exit two hops from the cause. A Zod v3 schema
|
||||
hard-errors with a pointer at `fromJsonSchema()`. Zod **≥4.2.0** self-converts via
|
||||
`~standard.jsonSchema` — the supported path. Zod **4.0–4.1** lacks it, so the SDK falls
|
||||
back to its bundled Zod's `z.toJSONSchema()` with a one-time `[mcp-sdk]` console
|
||||
warning; and because `.describe()` field descriptions live in the _authoring_ Zod's
|
||||
registry, the fallback **drops them** from the generated JSON Schema. Fix ladder:
|
||||
(1) upgrade to `zod ^4.2.0`; (2) if you must pin an older or separate Zod, attach a
|
||||
`~standard.jsonSchema` provider backed by _your_ Zod's `toJSONSchema` so conversion
|
||||
(and descriptions) run through your instance; (3) author the schema as raw JSON Schema
|
||||
via `fromJsonSchema()`. (Raw shapes are wrapped with the SDK's **bundled** Zod — built
|
||||
with a foreign Zod they fail at registration or at the first `tools/list`; pass
|
||||
`z.object()`-wrapped schemas from your own Zod instead.)
|
||||
|
||||
The deprecated raw-shape overloads exist only on `registerTool` / `registerPrompt`.
|
||||
`RegisteredTool.update()` / `RegisteredPrompt.update()` take **schema objects**
|
||||
(`paramsSchema` / `outputSchema`: `StandardSchemaWithJSON`) — a raw shape passed to
|
||||
`update()` is not auto-wrapped; wrap it with `z.object()` yourself.
|
||||
|
||||
```typescript
|
||||
import * as z from 'zod/v4';
|
||||
|
|
@ -396,6 +437,12 @@ have no replacement (internal Zod introspection). `SchemaInput<T>` →
|
|||
internal `standardSchemaToJsonSchema` / `validateStandardSchema` helpers are **not** part
|
||||
of the public surface — do not import them.
|
||||
|
||||
v1's second compat module, `server/zod-json-schema-compat.js` (`toJsonSchemaCompat`), is
|
||||
also removed — and the codemod does **not** rewrite its import (expect `TS2307`). If you
|
||||
build `Tool` / `Prompt` advertisements yourself, use your schema library's native
|
||||
conversion: zod 4's `z.toJSONSchema(schema, { io: 'input', target: 'draft-2020-12' })`
|
||||
produces the dialect v2 advertises.
|
||||
|
||||
### HTTP & headers
|
||||
|
||||
Transport APIs and `ctx.http?.req?.headers` use the Web Standard `Headers` object
|
||||
|
|
@ -443,21 +490,21 @@ The codemod renames `McpError` → `ProtocolError`, `ErrorCode` → `ProtocolErr
|
|||
checks already name the v2 classes — what's left is choosing which `SdkErrorCode` /
|
||||
class to match per scenario:
|
||||
|
||||
| Scenario | v1 | v2 |
|
||||
| --- | --- | --- |
|
||||
| Request timeout | `McpError` + `ErrorCode.RequestTimeout` | `SdkError` + `SdkErrorCode.RequestTimeout` |
|
||||
| Connection closed | `McpError` + `ErrorCode.ConnectionClosed` | `SdkError` + `SdkErrorCode.ConnectionClosed` |
|
||||
| Capability not supported | `new Error(...)` | `SdkError` + `SdkErrorCode.CapabilityNotSupported` |
|
||||
| Not connected | `new Error('Not connected')` | `SdkError` + `SdkErrorCode.NotConnected` |
|
||||
| Response result fails schema | raw `ZodError` | `SdkError` + `SdkErrorCode.InvalidResult` |
|
||||
| Invalid params (server response) | `McpError` + `ErrorCode.InvalidParams` | `ProtocolError` + `ProtocolErrorCode.InvalidParams` |
|
||||
| HTTP transport error | `StreamableHTTPError` | `SdkHttpError` + `SdkErrorCode.ClientHttp*` |
|
||||
| Failed to open SSE stream | `StreamableHTTPError` | `SdkHttpError` + `SdkErrorCode.ClientHttpFailedToOpenStream` |
|
||||
| 401 after re-auth (circuit break) | `StreamableHTTPError` | `SdkHttpError` + `SdkErrorCode.ClientHttpAuthentication` |
|
||||
| `SSEClientTransport.send()` 401 after re-auth | `UnauthorizedError` | `SdkHttpError` + `SdkErrorCode.ClientHttpAuthentication` |
|
||||
| 403 `insufficient_scope` after step-up retry cap | `StreamableHTTPError` | `SdkHttpError` + `SdkErrorCode.ClientHttpForbidden` |
|
||||
| Unexpected content type | `StreamableHTTPError` | `SdkError` + `SdkErrorCode.ClientHttpUnexpectedContent` |
|
||||
| Session termination failed | `StreamableHTTPError` | `SdkHttpError` + `SdkErrorCode.ClientHttpFailedToTerminateSession` |
|
||||
| Scenario | v1 | v2 |
|
||||
| ------------------------------------------------ | ----------------------------------------- | ------------------------------------------------------------------ |
|
||||
| Request timeout | `McpError` + `ErrorCode.RequestTimeout` | `SdkError` + `SdkErrorCode.RequestTimeout` |
|
||||
| Connection closed | `McpError` + `ErrorCode.ConnectionClosed` | `SdkError` + `SdkErrorCode.ConnectionClosed` |
|
||||
| Capability not supported | `new Error(...)` | `SdkError` + `SdkErrorCode.CapabilityNotSupported` |
|
||||
| Not connected | `new Error('Not connected')` | `SdkError` + `SdkErrorCode.NotConnected` |
|
||||
| Response result fails schema | raw `ZodError` | `SdkError` + `SdkErrorCode.InvalidResult` |
|
||||
| Invalid params (server response) | `McpError` + `ErrorCode.InvalidParams` | `ProtocolError` + `ProtocolErrorCode.InvalidParams` |
|
||||
| HTTP transport error | `StreamableHTTPError` | `SdkHttpError` + `SdkErrorCode.ClientHttp*` |
|
||||
| Failed to open SSE stream | `StreamableHTTPError` | `SdkHttpError` + `SdkErrorCode.ClientHttpFailedToOpenStream` |
|
||||
| 401 after re-auth (circuit break) | `StreamableHTTPError` | `SdkHttpError` + `SdkErrorCode.ClientHttpAuthentication` |
|
||||
| `SSEClientTransport.send()` 401 after re-auth | `UnauthorizedError` | `SdkHttpError` + `SdkErrorCode.ClientHttpAuthentication` |
|
||||
| 403 `insufficient_scope` after step-up retry cap | `StreamableHTTPError` | `SdkHttpError` + `SdkErrorCode.ClientHttpForbidden` |
|
||||
| Unexpected content type | `StreamableHTTPError` | `SdkError` + `SdkErrorCode.ClientHttpUnexpectedContent` |
|
||||
| Session termination failed | `StreamableHTTPError` | `SdkHttpError` + `SdkErrorCode.ClientHttpFailedToTerminateSession` |
|
||||
|
||||
```typescript
|
||||
// v1
|
||||
|
|
@ -481,29 +528,69 @@ if (error instanceof SdkHttpError) {
|
|||
|
||||
`StreamableHTTPError` is removed.
|
||||
|
||||
**Status read off `.code` by duck-typing.** Code that classified HTTP failures by the
|
||||
status without an `instanceof` — `if ('code' in e && e.code === 403)` — silently stops
|
||||
matching: on `SdkHttpError` the HTTP status moved to `.status` (its `.code` is a
|
||||
`SdkErrorCode` string). The codemod renames `instanceof StreamableHTTPError`, but a
|
||||
status read that never named the class is invisible to it. Watch the inconsistency:
|
||||
`SseError` still carries its HTTP status on numeric `.code`, so one duck-typed
|
||||
`.code === 401` that caught both transports in v1 now catches only SSE.
|
||||
|
||||
```typescript
|
||||
// v1 — one duck-typed check caught both Streamable HTTP and SSE
|
||||
if ('code' in e && (e.code === 401 || e.code === 403)) reauth();
|
||||
// v2 — match each explicitly
|
||||
if (e instanceof SdkHttpError && (e.status === 401 || e.status === 403)) reauth(); // Streamable HTTP
|
||||
if (e instanceof SseError && (e.code === 401 || e.code === 403)) reauth(); // SSE still uses .code
|
||||
```
|
||||
|
||||
Silent at runtime (no compile error) — grep for `.code ===` status comparisons.
|
||||
|
||||
**Raw numeric code comparisons.** The codemod rewrites `ErrorCode.X` symbol references,
|
||||
but a check against the raw JSON-RPC number — `(e as { code?: unknown }).code === -32000`
|
||||
— is invisible to it and silently never matches in v2, because the two SDK-local codes
|
||||
it usually targeted are now **string** `SdkErrorCode` values:
|
||||
|
||||
| v1 numeric | v2 |
|
||||
| --------------------------- | -------------------------------------------- |
|
||||
| `-32000` (ConnectionClosed) | `SdkError` + `SdkErrorCode.ConnectionClosed` |
|
||||
| `-32001` (RequestTimeout) | `SdkError` + `SdkErrorCode.RequestTimeout` |
|
||||
|
||||
Replace the literal with the named code. Loud (`TS2367`) when the compared value is
|
||||
typed `SdkErrorCode`; silent when the left side is `unknown` or a cast — grep for
|
||||
`=== -32000` / `=== -32001`.
|
||||
|
||||
**Constructing the error (test stubs, custom transports).** v1
|
||||
`new StreamableHTTPError(code, message)` becomes
|
||||
`new SdkHttpError(code, message, data)`: the first argument is now a `SdkErrorCode`
|
||||
string (pick the branch from the scenario table above) and the HTTP status moves into
|
||||
the third argument — `new SdkHttpError(SdkErrorCode.ClientHttpNotImplemented,
|
||||
'Not Found', { status: 404, statusText: 'Not Found' })`. v1's implicit
|
||||
`Streamable HTTP error: ` message prefix is gone; pass the full message you want.
|
||||
|
||||
#### `SdkErrorCode` enum (complete)
|
||||
|
||||
| Code | When thrown |
|
||||
| --- | --- |
|
||||
| `NotConnected` | Transport is not connected |
|
||||
| `AlreadyConnected` | Transport is already connected |
|
||||
| `NotInitialized` | Protocol is not initialized |
|
||||
| `CapabilityNotSupported` | Required capability is not supported |
|
||||
| `RequestTimeout` | Request timed out waiting for response |
|
||||
| `ConnectionClosed` | Connection was closed |
|
||||
| `SendFailed` | Failed to send message |
|
||||
| `InvalidResult` | Response result failed local schema validation |
|
||||
| `UnsupportedResultType` | A 2026-era response carried an unrecognized `resultType` |
|
||||
| `InputRequiredRoundsExceeded` | Multi-round-trip auto-fulfilment hit `maxRounds` |
|
||||
| `ListPaginationExceeded` | No-arg `list*()` aggregate walk hit `listMaxPages` |
|
||||
| `MethodNotSupportedByProtocolVersion` | Outbound spec method does not exist on the negotiated protocol version |
|
||||
| `EraNegotiationFailed` | `connect()` could not negotiate a protocol era (probe failed / no overlap) |
|
||||
| `ClientHttpNotImplemented` | HTTP POST request failed |
|
||||
| `ClientHttpAuthentication` | Server returned 401 after re-authentication |
|
||||
| `ClientHttpForbidden` | Server returned 403 `insufficient_scope` after step-up retry cap |
|
||||
| `ClientHttpUnexpectedContent` | Unexpected content type in HTTP response |
|
||||
| `ClientHttpFailedToOpenStream` | Failed to open SSE stream |
|
||||
| `ClientHttpFailedToTerminateSession` | Failed to terminate session |
|
||||
| Code | When thrown |
|
||||
| ------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `NotConnected` | Transport is not connected |
|
||||
| `AlreadyConnected` | Transport is already connected |
|
||||
| `NotInitialized` | Protocol is not initialized |
|
||||
| `CapabilityNotSupported` | Required capability is not supported |
|
||||
| `RequestTimeout` | Request timed out waiting for response |
|
||||
| `ConnectionClosed` | Connection was closed |
|
||||
| `SendFailed` | Failed to send message |
|
||||
| `InvalidResult` | Response result failed local schema validation |
|
||||
| `UnsupportedResultType` | A 2026-era response carried an unrecognized `resultType` |
|
||||
| `InputRequiredRoundsExceeded` | Multi-round-trip auto-fulfilment hit `maxRounds` |
|
||||
| `ListPaginationExceeded` | No-arg `list*()` aggregate walk hit `listMaxPages` |
|
||||
| `MethodNotSupportedByProtocolVersion` | Outbound spec method does not exist on the negotiated protocol version |
|
||||
| `EraNegotiationFailed` | `connect()` could not negotiate a protocol era (probe failed / no overlap) |
|
||||
| `ClientHttpNotImplemented` | HTTP POST request failed |
|
||||
| `ClientHttpAuthentication` | Server returned 401 after re-authentication |
|
||||
| `ClientHttpForbidden` | Server returned 403 `insufficient_scope` after step-up retry cap |
|
||||
| `ClientHttpUnexpectedContent` | Unexpected content type in HTTP response |
|
||||
| `ClientHttpFailedToOpenStream` | Failed to open SSE stream |
|
||||
| `ClientHttpFailedToTerminateSession` | Failed to terminate session |
|
||||
|
||||
#### Typed `ProtocolError` subclasses
|
||||
|
||||
|
|
@ -511,7 +598,10 @@ if (error instanceof SdkHttpError) {
|
|||
(carries `data.requiredCapabilities`) are new typed `ProtocolError` subclasses.
|
||||
`resources/read` for an unknown URI now answers `-32602` on every protocol revision
|
||||
(v1.x already emitted `-32602`; an interim `-32002` from earlier v2 alphas is mapped at
|
||||
the encode seam). `ProtocolErrorCode.ResourceNotFound` (`-32002`) stays importable as
|
||||
the encode seam). The encode-seam mapping applies to **your own throws too**: a handler
|
||||
that deliberately throws `ProtocolError(ProtocolErrorCode.ResourceNotFound, …)` reaches
|
||||
peers as `-32602` — a server can no longer emit `-32002` on the wire.
|
||||
`ProtocolErrorCode.ResourceNotFound` (`-32002`) stays importable as
|
||||
receive-tolerated vocabulary — accept both `-32602` and `-32002` from peers.
|
||||
`ProtocolError.fromError(code, message, data)` reconstructs the typed subclass from
|
||||
code + data alone, so it works across bundle boundaries where `instanceof` doesn't.
|
||||
|
|
@ -524,26 +614,26 @@ The individual OAuth error classes are replaced with a single `OAuthError` + `OA
|
|||
The `OAUTH_ERRORS` constant is removed. The codemod does not rewrite `instanceof` checks
|
||||
on these classes — switch on `error.code` instead.
|
||||
|
||||
| v1 class | v2 equivalent |
|
||||
| --- | --- |
|
||||
| `InvalidRequestError` | `OAuthError` + `OAuthErrorCode.InvalidRequest` |
|
||||
| `InvalidClientError` | `OAuthError` + `OAuthErrorCode.InvalidClient` |
|
||||
| `InvalidGrantError` | `OAuthError` + `OAuthErrorCode.InvalidGrant` |
|
||||
| `UnauthorizedClientError` | `OAuthError` + `OAuthErrorCode.UnauthorizedClient` |
|
||||
| `UnsupportedGrantTypeError` | `OAuthError` + `OAuthErrorCode.UnsupportedGrantType` |
|
||||
| `InvalidScopeError` | `OAuthError` + `OAuthErrorCode.InvalidScope` |
|
||||
| `AccessDeniedError` | `OAuthError` + `OAuthErrorCode.AccessDenied` |
|
||||
| `ServerError` | `OAuthError` + `OAuthErrorCode.ServerError` |
|
||||
| `TemporarilyUnavailableError` | `OAuthError` + `OAuthErrorCode.TemporarilyUnavailable` |
|
||||
| v1 class | v2 equivalent |
|
||||
| ------------------------------ | ------------------------------------------------------- |
|
||||
| `InvalidRequestError` | `OAuthError` + `OAuthErrorCode.InvalidRequest` |
|
||||
| `InvalidClientError` | `OAuthError` + `OAuthErrorCode.InvalidClient` |
|
||||
| `InvalidGrantError` | `OAuthError` + `OAuthErrorCode.InvalidGrant` |
|
||||
| `UnauthorizedClientError` | `OAuthError` + `OAuthErrorCode.UnauthorizedClient` |
|
||||
| `UnsupportedGrantTypeError` | `OAuthError` + `OAuthErrorCode.UnsupportedGrantType` |
|
||||
| `InvalidScopeError` | `OAuthError` + `OAuthErrorCode.InvalidScope` |
|
||||
| `AccessDeniedError` | `OAuthError` + `OAuthErrorCode.AccessDenied` |
|
||||
| `ServerError` | `OAuthError` + `OAuthErrorCode.ServerError` |
|
||||
| `TemporarilyUnavailableError` | `OAuthError` + `OAuthErrorCode.TemporarilyUnavailable` |
|
||||
| `UnsupportedResponseTypeError` | `OAuthError` + `OAuthErrorCode.UnsupportedResponseType` |
|
||||
| `UnsupportedTokenTypeError` | `OAuthError` + `OAuthErrorCode.UnsupportedTokenType` |
|
||||
| `InvalidTokenError` | `OAuthError` + `OAuthErrorCode.InvalidToken` |
|
||||
| `MethodNotAllowedError` | `OAuthError` + `OAuthErrorCode.MethodNotAllowed` |
|
||||
| `TooManyRequestsError` | `OAuthError` + `OAuthErrorCode.TooManyRequests` |
|
||||
| `InvalidClientMetadataError` | `OAuthError` + `OAuthErrorCode.InvalidClientMetadata` |
|
||||
| `InsufficientScopeError` | `OAuthError` + `OAuthErrorCode.InsufficientScope` ¹ |
|
||||
| `InvalidTargetError` | `OAuthError` + `OAuthErrorCode.InvalidTarget` |
|
||||
| `CustomOAuthError` | `new OAuthError(customCode, message)` |
|
||||
| `UnsupportedTokenTypeError` | `OAuthError` + `OAuthErrorCode.UnsupportedTokenType` |
|
||||
| `InvalidTokenError` | `OAuthError` + `OAuthErrorCode.InvalidToken` |
|
||||
| `MethodNotAllowedError` | `OAuthError` + `OAuthErrorCode.MethodNotAllowed` |
|
||||
| `TooManyRequestsError` | `OAuthError` + `OAuthErrorCode.TooManyRequests` |
|
||||
| `InvalidClientMetadataError` | `OAuthError` + `OAuthErrorCode.InvalidClientMetadata` |
|
||||
| `InsufficientScopeError` | `OAuthError` + `OAuthErrorCode.InsufficientScope` ¹ |
|
||||
| `InvalidTargetError` | `OAuthError` + `OAuthErrorCode.InvalidTarget` |
|
||||
| `CustomOAuthError` | `new OAuthError(customCode, message)` |
|
||||
|
||||
¹ Unrelated to the new transport-layer `InsufficientScopeError` (SEP-2350) exported from
|
||||
`@modelcontextprotocol/client`, which carries an RFC 6750 challenge from the resource
|
||||
|
|
@ -557,6 +647,16 @@ import { OAuthError, OAuthErrorCode } from '@modelcontextprotocol/client';
|
|||
if (error instanceof OAuthError && error.code === OAuthErrorCode.InvalidClient) { ... }
|
||||
```
|
||||
|
||||
⚠ **Token verifiers must throw the v2 `OAuthError`.** `requireBearerAuth` (from
|
||||
`@modelcontextprotocol/express`) classifies the error your
|
||||
`OAuthTokenVerifier.verifyAccessToken()` throws: a v2
|
||||
`OAuthError(OAuthErrorCode.InvalidToken)` produces the proper `401` +
|
||||
`WWW-Authenticate` challenge, while the legacy `InvalidTokenError` (from
|
||||
`server-legacy`) or a generic `Error` falls through as unexpected — **invalid tokens
|
||||
become HTTP `500`**. When you re-point `requireBearerAuth` at
|
||||
`@modelcontextprotocol/express`, migrate the error classes your verifier throws in the
|
||||
same change.
|
||||
|
||||
A frozen copy of the v1 classes (and `mcpAuthRouter`) is available from
|
||||
`@modelcontextprotocol/server-legacy/auth` during migration.
|
||||
|
||||
|
|
@ -591,13 +691,13 @@ The OAuth client flow now throws dedicated classes from `@modelcontextprotocol/c
|
|||
(all extend `OAuthClientFlowError`, **not** `OAuthError` — `auth()`'s `OAuthError` retry
|
||||
path will not catch them):
|
||||
|
||||
| Throw site | v2 class |
|
||||
| --- | --- |
|
||||
| `registerClient()` rejected by AS (⚠ `@deprecated` — see [§Deprecated in v2](#deprecated-in-v2-sep-2577)) | `RegistrationRejectedError` (`status`, `body`, `submittedMetadata`) |
|
||||
| Token-exchange / refresh / `fetchToken` / Cross-App grant on a non-`https:` token endpoint | `InsecureTokenEndpointError` (`tokenEndpoint`) |
|
||||
| RFC 9207 `iss` mismatch / RFC 8414 §3.3 issuer-echo mismatch | `IssuerMismatchError` (`kind`, `expected`, `received`) |
|
||||
| Throw site | v2 class |
|
||||
| ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |
|
||||
| `registerClient()` rejected by AS (⚠ `@deprecated` — see [§Deprecated in v2](#deprecated-in-v2-sep-2577)) | `RegistrationRejectedError` (`status`, `body`, `submittedMetadata`) |
|
||||
| Token-exchange / refresh / `fetchToken` / Cross-App grant on a non-`https:` token endpoint | `InsecureTokenEndpointError` (`tokenEndpoint`) |
|
||||
| RFC 9207 `iss` mismatch / RFC 8414 §3.3 issuer-echo mismatch | `IssuerMismatchError` (`kind`, `expected`, `received`) |
|
||||
| Transport 403 `insufficient_scope` with `onInsufficientScope: 'throw'`, or default mode without an `OAuthClientProvider` | `InsufficientScopeError` (`requiredScope`, `resourceMetadataUrl`, `errorDescription`) |
|
||||
| `auth()` callback leg: discovery resolves a different AS than the recorded redirect target | `AuthorizationServerMismatchError` (`recordedIssuer`, `currentIssuer`) |
|
||||
| `auth()` callback leg: discovery resolves a different AS than the recorded redirect target | `AuthorizationServerMismatchError` (`recordedIssuer`, `currentIssuer`) |
|
||||
|
||||
#### `auth()` options are now `AuthOptions`
|
||||
|
||||
|
|
@ -687,7 +787,13 @@ same handling as the POST send path.
|
|||
methods plus `tokens()` / `clientInformation()`. On read, a stored value whose `issuer`
|
||||
names a different AS is treated as `undefined` and the flow re-registers / re-authorizes.
|
||||
**Round-trip the stored object verbatim and you're protected** — single-slot storage
|
||||
works. To hold credentials for several authorization servers at once, key your storage
|
||||
works. The failure modes differ: a stamp naming a **different** AS reads back as
|
||||
`undefined` and the flow re-registers / re-authorizes. A **missing** stamp (a
|
||||
`saveTokens()` that rebuilds the object field-by-field and drops `issuer`, or
|
||||
pre-upgrade storage) is used **as-is** with a `[mcp-sdk]` console warning — SEP-2352
|
||||
isolation is silently inactive for that read; `auth()` re-stamps on first use where the
|
||||
provider can persist it. If you see that warning repeatedly, your provider is not
|
||||
round-tripping the stored object. To hold credentials for several authorization servers at once, key your storage
|
||||
on `ctx.issuer` (treat **`ctx === undefined` as "return the most-recently-saved token
|
||||
set"** — the transport's per-request `Authorization: Bearer` read calls `tokens()` with
|
||||
no `ctx`). New TypeScript-only aliases `StoredOAuthTokens` / `StoredOAuthClientInformation`
|
||||
|
|
@ -760,6 +866,9 @@ const result = specTypeSchemas.CallToolResult['~standard'].validate(value);
|
|||
`isSpecType` and `specTypeSchemas` are keyed by `SpecTypeName` — a literal union of
|
||||
every named type in the MCP spec — so you get autocomplete and a compile error on typos.
|
||||
`specTypeSchemas.X` is a `StandardSchemaV1Sync<In, Out>` (`validate()` is synchronous).
|
||||
`validate()` returns `{ value }` or `{ issues }` and never throws — unlike `.parse()` on
|
||||
the real schema; code that caught a `ZodError` should inspect `result.issues` (or keep
|
||||
`.parse()` on the schema imported from `@modelcontextprotocol/core`).
|
||||
The pre-existing `isCallToolResult(value)` guard still works.
|
||||
|
||||
**`specTypeSchemas.X` is `StandardSchemaV1`, not `ZodType`.** Zod-specific composition
|
||||
|
|
@ -779,18 +888,18 @@ include task vocabulary; the deprecated `Task*` types remain importable on their
|
|||
|
||||
#### Removed type aliases
|
||||
|
||||
| Removed | Replacement |
|
||||
| --- | --- |
|
||||
| `JSONRPCError` | `JSONRPCErrorResponse` |
|
||||
| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` |
|
||||
| `isJSONRPCError` | `isJSONRPCErrorResponse` |
|
||||
| `isJSONRPCResponse` (deprecated in v1) | `isJSONRPCResultResponse` ² |
|
||||
| `JSONRPCResponseSchema` (result-only in v1) | `JSONRPCResultResponseSchema` ² |
|
||||
| `JSONRPCResponse` (result-only in v1) | `JSONRPCResultResponse` ² |
|
||||
| `ResourceReference` / `ResourceReferenceSchema` | `ResourceTemplateReference` / `ResourceTemplateReferenceSchema` |
|
||||
| `IsomorphicHeaders` | Web Standard `Headers` |
|
||||
| `RequestHandlerExtra` | `ServerContext` / `ClientContext` / `BaseContext` |
|
||||
| `ResourceTemplate` (the spec wire **type** from `sdk/types.js`) | `ResourceTemplateType` ³ |
|
||||
| Removed | Replacement |
|
||||
| --------------------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| `JSONRPCError` | `JSONRPCErrorResponse` |
|
||||
| `JSONRPCErrorSchema` | `JSONRPCErrorResponseSchema` |
|
||||
| `isJSONRPCError` | `isJSONRPCErrorResponse` |
|
||||
| `isJSONRPCResponse` (deprecated in v1) | `isJSONRPCResultResponse` ² |
|
||||
| `JSONRPCResponseSchema` (result-only in v1) | `JSONRPCResultResponseSchema` ² |
|
||||
| `JSONRPCResponse` (result-only in v1) | `JSONRPCResultResponse` ² |
|
||||
| `ResourceReference` / `ResourceReferenceSchema` | `ResourceTemplateReference` / `ResourceTemplateReferenceSchema` |
|
||||
| `IsomorphicHeaders` | Web Standard `Headers` |
|
||||
| `RequestHandlerExtra` | `ServerContext` / `ClientContext` / `BaseContext` |
|
||||
| `ResourceTemplate` (the spec wire **type** from `sdk/types.js`) | `ResourceTemplateType` ³ |
|
||||
|
||||
² v2 introduces **new** `isJSONRPCResponse` / `JSONRPCResponse` / `JSONRPCResponseSchema`
|
||||
with corrected semantics — they match **both** result and error responses (the schema is
|
||||
|
|
@ -808,6 +917,14 @@ names — import the TypeScript types, error classes, enums, and type guards fro
|
|||
`@modelcontextprotocol/client` or `@modelcontextprotocol/server`, and the Zod
|
||||
`*Schema` constants from `@modelcontextprotocol/core`.
|
||||
|
||||
The `Protocol` base class itself is no longer exported (it is internal engine). If you
|
||||
were reaching into protocol internals — rare, mostly debugging tools —
|
||||
`client.fallbackRequestHandler` / `server.fallbackRequestHandler` receives every
|
||||
inbound request that no registered handler matches, before capability gating. Delete
|
||||
the v1 `shared/protocol.js` import: `Protocol` has no v2 import path. The codemod
|
||||
currently rewrites it to a named import from `@modelcontextprotocol/client` that does
|
||||
not exist (a codemod fix is tracked) — delete that import.
|
||||
|
||||
#### JSON Schema 2020-12 posture (SEP-1613, SEP-2106)
|
||||
|
||||
The default validator supports **JSON Schema 2020-12 only**. On Node it is now `Ajv2020`
|
||||
|
|
@ -820,19 +937,27 @@ declaring a different `$schema` are rejected with `Error("…unsupported dialect
|
|||
`$ref` is not dereferenced (unchanged from v1; Ajv throws `MissingRefError` at compile,
|
||||
surfaced per-tool on `callTool`).
|
||||
|
||||
| v1 pattern | Mechanical fix |
|
||||
| --- | --- |
|
||||
| `result.structuredContent.<key>` / `result.structuredContent?.<k>` | narrow first: `const sc = result.structuredContent; if (typeof sc === 'object' && sc !== null && '<k>' in sc) { sc.<k> }` |
|
||||
| `if (!result.structuredContent)` | `if (result.structuredContent === undefined)` |
|
||||
| relying on default `Ajv` being draft-07 | `new AjvJsonSchemaValidator(new Ajv({ strict: false, validateFormats: true, validateSchema: false, allErrors: true }))` (import `Ajv`, `addFormats`, `AjvJsonSchemaValidator` from `…/validators/ajv`) |
|
||||
| draft-07 idioms via `fromJsonSchema(schema)` | `fromJsonSchema(schema, new AjvJsonSchemaValidator(ajv))` — the `McpServer`/`Client` `jsonSchemaValidator` option does **not** reach `fromJsonSchema`-authored schemas |
|
||||
| `outputSchema` / `inputSchema` with absolute-URI `$ref` | inline under `$defs` and reference with `#/$defs/Name` |
|
||||
| v1 pattern | Mechanical fix |
|
||||
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `result.structuredContent.<key>` / `result.structuredContent?.<k>` | narrow first: `const sc = result.structuredContent; if (typeof sc === 'object' && sc !== null && '<k>' in sc) { sc.<k> }` |
|
||||
| `if (!result.structuredContent)` | `if (result.structuredContent === undefined)` |
|
||||
| relying on default `Ajv` being draft-07 | `new AjvJsonSchemaValidator(new Ajv({ strict: false, validateFormats: true, validateSchema: false, allErrors: true }))` (import `Ajv`, `addFormats`, `AjvJsonSchemaValidator` from `…/validators/ajv`) |
|
||||
| draft-07 idioms via `fromJsonSchema(schema)` | `fromJsonSchema(schema, new AjvJsonSchemaValidator(ajv))` — the `McpServer`/`Client` `jsonSchemaValidator` option does **not** reach `fromJsonSchema`-authored schemas |
|
||||
| `outputSchema` / `inputSchema` with absolute-URI `$ref` | inline under `$defs` and reference with `#/$defs/Name` |
|
||||
|
||||
A tool may now register an `outputSchema` whose root is `type:"array"`, `type:"string"`,
|
||||
etc.; toward 2025-era clients the codec wraps it in a `{result:…}` envelope, and toward
|
||||
every era a non-object `structuredContent` with no `text` block of its own gets a
|
||||
`JSON.stringify(...)` `text` block auto-appended. See [support-2026-07-28.md › Per-era wire codecs](./support-2026-07-28.md#per-era-wire-codecs) for how the codec applies these per era.
|
||||
|
||||
**Your advertised tool schemas change shape on the wire.** The same `registerTool`
|
||||
calls produce `tools/list` entries whose generated `inputSchema` differs from v1:
|
||||
JSON Schema 2020-12 idioms (zod 4 conversion), different `additionalProperties`
|
||||
handling (no `additionalProperties: false` by default; passthrough objects emit
|
||||
`"additionalProperties": {}` instead of `true`), and no `execution.taskSupport` member.
|
||||
Golden tests, transcript pins, and strict client-side validators of your advertised
|
||||
tool list need re-baselining — the new shapes are spec-conformant.
|
||||
|
||||
### Behavioral changes
|
||||
|
||||
These are runtime-behavior changes that may affect tests and assertions; no source
|
||||
|
|
@ -844,6 +969,10 @@ rewrite required unless noted.
|
|||
instead of resolving `CallToolResult{isError: true}`. v1 callers that checked
|
||||
`result.isError` for an unknown tool will get an unhandled rejection — catch the
|
||||
rejected promise instead.
|
||||
- **The `MCP error <code>: ` message prefix is gone.** v1 prefixed relayed JSON-RPC
|
||||
error messages (`MCP error -32602: …`); v2's `ProtocolError.message` carries the
|
||||
peer's message verbatim. Tests and log scrapers that matched the prefix or the numeric
|
||||
code in rendered text should match `error.code` instead.
|
||||
- **In-flight request handlers are aborted on transport close** — `ctx.mcpReq.signal`
|
||||
fires (v1 let them run to completion). `InMemoryTransport.close()` no longer
|
||||
double-fires `onclose` on the initiating side.
|
||||
|
|
@ -855,6 +984,35 @@ rewrite required unless noted.
|
|||
invalid URL) in Node and Cloudflare Workers instead of swallowing it as a CORS miss
|
||||
→ `undefined`. The CORS-swallow remains browser-only.
|
||||
|
||||
#### Client connection & dispatch
|
||||
|
||||
- **`connect()` skips the `initialize` handshake when the transport already exposes a
|
||||
`sessionId`** — it assumes it is reconnecting to an existing session (v1 always
|
||||
initialized). A custom or test transport that sets `sessionId` at construction
|
||||
silently skips initialization: `getServerCapabilities()` stays `undefined` and the
|
||||
list verbs return empty results. Expose `sessionId` only after the first request has
|
||||
been sent.
|
||||
- **The typed verbs dispatch after async pre-work.** `Protocol.request()` itself still
|
||||
hands the frame to the transport before its first `await` (v1-compatible). The typed
|
||||
verbs on top of it — `callTool()` and the cacheable list verbs — perform async work
|
||||
first (header-mirroring scan, response-cache freshness, output-validator resolution),
|
||||
so an abort fired in the same tick can land before the frame is ever sent: the call
|
||||
rejects with `SdkError(RequestTimeout, reason)` and **no `notifications/cancelled` is
|
||||
emitted** (nothing was in flight). v1 sent the frame synchronously from these verbs.
|
||||
Once the frame is on the wire, aborting still sends `notifications/cancelled` before
|
||||
rejecting.
|
||||
- **Protocol-version pinning is a first-class option.**
|
||||
`ProtocolOptions.supportedProtocolVersions` pins the legacy `initialize` handshake:
|
||||
the **first** pre-2026 entry in the list is offered (list order is preference order),
|
||||
a counter-offer is accepted only if it is one of the list's pre-2026 entries, and a
|
||||
list with no pre-2026 entry makes the handshake throw. Under
|
||||
`versionNegotiation: 'auto'` the modern probe candidates are the list's modern
|
||||
entries when it has any (otherwise the SDK's default modern set); a `{ pin }` is
|
||||
honored as given and is not checked against the list (see
|
||||
[support-2026-07-28.md](./support-2026-07-28.md#client-side-versionnegotiation)).
|
||||
v1 had no public equivalent (`SUPPORTED_PROTOCOL_VERSIONS` was a fixed constant) —
|
||||
replace any workaround that patched the offered version with this option.
|
||||
|
||||
#### stdio transport
|
||||
|
||||
- A configurable `maxBufferSize` (default **10 MB**) caps the stdio read buffer. A
|
||||
|
|
@ -880,7 +1038,10 @@ rewrite required unless noted.
|
|||
return `nextCursor: undefined`. Passing `{ cursor }` still fetches one page. Manual
|
||||
pagination loops keep working (the first iteration returns everything); replace them
|
||||
with the bare no-arg call. The walk is capped at `ClientOptions.listMaxPages` (default
|
||||
64); overrun throws `SdkError(ListPaginationExceeded)`.
|
||||
64); overrun throws `SdkError(ListPaginationExceeded)`. There is no way to fetch only
|
||||
the **first** page through the typed verbs — for page-level observation
|
||||
(pagination tooling, per-page stats) drop to
|
||||
`client.request({ method: 'tools/list', params })`, which never aggregates.
|
||||
- Output-schema validator compilation is now **lazy** — validators compile on the first
|
||||
`callTool()` against the cached `tools/list` entry, not eagerly inside `listTools()`.
|
||||
In v1, `listTools()` threw on an uncompilable `outputSchema`; now `listTools()`
|
||||
|
|
@ -925,7 +1086,7 @@ rewrite required unless noted.
|
|||
for every primitive capability declared in `ServerOptions.capabilities`, even with
|
||||
zero registrations. `new McpServer(info, { capabilities: { tools: {} } })` with no
|
||||
registered tools answers `tools/list` with `{ tools: [] }` instead of `-32601 Method
|
||||
not found`. Low-level `Server` users remain responsible for registering handlers for
|
||||
not found`. Low-level `Server` users remain responsible for registering handlers for
|
||||
declared capabilities.
|
||||
- **`WebStandardStreamableHTTPServerTransport` store-first `eventStore` semantics.**
|
||||
Request-related events emitted after `closeSSE()` — and the final response when no
|
||||
|
|
@ -953,6 +1114,11 @@ requests, the per-request `_meta.logLevel` envelope key is the filter — see
|
|||
affordance was removed. Tool handlers MUST include `content` (the TypeScript surface
|
||||
always required it; `content: []` is fine). A handler result without it is rejected
|
||||
with `-32602`.
|
||||
- **`ElicitResult.content` values are typed and validated as
|
||||
`string | number | boolean | string[]`.** v1's TypeScript surface accepted
|
||||
`Record<string, unknown>` content values; an elicitation handler returning arbitrary
|
||||
objects now fails to compile (and fails schema validation) — narrow to the primitives
|
||||
the elicitation spec allows.
|
||||
- **Custom (3-arg) handlers receive `_meta`.** `setRequestHandler(method, {params}, handler)`
|
||||
used to delete `params._meta` before validation; it now passes `_meta` through (minus
|
||||
the reserved `io.modelcontextprotocol/*` envelope keys). If your params schema is
|
||||
|
|
@ -1043,8 +1209,8 @@ The following are unchanged between v1 and v2 (only the import path changed):
|
|||
> The `Server` (low-level) constructor and **most** of its methods are unchanged, but
|
||||
> `setRequestHandler` / `setNotificationHandler` and `request()` signatures changed
|
||||
> ([Low-level protocol](#low-level-protocol--handler-context-ctx)). The Zod `*Schema`
|
||||
> constants are **not** part of the unchanged surface — they are no longer public
|
||||
> ([Types & schemas](#types--schemas)).
|
||||
> constants are **not** part of the unchanged surface — they moved to
|
||||
> `@modelcontextprotocol/core` ([Types & schemas](#types--schemas)).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ Codemods for migrating MCP TypeScript SDK code between major versions.
|
|||
## Usage
|
||||
|
||||
```bash
|
||||
npx @modelcontextprotocol/codemod@alpha v1-to-v2 ./src
|
||||
npx @modelcontextprotocol/codemod@alpha v1-to-v2 .
|
||||
```
|
||||
|
||||
The codemod rewrites TypeScript and JavaScript source files
|
||||
|
|
|
|||
Loading…
Reference in a new issue