Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/draft-protocol-version-constants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@modelcontextprotocol/core': patch
'@modelcontextprotocol/client': patch
'@modelcontextprotocol/server': patch
---

Add `STATEFUL_PROTOCOL_VERSIONS` (the closed list of protocol versions negotiated via the `initialize` handshake) and `DRAFT_PROTOCOL_VERSION_2026` / `DRAFT_PROTOCOL_VERSIONS` constants. Protocol revisions after 2025-11-25 are never negotiated via `initialize`: clients request
and servers accept/fall back to stateful versions only. Behavior change: `supportedProtocolVersions` entries outside the stateful list (custom or future strings) no longer participate in the handshake — see migration.md.
6 changes: 6 additions & 0 deletions docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ const systemPrompt = ['You are a helpful assistant.', instructions].filter(Boole
console.log(systemPrompt);
```

### Protocol versions

During initialization the client requests the first stateful entry of its supported version list and accepts a response within that stateful subset — by default the versions in {@linkcode @modelcontextprotocol/client!index.SUPPORTED_PROTOCOL_VERSIONS | SUPPORTED_PROTOCOL_VERSIONS}. Pass `supportedProtocolVersions` in the client options to restrict or reorder that list.

Only the stateful protocol versions in {@linkcode @modelcontextprotocol/client!index.STATEFUL_PROTOCOL_VERSIONS | STATEFUL_PROTOCOL_VERSIONS} negotiate via the initialize handshake. Every revision after 2025-11-25 — including the draft revision in {@linkcode @modelcontextprotocol/client!index.DRAFT_PROTOCOL_VERSIONS | DRAFT_PROTOCOL_VERSIONS} — is stateless and negotiates per-request, which arrives with a later release.

## 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.
Expand Down
84 changes: 45 additions & 39 deletions docs/migration-SKILL.md

Large diffs are not rendered by default.

65 changes: 41 additions & 24 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ const transport = new StreamableHTTPClientTransport(new URL('http://localhost:30

Resource Server helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`, `getOAuthProtectedResourceMetadataUrl`, `OAuthTokenVerifier`) are first-class in `@modelcontextprotocol/express`.

Authorization Server helpers (`mcpAuthRouter`, `OAuthServerProvider`, `ProxyOAuthServerProvider`, `authenticateClient`, `allowedMethods`, etc.) have been removed from the core SDK; new code should use a dedicated IdP/OAuth library. See the [examples](../examples/server/src/) for a working demo with `better-auth`.
Authorization Server helpers (`mcpAuthRouter`, `OAuthServerProvider`, `ProxyOAuthServerProvider`, `authenticateClient`, `allowedMethods`, etc.) have been removed from the core SDK; new code should use a dedicated IdP/OAuth library. See the [examples](../examples/server/src/) for
a working demo with `better-auth`.

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

Expand Down Expand Up @@ -387,7 +388,11 @@ const AcmeSearch = z.object({
params: z.object({ query: z.string(), limit: z.number().int() })
});
server.setRequestHandler(AcmeSearch, async request => {
return { items: [/* ... */] };
return {
items: [
/* ... */
]
};
});
```

Expand All @@ -398,7 +403,11 @@ const SearchParams = z.object({ query: z.string(), limit: z.number().int() });
const SearchResult = z.object({ items: z.array(z.string()) });

server.setRequestHandler('acme/search', { params: SearchParams, result: SearchResult }, async (params, ctx) => {
return { items: [/* ... */] };
return {
items: [
/* ... */
]
};
});
```

Expand Down Expand Up @@ -437,8 +446,8 @@ Common method string replacements:

### `Protocol.request()`, `ctx.mcpReq.send()`, and `Client.callTool()` no longer require a schema parameter for spec methods

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

**`client.request()` — Before (v1):**

