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:
Felix Weinberger 2026-06-26 14:01:10 +01:00 • committed by GitHub
commit 1823aae891
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 69131 additions and 9001 deletions

View 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.

View file

@ -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.

View file

@ -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) |

View file

@ -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?

View file

@ -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

File diff suppressed because it is too large Load diff

44
docs/migration/index.md Normal file
View 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)

View 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)` |

File diff suppressed because it is too large Load diff

View file

@ -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.).

View file

@ -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) |