Implement MCP 2026-07-28 (#2286)
Co-authored-by: Matt <77928207+mattzcarey@users.noreply.github.com> Co-authored-by: Konstantin Konstantinov <KKonstantinov@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
e4227d135c
commit
1823aae891
11 changed files with 69131 additions and 9001 deletions
49
docs/behavior-surface-pins.md
Normal file
49
docs/behavior-surface-pins.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Behavior-surface pins
|
||||
|
||||
Some tests in this repo are **pins**: they assert the exact current value of a
|
||||
wire- or consumer-visible behavior — an error code, a schema boundary, an
|
||||
export map, the stdio env safelist — rather than checking that a feature
|
||||
works. Their job is to distinguish a deliberate surface change from an
|
||||
accidental one: the regular suite stays green through either; a pin goes red
|
||||
through both.
|
||||
|
||||
## When a pin goes red on your change
|
||||
|
||||
A red pin does **not** mean the change is forbidden. It means the change is
|
||||
surface-visible and must be deliberate:
|
||||
|
||||
1. Confirm the change is intended. If it isn't, the pin just caught an
|
||||
accidental break.
|
||||
2. Update the pin in the same PR.
|
||||
3. Add a changeset if the surface is consumer-facing.
|
||||
4. Update `docs/migration/upgrade-to-v2.md` (or `docs/migration/support-2026-07-28.md` if 2026-only) where consumer-facing.
|
||||
|
||||
Never weaken a pin (loosen an exact match, delete an assertion) just to make
|
||||
CI pass — that reopens the silent-drift hole the pin exists to close.
|
||||
|
||||
## Where pins live
|
||||
|
||||
| Surface | File |
|
||||
| --- | --- |
|
||||
| Wire error-code tables, error classes, version constants | `packages/core-internal/test/types/errorSurfacePins.test.ts` |
|
||||
| Schema strict/strip/loose boundaries, key existence | `packages/core-internal/test/types/schemaBoundaryPins.test.ts` |
|
||||
| Published package set, export maps, ESM-only topology | `packages/core-internal/test/packageTopologyPins.test.ts` |
|
||||
| stdio environment-inheritance safelist | `packages/client/test/client/stdioEnvPins.test.ts` |
|
||||
|
||||
## Writing a new pin
|
||||
|
||||
- The expectation side must be a literal frozen in the test, never a value
|
||||
imported from src. Comparing a source constant against itself pins nothing.
|
||||
- Mutation-check it once before landing: flip the source behavior locally and
|
||||
confirm the pin actually goes red. A pin that stays green under the drift it
|
||||
claims to guard is worse than no pin.
|
||||
- Pin behavior a deployed peer or consumer can observe. Internal details that
|
||||
are invisible across the wire and the public API don't need pins.
|
||||
- Don't pin a known bug to make it load-bearing — file an issue instead.
|
||||
|
||||
## History
|
||||
|
||||
The original, much broader inventory was developed against v1.x in #2258 and
|
||||
#2262 (closed unmerged). This sweep ports only the boundary surfaces above;
|
||||
see those PRs for the fuller exploration and the reasoning behind what was
|
||||
left out.
|
||||
|
|
@ -422,5 +422,5 @@ If you see:
|
|||
Now that you have a working client, here are some ways to go further:
|
||||
|
||||
- [**Client guide**](./client.md) — Add OAuth, middleware, sampling, and more to your client.
|
||||
- [**Example clients**](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/client) — Browse runnable client examples.
|
||||
- [**Example clients**](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples) — Browse runnable client examples.
|
||||
- [**FAQ**](./faq.md) — Troubleshoot common errors.
|
||||
|
|
|
|||
378
docs/client.md
378
docs/client.md
|
|
@ -12,8 +12,16 @@ A client connects to a server, discovers what it offers — tools, resources, pr
|
|||
|
||||
The examples below use these imports. Adjust based on which features and transport you need:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#imports"
|
||||
import type { AuthProvider, Prompt, Resource, Tool } from '@modelcontextprotocol/client';
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#imports"
|
||||
import type {
|
||||
AuthProvider,
|
||||
OAuthClientInformationContext,
|
||||
OAuthClientInformationMixed,
|
||||
OAuthClientMetadata,
|
||||
OAuthClientProvider,
|
||||
OAuthDiscoveryState,
|
||||
OAuthTokens
|
||||
} from '@modelcontextprotocol/client';
|
||||
import {
|
||||
applyMiddlewares,
|
||||
Client,
|
||||
|
|
@ -21,6 +29,7 @@ import {
|
|||
createMiddleware,
|
||||
CrossAppAccessProvider,
|
||||
discoverAndRequestJwtAuthGrant,
|
||||
IssuerMismatchError,
|
||||
PrivateKeyJwtProvider,
|
||||
ProtocolError,
|
||||
SdkError,
|
||||
|
|
@ -28,7 +37,8 @@ import {
|
|||
SSEClientTransport,
|
||||
StreamableHTTPClientTransport,
|
||||
TRACEPARENT_META_KEY,
|
||||
TRACESTATE_META_KEY
|
||||
TRACESTATE_META_KEY,
|
||||
UnauthorizedError
|
||||
} from '@modelcontextprotocol/client';
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
|
||||
```
|
||||
|
|
@ -39,7 +49,7 @@ import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
|
|||
|
||||
For remote HTTP servers, use {@linkcode @modelcontextprotocol/client!client/streamableHttp.StreamableHTTPClientTransport | StreamableHTTPClientTransport}:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#connect_streamableHttp"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#connect_streamableHttp"
|
||||
const client = new Client({ name: 'my-client', version: '1.0.0' });
|
||||
|
||||
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'));
|
||||
|
|
@ -47,13 +57,13 @@ const transport = new StreamableHTTPClientTransport(new URL('http://localhost:30
|
|||
await client.connect(transport);
|
||||
```
|
||||
|
||||
For a full interactive client over Streamable HTTP, see [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleStreamableHttp.ts).
|
||||
For a full interactive client over Streamable HTTP, see [`repl/client.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/repl/client.ts).
|
||||
|
||||
### stdio
|
||||
|
||||
For local, process-spawned servers (Claude Desktop, CLI tools), use {@linkcode @modelcontextprotocol/client!client/stdio.StdioClientTransport | StdioClientTransport}. The transport spawns the server process and communicates over stdin/stdout:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#connect_stdio"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#connect_stdio"
|
||||
const client = new Client({ name: 'my-client', version: '1.0.0' });
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
|
|
@ -66,9 +76,10 @@ await client.connect(transport);
|
|||
|
||||
### SSE fallback for legacy servers
|
||||
|
||||
To support both modern Streamable HTTP and legacy SSE servers, try {@linkcode @modelcontextprotocol/client!client/streamableHttp.StreamableHTTPClientTransport | StreamableHTTPClientTransport} first and fall back to {@linkcode @modelcontextprotocol/client!client/sse.SSEClientTransport | SSEClientTransport} on failure:
|
||||
To support both modern Streamable HTTP and legacy SSE servers, try {@linkcode @modelcontextprotocol/client!client/streamableHttp.StreamableHTTPClientTransport | StreamableHTTPClientTransport} first and fall back to {@linkcode
|
||||
@modelcontextprotocol/client!client/sse.SSEClientTransport | SSEClientTransport} on failure:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#connect_sseFallback"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#connect_sseFallback"
|
||||
const baseUrl = new URL(url);
|
||||
|
||||
try {
|
||||
|
|
@ -86,7 +97,48 @@ try {
|
|||
}
|
||||
```
|
||||
|
||||
For a complete example with error reporting, see [`streamableHttpWithSseFallbackClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/streamableHttpWithSseFallbackClient.ts).
|
||||
The snippet above is the complete pattern; wrap the `catch` body with whatever error reporting your host needs.
|
||||
|
||||
### Protocol version negotiation (2026-07-28 revision)
|
||||
|
||||
By default the client negotiates a 2025-era protocol version via the `initialize` handshake — exactly the v1.x behavior, byte for byte. To talk to a server on the 2026-07-28 revision, opt into version negotiation via `ClientOptions.versionNegotiation`:
|
||||
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#Client_versionNegotiation"
|
||||
// Auto-negotiate: probe with server/discover, fall back to the 2025 handshake
|
||||
// against a 2025-only server.
|
||||
const client = new Client({ name: 'my-client', version: '1.0.0' }, { versionNegotiation: { mode: 'auto' } });
|
||||
await client.connect(transport);
|
||||
|
||||
client.getProtocolEra(); // 'modern' or 'legacy'
|
||||
client.getNegotiatedProtocolVersion(); // '2026-07-28' or '2025-11-25'
|
||||
```
|
||||
|
||||
- **absent / `mode: 'legacy'` (the default)** — today's 2025 connect sequence; no probe, no new headers.
|
||||
- **`mode: 'auto'`** — `connect()` probes with `server/discover`; a 2025-only server rejects the probe and the client falls back to the plain `initialize` handshake on the same connection, byte-equivalent to a 2025 client. The probe costs one round trip against an old server.
|
||||
- **`mode: { pin: '2026-07-28' }`** — modern era at exactly that revision; no fallback. Against a 2025-only server `connect()` rejects with a typed error. Use `pin` where a silent downgrade would be worse than an error (tests, CI, servers you control).
|
||||
|
||||
Once a modern era is negotiated, the client automatically attaches the per-request `_meta` envelope (the reserved protocol-version / client-info / client-capabilities keys) to every outgoing request and notification. You can also configure negotiation pre-connect on an
|
||||
already-constructed instance via {@linkcode @modelcontextprotocol/client!client/client.Client#setVersionNegotiation | client.setVersionNegotiation()}. See the [2026-07-28 support guide › Probe policy](./migration/support-2026-07-28.md#probe-policy) for the full failure semantics and probe-timeout behavior.
|
||||
|
||||
#### Skipping the probe: `connect({ prior })`
|
||||
|
||||
A gateway, proxy, or worker fleet that already knows the server's `server/discover` advertisement can skip the probe entirely. Pass a previously-obtained {@linkcode @modelcontextprotocol/client!index.DiscoverResult | DiscoverResult} via
|
||||
{@linkcode @modelcontextprotocol/client!client/client.ConnectOptions | ConnectOptions.prior} and `connect()` adopts it directly with **zero round trips** — the 2026-07-28 protocol is stateless on HTTP, so once the advertisement is known there is nothing left to negotiate.
|
||||
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#Client_connect_prior"
|
||||
// Probe once (here via the 'auto'-mode connect), persist the result …
|
||||
const bootstrap = new Client({ name: 'gateway', version: '1.0.0' }, { versionNegotiation: { mode: 'auto' } });
|
||||
await bootstrap.connect(new StreamableHTTPClientTransport(url));
|
||||
const persisted = JSON.stringify(bootstrap.getDiscoverResult());
|
||||
|
||||
// … then every worker connects with zero round trips.
|
||||
const worker = new Client({ name: 'worker', version: '1.0.0' });
|
||||
await worker.connect(new StreamableHTTPClientTransport(url), { prior: JSON.parse(persisted) });
|
||||
```
|
||||
|
||||
{@linkcode @modelcontextprotocol/client!client/client.Client#getDiscoverResult | client.getDiscoverResult()} returns the value that the `'auto'`/pinned probe path, an explicit {@linkcode @modelcontextprotocol/client!client/client.Client#discover | client.discover()} call, or a
|
||||
prior `connect({ prior })` recorded; it round-trips through `JSON.stringify`/`JSON.parse`. `connect({ prior })` is **2026-07-28+ only** — it rejects with `SdkError(EraNegotiationFailed)` when the supplied result and the client share no modern revision. Only reuse a persisted
|
||||
`DiscoverResult` across clients that present the **same authorization context** as the one that obtained it. See the [`gateway/` example](../examples/gateway/README.md) for the full probe-once / connect-many pattern with a server-side proof.
|
||||
|
||||
### Disconnecting
|
||||
|
||||
|
|
@ -94,7 +146,7 @@ Call {@linkcode @modelcontextprotocol/client!client/client.Client#close | await
|
|||
|
||||
For Streamable HTTP, terminate the server-side session first (per the MCP specification):
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#disconnect_streamableHttp"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#disconnect_streamableHttp"
|
||||
await transport.terminateSession(); // notify the server (recommended)
|
||||
await client.close();
|
||||
```
|
||||
|
|
@ -103,9 +155,10 @@ For stdio, `client.close()` handles graceful process shutdown (closes stdin, the
|
|||
|
||||
### Server instructions
|
||||
|
||||
Servers can provide an `instructions` string during initialization that describes how to use them — cross-tool relationships, workflow patterns, and constraints (see [Instructions](https://modelcontextprotocol.io/specification/latest/basic/lifecycle#instructions) in the MCP specification). Retrieve it after connecting and include it in the model's system prompt:
|
||||
Servers can provide an `instructions` string during initialization that describes how to use them — cross-tool relationships, workflow patterns, and constraints (see [Instructions](https://modelcontextprotocol.io/specification/latest/basic/lifecycle#instructions) in the MCP
|
||||
specification). Retrieve it after connecting and include it in the model's system prompt:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#serverInstructions_basic"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#serverInstructions_basic"
|
||||
const instructions = client.getInstructions();
|
||||
|
||||
const systemPrompt = ['You are a helpful assistant.', instructions].filter(Boolean).join('\n\n');
|
||||
|
|
@ -115,25 +168,27 @@ console.log(systemPrompt);
|
|||
|
||||
## Authentication
|
||||
|
||||
MCP servers can require authentication before accepting client connections (see [Authorization](https://modelcontextprotocol.io/specification/latest/basic/authorization) in the MCP specification). Pass an {@linkcode @modelcontextprotocol/client!client/auth.AuthProvider | AuthProvider} to {@linkcode @modelcontextprotocol/client!client/streamableHttp.StreamableHTTPClientTransport | StreamableHTTPClientTransport}. The transport calls `token()` before every request and `onUnauthorized()` (if provided) on 401, then retries once.
|
||||
MCP servers can require authentication before accepting client connections (see [Authorization](https://modelcontextprotocol.io/specification/latest/basic/authorization) in the MCP specification). Pass an {@linkcode @modelcontextprotocol/client!client/auth.AuthProvider |
|
||||
AuthProvider} to {@linkcode @modelcontextprotocol/client!client/streamableHttp.StreamableHTTPClientTransport | StreamableHTTPClientTransport}. The transport calls `token()` before every request and `onUnauthorized()` (if provided) on 401, then retries once.
|
||||
|
||||
### Bearer tokens
|
||||
|
||||
For servers that accept bearer tokens managed outside the SDK — API keys, tokens from a gateway or proxy, service-account credentials — implement only `token()`. With no `onUnauthorized()`, a 401 throws {@linkcode @modelcontextprotocol/client!client/auth.UnauthorizedError | UnauthorizedError} immediately:
|
||||
For servers that accept bearer tokens managed outside the SDK — API keys, tokens from a gateway or proxy, service-account credentials — implement only `token()`. With no `onUnauthorized()`, a 401 throws {@linkcode @modelcontextprotocol/client!client/auth.UnauthorizedError |
|
||||
UnauthorizedError} immediately:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#auth_tokenProvider"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#auth_tokenProvider"
|
||||
const authProvider: AuthProvider = { token: async () => getStoredToken() };
|
||||
|
||||
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'), { authProvider });
|
||||
```
|
||||
|
||||
See [`simpleTokenProvider.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleTokenProvider.ts) for a complete runnable example.
|
||||
See [`simpleTokenProvider.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/oauth/simpleTokenProvider.ts) for a complete runnable example.
|
||||
|
||||
### Client credentials
|
||||
|
||||
{@linkcode @modelcontextprotocol/client!client/authExtensions.ClientCredentialsProvider | ClientCredentialsProvider} handles the `client_credentials` grant flow for service-to-service communication:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#auth_clientCredentials"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#auth_clientCredentials"
|
||||
const authProvider = new ClientCredentialsProvider({
|
||||
clientId: 'my-service',
|
||||
clientSecret: 'my-secret'
|
||||
|
|
@ -150,7 +205,7 @@ await client.connect(transport);
|
|||
|
||||
{@linkcode @modelcontextprotocol/client!client/authExtensions.PrivateKeyJwtProvider | PrivateKeyJwtProvider} signs JWT assertions for the `private_key_jwt` token endpoint auth method, avoiding a shared client secret:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#auth_privateKeyJwt"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#auth_privateKeyJwt"
|
||||
const authProvider = new PrivateKeyJwtProvider({
|
||||
clientId: 'my-service',
|
||||
privateKey: pemEncodedKey,
|
||||
|
|
@ -160,23 +215,134 @@ const authProvider = new PrivateKeyJwtProvider({
|
|||
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'), { authProvider });
|
||||
```
|
||||
|
||||
For a runnable example supporting both auth methods via environment variables, see [`simpleClientCredentials.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleClientCredentials.ts).
|
||||
For a runnable `client_credentials` example, see [`oauth-client-credentials/client.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/oauth-client-credentials/client.ts) — its README shows the `private_key_jwt` swap (the in-repo demo Authorization
|
||||
Server only implements `client_secret_basic`/`client_secret_post`, so there is no runnable `private_key_jwt` leg).
|
||||
|
||||
### Full OAuth with user authorization
|
||||
|
||||
For user-facing applications, implement the {@linkcode @modelcontextprotocol/client!client/auth.OAuthClientProvider | OAuthClientProvider} interface to handle the full authorization code flow (redirects, code verifiers, token storage, dynamic client registration). The {@linkcode @modelcontextprotocol/client!client/client.Client#connect | connect()} call will throw {@linkcode @modelcontextprotocol/client!client/auth.UnauthorizedError | UnauthorizedError} when authorization is needed — catch it, complete the browser flow, call {@linkcode @modelcontextprotocol/client!client/streamableHttp.StreamableHTTPClientTransport#finishAuth | transport.finishAuth(code)}, and reconnect.
|
||||
For user-facing applications, implement the {@linkcode @modelcontextprotocol/client!client/auth.OAuthClientProvider | OAuthClientProvider} interface to handle the full authorization code flow (redirects, code verifiers, token storage, dynamic client registration). Key persisted
|
||||
client credentials by the `ctx.issuer` passed to `clientInformation()` / `saveClientInformation()` so credentials registered with one authorization server are never sent to another:
|
||||
|
||||
For a complete working OAuth flow, see [`simpleOAuthClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleOAuthClient.ts) and [`simpleOAuthClientProvider.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleOAuthClientProvider.ts).
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#auth_oauthClientProvider"
|
||||
class MyOAuthProvider implements OAuthClientProvider {
|
||||
// Key DCR-obtained credentials by issuer so a client_id registered with one
|
||||
// authorization server is never returned for another (SEP-2352).
|
||||
private creds = new Map<string, OAuthClientInformationMixed>();
|
||||
private storedTokens?: OAuthTokens;
|
||||
private verifier?: string;
|
||||
private discovery?: OAuthDiscoveryState;
|
||||
lastState?: string;
|
||||
|
||||
readonly redirectUrl = 'http://localhost:8090/callback';
|
||||
readonly clientMetadata: OAuthClientMetadata = {
|
||||
client_name: 'My MCP Client',
|
||||
redirect_uris: ['http://localhost:8090/callback'],
|
||||
// Loopback redirect → the SDK would default this to 'native'; set
|
||||
// explicitly when the heuristic is wrong for your deployment (SEP-837).
|
||||
application_type: 'native'
|
||||
};
|
||||
|
||||
clientInformation(ctx?: OAuthClientInformationContext) {
|
||||
return ctx ? this.creds.get(ctx.issuer) : undefined;
|
||||
}
|
||||
saveClientInformation(info: OAuthClientInformationMixed, ctx?: OAuthClientInformationContext) {
|
||||
if (ctx) this.creds.set(ctx.issuer, info);
|
||||
}
|
||||
tokens() {
|
||||
return this.storedTokens;
|
||||
}
|
||||
saveTokens(tokens: OAuthTokens) {
|
||||
// In production, persist to OS keychain / secure storage — never plain files.
|
||||
this.storedTokens = tokens;
|
||||
}
|
||||
// CSRF binding for the redirect — the SDK puts this on the authorize URL;
|
||||
// your callback handler compares it before calling `finishAuth`.
|
||||
state() {
|
||||
this.lastState = crypto.randomUUID();
|
||||
return this.lastState;
|
||||
}
|
||||
// Callback-leg AS-binding (SEP-2352): record what discovery resolved before
|
||||
// the redirect so the SDK can verify the code is exchanged at the same AS.
|
||||
saveDiscoveryState(state: OAuthDiscoveryState) {
|
||||
this.discovery = state;
|
||||
}
|
||||
discoveryState() {
|
||||
return this.discovery;
|
||||
}
|
||||
redirectToAuthorization(url: URL) {
|
||||
onRedirect(url);
|
||||
}
|
||||
saveCodeVerifier(v: string) {
|
||||
this.verifier = v;
|
||||
}
|
||||
codeVerifier() {
|
||||
if (!this.verifier) throw new Error('no code verifier');
|
||||
return this.verifier;
|
||||
}
|
||||
}
|
||||
|
||||
const provider = new MyOAuthProvider();
|
||||
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'), {
|
||||
authProvider: provider
|
||||
});
|
||||
```
|
||||
|
||||
The {@linkcode @modelcontextprotocol/client!client/client.Client#connect | connect()} call throws {@linkcode @modelcontextprotocol/client!client/auth.UnauthorizedError | UnauthorizedError} when authorization is needed — catch it, complete the browser flow, hand the callback query
|
||||
to {@linkcode @modelcontextprotocol/client!client/streamableHttp.StreamableHTTPClientTransport#finishAuth | transport.finishAuth()}, and reconnect. Passing the whole `URLSearchParams` lets the SDK extract `code` and validate the RFC 9207 `iss` parameter for you:
|
||||
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#auth_finishAuth"
|
||||
const client = new Client({ name: 'my-client', version: '1.0.0' });
|
||||
const transport = new StreamableHTTPClientTransport(url, { authProvider: provider });
|
||||
try {
|
||||
await client.connect(transport);
|
||||
return client;
|
||||
} catch (error) {
|
||||
// With version negotiation, the connect-time 401 may surface wrapped as
|
||||
// SdkError(EraNegotiationFailed) whose .data.cause is the UnauthorizedError.
|
||||
const root = error instanceof UnauthorizedError ? error : (error as { data?: { cause?: unknown } }).data?.cause;
|
||||
if (!(root instanceof UnauthorizedError)) throw error;
|
||||
// The transport called redirectToAuthorization(); fall through to the browser callback.
|
||||
}
|
||||
|
||||
const callbackUrl = await waitForCallback();
|
||||
const params = new URL(callbackUrl).searchParams;
|
||||
|
||||
// The SDK does not validate `state` — compare it to the value your provider generated.
|
||||
if (params.get('state') !== provider.lastState) throw new Error('state mismatch');
|
||||
|
||||
try {
|
||||
// Preferred: hand over the whole query — the SDK extracts `code` and
|
||||
// `iss`, validates `iss` (RFC 9207), and never surfaces callback-derived
|
||||
// `error`/`error_description` text on mismatch.
|
||||
await transport.finishAuth(params);
|
||||
} catch (error) {
|
||||
if (error instanceof IssuerMismatchError) {
|
||||
// Mix-up attack: do NOT render params.get('error_description') to the user.
|
||||
throw new Error('Authorization failed: issuer mismatch');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Reconnect on a FRESH transport — a started transport cannot be restarted;
|
||||
// OAuth state (tokens, verifier, discovery) lives on the provider, not the transport.
|
||||
await client.connect(new StreamableHTTPClientTransport(url, { authProvider: provider }));
|
||||
return client;
|
||||
```
|
||||
|
||||
For a complete working OAuth flow, see [`simpleOAuthClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/oauth/simpleOAuthClient.ts) and
|
||||
[`simpleOAuthClientProvider.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/oauth/simpleOAuthClientProvider.ts).
|
||||
|
||||
### Cross-App Access (Enterprise Managed Authorization)
|
||||
|
||||
{@linkcode @modelcontextprotocol/client!client/authExtensions.CrossAppAccessProvider | CrossAppAccessProvider} implements Enterprise Managed Authorization (SEP-990) for scenarios where users authenticate with an enterprise identity provider (IdP) and clients need to access protected MCP servers on their behalf.
|
||||
{@linkcode @modelcontextprotocol/client!client/authExtensions.CrossAppAccessProvider | CrossAppAccessProvider} implements Enterprise Managed Authorization (SEP-990) for scenarios where users authenticate with an enterprise identity provider (IdP) and clients need to access
|
||||
protected MCP servers on their behalf.
|
||||
|
||||
This provider handles a two-step OAuth flow:
|
||||
|
||||
1. Exchange the user's ID Token from the enterprise IdP for a JWT Authorization Grant (JAG) via RFC 8693 token exchange
|
||||
2. Exchange the JAG for an access token from the MCP server via RFC 7523 JWT bearer grant
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#auth_crossAppAccess"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#auth_crossAppAccess"
|
||||
const authProvider = new CrossAppAccessProvider({
|
||||
assertion: async ctx => {
|
||||
// ctx provides: authorizationServerUrl, resourceUrl, scope, fetchFn
|
||||
|
|
@ -200,36 +366,34 @@ const transport = new StreamableHTTPClientTransport(new URL('http://localhost:30
|
|||
```
|
||||
|
||||
The `assertion` callback receives a context object with:
|
||||
|
||||
- `authorizationServerUrl` – The MCP server's authorization server (discovered automatically)
|
||||
- `resourceUrl` – The MCP resource URL (discovered automatically)
|
||||
- `scope` – Optional scope passed to `auth()` or from `clientMetadata`
|
||||
- `fetchFn` – Fetch implementation to use for HTTP requests
|
||||
|
||||
For manual control over the token exchange steps, use the Layer 2 utilities from `@modelcontextprotocol/client`:
|
||||
|
||||
- `requestJwtAuthorizationGrant()` – Exchange ID Token for JAG at IdP
|
||||
- `discoverAndRequestJwtAuthGrant()` – Discovery + JAG acquisition
|
||||
- `exchangeJwtAuthGrant()` – Exchange JAG for access token at MCP server
|
||||
|
||||
> [!NOTE]
|
||||
> See [RFC 8693 (Token Exchange)](https://datatracker.ietf.org/doc/html/rfc8693), [RFC 7523 (JWT Bearer Grant)](https://datatracker.ietf.org/doc/html/rfc7523), and [RFC 9728 (Resource Discovery)](https://datatracker.ietf.org/doc/html/rfc9728) for the underlying OAuth standards.
|
||||
> See [RFC 8693 (Token Exchange)](https://datatracker.ietf.org/doc/html/rfc8693), [RFC 7523 (JWT Bearer Grant)](https://datatracker.ietf.org/doc/html/rfc7523), and [RFC 9728 (Resource Discovery)](https://datatracker.ietf.org/doc/html/rfc9728) for the underlying OAuth
|
||||
> standards.
|
||||
|
||||
## Tools
|
||||
|
||||
Tools are callable actions offered by servers — discovering and invoking them is usually how your client enables an LLM to take action (see [Tools](https://modelcontextprotocol.io/docs/learn/server-concepts#tools) in the MCP overview).
|
||||
|
||||
Use {@linkcode @modelcontextprotocol/client!client/client.Client#listTools | listTools()} to discover available tools, and {@linkcode @modelcontextprotocol/client!client/client.Client#callTool | callTool()} to invoke one. Results may be paginated — loop on `nextCursor` to collect all pages:
|
||||
Use {@linkcode @modelcontextprotocol/client!client/client.Client#listTools | listTools()} to discover available tools, and {@linkcode @modelcontextprotocol/client!client/client.Client#callTool | callTool()} to invoke one. `listTools()` walks every page on your behalf and returns
|
||||
the complete list (pass an explicit `{ cursor }` for per-page control):
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#callTool_basic"
|
||||
const allTools: Tool[] = [];
|
||||
let toolCursor: string | undefined;
|
||||
do {
|
||||
const { tools, nextCursor } = await client.listTools({ cursor: toolCursor });
|
||||
allTools.push(...tools);
|
||||
toolCursor = nextCursor;
|
||||
} while (toolCursor);
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#callTool_basic"
|
||||
const { tools } = await client.listTools();
|
||||
console.log(
|
||||
'Available tools:',
|
||||
allTools.map(t => t.name)
|
||||
tools.map(t => t.name)
|
||||
);
|
||||
|
||||
const result = await client.callTool({
|
||||
|
|
@ -239,17 +403,21 @@ const result = await client.callTool({
|
|||
console.log(result.content);
|
||||
```
|
||||
|
||||
Tool results may include a `structuredContent` field — a machine-readable JSON object for programmatic use by the client application, complementing `content` which is for the LLM:
|
||||
Tool results may include a `structuredContent` field — a machine-readable JSON value (any JSON type per SEP-2106) for programmatic use by the client application, complementing `content` which is for the LLM:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#callTool_structuredOutput"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#callTool_structuredOutput"
|
||||
const result = await client.callTool({
|
||||
name: 'calculate-bmi',
|
||||
arguments: { weightKg: 70, heightM: 1.75 }
|
||||
});
|
||||
|
||||
// Machine-readable output for the client application
|
||||
if (result.structuredContent) {
|
||||
console.log(result.structuredContent); // e.g. { bmi: 22.86 }
|
||||
// Machine-readable output for the client application. SEP-2106: structuredContent is
|
||||
// `unknown` (any JSON value). Check for presence with `!== undefined` and narrow before use.
|
||||
if (result.structuredContent !== undefined) {
|
||||
const sc: unknown = result.structuredContent; // e.g. { bmi: 22.86 }
|
||||
if (typeof sc === 'object' && sc !== null && 'bmi' in sc) {
|
||||
console.log(sc.bmi);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -257,7 +425,7 @@ if (result.structuredContent) {
|
|||
|
||||
Pass `onprogress` to receive incremental progress notifications from long-running tools. Use `resetTimeoutOnProgress` to keep the request alive while the server is actively reporting, and `maxTotalTimeout` as an absolute cap:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#callTool_progress"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#callTool_progress"
|
||||
const result = await client.callTool(
|
||||
{ name: 'long-operation', arguments: {} },
|
||||
{
|
||||
|
|
@ -271,23 +439,28 @@ const result = await client.callTool(
|
|||
console.log(result.content);
|
||||
```
|
||||
|
||||
### `x-mcp-header` parameter mirroring (2026-07-28 draft)
|
||||
|
||||
On a 2026-07-28 connection over Streamable HTTP, `callTool()` mirrors any argument whose `inputSchema` property carries an `x-mcp-header` annotation into an `Mcp-Param-{Name}` HTTP request header so intermediaries can route on it without parsing the body. The mirrored headers
|
||||
are built from the client's internal `tools/list` cache; if you already hold the tool definition (e.g. from configuration), pass it via `CallToolRequestOptions.toolDefinition` so mirroring runs without a prior list. On a cache miss the call is sent without `Mcp-Param-*` headers
|
||||
and, when a conforming server rejects it with `-32020` (`HeaderMismatch`), `callTool()` refreshes the definition cache once and retries.
|
||||
|
||||
On a non-stdio modern connection `listTools()` (and the internal `tools/list` cache) exclude tool definitions whose `x-mcp-header` declarations violate the spec's constraints, logging a warning that names the tool and the reason. Browser clients skip mirroring (dynamically named
|
||||
headers cannot be statically allow-listed for credentialed CORS), so calling an `x-mcp-header` tool with a non-null designated argument from a browser against a server that enforces SEP-2243 validation will be rejected — a known limitation. The legacy-era `callTool`/`listTools`
|
||||
paths are unchanged.
|
||||
|
||||
## Resources
|
||||
|
||||
Resources are read-only data — files, database schemas, configuration — that your application can retrieve from a server and attach as context for the model (see [Resources](https://modelcontextprotocol.io/docs/learn/server-concepts#resources) in the MCP overview).
|
||||
|
||||
Use {@linkcode @modelcontextprotocol/client!client/client.Client#listResources | listResources()} and {@linkcode @modelcontextprotocol/client!client/client.Client#readResource | readResource()} to discover and read server-provided data. Results may be paginated — loop on `nextCursor` to collect all pages:
|
||||
Use {@linkcode @modelcontextprotocol/client!client/client.Client#listResources | listResources()} and {@linkcode @modelcontextprotocol/client!client/client.Client#readResource | readResource()} to discover and read server-provided data. `listResources()` walks every page on your
|
||||
behalf and returns the complete list (pass an explicit `{ cursor }` for per-page control):
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#readResource_basic"
|
||||
const allResources: Resource[] = [];
|
||||
let resourceCursor: string | undefined;
|
||||
do {
|
||||
const { resources, nextCursor } = await client.listResources({ cursor: resourceCursor });
|
||||
allResources.push(...resources);
|
||||
resourceCursor = nextCursor;
|
||||
} while (resourceCursor);
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#readResource_basic"
|
||||
const { resources } = await client.listResources();
|
||||
console.log(
|
||||
'Available resources:',
|
||||
allResources.map(r => r.name)
|
||||
resources.map(r => r.name)
|
||||
);
|
||||
|
||||
const { contents } = await client.readResource({ uri: 'config://app' });
|
||||
|
|
@ -302,7 +475,7 @@ To discover URI templates for dynamic resources, use {@linkcode @modelcontextpro
|
|||
|
||||
If the server supports resource subscriptions, use {@linkcode @modelcontextprotocol/client!client/client.Client#subscribeResource | subscribeResource()} to receive notifications when a resource changes, then re-read it:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#subscribeResource_basic"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#subscribeResource_basic"
|
||||
await client.subscribeResource({ uri: 'config://app' });
|
||||
|
||||
client.setNotificationHandler('notifications/resources/updated', async notification => {
|
||||
|
|
@ -320,19 +493,14 @@ await client.unsubscribeResource({ uri: 'config://app' });
|
|||
|
||||
Prompts are reusable message templates that servers offer to help structure interactions with models (see [Prompts](https://modelcontextprotocol.io/docs/learn/server-concepts#prompts) in the MCP overview).
|
||||
|
||||
Use {@linkcode @modelcontextprotocol/client!client/client.Client#listPrompts | listPrompts()} and {@linkcode @modelcontextprotocol/client!client/client.Client#getPrompt | getPrompt()} to list available prompts and retrieve them with arguments. Results may be paginated — loop on `nextCursor` to collect all pages:
|
||||
Use {@linkcode @modelcontextprotocol/client!client/client.Client#listPrompts | listPrompts()} and {@linkcode @modelcontextprotocol/client!client/client.Client#getPrompt | getPrompt()} to list available prompts and retrieve them with arguments. `listPrompts()` walks every page on
|
||||
your behalf and returns the complete list (pass an explicit `{ cursor }` for per-page control):
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#getPrompt_basic"
|
||||
const allPrompts: Prompt[] = [];
|
||||
let promptCursor: string | undefined;
|
||||
do {
|
||||
const { prompts, nextCursor } = await client.listPrompts({ cursor: promptCursor });
|
||||
allPrompts.push(...prompts);
|
||||
promptCursor = nextCursor;
|
||||
} while (promptCursor);
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#getPrompt_basic"
|
||||
const { prompts } = await client.listPrompts();
|
||||
console.log(
|
||||
'Available prompts:',
|
||||
allPrompts.map(p => p.name)
|
||||
prompts.map(p => p.name)
|
||||
);
|
||||
|
||||
const { messages } = await client.getPrompt({
|
||||
|
|
@ -346,7 +514,7 @@ console.log(messages);
|
|||
|
||||
Both prompts and resources can support argument completions. Use {@linkcode @modelcontextprotocol/client!client/client.Client#complete | complete()} to request autocompletion suggestions from the server as a user types:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#complete_basic"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#complete_basic"
|
||||
const { completion } = await client.complete({
|
||||
ref: {
|
||||
type: 'ref/prompt',
|
||||
|
|
@ -364,9 +532,10 @@ console.log(completion.values); // e.g. ['typescript']
|
|||
|
||||
### Automatic list-change tracking
|
||||
|
||||
The {@linkcode @modelcontextprotocol/client!client/client.ClientOptions | listChanged} client option keeps a local cache of tools, prompts, or resources in sync with the server. It provides automatic server capability gating, debouncing (300 ms by default), auto-refresh, and error-first callbacks:
|
||||
The {@linkcode @modelcontextprotocol/client!client/client.ClientOptions | listChanged} client option keeps a local cache of tools, prompts, or resources in sync with the server. It provides automatic server capability gating, debouncing (300 ms by default), auto-refresh, and
|
||||
error-first callbacks:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#listChanged_basic"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#listChanged_basic"
|
||||
const client = new Client(
|
||||
{ name: 'my-client', version: '1.0.0' },
|
||||
{
|
||||
|
|
@ -392,7 +561,7 @@ const client = new Client(
|
|||
|
||||
For full control — or for notification types not covered by `listChanged` (such as log messages) — register handlers directly with {@linkcode @modelcontextprotocol/client!client/client.Client#setNotificationHandler | setNotificationHandler()}:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#notificationHandler_basic"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#notificationHandler_basic"
|
||||
// Server log messages (sent by the server during request processing)
|
||||
client.setNotificationHandler('notifications/message', notification => {
|
||||
const { level, data } = notification.params;
|
||||
|
|
@ -407,11 +576,12 @@ client.setNotificationHandler('notifications/resources/list_changed', async () =
|
|||
```
|
||||
|
||||
> [!WARNING]
|
||||
> MCP logging (including `setLoggingLevel()` and `notifications/message`) is deprecated as of protocol version 2026-07-28 (SEP-2577). It remains fully functional during the deprecation window (at least twelve months); see the [deprecated features registry](https://modelcontextprotocol.io/specification/draft/deprecated). Servers should migrate to stderr logging (STDIO) or OpenTelemetry.
|
||||
> MCP logging (including `setLoggingLevel()` and `notifications/message`) is deprecated as of protocol version 2026-07-28 (SEP-2577). It remains fully functional during the deprecation window (at least twelve months); see the
|
||||
> [deprecated features registry](https://modelcontextprotocol.io/specification/draft/deprecated). Servers should migrate to stderr logging (STDIO) or OpenTelemetry.
|
||||
|
||||
To control the minimum severity of log messages the server sends, use {@linkcode @modelcontextprotocol/client!client/client.Client#setLoggingLevel | setLoggingLevel()}:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#setLoggingLevel_basic"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#setLoggingLevel_basic"
|
||||
await client.setLoggingLevel('warning');
|
||||
```
|
||||
|
||||
|
|
@ -420,9 +590,10 @@ await client.setLoggingLevel('warning');
|
|||
|
||||
## Handling server-initiated requests
|
||||
|
||||
MCP is bidirectional — servers can send requests *to* the client during tool execution, as long as the client declares matching capabilities (see [Architecture](https://modelcontextprotocol.io/docs/learn/architecture) in the MCP overview). Declare the corresponding capability when constructing the {@linkcode @modelcontextprotocol/client!client/client.Client | Client} and register a request handler:
|
||||
MCP is bidirectional — servers can send requests _to_ the client during tool execution, as long as the client declares matching capabilities (see [Architecture](https://modelcontextprotocol.io/docs/learn/architecture) in the MCP overview). Declare the corresponding capability
|
||||
when constructing the {@linkcode @modelcontextprotocol/client!client/client.Client | Client} and register a request handler:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#capabilities_declaration"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#capabilities_declaration"
|
||||
const client = new Client(
|
||||
{ name: 'my-client', version: '1.0.0' },
|
||||
{
|
||||
|
|
@ -437,11 +608,12 @@ const client = new Client(
|
|||
### Sampling
|
||||
|
||||
> [!WARNING]
|
||||
> Sampling is deprecated as of protocol version 2026-07-28 (SEP-2577). It remains fully functional during the deprecation window (at least twelve months); see the [deprecated features registry](https://modelcontextprotocol.io/specification/draft/deprecated). Servers should migrate to calling LLM provider APIs directly.
|
||||
> Sampling is deprecated as of protocol version 2026-07-28 (SEP-2577). It remains fully functional during the deprecation window (at least twelve months); see the [deprecated features registry](https://modelcontextprotocol.io/specification/draft/deprecated). Servers
|
||||
> should migrate to calling LLM provider APIs directly.
|
||||
|
||||
When a server needs an LLM completion during tool execution, it sends a `sampling/createMessage` request to the client (see [Sampling](https://modelcontextprotocol.io/docs/learn/client-concepts#sampling) in the MCP overview). Register a handler to fulfill it:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#sampling_handler"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#sampling_handler"
|
||||
client.setRequestHandler('sampling/createMessage', async request => {
|
||||
const lastMessage = request.params.messages.at(-1);
|
||||
console.log('Sampling request:', lastMessage);
|
||||
|
|
@ -460,9 +632,10 @@ client.setRequestHandler('sampling/createMessage', async request => {
|
|||
|
||||
### Elicitation
|
||||
|
||||
When a server needs user input during tool execution, it sends an `elicitation/create` request to the client (see [Elicitation](https://modelcontextprotocol.io/docs/learn/client-concepts#elicitation) in the MCP overview). The client should present the form to the user and return the collected data, or `{ action: 'decline' }`:
|
||||
When a server needs user input during tool execution, it sends an `elicitation/create` request to the client (see [Elicitation](https://modelcontextprotocol.io/docs/learn/client-concepts#elicitation) in the MCP overview). The client should present the form to the user and return
|
||||
the collected data, or `{ action: 'decline' }`:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#elicitation_handler"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#elicitation_handler"
|
||||
client.setRequestHandler('elicitation/create', async request => {
|
||||
console.log('Server asks:', request.params.message);
|
||||
|
||||
|
|
@ -476,16 +649,18 @@ client.setRequestHandler('elicitation/create', async request => {
|
|||
});
|
||||
```
|
||||
|
||||
For a full form-based elicitation handler with AJV validation, see [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleStreamableHttp.ts). For URL elicitation mode, see [`elicitationUrlExample.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/elicitationUrlExample.ts).
|
||||
For a full form-based elicitation handler with AJV validation, see [`repl/client.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/repl/client.ts). For URL elicitation mode (both the 2025-era push/throw style and the 2026-07-28 `inputRequired`
|
||||
return), see [`elicitation/client.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/elicitation/client.ts).
|
||||
|
||||
### Roots
|
||||
|
||||
> [!WARNING]
|
||||
> Roots are deprecated as of protocol version 2026-07-28 (SEP-2577). They remain fully functional during the deprecation window (at least twelve months); see the [deprecated features registry](https://modelcontextprotocol.io/specification/draft/deprecated). Migrate to passing paths via tool parameters, resource URIs, or configuration.
|
||||
> Roots are deprecated as of protocol version 2026-07-28 (SEP-2577). They remain fully functional during the deprecation window (at least twelve months); see the [deprecated features registry](https://modelcontextprotocol.io/specification/draft/deprecated). Migrate to
|
||||
> passing paths via tool parameters, resource URIs, or configuration.
|
||||
|
||||
Roots let the client expose filesystem boundaries to the server (see [Roots](https://modelcontextprotocol.io/docs/learn/client-concepts#roots) in the MCP overview). Declare the `roots` capability and register a `roots/list` handler:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#roots_handler"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#roots_handler"
|
||||
client.setRequestHandler('roots/list', async () => {
|
||||
return {
|
||||
roots: [
|
||||
|
|
@ -502,9 +677,9 @@ When the available roots change, notify the server with {@linkcode @modelcontext
|
|||
|
||||
### Tool errors vs protocol errors
|
||||
|
||||
{@linkcode @modelcontextprotocol/client!client/client.Client#callTool | callTool()} has two error surfaces: the tool can *run but report failure* via `isError: true` in the result, or the *request itself can fail* and throw an exception. Always check both:
|
||||
{@linkcode @modelcontextprotocol/client!client/client.Client#callTool | callTool()} has two error surfaces: the tool can _run but report failure_ via `isError: true` in the result, or the _request itself can fail_ and throw an exception. Always check both:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#errorHandling_toolErrors"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#errorHandling_toolErrors"
|
||||
try {
|
||||
const result = await client.callTool({
|
||||
name: 'fetch-data',
|
||||
|
|
@ -530,13 +705,16 @@ try {
|
|||
}
|
||||
```
|
||||
|
||||
{@linkcode @modelcontextprotocol/client!index.ProtocolError | ProtocolError} represents JSON-RPC errors from the server (method not found, invalid params, internal error). {@linkcode @modelcontextprotocol/client!index.SdkError | SdkError} represents local SDK errors — {@linkcode @modelcontextprotocol/client!index.SdkErrorCode.RequestTimeout | REQUEST_TIMEOUT}, {@linkcode @modelcontextprotocol/client!index.SdkErrorCode.ConnectionClosed | CONNECTION_CLOSED}, {@linkcode @modelcontextprotocol/client!index.SdkErrorCode.CapabilityNotSupported | CAPABILITY_NOT_SUPPORTED}, and others.
|
||||
{@linkcode @modelcontextprotocol/client!index.ProtocolError | ProtocolError} represents JSON-RPC errors from the server (method not found, invalid params, internal error). {@linkcode @modelcontextprotocol/client!index.SdkError | SdkError} represents local SDK errors — {@linkcode
|
||||
@modelcontextprotocol/client!index.SdkErrorCode.RequestTimeout | REQUEST_TIMEOUT}, {@linkcode @modelcontextprotocol/client!index.SdkErrorCode.ConnectionClosed | CONNECTION_CLOSED}, {@linkcode @modelcontextprotocol/client!index.SdkErrorCode.CapabilityNotSupported |
|
||||
CAPABILITY_NOT_SUPPORTED}, and others.
|
||||
|
||||
### Connection lifecycle
|
||||
|
||||
Set {@linkcode @modelcontextprotocol/client!client/client.Client#onerror | client.onerror} to catch out-of-band transport errors (SSE disconnects, parse errors). Set {@linkcode @modelcontextprotocol/client!client/client.Client#onclose | client.onclose} to detect when the connection drops — pending requests are rejected with a {@linkcode @modelcontextprotocol/client!index.SdkErrorCode.ConnectionClosed | CONNECTION_CLOSED} error:
|
||||
Set {@linkcode @modelcontextprotocol/client!client/client.Client#onerror | client.onerror} to catch out-of-band transport errors (SSE disconnects, parse errors). Set {@linkcode @modelcontextprotocol/client!client/client.Client#onclose | client.onclose} to detect when the
|
||||
connection drops — pending requests are rejected with a {@linkcode @modelcontextprotocol/client!index.SdkErrorCode.ConnectionClosed | CONNECTION_CLOSED} error:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#errorHandling_lifecycle"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#errorHandling_lifecycle"
|
||||
// Out-of-band errors (SSE disconnects, parse errors)
|
||||
client.onerror = error => {
|
||||
console.error('Transport error:', error.message);
|
||||
|
|
@ -550,9 +728,10 @@ client.onclose = () => {
|
|||
|
||||
### Timeouts
|
||||
|
||||
All requests have a 60-second default timeout. Pass a custom `timeout` in the options to override it. On timeout, the SDK sends a cancellation notification to the server and rejects the promise with {@linkcode @modelcontextprotocol/client!index.SdkErrorCode.RequestTimeout | SdkErrorCode.RequestTimeout}:
|
||||
All requests have a 60-second default timeout. Pass a custom `timeout` in the options to override it. On timeout, the SDK sends a cancellation notification to the server and rejects the promise with {@linkcode @modelcontextprotocol/client!index.SdkErrorCode.RequestTimeout |
|
||||
SdkErrorCode.RequestTimeout}:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#errorHandling_timeout"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#errorHandling_timeout"
|
||||
try {
|
||||
const result = await client.callTool(
|
||||
{ name: 'slow-operation', arguments: {} },
|
||||
|
|
@ -568,9 +747,10 @@ try {
|
|||
|
||||
## Client middleware
|
||||
|
||||
Use {@linkcode @modelcontextprotocol/client!client/middleware.createMiddleware | createMiddleware()} and {@linkcode @modelcontextprotocol/client!client/middleware.applyMiddlewares | applyMiddlewares()} to compose fetch middleware pipelines. Middleware wraps the underlying `fetch` call and can add headers, handle retries, or log requests. Pass the enhanced fetch to the transport via the `fetch` option:
|
||||
Use {@linkcode @modelcontextprotocol/client!client/middleware.createMiddleware | createMiddleware()} and {@linkcode @modelcontextprotocol/client!client/middleware.applyMiddlewares | applyMiddlewares()} to compose fetch middleware pipelines. Middleware wraps the underlying `fetch`
|
||||
call and can add headers, handle retries, or log requests. Pass the enhanced fetch to the transport via the `fetch` option:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#middleware_basic"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#middleware_basic"
|
||||
const authMiddleware = createMiddleware(async (next, input, init) => {
|
||||
const headers = new Headers(init?.headers);
|
||||
headers.set('X-Custom-Header', 'my-value');
|
||||
|
|
@ -584,11 +764,13 @@ const transport = new StreamableHTTPClientTransport(new URL('http://localhost:30
|
|||
|
||||
## Trace context propagation
|
||||
|
||||
The MCP specification ([SEP-414](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/414)) reserves the unprefixed `_meta` keys `traceparent`, `tracestate`, and `baggage` for distributed trace context, as an exception to the usual `_meta` key prefix rule. When present, the values must follow the [W3C Trace Context](https://www.w3.org/TR/trace-context/) and [W3C Baggage](https://www.w3.org/TR/baggage/) formats. The SDK does not interpret these keys — `_meta` passes through both directions untouched — so you can propagate OpenTelemetry context across any transport, including stdio where HTTP headers are unavailable. The key names are exported as `TRACEPARENT_META_KEY`, `TRACESTATE_META_KEY`, and `BAGGAGE_META_KEY`.
|
||||
The MCP specification ([SEP-414](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/414)) reserves the unprefixed `_meta` keys `traceparent`, `tracestate`, and `baggage` for distributed trace context, as an exception to the usual `_meta` key prefix rule. When
|
||||
present, the values must follow the [W3C Trace Context](https://www.w3.org/TR/trace-context/) and [W3C Baggage](https://www.w3.org/TR/baggage/) formats. The SDK does not interpret these keys — `_meta` passes through both directions untouched — so you can propagate OpenTelemetry
|
||||
context across any transport, including stdio where HTTP headers are unavailable. The key names are exported as `TRACEPARENT_META_KEY`, `TRACESTATE_META_KEY`, and `BAGGAGE_META_KEY`.
|
||||
|
||||
Attach trace context to a single request via `_meta`:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#traceContext_perRequest"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#traceContext_perRequest"
|
||||
// Values would normally come from your tracer's active span context.
|
||||
const result = await client.callTool({
|
||||
name: 'calculate-bmi',
|
||||
|
|
@ -603,7 +785,7 @@ console.log(result.content);
|
|||
|
||||
Or inject it into every outgoing request with fetch middleware (Streamable HTTP transport):
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#traceContext_middleware"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#traceContext_middleware"
|
||||
const traceContextMiddleware = createMiddleware(async (next, input, init) => {
|
||||
if (typeof init?.body !== 'string') {
|
||||
return next(input, init);
|
||||
|
|
@ -638,7 +820,7 @@ On the server side, handlers can read the incoming trace context from `ctx.mcpRe
|
|||
|
||||
When using SSE-based streaming, the server can assign event IDs. Pass `onresumptiontoken` to track them, and `resumptionToken` to resume from where you left off after a disconnection:
|
||||
|
||||
```ts source="../examples/client/src/clientGuide.examples.ts#resumptionToken_basic"
|
||||
```ts source="../examples/guides/clientGuide.examples.ts#resumptionToken_basic"
|
||||
let lastToken: string | undefined;
|
||||
|
||||
const result = await client.request(
|
||||
|
|
@ -657,21 +839,21 @@ const result = await client.request(
|
|||
console.log(result);
|
||||
```
|
||||
|
||||
For an end-to-end example of server-initiated SSE disconnection and automatic client reconnection with event replay, see [`ssePollingClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/ssePollingClient.ts).
|
||||
For an end-to-end example of server-initiated SSE disconnection and automatic client reconnection with event replay, see [`sse-polling/client.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/sse-polling/client.ts).
|
||||
|
||||
## See also
|
||||
|
||||
- [`examples/client/`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/client) — Full runnable client examples
|
||||
- [`examples/`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples) — Full runnable client examples
|
||||
- [Server guide](./server.md) — Building MCP servers with this SDK
|
||||
- [MCP overview](https://modelcontextprotocol.io/docs/learn/architecture) — Protocol-level concepts: participants, layers, primitives
|
||||
- [Migration guide](./migration.md) — Upgrading from previous SDK versions
|
||||
- [Migration guide](./migration/index.md) — Upgrading from previous SDK versions
|
||||
- [FAQ](./faq.md) — Frequently asked questions and troubleshooting
|
||||
|
||||
### Additional examples
|
||||
|
||||
| Feature | Description | Example |
|
||||
|---------|-------------|---------|
|
||||
| Parallel tool calls | Run multiple tool calls concurrently via `Promise.all` | [`parallelToolCallsClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/parallelToolCallsClient.ts) |
|
||||
| SSE disconnect / reconnection | Server-initiated SSE disconnect with automatic reconnection and event replay | [`ssePollingClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/ssePollingClient.ts) |
|
||||
| Multiple clients | Independent client lifecycles to the same server | [`multipleClientsParallel.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/multipleClientsParallel.ts) |
|
||||
| URL elicitation | Handle sensitive data collection via browser | [`elicitationUrlExample.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/elicitationUrlExample.ts) |
|
||||
| Feature | Description | Example |
|
||||
| ----------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Parallel tool calls | Run multiple tool calls concurrently via `Promise.all` | [`parallel-calls/client.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/parallel-calls/client.ts) |
|
||||
| SSE disconnect / reconnection | Server-initiated SSE disconnect with automatic reconnection and event replay | [`sse-polling/client.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/sse-polling/client.ts) |
|
||||
| Multiple clients | Independent client lifecycles to the same server | [`parallel-calls/client.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/parallel-calls/client.ts) |
|
||||
| URL elicitation | Handle sensitive data collection via browser | [`elicitation/client.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/elicitation/client.ts) |
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ For production use, you can either:
|
|||
|
||||
### Where can I find runnable server examples?
|
||||
|
||||
The [server quickstart](./server-quickstart.md) walks you through building a weather server from scratch. Its complete source lives in [`examples/server-quickstart/`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/server-quickstart/). For more advanced examples (OAuth, streaming, sessions, etc.), see the server examples index in [`examples/server/README.md`](../examples/server/README.md).
|
||||
The [server quickstart](./server-quickstart.md) walks you through building a weather server from scratch. Its complete source lives in [`examples/server-quickstart/`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/server-quickstart/). For more advanced examples (OAuth, streaming, sessions, etc.), see the server examples index in [`examples/README.md`](../examples/README.md).
|
||||
|
||||
### Where are the server auth helpers?
|
||||
|
||||
|
|
|
|||
|
|
@ -1,571 +0,0 @@
|
|||
---
|
||||
name: migrate-v1-to-v2
|
||||
description: Migrate MCP TypeScript SDK code from v1 (@modelcontextprotocol/sdk) to v2 (@modelcontextprotocol/client, /server, /core). 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.
|
||||
|
||||
## 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()`.
|
||||
|
||||
## 2. Dependencies
|
||||
|
||||
Remove the old package and install only what you need:
|
||||
|
||||
```bash
|
||||
npm uninstall @modelcontextprotocol/sdk
|
||||
```
|
||||
|
||||
| You need | Install |
|
||||
| --------------------------- | ------------------------------------------------------------------------ |
|
||||
| Client only | `npm install @modelcontextprotocol/client` |
|
||||
| Server only | `npm install @modelcontextprotocol/server` |
|
||||
| Server + Node.js HTTP | `npm install @modelcontextprotocol/server @modelcontextprotocol/node` |
|
||||
| Server + Express | `npm install @modelcontextprotocol/server @modelcontextprotocol/express` |
|
||||
| Server + Hono | `npm install @modelcontextprotocol/server @modelcontextprotocol/hono` |
|
||||
| Raw Zod `*Schema` constants | `npm install @modelcontextprotocol/core` |
|
||||
|
||||
`@modelcontextprotocol/client` and `@modelcontextprotocol/server` are self-contained — the shared types, protocol layer, and transports are bundled in and re-exported, so there is no separate runtime package to install for them. Install `@modelcontextprotocol/core` only if you
|
||||
import the raw Zod `*Schema` constants directly (see §11).
|
||||
|
||||
## 3. Import Mapping
|
||||
|
||||
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/stdio` |
|
||||
| `@modelcontextprotocol/sdk/client/websocket.js` | REMOVED (use Streamable HTTP or stdio; implement `Transport` for custom needs) |
|
||||
|
||||
### Server imports
|
||||
|
||||
| v1 import path | v2 package |
|
||||
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `@modelcontextprotocol/sdk/server/mcp.js` | `@modelcontextprotocol/server` |
|
||||
| `@modelcontextprotocol/sdk/server/index.js` | `@modelcontextprotocol/server` |
|
||||
| `@modelcontextprotocol/sdk/server/stdio.js` | `@modelcontextprotocol/server/stdio` |
|
||||
| `@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); legacy bridge: `@modelcontextprotocol/server-legacy/sse` |
|
||||
| `@modelcontextprotocol/sdk/server/auth/*` | RS helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`, `OAuthTokenVerifier`) → `@modelcontextprotocol/express`; AS helpers (`mcpAuthRouter`, `OAuthServerProvider`, etc.) → `@modelcontextprotocol/server-legacy/auth` (deprecated); migrate AS to an external IdP/OAuth library |
|
||||
| `@modelcontextprotocol/sdk/server/middleware.js` | `@modelcontextprotocol/express` (signature changed, see section 8) |
|
||||
|
||||
### Types / shared imports
|
||||
|
||||
| v1 import path | v2 package |
|
||||
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `@modelcontextprotocol/sdk/types.js` | Types / error classes / enums / guards → `@modelcontextprotocol/client` or `@modelcontextprotocol/server`; Zod `*Schema` constants → `@modelcontextprotocol/core` |
|
||||
| `@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` | Types / classes → `@modelcontextprotocol/client` or `@modelcontextprotocol/server`; OAuth/OpenID Zod `*Schema` constants (e.g. `OAuthTokensSchema`, `OAuthMetadataSchema`) → `@modelcontextprotocol/core` |
|
||||
| `@modelcontextprotocol/sdk/shared/stdio.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` (`ReadBuffer`, `serializeMessage`, `deserializeMessage` are in the root barrel; the `./stdio` subpath only has the transport class) |
|
||||
|
||||
Notes:
|
||||
|
||||
- `@modelcontextprotocol/client` and `@modelcontextprotocol/server` both re-export shared types from `@modelcontextprotocol/core-internal`, so import from whichever package you already depend on. Do not import from `@modelcontextprotocol/core-internal` 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
|
||||
|
||||
| v1 symbol | v2 symbol | v2 package |
|
||||
| ------------------------------- | ----------------------------------- | ---------------------------- |
|
||||
| `StreamableHTTPServerTransport` | `NodeStreamableHTTPServerTransport` | `@modelcontextprotocol/node` |
|
||||
|
||||
## 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) |
|
||||
| `JSONRPCResponseSchema` (result-only in v1) | `JSONRPCResultResponseSchema` (from `@modelcontextprotocol/core`; **not** v2's new `JSONRPCResponseSchema`, a `z.union` that also accepts error responses) |
|
||||
| `JSONRPCResponse` (type, result-only in v1) | `JSONRPCResultResponse` (**not** v2's new `JSONRPCResponse` type, which is the result\|error union) |
|
||||
| `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 `SdkHttpError` with `SdkErrorCode.ClientHttp*`) |
|
||||
| `WebSocketClientTransport` | REMOVED (use `StreamableHTTPClientTransport` or `StdioClientTransport`) |
|
||||
|
||||
All other **type** symbols from `@modelcontextprotocol/sdk/types.js` retain their original names — import them from `@modelcontextprotocol/client` or `@modelcontextprotocol/server`. The **Zod schemas** (e.g., `CallToolResultSchema`, `ListToolsResultSchema`) move to
|
||||
`@modelcontextprotocol/core`; `<Name>Schema.parse(value)` / `.safeParse(value)` keep working unchanged (the codemod rewrites the import path). To validate **without** depending on Zod, use `isSpecType.TypeName(value)` (e.g., `isSpecType.CallToolResult(v)`) or
|
||||
`specTypeSchemas.TypeName` (a `StandardSchemaV1Sync` validator) from `@modelcontextprotocol/client` / `@modelcontextprotocol/server`; the keys are typed as `SpecTypeName`, a literal union of all spec type names.
|
||||
|
||||
### Error class changes
|
||||
|
||||
Three error classes now exist:
|
||||
|
||||
- **`ProtocolError`** (renamed from `McpError`): Protocol errors that cross the wire as JSON-RPC responses
|
||||
- **`SdkError`** (new): Local SDK errors that never cross the wire
|
||||
- **`SdkHttpError`** (extends `SdkError`): HTTP transport errors with typed `.status` and `.statusText` accessors
|
||||
|
||||
| Error scenario | v1 type | v2 type |
|
||||
| --------------------------------- | -------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| Request timeout | `McpError` with `ErrorCode.RequestTimeout` | `SdkError` with `SdkErrorCode.RequestTimeout` |
|
||||
| Connection closed | `McpError` with `ErrorCode.ConnectionClosed` | `SdkError` with `SdkErrorCode.ConnectionClosed` |
|
||||
| Capability not supported | `new Error(...)` | `SdkError` with `SdkErrorCode.CapabilityNotSupported` |
|
||||
| Not connected | `new Error('Not connected')` | `SdkError` with `SdkErrorCode.NotConnected` |
|
||||
| Invalid params (server response) | `McpError` with `ErrorCode.InvalidParams` | `ProtocolError` with `ProtocolErrorCode.InvalidParams` |
|
||||
| HTTP transport error | `StreamableHTTPError` | `SdkHttpError` with `SdkErrorCode.ClientHttp*` |
|
||||
| Failed to open SSE stream | `StreamableHTTPError` | `SdkHttpError` with `SdkErrorCode.ClientHttpFailedToOpenStream` |
|
||||
| 401 after re-auth (circuit break) | `StreamableHTTPError` | `SdkHttpError` with `SdkErrorCode.ClientHttpAuthentication` |
|
||||
| 403 after upscoping | `StreamableHTTPError` | `SdkHttpError` with `SdkErrorCode.ClientHttpForbidden` |
|
||||
| Unexpected content type | `StreamableHTTPError` | `SdkError` with `SdkErrorCode.ClientHttpUnexpectedContent` |
|
||||
| Session termination failed | `StreamableHTTPError` | `SdkHttpError` with `SdkErrorCode.ClientHttpFailedToTerminateSession` |
|
||||
| Response result fails schema | `ZodError` (raw) | `SdkError` with `SdkErrorCode.InvalidResult` |
|
||||
|
||||
New `SdkErrorCode` enum values:
|
||||
|
||||
- `SdkErrorCode.NotConnected` = `'NOT_CONNECTED'`
|
||||
- `SdkErrorCode.AlreadyConnected` = `'ALREADY_CONNECTED'`
|
||||
- `SdkErrorCode.NotInitialized` = `'NOT_INITIALIZED'`
|
||||
- `SdkErrorCode.CapabilityNotSupported` = `'CAPABILITY_NOT_SUPPORTED'`
|
||||
- `SdkErrorCode.RequestTimeout` = `'REQUEST_TIMEOUT'`
|
||||
- `SdkErrorCode.ConnectionClosed` = `'CONNECTION_CLOSED'`
|
||||
- `SdkErrorCode.SendFailed` = `'SEND_FAILED'`
|
||||
- `SdkErrorCode.InvalidResult` = `'INVALID_RESULT'`
|
||||
- `SdkErrorCode.ClientHttpNotImplemented` = `'CLIENT_HTTP_NOT_IMPLEMENTED'`
|
||||
- `SdkErrorCode.ClientHttpAuthentication` = `'CLIENT_HTTP_AUTHENTICATION'`
|
||||
- `SdkErrorCode.ClientHttpForbidden` = `'CLIENT_HTTP_FORBIDDEN'`
|
||||
- `SdkErrorCode.ClientHttpUnexpectedContent` = `'CLIENT_HTTP_UNEXPECTED_CONTENT'`
|
||||
- `SdkErrorCode.ClientHttpFailedToOpenStream` = `'CLIENT_HTTP_FAILED_TO_OPEN_STREAM'`
|
||||
- `SdkErrorCode.ClientHttpFailedToTerminateSession` = `'CLIENT_HTTP_FAILED_TO_TERMINATE_SESSION'`
|
||||
|
||||
Update error handling:
|
||||
|
||||
```typescript
|
||||
// v1
|
||||
if (error instanceof McpError && error.code === ErrorCode.RequestTimeout) { ... }
|
||||
|
||||
// v2
|
||||
import { SdkError, SdkErrorCode } from '@modelcontextprotocol/client';
|
||||
if (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) { ... }
|
||||
```
|
||||
|
||||
Update HTTP transport error handling:
|
||||
|
||||
```typescript
|
||||
// v1
|
||||
import { StreamableHTTPError } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||
if (error instanceof StreamableHTTPError) {
|
||||
console.log('HTTP status:', error.code);
|
||||
}
|
||||
|
||||
// v2
|
||||
import { SdkHttpError, SdkErrorCode } from '@modelcontextprotocol/client';
|
||||
if (error instanceof SdkHttpError) {
|
||||
console.log('HTTP status:', error.status); // number — typed accessor
|
||||
console.log('Status text:', error.statusText); // string | undefined
|
||||
switch (error.code) {
|
||||
case SdkErrorCode.ClientHttpAuthentication: // 401 after re-auth
|
||||
case SdkErrorCode.ClientHttpForbidden: // 403 after upscoping
|
||||
case SdkErrorCode.ClientHttpFailedToOpenStream:
|
||||
case SdkErrorCode.ClientHttpNotImplemented:
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### OAuth error consolidation
|
||||
|
||||
Individual OAuth error classes replaced with single `OAuthError` class and `OAuthErrorCode` enum:
|
||||
|
||||
| v1 Class | v2 Equivalent |
|
||||
| ------------------------------ | ---------------------------------------------------------- |
|
||||
| `InvalidRequestError` | `OAuthError` with `OAuthErrorCode.InvalidRequest` |
|
||||
| `InvalidClientError` | `OAuthError` with `OAuthErrorCode.InvalidClient` |
|
||||
| `InvalidGrantError` | `OAuthError` with `OAuthErrorCode.InvalidGrant` |
|
||||
| `UnauthorizedClientError` | `OAuthError` with `OAuthErrorCode.UnauthorizedClient` |
|
||||
| `UnsupportedGrantTypeError` | `OAuthError` with `OAuthErrorCode.UnsupportedGrantType` |
|
||||
| `InvalidScopeError` | `OAuthError` with `OAuthErrorCode.InvalidScope` |
|
||||
| `AccessDeniedError` | `OAuthError` with `OAuthErrorCode.AccessDenied` |
|
||||
| `ServerError` | `OAuthError` with `OAuthErrorCode.ServerError` |
|
||||
| `TemporarilyUnavailableError` | `OAuthError` with `OAuthErrorCode.TemporarilyUnavailable` |
|
||||
| `UnsupportedResponseTypeError` | `OAuthError` with `OAuthErrorCode.UnsupportedResponseType` |
|
||||
| `UnsupportedTokenTypeError` | `OAuthError` with `OAuthErrorCode.UnsupportedTokenType` |
|
||||
| `InvalidTokenError` | `OAuthError` with `OAuthErrorCode.InvalidToken` |
|
||||
| `MethodNotAllowedError` | `OAuthError` with `OAuthErrorCode.MethodNotAllowed` |
|
||||
| `TooManyRequestsError` | `OAuthError` with `OAuthErrorCode.TooManyRequests` |
|
||||
| `InvalidClientMetadataError` | `OAuthError` with `OAuthErrorCode.InvalidClientMetadata` |
|
||||
| `InsufficientScopeError` | `OAuthError` with `OAuthErrorCode.InsufficientScope` |
|
||||
| `InvalidTargetError` | `OAuthError` with `OAuthErrorCode.InvalidTarget` |
|
||||
| `CustomOAuthError` | `new OAuthError(customCode, message)` |
|
||||
|
||||
Removed: `OAUTH_ERRORS` constant.
|
||||
|
||||
Update OAuth error handling:
|
||||
|
||||
```typescript
|
||||
// v1
|
||||
import { InvalidClientError, InvalidGrantError } from '@modelcontextprotocol/client';
|
||||
if (error instanceof InvalidClientError) { ... }
|
||||
|
||||
// v2
|
||||
import { OAuthError, OAuthErrorCode } from '@modelcontextprotocol/client';
|
||||
if (error instanceof OAuthError && error.code === OAuthErrorCode.InvalidClient) { ... }
|
||||
```
|
||||
|
||||
**Unchanged APIs** (only import paths changed): `Client` constructor and most methods, `McpServer` constructor, `server.connect()`, `server.close()`, all client transports (`StreamableHTTPClientTransport`, `SSEClientTransport`, `StdioClientTransport`), `StdioServerTransport`, all
|
||||
Zod schemas, all callback return types. Note: `callTool()` and `request()` signatures changed (schema parameter removed, see section 11).
|
||||
|
||||
## 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`.
|
||||
|
||||
### Tools
|
||||
|
||||
```typescript
|
||||
// v1: server.tool(name, schema, callback) - raw shape worked
|
||||
server.tool('greet', { name: z.string() }, async ({ name }) => {
|
||||
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
|
||||
});
|
||||
|
||||
// v1: server.tool(name, description, schema, callback)
|
||||
server.tool('greet', 'Greet a user', { name: z.string() }, async ({ name }) => {
|
||||
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
|
||||
});
|
||||
|
||||
// v2: server.registerTool(name, config, callback)
|
||||
server.registerTool(
|
||||
'greet',
|
||||
{
|
||||
description: 'Greet a user',
|
||||
inputSchema: z.object({ name: z.string() })
|
||||
},
|
||||
async ({ name }) => {
|
||||
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Config object fields: `title?`, `description?`, `inputSchema?`, `outputSchema?`, `annotations?`, `_meta?`
|
||||
|
||||
### Prompts
|
||||
|
||||
```typescript
|
||||
// v1: server.prompt(name, schema, callback) - raw shape worked
|
||||
server.prompt('summarize', { text: z.string() }, async ({ text }) => {
|
||||
return { messages: [{ role: 'user', content: { type: 'text', text } }] };
|
||||
});
|
||||
|
||||
// v2: server.registerPrompt(name, config, callback)
|
||||
server.registerPrompt(
|
||||
'summarize',
|
||||
{
|
||||
argsSchema: z.object({ text: z.string() })
|
||||
},
|
||||
async ({ text }) => {
|
||||
return { messages: [{ role: 'user', content: { type: 'text', text } }] };
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Config object fields: `title?`, `description?`, `argsSchema?`
|
||||
|
||||
### Resources
|
||||
|
||||
```typescript
|
||||
// v1: server.resource(name, uri, callback)
|
||||
server.resource('config', 'config://app', async uri => {
|
||||
return { contents: [{ uri: uri.href, text: '{}' }] };
|
||||
});
|
||||
|
||||
// v2: server.registerResource(name, uri, metadata, callback)
|
||||
server.registerResource('config', 'config://app', {}, async uri => {
|
||||
return { contents: [{ uri: uri.href, text: '{}' }] };
|
||||
});
|
||||
```
|
||||
|
||||
Note: the third argument (`metadata`) is required — pass `{}` if no metadata.
|
||||
|
||||
### Schema Migration Quick Reference
|
||||
|
||||
| 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-internal` | Replacement |
|
||||
| ------------------------------------------------------------------------------------ | ----------------------------------------- |
|
||||
| `schemaToJson(schema)` | `standardSchemaToJsonSchema(schema)` |
|
||||
| `parseSchemaAsync(schema, data)` | `validateStandardSchema(schema, data)` |
|
||||
| `SchemaInput<T>` | `StandardSchemaWithJSON.InferInput<T>` |
|
||||
| `getSchemaShape`, `getSchemaDescription`, `isOptionalSchema`, `unwrapOptionalSchema` | none (internal Zod introspection helpers) |
|
||||
|
||||
## 7. Headers API
|
||||
|
||||
Transport constructors now use the Web Standard `Headers` object instead of plain objects. The custom `RequestInfo` type has been replaced with the standard Web `Request` object, giving access to headers, URL, query parameters, and method.
|
||||
|
||||
```typescript
|
||||
// v1: plain object, bracket access, custom RequestInfo
|
||||
headers: { 'Authorization': 'Bearer token' }
|
||||
extra.requestInfo?.headers['mcp-session-id']
|
||||
|
||||
// v2: Headers object, .get() access, standard Web Request
|
||||
headers: new Headers({ 'Authorization': 'Bearer token' })
|
||||
ctx.http?.req?.headers.get('mcp-session-id')
|
||||
new URL(ctx.http?.req?.url).searchParams.get('debug')
|
||||
```
|
||||
|
||||
## 8. Removed Server Features
|
||||
|
||||
### SSE server transport
|
||||
|
||||
`SSEServerTransport` removed entirely. Migrate to `NodeStreamableHTTPServerTransport` (from `@modelcontextprotocol/node`). Client-side `SSEClientTransport` still available for connecting to legacy servers. Legacy bridge:
|
||||
`import { SSEServerTransport } from '@modelcontextprotocol/server-legacy/sse'` (deprecated, frozen v1 copy).
|
||||
|
||||
### Server-side auth
|
||||
|
||||
Resource Server helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`, `getOAuthProtectedResourceMetadataUrl`, `OAuthTokenVerifier`) are first-class in `@modelcontextprotocol/express`. Authorization Server helpers (`mcpAuthRouter`, `OAuthServerProvider`,
|
||||
`ProxyOAuthServerProvider`, `authenticateClient`, `allowedMethods`, etc.) are available from `@modelcontextprotocol/server-legacy/auth` (deprecated, frozen v1 copy). Migrate AS to an external IdP/OAuth library for production use. See `examples/server/src/` for demos.
|
||||
|
||||
### Host header validation (Express)
|
||||
|
||||
`hostHeaderValidation()` and `localhostHostValidation()` moved from server package to `@modelcontextprotocol/express`. Signature changed: takes `string[]` instead of options object.
|
||||
|
||||
```typescript
|
||||
// v1
|
||||
import { hostHeaderValidation } from '@modelcontextprotocol/sdk/server/middleware.js';
|
||||
app.use(hostHeaderValidation({ allowedHosts: ['example.com'] }));
|
||||
|
||||
// v2
|
||||
import { hostHeaderValidation } from '@modelcontextprotocol/express';
|
||||
app.use(hostHeaderValidation(['example.com']));
|
||||
```
|
||||
|
||||
The server package now exports framework-agnostic alternatives: `validateHostHeader()`, `localhostAllowedHostnames()`, `hostHeaderValidationResponse()`.
|
||||
|
||||
## 9. `setRequestHandler` / `setNotificationHandler` API
|
||||
|
||||
The low-level handler registration methods now take a method string instead of a Zod schema.
|
||||
|
||||
```typescript
|
||||
// v1: schema-based
|
||||
server.setRequestHandler(InitializeRequestSchema, async (request) => { ... });
|
||||
server.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => { ... });
|
||||
|
||||
// v2: method string
|
||||
server.setRequestHandler('initialize', async (request) => { ... });
|
||||
server.setNotificationHandler('notifications/message', (notification) => { ... });
|
||||
```
|
||||
|
||||
For custom (non-spec) methods, use the 3-arg form `(method, schemas, handler)`:
|
||||
|
||||
```typescript
|
||||
// v1: Zod schema with method literal
|
||||
server.setRequestHandler(z.object({ method: z.literal('acme/search'), params: P }), async req => { ... });
|
||||
|
||||
// v2: method string + schemas object; handler receives parsed params
|
||||
server.setRequestHandler('acme/search', { params: P, result: R }, async (params, ctx) => { ... });
|
||||
client.setNotificationHandler('acme/progress', { params: P }, (params, notification) => { ... });
|
||||
```
|
||||
|
||||
The 3-arg notification handler receives the raw notification as its second argument, so `_meta` is recoverable via `notification.params?._meta`.
|
||||
|
||||
To send a custom-method request, pass a result schema as the second argument to `request()` (and `ctx.mcpReq.send()`):
|
||||
|
||||
```typescript
|
||||
// v1
|
||||
await client.request({ method: 'acme/search', params }, ResultSchema);
|
||||
// v2 (unchanged; now any Standard Schema, not Zod-only)
|
||||
await client.request({ method: 'acme/search', params }, ResultSchema);
|
||||
```
|
||||
|
||||
Schema to method string mapping:
|
||||
|
||||
| v1 Schema | v2 Method String |
|
||||
| --------------------------------------- | ---------------------------------------- |
|
||||
| `InitializeRequestSchema` | `'initialize'` |
|
||||
| `CallToolRequestSchema` | `'tools/call'` |
|
||||
| `ListToolsRequestSchema` | `'tools/list'` |
|
||||
| `ListPromptsRequestSchema` | `'prompts/list'` |
|
||||
| `GetPromptRequestSchema` | `'prompts/get'` |
|
||||
| `ListResourcesRequestSchema` | `'resources/list'` |
|
||||
| `ReadResourceRequestSchema` | `'resources/read'` |
|
||||
| `CreateMessageRequestSchema` | `'sampling/createMessage'` |
|
||||
| `ElicitRequestSchema` | `'elicitation/create'` |
|
||||
| `SetLevelRequestSchema` | `'logging/setLevel'` |
|
||||
| `PingRequestSchema` | `'ping'` |
|
||||
| `LoggingMessageNotificationSchema` | `'notifications/message'` |
|
||||
| `ToolListChangedNotificationSchema` | `'notifications/tools/list_changed'` |
|
||||
| `ResourceListChangedNotificationSchema` | `'notifications/resources/list_changed'` |
|
||||
| `PromptListChangedNotificationSchema` | `'notifications/prompts/list_changed'` |
|
||||
| `ProgressNotificationSchema` | `'notifications/progress'` |
|
||||
| `CancelledNotificationSchema` | `'notifications/cancelled'` |
|
||||
| `InitializedNotificationSchema` | `'notifications/initialized'` |
|
||||
|
||||
Request/notification params remain fully typed. Remove unused schema imports after migration.
|
||||
|
||||
## 10. Request Handler Context Types
|
||||
|
||||
`RequestHandlerExtra` → structured context types with nested groups. Rename `extra` → `ctx` in all handler callbacks.
|
||||
|
||||
| v1 | v2 |
|
||||
| ------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `RequestHandlerExtra` | `ServerContext` (server) / `ClientContext` (client) / `BaseContext` (base) |
|
||||
| `extra` (param name) | `ctx` |
|
||||
| `extra.signal` | `ctx.mcpReq.signal` |
|
||||
| `extra.requestId` | `ctx.mcpReq.id` |
|
||||
| `extra._meta` | `ctx.mcpReq._meta` |
|
||||
| `extra.sendRequest(...)` | `ctx.mcpReq.send(...)` |
|
||||
| `extra.sendNotification(...)` | `ctx.mcpReq.notify(...)` |
|
||||
| `extra.authInfo` | `ctx.http?.authInfo` |
|
||||
| `extra.sessionId` | `ctx.sessionId` |
|
||||
| `extra.requestInfo` | `ctx.http?.req` (standard Web `Request`, only `ServerContext`) |
|
||||
| `extra.closeSSEStream` | `ctx.http?.closeSSE` (only `ServerContext`) |
|
||||
| `extra.closeStandaloneSSEStream` | `ctx.http?.closeStandaloneSSE` (only `ServerContext`) |
|
||||
| `extra.taskStore` / `taskId` / `taskRequestedTtl` | _removed; see §12_ |
|
||||
|
||||
`ServerContext` convenience methods (new in v2, no v1 equivalent):
|
||||
|
||||
| Method | Description | Replaces |
|
||||
| ---------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------- |
|
||||
| `ctx.mcpReq.log(level, data, logger?)` | Send log notification (respects client's level filter) | `server.sendLoggingMessage(...)` from within handler |
|
||||
| `ctx.mcpReq.elicitInput(params, options?)` | Elicit user input (form or URL) | `server.elicitInput(...)` from within handler |
|
||||
| `ctx.mcpReq.requestSampling(params, options?)` | Request LLM sampling from client | `server.createMessage(...)` from within handler |
|
||||
|
||||
## 11. Schema parameter removed from `request()`, `send()`, and `callTool()` (spec methods)
|
||||
|
||||
For **spec** methods, `Protocol.request()`, `BaseContext.mcpReq.send()`, and `Client.callTool()` no longer require a Zod result schema argument. The SDK resolves the schema internally from the method name.
|
||||
|
||||
```typescript
|
||||
// v1: schema required
|
||||
import { CallToolResultSchema, ElicitResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||
const result = await client.request({ method: 'tools/call', params: { ... } }, CallToolResultSchema);
|
||||
const elicit = await ctx.mcpReq.send({ method: 'elicitation/create', params: { ... } }, ElicitResultSchema);
|
||||
const tool = await client.callTool({ name: 'my-tool', arguments: {} }, CompatibilityCallToolResultSchema);
|
||||
|
||||
// v2: no schema argument
|
||||
const result = await client.request({ method: 'tools/call', params: { ... } });
|
||||
const elicit = await ctx.mcpReq.send({ method: 'elicitation/create', params: { ... } });
|
||||
const tool = await client.callTool({ name: 'my-tool', arguments: {} });
|
||||
```
|
||||
|
||||
| v1 call | v2 call |
|
||||
| ------------------------------------------------------------ | ---------------------------------- |
|
||||
| `client.request(req, ResultSchema)` | `client.request(req)` |
|
||||
| `client.request(req, ResultSchema, options)` | `client.request(req, options)` |
|
||||
| `ctx.mcpReq.send(req, ResultSchema)` | `ctx.mcpReq.send(req)` |
|
||||
| `ctx.mcpReq.send(req, ResultSchema, options)` | `ctx.mcpReq.send(req, options)` |
|
||||
| `client.callTool(params, CompatibilityCallToolResultSchema)` | `client.callTool(params)` |
|
||||
| `client.callTool(params, schema, options)` | `client.callTool(params, options)` |
|
||||
|
||||
For **custom (non-spec)** methods, keep the result-schema argument — see §9. Only apply the rewrites above when `req.method` is a spec method.
|
||||
|
||||
Remove unused schema imports: `CallToolResultSchema`, `CompatibilityCallToolResultSchema`, `ElicitResultSchema`, `CreateMessageResultSchema`, etc., when they were only used in `request()`/`send()`/`callTool()` calls.
|
||||
|
||||
If a `*Schema` constant was used for **runtime validation** (not just as a `request()` argument), replace with `isSpecType` / `specTypeSchemas`:
|
||||
|
||||
| v1 pattern | v2 replacement |
|
||||
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `CallToolResultSchema.safeParse(value).success` | `isSpecType.CallToolResult(value)` |
|
||||
| `<TypeName>Schema.safeParse(value).success` | `isSpecType.<TypeName>(value)` |
|
||||
| `<TypeName>Schema.parse(value)` | `specTypeSchemas.<TypeName>['~standard'].validate(value)` (returns a `Result` synchronously, not the value) |
|
||||
| Passing `<TypeName>Schema` as a validator argument | `specTypeSchemas.<TypeName>` (a `StandardSchemaV1Sync<In, Out>`) |
|
||||
|
||||
`isCallToolResult(value)` still works, but `isSpecType` covers every spec type by name.
|
||||
|
||||
## 12. Experimental tasks interception removed
|
||||
|
||||
The 2025-11 task side-channel through `Protocol` is removed (was always `@experimental`). No mechanical migration; remove usages.
|
||||
|
||||
| Removed | Notes |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
|
||||
| `ProtocolOptions.tasks` | drop the option |
|
||||
| `protocol.taskManager` | gone |
|
||||
| `RequestOptions.task` / `.relatedTask`, `NotificationOptions.relatedTask` | drop the option |
|
||||
| `BaseContext.task` (`ctx.task?.*`) | gone |
|
||||
| `assertTaskCapability` / `assertTaskHandlerCapability` overrides | delete the override |
|
||||
| `*.experimental.tasks.*` accessors, `Experimental{Client,Server,McpServer}Tasks` | removed |
|
||||
| `requestStream` / `callToolStream` / `createMessageStream` / `elicitInputStream` | removed; no streaming variant |
|
||||
| `registerToolTask`, `ToolTaskHandler`, `TaskRequestHandler`, `CreateTaskRequestHandler` | removed |
|
||||
| `TaskMessageQueue`, `InMemoryTaskMessageQueue`, `BaseQueuedMessage`, `Queued*`, `CreateTaskServerContext`, `TaskServerContext`, `TaskToolExecution` | removed |
|
||||
| `ResponseMessage`, `BaseResponseMessage`, `ErrorMessage`, `AsyncGeneratorValue`, `TaskStatusMessage`, `TaskCreatedMessage`, `ResultMessage`, `takeResult`, `toArrayAsync` | removed |
|
||||
|
||||
`TaskStore` / `InMemoryTaskStore` / `CreateTaskOptions` / `isTerminal` (storage layer) are also removed; they will return with the SEP-2663 server-directed plugin.
|
||||
|
||||
NOT removed (wire surface, kept for 2025-11-25 interop): task Zod schemas + inferred types (`Task`, `TaskStatus`, `TaskMetadata`, `RelatedTaskMetadata`, `CreateTaskResult`, `GetTask*`, `ListTasks*`, `CancelTask*`, `TaskStatusNotification*`, `TaskAugmentedRequestParams`), task
|
||||
members of the request/result/notification unions, the `tasks` capability key, `isTaskAugmentedRequestParams`, `RELATED_TASK_META_KEY`. Inbound `tasks/*` requests → `-32601`.
|
||||
|
||||
## 13. Behavioral Changes
|
||||
|
||||
### Client
|
||||
|
||||
`Client.listPrompts()`, `listResources()`, `listResourceTemplates()`, `listTools()` now return empty results when the server lacks the corresponding capability (instead of sending the request). Set `enforceStrictCapabilities: true` in `ClientOptions` to throw an error instead.
|
||||
|
||||
### Server (Streamable HTTP transport)
|
||||
|
||||
No code changes required; these are wire-behavior notes:
|
||||
|
||||
- Resumability behavior (SSE priming events, `closeSSEStream` / `closeStandaloneSSEStream` callbacks) is only enabled for protocol versions in the transport's supported-versions list that are `>= 2025-11-25`. Unknown future version strings in an `initialize` request body no
|
||||
longer enable it. Behavior for all currently supported protocol versions is unchanged.
|
||||
- Session-ID mismatch still responds `404 Not Found` with JSON-RPC error code `-32001` (`Session not found`), unchanged from v1. This `-32001` usage is an SDK convention, not a spec-assigned code, and may be re-derived as 2026 protocol revision error handling is adopted —
|
||||
migrated client code should key off the HTTP `404` status, not the `-32001` code.
|
||||
|
||||
## 14. Runtime-Specific JSON Schema Validators (Enhancement)
|
||||
|
||||
The SDK now auto-selects the appropriate JSON Schema validator based on runtime:
|
||||
|
||||
- Node.js → AJV (no change from v1)
|
||||
- Cloudflare Workers (workerd) → `@cfworker/json-schema` (previously required manual config)
|
||||
|
||||
**No action required** for most users. Cloudflare Workers users can remove explicit `jsonSchemaValidator` configuration:
|
||||
|
||||
```typescript
|
||||
// v1 (Cloudflare Workers): Required explicit validator
|
||||
new McpServer(
|
||||
{ name: 'server', version: '1.0.0' },
|
||||
{
|
||||
jsonSchemaValidator: new CfWorkerJsonSchemaValidator()
|
||||
}
|
||||
);
|
||||
|
||||
// v2 (Cloudflare Workers): Auto-selected, explicit config optional
|
||||
new McpServer({ name: 'server', version: '1.0.0' }, {});
|
||||
```
|
||||
|
||||
Validator behavior:
|
||||
|
||||
- Do not add validator imports for normal migrations.
|
||||
- Do not install `ajv`, `ajv-formats`, or `@cfworker/json-schema` for the default path; client/server bundle the runtime-selected defaults and the root entry point does not pull either dep in.
|
||||
- To customize the built-in backend (e.g. register custom AJV formats, change `@cfworker/json-schema` draft), import the named class from the package subpath: `@modelcontextprotocol/{client,server}/validators/ajv` for `AjvJsonSchemaValidator`,
|
||||
`@modelcontextprotocol/{client,server}/validators/cf-worker` for `CfWorkerJsonSchemaValidator`. Importing from a subpath means the corresponding peer dep must be in your `package.json`.
|
||||
- To replace validation entirely, pass `jsonSchemaValidator: myCustomValidator` with your own implementation of the `jsonSchemaValidator` interface.
|
||||
|
||||
## 15. Migration Steps (apply in this order)
|
||||
|
||||
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
|
||||
4. Replace `.tool()` / `.prompt()` / `.resource()` calls with `registerTool` / `registerPrompt` / `registerResource` per section 6
|
||||
5. **Wrap all raw Zod shapes with `z.object()`**: Change `inputSchema: { name: z.string() }` → `inputSchema: z.object({ name: z.string() })`. Same for `outputSchema` in tools and `argsSchema` in prompts.
|
||||
6. Replace plain header objects with `new Headers({...})` and bracket access (`headers['x']`) with `.get()` calls per section 7
|
||||
7. If using `hostHeaderValidation` from server, update import and signature per section 8
|
||||
8. If using server SSE transport, migrate to Streamable HTTP
|
||||
9. If using server auth from the SDK: RS helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`, `OAuthTokenVerifier`) → `@modelcontextprotocol/express`; AS helpers → `@modelcontextprotocol/server-legacy/auth` (deprecated); migrate AS to external IdP/OAuth library
|
||||
10. If relying on `listTools()`/`listPrompts()`/etc. throwing on missing capabilities, set `enforceStrictCapabilities: true`
|
||||
11. Format the changed files with the project's formatter (`prettier --write`, `eslint --fix`, or `biome format --write`) — edits are not reformatted automatically, and the wrapped schemas (step 5) and rewritten `setRequestHandler` method strings (section 9) frequently need it to
|
||||
satisfy lint
|
||||
12. Verify: build with `tsc` / run tests
|
||||
1043
docs/migration.md
1043
docs/migration.md
File diff suppressed because it is too large
Load diff
44
docs/migration/index.md
Normal file
44
docs/migration/index.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# MCP TypeScript SDK — Migration Guides
|
||||
|
||||
Pick the guide for your starting point.
|
||||
|
||||
## Upgrading from v1.x (`@modelcontextprotocol/sdk`)
|
||||
|
||||
→ **[upgrade-to-v2.md](./upgrade-to-v2.md)**
|
||||
|
||||
You are on the monolithic `@modelcontextprotocol/sdk` package and want to move to the
|
||||
v2 packages (`@modelcontextprotocol/client`, `@modelcontextprotocol/server`, …).
|
||||
|
||||
Start by running the codemod:
|
||||
|
||||
```bash
|
||||
npx @modelcontextprotocol/codemod@alpha v1-to-v2 ./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
|
||||
architectural and not codemod-automatable; see [support-2026-07-28.md](./support-2026-07-28.md).
|
||||
|
||||
## Already on v2, adopting protocol revision 2026-07-28
|
||||
|
||||
→ **[support-2026-07-28.md](./support-2026-07-28.md)**
|
||||
|
||||
You are already on the v2 packages and want your server or client to speak the
|
||||
2026-07-28 protocol revision (per-request `_meta` envelope, `createMcpHandler`,
|
||||
`serveStdio`, `versionNegotiation`, multi-round-trip requests, per-era wire codecs).
|
||||
|
||||
This guide also covers code written against an earlier **v2 alpha** that read
|
||||
wire-only members (`resultType`, envelope keys) directly.
|
||||
|
||||
## Using an LLM agent to migrate
|
||||
|
||||
[upgrade-to-v2.md](./upgrade-to-v2.md) is the agent skill — it carries skill
|
||||
frontmatter and is structured for mechanical application. Point the agent at
|
||||
the codemod first; the guide is the codemod's companion for what's left.
|
||||
|
||||
## See also
|
||||
|
||||
- [`@modelcontextprotocol/codemod` README](../../packages/codemod/README.md)
|
||||
- [FAQ](../faq.md)
|
||||
- [Examples](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples)
|
||||
443
docs/migration/support-2026-07-28.md
Normal file
443
docs/migration/support-2026-07-28.md
Normal file
|
|
@ -0,0 +1,443 @@
|
|||
# Supporting protocol revision 2026-07-28
|
||||
|
||||
This guide is for code **already on the v2 packages** that wants to speak the 2026-07-28
|
||||
protocol revision — and for code written against an earlier **v2 alpha** that read
|
||||
wire-only members directly. If you are on `@modelcontextprotocol/sdk` (v1.x), start with
|
||||
[upgrade-to-v2.md](./upgrade-to-v2.md) instead.
|
||||
|
||||
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
|
||||
below.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Serving the 2026-07-28 revision](#serving-the-2026-07-28-revision)
|
||||
- [Replacing per-session state: `requestState`](#replacing-per-session-state-requeststate)
|
||||
- [Auth on 2026-07-28](#auth-on-2026-07-28)
|
||||
- [Per-era wire codecs](#per-era-wire-codecs)
|
||||
- [Wire-only members hidden from public types](#wire-only-members-hidden-from-public-types)
|
||||
- [Multi-round-trip requests](#multi-round-trip-requests)
|
||||
- [`subscriptions/listen`](#subscriptionslisten)
|
||||
- [`Mcp-Param-*` and standard headers (SEP-2243)](#mcp-param--and-standard-headers-sep-2243)
|
||||
- [Cache fields and cache hints](#cache-fields-and-cache-hints)
|
||||
- [Tasks: deprecated wire vocabulary](#tasks-deprecated-wire-vocabulary)
|
||||
- [Appendix: 2025-era vs 2026-era behavior matrix](#appendix-2025-era-vs-2026-era-behavior-matrix)
|
||||
|
||||
---
|
||||
|
||||
## Serving the 2026-07-28 revision
|
||||
|
||||
These entry points are documented in full in the server and client guides; this section
|
||||
contextualizes them as the migration path.
|
||||
|
||||
### Client side: `versionNegotiation`
|
||||
|
||||
By default `Client.connect()` performs the same 2025 `initialize` handshake as v1.x,
|
||||
byte for byte. To negotiate the 2026-07-28 era, opt in via `ClientOptions.versionNegotiation` —
|
||||
see [client.md › Protocol version negotiation](../client.md#protocol-version-negotiation-2026-07-28-revision).
|
||||
|
||||
```typescript
|
||||
const client = new Client({ name: 'my-client', version: '1.0.0' }, { versionNegotiation: { mode: 'auto' } });
|
||||
await client.connect(transport);
|
||||
client.getProtocolEra(); // 'modern' | 'legacy'
|
||||
```
|
||||
|
||||
- **absent / `mode: 'legacy'`** (default) — today's behavior, no probe.
|
||||
- **`mode: 'auto'`** — probe with `server/discover`; fall back to the 2025 handshake on
|
||||
the same connection against a 2025-only server (one extra round trip).
|
||||
- **`mode: { pin: '2026-07-28' }`** — modern only; no fallback, `connect()` rejects with
|
||||
`SdkError(EraNegotiationFailed)` against a 2025-only server.
|
||||
|
||||
#### Probe policy
|
||||
|
||||
Failure semantics under `'auto'` are deliberately conservative but never silent about
|
||||
infrastructure problems. Anything the probe does not positively recognize as modern
|
||||
falls back to the legacy era — provided the supported-versions list still contains a
|
||||
2025-era revision; with a modern-only list `connect()` rejects with
|
||||
`SdkError(EraNegotiationFailed)` instead. A network outage rejects with a typed connect
|
||||
error. Probe timeouts are **transport-aware**: on **stdio** a server that does not
|
||||
answer within `timeoutMs` is treated as legacy and the client falls back to `initialize`
|
||||
on the same stream (some legacy servers never respond to unknown pre-`initialize`
|
||||
requests at all); on **HTTP** a probe timeout rejects with `SdkError(RequestTimeout)` —
|
||||
a dead HTTP server is never misreported as legacy. One browser-specific exception: an
|
||||
opaque CORS/preflight `TypeError` during the probe falls back to the legacy era, because
|
||||
deployed 2025 servers commonly have CORS allow-lists that predate the 2026 headers.
|
||||
|
||||
```typescript
|
||||
versionNegotiation: {
|
||||
mode: 'auto',
|
||||
probe: {
|
||||
timeoutMs: 10_000, // default: the standard request timeout
|
||||
maxRetries: 0 // default: no retries — governs timeout re-sends only
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`maxRetries` governs timeout re-sends only (the spec-mandated `-32022` corrective
|
||||
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
|
||||
probe entirely with `client.connect(transport, { prior: persistedDiscoverResult })`.
|
||||
|
||||
### Server over HTTP: `createMcpHandler`
|
||||
|
||||
`createMcpHandler(factory)` from `@modelcontextprotocol/server` is the v2 HTTP entry
|
||||
that serves 2026-07-28 per request — and, by default (`legacy: 'stateless'`), also
|
||||
serves 2025-era traffic per request through the established stateless idiom. One
|
||||
factory, one endpoint, both eras.
|
||||
|
||||
```typescript
|
||||
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
|
||||
|
||||
const handler = createMcpHandler(() => {
|
||||
const server = new McpServer({ name: 'my-server', version: '1.0.0' }, { capabilities: { tools: {} } });
|
||||
// register tools/resources/prompts once — the same factory backs both eras
|
||||
return server;
|
||||
});
|
||||
// Web-standard runtimes: export default handler;
|
||||
// Node frameworks: app.all('/mcp', toNodeHandler(handler)) from @modelcontextprotocol/node
|
||||
```
|
||||
|
||||
A v1 stateless `StreamableHTTPServerTransport` hosting (`sessionIdGenerator: undefined`,
|
||||
fresh transport per request) maps directly onto the default entry. An existing
|
||||
**sessionful** v1 Streamable HTTP setup keeps serving 2025 clients by routing it in
|
||||
front of a strict (`legacy: 'reject'`) entry with `isLegacyRequest(request)`:
|
||||
|
||||
```typescript
|
||||
const modern = createMcpHandler(factory, { legacy: 'reject' });
|
||||
export default {
|
||||
async fetch(request: Request) {
|
||||
if (await isLegacyRequest(request)) return myExistingLegacyHandler(request);
|
||||
return modern.fetch(request);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
`isLegacyRequest` returns `true` only for requests with no per-request `_meta` envelope
|
||||
claim; route `false` traffic to the modern handler (a malformed modern claim is `false`
|
||||
and answered `-32602` / `-32020` by the modern path). The handler is web-standards-only
|
||||
(`{ fetch, close, notify, bus }`); on Node frameworks wrap once with
|
||||
`toNodeHandler(handler, { onerror? })` from `@modelcontextprotocol/node`. The exported
|
||||
`legacyStatelessFallback(factory)` is the same stateless 2025 serving as a standalone
|
||||
fetch-shaped handler.
|
||||
|
||||
> **If you were on a v2 alpha:** `handler.node(req, res, body)` is gone — replace with
|
||||
> `toNodeHandler(handler)` and add the `@modelcontextprotocol/node` import.
|
||||
> `NodeIncomingMessageLike` / `NodeServerResponseLike` are now exported from
|
||||
> `@modelcontextprotocol/node`, not `@modelcontextprotocol/server`.
|
||||
|
||||
### Server over stdio / long-lived connections: `serveStdio`
|
||||
|
||||
A hand-constructed `Server`/`McpServer` connected directly to a `StdioServerTransport`
|
||||
serves only the 2025-era protocol — upgrading the SDK changes nothing about what it puts
|
||||
on the wire. Serving 2026-07-28 (or both eras) on stdio goes through the
|
||||
connection-pinned `serveStdio(() => buildServer())` entry from
|
||||
`@modelcontextprotocol/server/stdio`; the opening exchange selects the connection's era,
|
||||
and one factory instance is pinned per connection. See
|
||||
[server.md › Serving the 2026-07-28 draft revision on stdio](../server.md#serving-the-2026-07-28-draft-revision-on-stdio).
|
||||
|
||||
To migrate an existing stdio server, replace
|
||||
`await server.connect(new StdioServerTransport())` with
|
||||
`serveStdio(() => buildServer())`. Pass `{ legacy: 'reject' }` to refuse 2025-era
|
||||
openings. On 2026-pinned connections, `getClientCapabilities()` / `getClientVersion()`
|
||||
return `undefined` (no `initialize` ever runs there) and handlers read per-request
|
||||
identity from `ctx.mcpReq.envelope`; `getNegotiatedProtocolVersion()` reports the pinned
|
||||
revision.
|
||||
|
||||
A client whose connection negotiated a modern era drops inbound server→client JSON-RPC
|
||||
requests (the 2026 era has no such channel) instead of answering them; legacy-era
|
||||
connections are unchanged.
|
||||
|
||||
### Client cancellation on Streamable HTTP
|
||||
|
||||
On a 2026-07-28 Streamable HTTP connection, aborting an in-flight client request
|
||||
(`signal` / timeout) closes that request's SSE response stream — the spec cancellation
|
||||
signal — instead of POSTing `notifications/cancelled`. Nothing to change in calling
|
||||
code. 2025-era connections and stdio at any era still send `notifications/cancelled`.
|
||||
Custom `Transport` implementations that open one underlying request per outbound message
|
||||
and honor `TransportSendOptions.requestSignal` may opt in by declaring
|
||||
`readonly hasPerRequestStream = true`.
|
||||
|
||||
### `ctx.mcpReq.log()` and the per-request `logLevel`
|
||||
|
||||
On a 2026-07-28 request, `ctx.mcpReq.log()` reads its level filter from the
|
||||
`io.modelcontextprotocol/logLevel` `_meta` envelope key (the modern replacement for the
|
||||
`logging/setLevel` RPC). When the key is **absent** the server emits no
|
||||
`notifications/message` for that request — absence is opt-out, not "no filter". The SDK
|
||||
`Client` does not auto-attach `logLevel`, so handler logs on a default 2026-era exchange
|
||||
are silently suppressed until the client opts in.
|
||||
|
||||
---
|
||||
|
||||
## Replacing per-session state: `requestState`
|
||||
|
||||
The 2026-07-28 revision is **per request** — `createMcpHandler` builds a fresh server per
|
||||
request and there is no `Mcp-Session-Id`. If your v1 server kept state keyed on the
|
||||
session id (`ctx.sessionId` / `extra.sessionId`), the 2026 answer is `requestState`: an
|
||||
opaque string the server returns with `inputRequired(...)` and the client echoes
|
||||
byte-for-byte on the retry. Read it at `ctx.mcpReq.requestState`.
|
||||
|
||||
`requestState` round-trips through the client and is therefore **untrusted input** —
|
||||
integrity-protect it (HMAC / AEAD over the payload, bound to principal, originating
|
||||
method/parameters, and an expiry) and reject failed verification on re-entry. Configure
|
||||
`ServerOptions.requestState.verify` and the seam runs it before the handler whenever
|
||||
`requestState` is present (a thrown rejection answers `-32602` above the tool funnel).
|
||||
The `createRequestStateCodec({ key, ttlSeconds?, bind? })` helper returns
|
||||
`{ mint, verify }` — `mint` HMAC-SHA256-seals a JSON-serializable payload and `verify`
|
||||
is exactly the function you assign to the hook. The codec is **signed, not encrypted**
|
||||
(the client can base64url-decode the payload). See `examples/mrtr/server.ts` and
|
||||
[Multi-round-trip requests](#multi-round-trip-requests) for the full handler shape.
|
||||
|
||||
---
|
||||
|
||||
## Auth on 2026-07-28
|
||||
|
||||
The 2026-07-28 specification's authorization requirements (RFC 9207 `iss` validation,
|
||||
SEP-2352 credential isolation, SEP-2350 scope step-up, SEP-837/SEP-2207 DCR + TLS) are
|
||||
implemented in v2 as **SDK-level opt-ins, not protocol-era gates** — they apply on every
|
||||
era once enabled. The migration steps live in
|
||||
[upgrade-to-v2.md › Auth](./upgrade-to-v2.md#auth). To be **2026-07-28-conformant**,
|
||||
enable the spec-2026 opt-ins listed there: pass `iss` (or the callback `URLSearchParams`)
|
||||
to `finishAuth`; round-trip the `issuer` stamp on stored credentials; implement
|
||||
`discoveryState()`; and either keep `onInsufficientScope: 'reauthorize'` or handle
|
||||
`InsufficientScopeError` yourself. Nothing in this section is era-switched at the wire
|
||||
layer.
|
||||
|
||||
---
|
||||
|
||||
## Per-era wire codecs
|
||||
|
||||
The wire layer is split into per-revision codecs inside the (private, bundled) core: one
|
||||
codec serves every 2025-era protocol version (2024-10-07 … 2025-11-25) and one serves
|
||||
2026-07-28. The codec is selected by the negotiated protocol version, which is
|
||||
**connection state** on the `Client`/`Server` instance (instances with no negotiated
|
||||
version default to the 2025 era). An edge classification (`MessageExtraInfo.classification`)
|
||||
no longer switches the era per message — it is validated against the instance era, and a
|
||||
mismatch is rejected as an entry/routing error (`-32022 Unsupported protocol version`
|
||||
for requests; drop + `onerror` for notifications).
|
||||
|
||||
Methods deleted by a protocol revision are **physically absent** from that era's
|
||||
registry: an inbound `tasks/get` on a 2026-era connection gets `-32601` even if a
|
||||
handler is registered, and sending an era-mismatched spec method (e.g. `server/discover`
|
||||
toward a 2025-era peer, or any `tasks/*` method toward a 2026-era peer) throws
|
||||
`SdkError(MethodNotSupportedByProtocolVersion)` before anything reaches the transport.
|
||||
|
||||
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 |
|
||||
|
||||
The `resultType` / `EmptyResultSchema` / `specTypeSchemas` rules above have **no v1.x
|
||||
impact** — these members did not exist before 2026-07-28. The neutral-model wire
|
||||
tightening that **does** affect v1 code (`content` required, custom-handler `_meta`
|
||||
passthrough, `specTypeSchemas` narrowing) is in
|
||||
[upgrade-to-v2.md › Wire tightening](./upgrade-to-v2.md#wire-tightening-every-era).
|
||||
|
||||
> **If you were on a v2 alpha:** the 2026-07-28 draft error codes were renumbered:
|
||||
> `HeaderMismatch` `-32001`→`-32020`, `MissingRequiredClientCapability` `-32003`→`-32021`,
|
||||
> `UnsupportedProtocolVersion` `-32004`→`-32022`. No v1.x impact (these codes never
|
||||
> existed in v1); v2-alpha code that hard-coded the old literals must update — prefer
|
||||
> `ProtocolErrorCode.*` / `HEADER_MISMATCH_ERROR_CODE`.
|
||||
|
||||
---
|
||||
|
||||
## Wire-only members hidden from public types
|
||||
|
||||
The 2026-07-28 wire-level bookkeeping is handled internally and never reaches
|
||||
application code: the `resultType` discrimination field, the reserved per-request
|
||||
`_meta` envelope keys (`io.modelcontextprotocol/{protocolVersion,clientInfo,clientCapabilities,logLevel}`),
|
||||
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.
|
||||
- **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.**
|
||||
The `RequestMetaEnvelope` type and the four `*_META_KEY` constants stay exported.
|
||||
|
||||
The protocol layer enforces the same boundary at runtime:
|
||||
|
||||
- **Envelope lift.** On inbound requests and notifications, the reserved
|
||||
`io.modelcontextprotocol/*` keys are lifted out of `params._meta` before handlers run.
|
||||
For requests the envelope is readable at `ctx.mcpReq.envelope`
|
||||
(typed `Partial<RequestMetaEnvelope>`); for notifications there is no per-message
|
||||
context, so lifted envelope keys are dropped. On requests only, `inputResponses` /
|
||||
`requestState` are lifted from top-level params to `ctx.mcpReq.inputResponses` /
|
||||
`ctx.mcpReq.requestState`; notification params are never touched.
|
||||
- **Collision note for 2025-era peers.** The `_meta` lift is invisible to conforming
|
||||
2025 traffic (the `io.modelcontextprotocol/` prefix is reserved in 2025-11-25 too).
|
||||
The retry-field lift is the one collision: 2025-11-25 does not reserve the bare names
|
||||
`inputResponses`/`requestState`, so a 2025 peer's **custom-method request** that uses
|
||||
them as ordinary top-level params has them lifted out of `request.params` (still
|
||||
readable at `ctx.mcpReq.inputResponses` / `ctx.mcpReq.requestState`).
|
||||
- **Raw-first result discrimination.** On a 2026-era exchange, `'complete'` is consumed
|
||||
and stripped; `'input_required'` is fulfilled by the client's auto-fulfilment driver;
|
||||
any other kind rejects with `SdkError(UnsupportedResultType)` (kind in
|
||||
`error.data.resultType`). On a 2025-era connection a foreign `resultType` is stripped
|
||||
before validation. On a 2026-era exchange `resultType` is REQUIRED; an absent value is
|
||||
a spec violation surfaced as a typed error.
|
||||
|
||||
**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`, …) |
|
||||
|
||||
`MessageExtraInfo.classification` is an optional carrier (`{ era, revision?, envelope? }`)
|
||||
for transports that classify inbound messages at the edge; dispatch validates it against
|
||||
the instance's negotiated era.
|
||||
|
||||
---
|
||||
|
||||
## Multi-round-trip requests
|
||||
|
||||
The 2026-07-28 revision removes the server→client JSON-RPC request channel. Servers
|
||||
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 |
|
||||
| --- | --- |
|
||||
| `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 |
|
||||
|
||||
`inputRequired` / `acceptedContent` / `InputRequiredSpec` are exported from
|
||||
`@modelcontextprotocol/server`. On 2026-era requests the push-style APIs
|
||||
(`ctx.mcpReq.send` of server→client requests, `ctx.mcpReq.elicitInput`,
|
||||
`ctx.mcpReq.requestSampling`, instance-level `createMessage()`/`elicitInput()`/`listRoots()`/`ping()`)
|
||||
fail with a typed local error before anything reaches the wire; their behavior toward
|
||||
2025-era requests is unchanged.
|
||||
|
||||
`requestState` round-trips as an opaque, **untrusted** string — see
|
||||
[Replacing per-session state: `requestState`](#replacing-per-session-state-requeststate)
|
||||
for the sealing helper and verification hook.
|
||||
|
||||
**Client side — auto-fulfilment by default.** When a 2026-07-28 call answers
|
||||
`input_required`, the client fulfils the embedded requests through the same handlers
|
||||
registered with `setRequestHandler('elicitation/create' | 'sampling/createMessage' |
|
||||
'roots/list', …)` and retries (fresh request id, `inputResponses`, byte-exact
|
||||
`requestState` echo) up to `inputRequired.maxRounds` rounds (default 10). Configure or
|
||||
opt out via `ClientOptions.inputRequired` (`{ autoFulfill: false }`); drive manually per
|
||||
call with `allowInputRequired: true` plus `withInputRequired()`. Expect
|
||||
`SdkError(InputRequiredRoundsExceeded)` when the cap is exhausted.
|
||||
|
||||
---
|
||||
|
||||
## `subscriptions/listen`
|
||||
|
||||
The 2026-07-28 revision delivers `tools/prompts/resources` `list_changed` and
|
||||
`resources/updated` only on a `subscriptions/listen` stream the client opened — the
|
||||
server never sends an un-requested notification type.
|
||||
|
||||
**Server side.** Nothing to register: the serving entries handle `subscriptions/listen`
|
||||
themselves. `createMcpHandler` returns
|
||||
`.notify.{toolsChanged, promptsChanged, resourcesChanged, resourceUpdated(uri)}` typed
|
||||
publish sugar over an in-process bus (supply your own `ServerEventBus` for multi-process
|
||||
deployments). On stdio, `serveStdio` routes the pinned instance's existing
|
||||
`send*ListChanged()` calls onto the active subscriptions automatically. The 2025-era
|
||||
unsolicited delivery model is unchanged on legacy connections.
|
||||
|
||||
**Client side.** `ClientOptions.listChanged` keeps working: on a 2026-07-28 connection
|
||||
the SDK auto-opens a `subscriptions/listen` stream whose filter is the intersection of
|
||||
the configured sub-options and the server-advertised `listChanged` capabilities, so the
|
||||
same handlers fire on every published change. `client.listen(filter)` opens a stream
|
||||
explicitly. `resources/subscribe` is 2025-only — on a 2026-07-28 connection, request
|
||||
`notifications/resources/updated` via the `resourceSubscriptions` field of the listen
|
||||
filter instead.
|
||||
|
||||
**Graceful close.** When the server closes the listen stream deliberately (entry
|
||||
`close()`/shutdown), it sends the empty `subscriptions/listen` JSON-RPC result before
|
||||
closing the stream; `McpSubscription.closed` resolves `'graceful'`. A stream close
|
||||
without a result resolves `'remote'` and indicates an unexpected disconnect — re-listen
|
||||
if you still want events.
|
||||
|
||||
---
|
||||
|
||||
## `Mcp-Param-*` and standard headers (SEP-2243)
|
||||
|
||||
On a 2026-07-28 connection over Streamable HTTP, `Client.callTool()` mirrors tool
|
||||
arguments designated with `x-mcp-header` in the tool's `inputSchema` into
|
||||
`Mcp-Param-{Name}` HTTP request headers (Base64-sentinel-encoded where needed), and
|
||||
`createMcpHandler` rejects a `tools/call` whose `Mcp-Param-*` headers are missing for a
|
||||
present body value, malformed, or disagree with the body — `400 Bad Request` with
|
||||
JSON-RPC `-32020` (`HeaderMismatch`). The Streamable HTTP transport also emits the
|
||||
`Mcp-Name` standard header on every modern-enveloped request, and `createMcpHandler`
|
||||
validates the SEP-2243 standard headers (`MCP-Protocol-Version`, `Mcp-Method`,
|
||||
`Mcp-Name`) against the body on the modern path with the same rejection.
|
||||
|
||||
**Modern-era exception** to the `SdkHttpError` mapping: on a modern-enveloped request,
|
||||
an HTTP `400` whose body is a well-formed JSON-RPC error response addressed to the
|
||||
pending request id is delivered in-band as a `ProtocolError` (so the `-32020` recovery
|
||||
retry can fire). Legacy-era exchanges and generic HTTP failures still surface as
|
||||
`SdkHttpError`.
|
||||
|
||||
Additive options: `CallToolRequestOptions.toolDefinition` (pass the tool definition
|
||||
directly so mirroring and output-schema validation run without a prior `tools/list`),
|
||||
`TransportSendOptions.headers` (per-request HTTP headers; reserved standard/auth header
|
||||
names are skipped). Browser clients skip mirroring (dynamically named headers cannot be
|
||||
statically allow-listed for credentialed CORS).
|
||||
|
||||
---
|
||||
|
||||
## Cache fields and cache hints
|
||||
|
||||
The 2026-07-28 revision requires `ttlMs` and `cacheScope` on the cacheable results.
|
||||
When serving that revision, the SDK always emits both fields, defaulting to `ttlMs: 0`
|
||||
and `cacheScope: 'private'` (the most conservative policy). To advertise a real cache
|
||||
policy, set `ServerOptions.cacheHints` (per-operation) or `cacheHint` on a
|
||||
`registerResource` metadata object; resolution is per field, most-specific author first.
|
||||
2025-era responses never carry these fields.
|
||||
|
||||
---
|
||||
|
||||
## Tasks: deprecated wire vocabulary
|
||||
|
||||
The task **wire surface** defined by the 2025-11-25 protocol revision is still exported
|
||||
for interoperability with peers on that revision: the task Zod schemas and inferred
|
||||
types (`Task`, `TaskStatus`, `TaskMetadata`, `RelatedTaskMetadata`, `CreateTaskResult`,
|
||||
`GetTask*`, `ListTasks*`, `CancelTask*`, `TaskStatusNotification*`,
|
||||
`TaskAugmentedRequestParams`), the task members of the request/result/notification union
|
||||
types, the `tasks` capability key, `isTaskAugmentedRequestParams`, and
|
||||
`RELATED_TASK_META_KEY`. All are now `@deprecated` (importable wire vocabulary only;
|
||||
removable at the major version that drops 2025-era support).
|
||||
|
||||
Task methods are excluded from the typed method maps: `RequestMethod` / `RequestTypeMap`
|
||||
/ `ResultTypeMap` / `NotificationTypeMap` have no `tasks/*` or
|
||||
`notifications/tasks/status` entries, so the method-keyed overloads of `request()`,
|
||||
`ctx.mcpReq.send()`, `setRequestHandler()`, `setNotificationHandler()` reject task
|
||||
methods at compile time. `ResultTypeMap['tools/call']` is plain `CallToolResult` (no
|
||||
`| CreateTaskResult`); same for `sampling/createMessage` and `elicitation/create`. Where
|
||||
task interop is genuinely required, use the explicit-schema custom-method form
|
||||
(`request({ method: 'tasks/get', params }, GetTaskResultSchema)`). Inbound `tasks/*`
|
||||
requests → `-32601`.
|
||||
|
||||
The experimental tasks **interception** layer is removed entirely — see
|
||||
[upgrade-to-v2.md › Experimental tasks interception removed](./upgrade-to-v2.md#experimental-tasks-interception-removed).
|
||||
|
||||
---
|
||||
|
||||
## 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)` |
|
||||
1058
docs/migration/upgrade-to-v2.md
Normal file
1058
docs/migration/upgrade-to-v2.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -473,5 +473,5 @@ This isn't an error - it just means there are no current weather alerts for that
|
|||
Now that your server is running locally, here are some ways to go further:
|
||||
|
||||
- [**Server guide**](./server.md) — Add resources, prompts, logging, error handling, and remote transports to your server.
|
||||
- [**Example servers**](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/server) — Browse runnable examples covering OAuth, streaming, sessions, and more.
|
||||
- [**Example servers**](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples) — Browse runnable examples covering OAuth, streaming, sessions, and more.
|
||||
- [**FAQ**](./faq.md) — Troubleshoot common errors (Zod version conflicts, transport issues, etc.).
|
||||
|
|
|
|||
156
docs/server.md
156
docs/server.md
|
|
@ -16,7 +16,7 @@ Building a server takes three steps:
|
|||
|
||||
The examples below use these imports. Adjust based on which features and transport you need:
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#imports"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#imports"
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { createMcpExpressApp } from '@modelcontextprotocol/express';
|
||||
|
|
@ -38,7 +38,7 @@ MCP supports two transport mechanisms (see [Transport layer](https://modelcontex
|
|||
|
||||
Create a {@linkcode @modelcontextprotocol/node!streamableHttp.NodeStreamableHTTPServerTransport | NodeStreamableHTTPServerTransport} and connect it to your server:
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#streamableHttp_stateful"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#streamableHttp_stateful"
|
||||
const server = new McpServer({ name: 'my-server', version: '1.0.0' });
|
||||
|
||||
const transport = new NodeStreamableHTTPServerTransport({
|
||||
|
|
@ -50,23 +50,43 @@ await server.connect(transport);
|
|||
|
||||
**Options:** Set `sessionIdGenerator` to a function (shown above) for stateful sessions. Set it to `undefined` for stateless mode (simpler, but does not support resumability). Set `enableJsonResponse: true` to return plain JSON instead of SSE streams.
|
||||
|
||||
For a complete server with sessions, logging, and CORS mounted on Express, see [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/simpleStreamableHttp.ts).
|
||||
For a complete server with sessions and the browser-client CORS recipe, see [`legacy-routing/server.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/legacy-routing/server.ts).
|
||||
|
||||
### stdio
|
||||
|
||||
For local, process-spawned integrations, use {@linkcode @modelcontextprotocol/server!server/stdio.StdioServerTransport | StdioServerTransport}:
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#stdio_basic"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#stdio_basic"
|
||||
const server = new McpServer({ name: 'my-server', version: '1.0.0' });
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
```
|
||||
|
||||
#### Serving the 2026-07-28 draft revision on stdio
|
||||
|
||||
A hand-constructed stdio server speaks the 2025-era protocol it was written for: nothing about its wire behavior changes when you upgrade the SDK. Serving the 2026-07-28 draft revision goes through the connection-pinned `serveStdio` entry, which mirrors `createMcpHandler` for
|
||||
long-lived connections — the entry owns the transport and the era decision, and one instance from your factory serves the era the client opened the connection with:
|
||||
|
||||
```typescript
|
||||
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
||||
|
||||
serveStdio(() => {
|
||||
const server = new McpServer({ name: 'my-server', version: '1.0.0' }, { capabilities: { tools: {} } });
|
||||
// register tools/resources/prompts once — the same factory serves both eras
|
||||
return server;
|
||||
});
|
||||
```
|
||||
|
||||
Plain 2025 clients open with `initialize` and are served exactly as before; 2026-capable clients negotiate via `server/discover` and send each request with the per-request `_meta` envelope, and their connection is pinned to a 2026-era instance. Pass `legacy: 'reject'` to refuse
|
||||
2025-era openings with the unsupported-protocol-version error. On 2026-pinned connections, read per-request client identity from `ctx.mcpReq.envelope` in your handlers rather than the connection-scoped accessors (see the [2026-07-28 support guide](./migration/support-2026-07-28.md) for details). A runnable
|
||||
example lives at `examples/dual-era/server.ts`, with a two-legged client at `examples/dual-era/client.ts`.
|
||||
|
||||
## Server instructions
|
||||
|
||||
Instructions describe how to use the server and its features — cross-tool relationships, workflow patterns, and constraints (see [Instructions](https://modelcontextprotocol.io/specification/latest/basic/lifecycle#instructions) in the MCP specification). Clients may add them to the system prompt. Instructions should not duplicate information already in tool descriptions.
|
||||
Instructions describe how to use the server and its features — cross-tool relationships, workflow patterns, and constraints (see [Instructions](https://modelcontextprotocol.io/specification/latest/basic/lifecycle#instructions) in the MCP specification). Clients may add them to
|
||||
the system prompt. Instructions should not duplicate information already in tool descriptions.
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#instructions_basic"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#instructions_basic"
|
||||
const server = new McpServer(
|
||||
{ name: 'db-server', version: '1.0.0' },
|
||||
{
|
||||
|
|
@ -80,9 +100,13 @@ const server = new McpServer(
|
|||
|
||||
Tools let clients invoke actions on your server — they are usually the main way LLMs call into your application (see [Tools](https://modelcontextprotocol.io/docs/learn/server-concepts#tools) in the MCP overview).
|
||||
|
||||
Register a tool with {@linkcode @modelcontextprotocol/server!server/mcp.McpServer#registerTool | registerTool}. Provide an `inputSchema` (Zod) to validate arguments, and optionally an `outputSchema` for structured return values:
|
||||
Register a tool with {@linkcode @modelcontextprotocol/server!server/mcp.McpServer#registerTool | registerTool}. Provide an `inputSchema` (Zod) to validate arguments, and optionally an `outputSchema` for structured return values.
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#registerTool_basic"
|
||||
> On the 2026-07-28 draft serving path, a tool whose `inputSchema` carries an `x-mcp-header` annotation has that argument mirrored into an `Mcp-Param-{Name}` HTTP request header by conforming clients. `createMcpHandler` validates those headers before dispatch and rejects a
|
||||
> `tools/call` whose `Mcp-Param-*` headers are missing for a present body value, malformed, or disagree with the body — `400 Bad Request` with JSON-RPC `-32020` (`HeaderMismatch`). `registerTool` warns at registration time when an `x-mcp-header` declaration violates the
|
||||
> spec's constraints. The 2025-era serving paths and the low-level `Server` factory shape are unchanged.
|
||||
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#registerTool_basic"
|
||||
server.registerTool(
|
||||
'calculate-bmi',
|
||||
{
|
||||
|
|
@ -104,21 +128,11 @@ server.registerTool(
|
|||
);
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> When defining a named type for `structuredContent`, use a `type` alias rather than an `interface`. Named interfaces lack implicit index signatures in TypeScript, so they aren't assignable to `{ [key: string]: unknown }`:
|
||||
>
|
||||
> ```ts
|
||||
> type BmiResult = { bmi: number }; // assignable
|
||||
> interface BmiResult { bmi: number } // type error
|
||||
> ```
|
||||
>
|
||||
> Alternatively, spread the value: `structuredContent: { ...result }`.
|
||||
|
||||
### `ResourceLink` outputs
|
||||
|
||||
Tools can return `resource_link` content items to reference large resources without embedding them, letting clients fetch only what they need:
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#registerTool_resourceLink"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#registerTool_resourceLink"
|
||||
server.registerTool(
|
||||
'list-files',
|
||||
{
|
||||
|
|
@ -149,7 +163,7 @@ server.registerTool(
|
|||
|
||||
Tools can include annotations that hint at their behavior — whether a tool is read-only, destructive, or idempotent. Annotations help clients present tools appropriately without changing execution semantics:
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#registerTool_annotations"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#registerTool_annotations"
|
||||
server.registerTool(
|
||||
'delete-file',
|
||||
{
|
||||
|
|
@ -172,7 +186,7 @@ server.registerTool(
|
|||
|
||||
Tools, prompts, resources, and resource templates can advertise `icons` that a client may render in its UI — the same field is also accepted on your server's `Implementation` info. Each icon needs a `src` (a URL or `data:` URI) and may add a `mimeType`, the `sizes` it suits (`"WxH"` strings, or `"any"` for scalable formats like SVG), and a `theme` (`light` or `dark`). Icons are passed straight through to the relevant list response, such as `tools/list`:
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#registerTool_icons"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#registerTool_icons"
|
||||
server.registerTool(
|
||||
'generate-chart',
|
||||
{
|
||||
|
|
@ -200,7 +214,7 @@ server.registerTool(
|
|||
|
||||
Return `isError: true` to report tool-level errors. The LLM sees these and can self-correct, unlike protocol-level errors which are hidden from it:
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#registerTool_errorHandling"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#registerTool_errorHandling"
|
||||
server.registerTool(
|
||||
'fetch-data',
|
||||
{
|
||||
|
|
@ -232,11 +246,12 @@ If a handler throws instead of returning `isError`, the SDK catches the exceptio
|
|||
|
||||
## Resources
|
||||
|
||||
Resources expose read-only data — files, database schemas, configuration — that the host application can retrieve and attach as context for the model (see [Resources](https://modelcontextprotocol.io/docs/learn/server-concepts#resources) in the MCP overview). Unlike [tools](#tools), which the LLM invokes on its own, resources are application-controlled: the host decides which resources to fetch and how to present them.
|
||||
Resources expose read-only data — files, database schemas, configuration — that the host application can retrieve and attach as context for the model (see [Resources](https://modelcontextprotocol.io/docs/learn/server-concepts#resources) in the MCP overview). Unlike
|
||||
[tools](#tools), which the LLM invokes on its own, resources are application-controlled: the host decides which resources to fetch and how to present them.
|
||||
|
||||
A static resource at a fixed URI:
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#registerResource_static"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#registerResource_static"
|
||||
server.registerResource(
|
||||
'config',
|
||||
'config://app',
|
||||
|
|
@ -253,7 +268,7 @@ server.registerResource(
|
|||
|
||||
Dynamic resources use {@linkcode @modelcontextprotocol/server!server/mcp.ResourceTemplate | ResourceTemplate} with URI patterns. The `list` callback lets clients discover available instances:
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#registerResource_template"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#registerResource_template"
|
||||
server.registerResource(
|
||||
'user-profile',
|
||||
new ResourceTemplate('user://{userId}/profile', {
|
||||
|
|
@ -281,13 +296,15 @@ server.registerResource(
|
|||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Security note:** If a resource is backed by the filesystem (for example, a `file://` server or a template whose variables map onto file paths), the spec requires sanitizing any user-influenced path before use. Resolve the requested path and verify it stays within the intended root directory, rejecting traversal sequences such as `..` (including encoded forms) and symlinks that escape the root. Never pass template variables or client-supplied URIs to filesystem APIs unchecked.
|
||||
> **Security note:** If a resource is backed by the filesystem (for example, a `file://` server or a template whose variables map onto file paths), the spec requires sanitizing any user-influenced path before use. Resolve the requested path and verify it stays within
|
||||
> the intended root directory, rejecting traversal sequences such as `..` (including encoded forms) and symlinks that escape the root. Never pass template variables or client-supplied URIs to filesystem APIs unchecked.
|
||||
|
||||
## Prompts
|
||||
|
||||
Prompts are reusable templates that help structure interactions with models (see [Prompts](https://modelcontextprotocol.io/docs/learn/server-concepts#prompts) in the MCP overview). Use a prompt when you want to offer a canned interaction pattern that users invoke explicitly; use a [tool](#tools) when the LLM should decide when to call it.
|
||||
Prompts are reusable templates that help structure interactions with models (see [Prompts](https://modelcontextprotocol.io/docs/learn/server-concepts#prompts) in the MCP overview). Use a prompt when you want to offer a canned interaction pattern that users invoke explicitly; use
|
||||
a [tool](#tools) when the LLM should decide when to call it.
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#registerPrompt_basic"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#registerPrompt_basic"
|
||||
server.registerPrompt(
|
||||
'review-code',
|
||||
{
|
||||
|
|
@ -315,7 +332,7 @@ server.registerPrompt(
|
|||
|
||||
Both prompts and resources can support argument completions. Wrap a field in the `argsSchema` with {@linkcode @modelcontextprotocol/server!server/completable.completable | completable()} to provide autocompletion suggestions:
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#registerPrompt_completion"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#registerPrompt_completion"
|
||||
server.registerPrompt(
|
||||
'review-code',
|
||||
{
|
||||
|
|
@ -344,19 +361,20 @@ server.registerPrompt(
|
|||
## Logging
|
||||
|
||||
> [!WARNING]
|
||||
> MCP logging is deprecated as of protocol version 2026-07-28 (SEP-2577). It remains fully functional during the deprecation window (at least twelve months); see the [deprecated features registry](https://modelcontextprotocol.io/specification/draft/deprecated). Migrate to stderr logging (STDIO servers) or OpenTelemetry.
|
||||
> MCP logging is deprecated as of protocol version 2026-07-28 (SEP-2577). It remains fully functional during the deprecation window (at least twelve months); see the [deprecated features registry](https://modelcontextprotocol.io/specification/draft/deprecated). Migrate
|
||||
> to stderr logging (STDIO servers) or OpenTelemetry.
|
||||
|
||||
Logging lets your server send structured diagnostics — debug traces, progress updates, warnings — to the connected client as notifications (see [Logging](https://modelcontextprotocol.io/specification/latest/server/utilities/logging) in the MCP specification).
|
||||
|
||||
Declare the `logging` capability, then call `ctx.mcpReq.log(level, data)` (from {@linkcode @modelcontextprotocol/server!index.ServerContext | ServerContext}) inside any handler:
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#logging_capability"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#logging_capability"
|
||||
const server = new McpServer({ name: 'my-server', version: '1.0.0' }, { capabilities: { logging: {} } });
|
||||
```
|
||||
|
||||
Then log from any handler:
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#registerTool_logging"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#registerTool_logging"
|
||||
server.registerTool(
|
||||
'fetch-data',
|
||||
{
|
||||
|
|
@ -379,7 +397,7 @@ Progress notifications let a tool report incremental status updates during long-
|
|||
|
||||
If the client includes a `progressToken` in the request `_meta`, send `notifications/progress` via `ctx.mcpReq.notify()` (from {@linkcode @modelcontextprotocol/server!index.BaseContext | BaseContext}):
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#registerTool_progress"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#registerTool_progress"
|
||||
server.registerTool(
|
||||
'process-files',
|
||||
{
|
||||
|
|
@ -414,11 +432,13 @@ server.registerTool(
|
|||
|
||||
## Trace context propagation
|
||||
|
||||
The MCP specification ([SEP-414](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/414)) reserves the unprefixed `_meta` keys `traceparent`, `tracestate`, and `baggage` for distributed trace context, as an exception to the usual `_meta` key prefix rule. When present, the values must follow the [W3C Trace Context](https://www.w3.org/TR/trace-context/) and [W3C Baggage](https://www.w3.org/TR/baggage/) formats. The SDK does not interpret these keys — `_meta` passes through untouched on any transport, including stdio. The key names are exported as `TRACEPARENT_META_KEY`, `TRACESTATE_META_KEY`, and `BAGGAGE_META_KEY`.
|
||||
The MCP specification ([SEP-414](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/414)) reserves the unprefixed `_meta` keys `traceparent`, `tracestate`, and `baggage` for distributed trace context, as an exception to the usual `_meta` key prefix rule. When
|
||||
present, the values must follow the [W3C Trace Context](https://www.w3.org/TR/trace-context/) and [W3C Baggage](https://www.w3.org/TR/baggage/) formats. The SDK does not interpret these keys — `_meta` passes through untouched on any transport, including stdio. The key names are
|
||||
exported as `TRACEPARENT_META_KEY`, `TRACESTATE_META_KEY`, and `BAGGAGE_META_KEY`.
|
||||
|
||||
Read the caller's trace context from `ctx.mcpReq._meta` in a handler:
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#registerTool_traceContext"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#registerTool_traceContext"
|
||||
server.registerTool(
|
||||
'traced-operation',
|
||||
{
|
||||
|
|
@ -442,18 +462,20 @@ To propagate context onward (for example on a server-initiated sampling request,
|
|||
|
||||
## Server-initiated requests
|
||||
|
||||
MCP is bidirectional — servers can send requests *to* the client during tool execution, as long as the client declares matching capabilities (see [Architecture](https://modelcontextprotocol.io/docs/learn/architecture) in the MCP overview).
|
||||
MCP is bidirectional — servers can send requests _to_ the client during tool execution, as long as the client declares matching capabilities (see [Architecture](https://modelcontextprotocol.io/docs/learn/architecture) in the MCP overview).
|
||||
|
||||
### Sampling
|
||||
|
||||
> [!WARNING]
|
||||
> Sampling is deprecated as of protocol version 2026-07-28 (SEP-2577). It remains fully functional during the deprecation window (at least twelve months); see the [deprecated features registry](https://modelcontextprotocol.io/specification/draft/deprecated). Migrate to calling LLM provider APIs directly from your server.
|
||||
> Sampling is deprecated as of protocol version 2026-07-28 (SEP-2577). It remains fully functional during the deprecation window (at least twelve months); see the [deprecated features registry](https://modelcontextprotocol.io/specification/draft/deprecated). Migrate to
|
||||
> calling LLM provider APIs directly from your server.
|
||||
|
||||
Sampling lets a tool handler request an LLM completion from the connected client — the handler describes a prompt and the client returns the model's response (see [Sampling](https://modelcontextprotocol.io/docs/learn/client-concepts#sampling) in the MCP overview). Use sampling when a tool needs the model to generate or transform text mid-execution.
|
||||
Sampling lets a tool handler request an LLM completion from the connected client — the handler describes a prompt and the client returns the model's response (see [Sampling](https://modelcontextprotocol.io/docs/learn/client-concepts#sampling) in the MCP overview). Use sampling
|
||||
when a tool needs the model to generate or transform text mid-execution.
|
||||
|
||||
Call `ctx.mcpReq.requestSampling(params)` (from {@linkcode @modelcontextprotocol/server!index.ServerContext | ServerContext}) inside a tool handler:
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#registerTool_sampling"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#registerTool_sampling"
|
||||
server.registerTool(
|
||||
'summarize',
|
||||
{
|
||||
|
|
@ -485,7 +507,7 @@ server.registerTool(
|
|||
);
|
||||
```
|
||||
|
||||
For a full runnable example, see [`toolWithSampleServer.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/toolWithSampleServer.ts).
|
||||
For a full runnable example, see [`sampling/server.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/sampling/server.ts).
|
||||
|
||||
### Elicitation
|
||||
|
||||
|
|
@ -499,7 +521,7 @@ Elicitation lets a tool handler request direct input from the user — form fiel
|
|||
|
||||
Call `ctx.mcpReq.elicitInput(params)` (from {@linkcode @modelcontextprotocol/server!index.ServerContext | ServerContext}) inside a tool handler:
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#registerTool_elicitation"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#registerTool_elicitation"
|
||||
server.registerTool(
|
||||
'collect-feedback',
|
||||
{
|
||||
|
|
@ -539,16 +561,19 @@ server.registerTool(
|
|||
);
|
||||
```
|
||||
|
||||
For runnable examples, see [`elicitationFormExample.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/elicitationFormExample.ts) (form) and [`elicitationUrlExample.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/elicitationUrlExample.ts) (URL).
|
||||
For runnable examples, see [`elicitation/server.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/elicitation/server.ts) (form + URL mode, both protocol eras) and
|
||||
[`mrtr/server.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/mrtr/server.ts) (the secure `requestState` round-trip pattern).
|
||||
|
||||
### Roots
|
||||
|
||||
> [!WARNING]
|
||||
> Roots are deprecated as of protocol version 2026-07-28 (SEP-2577). They remain fully functional during the deprecation window (at least twelve months); see the [deprecated features registry](https://modelcontextprotocol.io/specification/draft/deprecated). Migrate to passing paths via tool parameters, resource URIs, or configuration.
|
||||
> Roots are deprecated as of protocol version 2026-07-28 (SEP-2577). They remain fully functional during the deprecation window (at least twelve months); see the [deprecated features registry](https://modelcontextprotocol.io/specification/draft/deprecated). Migrate to
|
||||
> passing paths via tool parameters, resource URIs, or configuration.
|
||||
|
||||
Roots let a tool handler discover the client's workspace directories — for example, to scope a file search or identify project boundaries (see [Roots](https://modelcontextprotocol.io/docs/learn/client-concepts#roots) in the MCP overview). Call {@linkcode @modelcontextprotocol/server!server/server.Server#listRoots | server.server.listRoots()} (requires the client to declare the `roots` capability):
|
||||
Roots let a tool handler discover the client's workspace directories — for example, to scope a file search or identify project boundaries (see [Roots](https://modelcontextprotocol.io/docs/learn/client-concepts#roots) in the MCP overview). Call {@linkcode
|
||||
@modelcontextprotocol/server!server/server.Server#listRoots | server.server.listRoots()} (requires the client to declare the `roots` capability):
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#registerTool_roots"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#registerTool_roots"
|
||||
server.registerTool(
|
||||
'list-workspace-files',
|
||||
{
|
||||
|
|
@ -567,7 +592,7 @@ server.registerTool(
|
|||
|
||||
For stateful multi-session HTTP servers, capture the `http.Server` from `app.listen()` so you can stop accepting connections, then close each session transport:
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#shutdown_statefulHttp"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#shutdown_statefulHttp"
|
||||
// Capture the http.Server so it can be closed on shutdown
|
||||
const httpServer = app.listen(3000);
|
||||
|
||||
|
|
@ -587,24 +612,26 @@ Calling {@linkcode @modelcontextprotocol/server!index.Transport#close | transpor
|
|||
|
||||
For stdio servers, {@linkcode @modelcontextprotocol/server!server/mcp.McpServer#close | server.close()} is sufficient:
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#shutdown_stdio"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#shutdown_stdio"
|
||||
process.on('SIGINT', async () => {
|
||||
await server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
```
|
||||
|
||||
For a complete multi-session server with shutdown handling, see [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/simpleStreamableHttp.ts).
|
||||
For a complete multi-session server with shutdown handling, see [`repl/server.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/repl/server.ts).
|
||||
|
||||
## Deployment
|
||||
|
||||
### DNS rebinding protection
|
||||
|
||||
Under normal circumstances, cross-origin browser restrictions limit what a malicious website can do to your localhost server. [DNS rebinding attacks](https://en.wikipedia.org/wiki/DNS_rebinding) get around those restrictions entirely by making the requests appear as same-origin, since the attacking domain resolves to localhost. Validating the host header on the server side protects against this scenario. **All localhost MCP servers should use DNS rebinding protection.**
|
||||
Under normal circumstances, cross-origin browser restrictions limit what a malicious website can do to your localhost server. [DNS rebinding attacks](https://en.wikipedia.org/wiki/DNS_rebinding) get around those restrictions entirely by making the requests appear as same-origin,
|
||||
since the attacking domain resolves to localhost. Validating the host header on the server side protects against this scenario. **All localhost MCP servers should use DNS rebinding protection.**
|
||||
|
||||
The recommended approach is to use {@linkcode @modelcontextprotocol/express!express.createMcpExpressApp | createMcpExpressApp()} (from `@modelcontextprotocol/express`) or {@linkcode @modelcontextprotocol/hono!hono.createMcpHonoApp | createMcpHonoApp()} (from `@modelcontextprotocol/hono`), which enable Host header validation by default:
|
||||
The recommended approach is to use {@linkcode @modelcontextprotocol/express!express.createMcpExpressApp | createMcpExpressApp()} (from `@modelcontextprotocol/express`) or {@linkcode @modelcontextprotocol/hono!hono.createMcpHonoApp | createMcpHonoApp()} (from
|
||||
`@modelcontextprotocol/hono`), which enable Host header validation by default:
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#dnsRebinding_basic"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#dnsRebinding_basic"
|
||||
// Default: DNS rebinding protection auto-enabled (host is 127.0.0.1)
|
||||
const app = createMcpExpressApp();
|
||||
|
||||
|
|
@ -617,7 +644,7 @@ const appOpen = createMcpExpressApp({ host: '0.0.0.0' });
|
|||
|
||||
When binding to `0.0.0.0` / `::`, provide an allow-list of hosts:
|
||||
|
||||
```ts source="../examples/server/src/serverGuide.examples.ts#dnsRebinding_allowedHosts"
|
||||
```ts source="../examples/guides/serverGuide.examples.ts#dnsRebinding_allowedHosts"
|
||||
const app = createMcpExpressApp({
|
||||
host: '0.0.0.0',
|
||||
allowedHosts: ['localhost', '127.0.0.1', 'myhost.local']
|
||||
|
|
@ -626,22 +653,27 @@ const app = createMcpExpressApp({
|
|||
|
||||
`createMcpHonoApp()` from `@modelcontextprotocol/hono` provides the same protection for Hono-based servers and Web Standard runtimes (Cloudflare Workers, Deno, Bun).
|
||||
|
||||
If you use `NodeStreamableHTTPServerTransport` directly with your own HTTP framework, you must implement Host header validation yourself. See the [`hostHeaderValidation`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/middleware/express/src/express.ts) middleware source for reference.
|
||||
The app factories also validate the `Origin` header with the same arming rules: localhost-class binds are protected by default, and an explicit `allowedOrigins` list (hostnames, port-agnostic — the same convention as `allowedHosts`) replaces the default localhost allowlist; there
|
||||
is no option that disables Origin validation for a localhost-class bind. Requests without an `Origin` header always pass, so MCP clients outside a browser are unaffected; a present `Origin` that is not allowed, or that cannot be parsed, is rejected with `403`. The per-framework
|
||||
middleware (`originValidation`, `localhostOriginValidation`) can also be mounted explicitly, and `@modelcontextprotocol/node` ships equivalent request guards for plain `node:http` servers.
|
||||
|
||||
If you use `NodeStreamableHTTPServerTransport` directly with your own HTTP framework, you must implement Host header validation yourself. See the [`hostHeaderValidation`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/middleware/express/src/express.ts)
|
||||
middleware source for reference. When mounting a handler bare on a fetch-native runtime, the framework-agnostic helpers from `@modelcontextprotocol/server` (`hostHeaderValidationResponse`, `originValidationResponse`) cover the same checks before the request reaches the handler.
|
||||
|
||||
## See also
|
||||
|
||||
- [`examples/server/`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/server) — Full runnable server examples
|
||||
- [`examples/`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples) — Full runnable server examples
|
||||
- [Client guide](./client.md) — Building MCP clients with this SDK
|
||||
- [MCP overview](https://modelcontextprotocol.io/docs/learn/architecture) — Protocol-level concepts: participants, layers, primitives
|
||||
- [Migration guide](./migration.md) — Upgrading from previous SDK versions
|
||||
- [Migration guide](./migration/index.md) — Upgrading from previous SDK versions
|
||||
- [FAQ](./faq.md) — Frequently asked questions and troubleshooting
|
||||
|
||||
### Additional examples
|
||||
|
||||
| Feature | Description | Example |
|
||||
|---------|-------------|---------|
|
||||
| Web Standard transport | Deploy on Cloudflare Workers, Deno, or Bun | [`honoWebStandardStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/honoWebStandardStreamableHttp.ts) |
|
||||
| Session management | Per-session transport routing, initialization, and cleanup | [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/simpleStreamableHttp.ts) |
|
||||
| Resumability | Replay missed SSE events via an event store | [`inMemoryEventStore.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/inMemoryEventStore.ts) |
|
||||
| CORS | Expose MCP headers for browser clients | [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/simpleStreamableHttp.ts) |
|
||||
| Multi-node deployment | Stateless, persistent-storage, and distributed routing patterns | [`examples/server/README.md`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/README.md#multi-node-deployment-patterns) |
|
||||
| Feature | Description | Example |
|
||||
| ---------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Web Standard transport | Deploy on Cloudflare Workers, Deno, or Bun | [`hono/server.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/hono/server.ts) |
|
||||
| Session management | Per-session transport routing, initialization, and cleanup | [`legacy-routing/server.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/legacy-routing/server.ts) |
|
||||
| Resumability | Replay missed SSE events via an event store | [`inMemoryEventStore.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/shared/src/inMemoryEventStore.ts) |
|
||||
| CORS | Expose MCP headers for browser clients | [`legacy-routing/server.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/legacy-routing/server.ts) |
|
||||
| Multi-node deployment | Stateless, persistent-storage, and distributed routing patterns | [`examples/README.md`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/README.md#multi-node-deployment-patterns) |
|
||||
|
|
|
|||
Loading…
Reference in a new issue