Expand Down Expand Up @@ -518,7 +527,8 @@ import { specTypeSchemas } from '@modelcontextprotocol/client';
const result = specTypeSchemas.CallToolResult['~standard'].validate(value);
```

`isSpecType` and `specTypeSchemas` are keyed by `SpecTypeName` — a literal union of every named type in the MCP spec — so you get autocomplete and a compile error on typos. `specTypeSchemas.X` is a `StandardSchemaV1Sync<In, Out>` — `validate()` returns the result synchronously, so you can access `.issues` / `.value` without `await`. It composes with any Standard-Schema-aware library. The pre-existing `isCallToolResult(value)` guard still works.
`isSpecType` and `specTypeSchemas` are keyed by `SpecTypeName` — a literal union of every named type in the MCP spec — so you get autocomplete and a compile error on typos. `specTypeSchemas.X` is a `StandardSchemaV1Sync<In, Out>` — `validate()` returns the result synchronously,
so you can access `.issues` / `.value` without `await`. It composes with any Standard-Schema-aware library. The pre-existing `isCallToolResult(value)` guard still works.

### Client list methods return empty results for missing capabilities

Expand Down Expand Up @@ -718,22 +728,22 @@ try {

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

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

#### `StreamableHTTPError` removed

Expand Down Expand Up @@ -762,7 +772,7 @@ try {
await transport.send(message);
} catch (error) {
if (error instanceof SdkHttpError) {
console.log('HTTP status:', error.status); // number — no cast needed
console.log('HTTP status:', error.status); // number — no cast needed
console.log('Status text:', error.statusText); // string | undefined
switch (error.code) {
case SdkErrorCode.ClientHttpAuthentication:
Expand Down Expand Up @@ -914,6 +924,11 @@ server.setRequestHandler('tools/call', async (request, ctx) => {

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

### `initialize` negotiates only known stateful protocol versions

`supportedProtocolVersions` entries outside `STATEFUL_PROTOCOL_VERSIONS` (the released versions up to 2025-11-25) no longer participate in the `initialize` handshake: clients request, and servers accept and fall back to, the first known stateful version in the list. A list
containing no stateful version makes `connect()` throw. Custom or future version strings were previously sent as-is; revisions after 2025-11-25 negotiate per-request instead (arriving in a later release), and newly released stateful versions require an SDK update.

## Enhancements

### Automatic JSON Schema validator selection by runtime
Expand Down Expand Up @@ -952,7 +967,8 @@ const server = new McpServer(
);
```

You do not need to install or import validator packages for the default behavior. The client and server packages bundle the validator backend selected by the runtime shim, so a normal `import { McpServer } from '@modelcontextprotocol/server'` does not pull `ajv` or `@cfworker/json-schema` into your bundle until you choose to customize.
You do not need to install or import validator packages for the default behavior. The client and server packages bundle the validator backend selected by the runtime shim, so a normal `import { McpServer } from '@modelcontextprotocol/server'` does not pull `ajv` or
`@cfworker/json-schema` into your bundle until you choose to customize.

If you want to customize the **built-in** backend (for example, pre-register schemas by `$id`, register custom AJV formats, or change the `@cfworker/json-schema` draft), import the named class from the explicit subpath and pass an instance through `jsonSchemaValidator`:

Expand Down Expand Up @@ -987,7 +1003,8 @@ const server = new McpServer(

(both subpaths are also available on `@modelcontextprotocol/client/validators/...`)

If you import from one of these subpaths in your own code, the corresponding peer dep (`ajv` + `ajv-formats`, or `@cfworker/json-schema`) needs to be installed in your `package.json`. The runtime shim continues to vendor a copy for the default code path, so you can use the subpath in some files and rely on the default in others.
If you import from one of these subpaths in your own code, the corresponding peer dep (`ajv` + `ajv-formats`, or `@cfworker/json-schema`) needs to be installed in your `package.json`. The runtime shim continues to vendor a copy for the default code path, so you can use the
subpath in some files and rely on the default in others.

To replace validation wholesale rather than customizing the built-in classes, implement the `jsonSchemaValidator` interface and pass your own implementation through the option above.

Expand Down
6 changes: 6 additions & 0 deletions docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ const transport = new StdioServerTransport();
await server.connect(transport);
```

### Protocol versions

A server negotiates the protocol version per connection from the stateful subset of its supported list — by default the versions in {@linkcode @modelcontextprotocol/server!index.SUPPORTED_PROTOCOL_VERSIONS | SUPPORTED_PROTOCOL_VERSIONS}. Pass `supportedProtocolVersions` in the server options to restrict or reorder that list.

Only the stateful protocol versions in {@linkcode @modelcontextprotocol/server!index.STATEFUL_PROTOCOL_VERSIONS | STATEFUL_PROTOCOL_VERSIONS} negotiate via the initialize handshake — the server neither accepts a newer revision nor falls back to one. Every revision after 2025-11-25, including the draft revision in {@linkcode @modelcontextprotocol/server!index.DRAFT_PROTOCOL_VERSIONS | DRAFT_PROTOCOL_VERSIONS}, is stateless and negotiates per-request, which arrives with a later release.

## 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.
Expand Down
27 changes: 15 additions & 12 deletions examples/server/src/customProtocolVersion.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
/**
* Example: Custom Protocol Version Support
* Example: Restricting Protocol Versions
*
* This demonstrates how to support protocol versions not yet in the SDK.
* First version in the list is used as fallback when client requests
* an unsupported version.
* Demonstrates pinning `supportedProtocolVersions` to a subset of the SDK's
* stateful versions (e.g. for compatibility testing against older clients).
*
* Only versions in STATEFUL_PROTOCOL_VERSIONS negotiate via the `initialize`
* handshake; revisions after 2025-11-25 negotiate per-request and are ignored
* by the handshake.
*
* Run with: pnpm tsx src/customProtocolVersion.ts
*/
Expand All @@ -13,15 +16,15 @@ import { createServer } from 'node:http';

import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type { CallToolResult } from '@modelcontextprotocol/server';
import { McpServer, SUPPORTED_PROTOCOL_VERSIONS } from '@modelcontextprotocol/server';
import { McpServer, STATEFUL_PROTOCOL_VERSIONS } from '@modelcontextprotocol/server';

// Add support for a newer protocol version (first in list is fallback)
const CUSTOM_VERSIONS = ['2026-01-01', ...SUPPORTED_PROTOCOL_VERSIONS];
// Pin to the two most recent stateful versions (newest first is preferred).
const PINNED_VERSIONS = STATEFUL_PROTOCOL_VERSIONS.slice(0, 2);

const server = new McpServer(
{ name: 'custom-protocol-server', version: '1.0.0' },
{ name: 'pinned-protocol-server', version: '1.0.0' },
{
supportedProtocolVersions: CUSTOM_VERSIONS,
supportedProtocolVersions: PINNED_VERSIONS,
capabilities: { tools: {} }
}
);
Expand All @@ -37,7 +40,7 @@ server.registerTool(
content: [
{
type: 'text',
text: JSON.stringify({ supportedVersions: CUSTOM_VERSIONS }, null, 2)
text: JSON.stringify({ supportedVersions: PINNED_VERSIONS }, null, 2)
}
]
})
Expand All @@ -60,6 +63,6 @@ createServer(async (req, res) => {
res.writeHead(404).end('Not Found');
}
}).listen(PORT, () => {
console.log(`MCP server with custom protocol versions on port ${PORT}`);
console.log(`Supported versions: ${CUSTOM_VERSIONS.join(', ')}`);
console.log(`MCP server with pinned protocol versions on port ${PORT}`);
console.log(`Supported versions: ${PINNED_VERSIONS.join(', ')}`);
});
14 changes: 11 additions & 3 deletions packages/client/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import {
extractTaskManagerOptions,
GetPromptResultSchema,
InitializeResultSchema,
LATEST_PROTOCOL_VERSION,
isStatefulProtocolVersion,
ListChangedOptionsBaseSchema,
ListPromptsResultSchema,
ListResourcesResultSchema,
Expand Down Expand Up @@ -492,11 +492,19 @@ export class Client extends Protocol<ClientContext> {
return;
}
try {
const statefulVersions = this._supportedProtocolVersions.filter(version => isStatefulProtocolVersion(version));
const requestedProtocolVersion = statefulVersions[0];
if (requestedProtocolVersion === undefined) {
throw new Error(
'initialize cannot negotiate protocol versions newer than 2025-11-25. Include at least one version from STATEFUL_PROTOCOL_VERSIONS in supportedProtocolVersions.'
);
}

const result = await this._requestWithSchema(
{
method: 'initialize',
params: {
protocolVersion: this._supportedProtocolVersions[0] ?? LATEST_PROTOCOL_VERSION,
protocolVersion: requestedProtocolVersion,
capabilities: this._capabilities,
clientInfo: this._clientInfo
}
Expand All @@ -509,7 +517,7 @@ export class Client extends Protocol<ClientContext> {
throw new Error(`Server sent invalid initialize result: ${result}`);
}

if (!this._supportedProtocolVersions.includes(result.protocolVersion)) {
if (!statefulVersions.includes(result.protocolVersion)) {
throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`);
}

Expand Down
Loading
Loading