Skip to content
Draft
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
7 changes: 7 additions & 0 deletions .changeset/stateless-sessionless-client-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@modelcontextprotocol/core': minor
'@modelcontextprotocol/client': minor
'@modelcontextprotocol/server': minor
---

Add stateless protocol (SEP-2575) and sessionless transport (SEP-2567) support for client→server requests on the draft 2026-07-28 revision: the per-request `_meta` envelope, `server/discover`, stateless dispatch on Streamable HTTP and stdio, discovery-based dual-era client connect, per-request logging, and the sessionless transport invariants. Opt-in by listing a non-stateful protocol version in `supportedProtocolVersions`; configurations that do not are unaffected.
14 changes: 7 additions & 7 deletions .github/workflows/update-spec-types.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,16 @@ jobs:
run: pnpm install

- name: Fetch latest spec types
run: pnpm run fetch:spec-types
run: pnpm run fetch:spec-types draft

- name: Check for changes
id: check_changes
run: |
if git diff --quiet packages/core/src/types/spec.types.ts; then
if git diff --quiet packages/core/src/types/spec.types.draft.ts; then
echo "has_changes=false" >> $GITHUB_OUTPUT
else
echo "has_changes=true" >> $GITHUB_OUTPUT
LATEST_SHA=$(grep "Last updated from commit:" packages/core/src/types/spec.types.ts | cut -d: -f2 | tr -d ' ')
LATEST_SHA=$(grep "Last updated from commit:" packages/core/src/types/spec.types.draft.ts | cut -d: -f2 | tr -d ' ')
echo "sha=$LATEST_SHA" >> $GITHUB_OUTPUT
fi

Expand All @@ -59,12 +59,12 @@ jobs:
git config user.email "github-actions[bot]@users.noreply.github.com"

git checkout -B update-spec-types
git add packages/core/src/types/spec.types.ts
git commit -m "chore: update spec.types.ts from upstream"
git add packages/core/src/types/spec.types.draft.ts
git commit -m "chore: update spec.types.draft.ts from upstream"
git push -f --no-verify origin update-spec-types

# Create PR if it doesn't exist, or update if it does
PR_BODY="This PR updates \`packages/core/src/types/spec.types.ts\` from the Model Context Protocol specification.
PR_BODY="This PR updates \`packages/core/src/types/spec.types.draft.ts\` from the Model Context Protocol specification.

Source file: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/${{ steps.check_changes.outputs.sha }}/schema/draft/schema.ts

Expand All @@ -77,7 +77,7 @@ jobs:
gh pr edit "$EXISTING_PR" --body "$PR_BODY"
else
gh pr create \
--title "chore: update spec.types.ts from upstream" \
--title "chore: update spec.types.draft.ts from upstream" \
--body "$PR_BODY" \
--base main \
--head update-spec-types
Expand Down
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ node_modules
pnpm-lock.yaml

# Ignore generated files
src/spec.types.ts
**/src/types/spec.types.2025-11-25.ts
**/src/types/spec.types.draft.ts

# Batch test cloned repos and results
packages/codemod/batch-test/repos
Expand Down
2 changes: 1 addition & 1 deletion REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ When verifying spec compliance, consult the spec directly rather than relying on

### Schema Compliance

- When editing Zod protocol schemas in `schemas.ts`, verify unknown-key handling matches the spec `schema.ts`: if the spec type has no `additionalProperties: false`, the SDK schema must use `z.looseObject()` / `.catchall(z.unknown())` rather than implicit strict — over-strict Zod (incl. `z.literal('object')` on `type`) rejects spec-valid payloads from other SDKs. Also confirm `spec.types.test.ts` still passes bidirectionally. (#1768, #1849, #1169)
- When editing Zod protocol schemas in `schemas.ts`, verify unknown-key handling matches the spec `schema.ts`: if the spec type has no `additionalProperties: false`, the SDK schema must use `z.looseObject()` / `.catchall(z.unknown())` rather than implicit strict — over-strict Zod (incl. `z.literal('object')` on `type`) rejects spec-valid payloads from other SDKs. Also confirm the `spec.types.*.test.ts` comparisons still pass bidirectionally. (#1768, #1849, #1169)

### Async / Lifecycle

Expand Down
2 changes: 1 addition & 1 deletion common/eslint-config/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export default defineConfig(
},
{
// Ignore generated protocol types everywhere
ignores: ['**/spec.types.ts']
ignores: ['**/spec.types.2025-11-25.ts', '**/spec.types.draft.ts']
},
{
files: ['packages/client/**/*.ts', 'packages/server/**/*.ts'],
Expand Down
52 changes: 51 additions & 1 deletion docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ import {
createMiddleware,
CrossAppAccessProvider,
discoverAndRequestJwtAuthGrant,
DRAFT_PROTOCOL_VERSION,
PrivateKeyJwtProvider,
ProtocolError,
SdkError,
SdkErrorCode,
SSEClientTransport,
StreamableHTTPClientTransport
StreamableHTTPClientTransport,
SUPPORTED_PROTOCOL_VERSIONS
} from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
```
Expand Down Expand Up @@ -111,6 +113,34 @@ 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 {@linkcode @modelcontextprotocol/client!index.DRAFT_PROTOCOL_VERSION | DRAFT_PROTOCOL_VERSION} — is stateless and negotiates per-request.

#### Per-request protocol revisions (draft opt-in)

Listing a per-request revision such as {@linkcode @modelcontextprotocol/client!index.DRAFT_PROTOCOL_VERSION | DRAFT_PROTOCOL_VERSION} in `supportedProtocolVersions` opts the client in to per-request negotiation:

```ts source="../examples/client/src/clientGuide.examples.ts#protocolVersions_perRequestOptIn"
const client = new Client(
{ name: 'my-client', version: '1.0.0' },
// Listing the draft revision is the opt-in. Keeping the default versions in the
// list lets connect() fall back to the initialize handshake for servers that do
// not speak a per-request revision.
{ supportedProtocolVersions: [DRAFT_PROTOCOL_VERSION, ...SUPPORTED_PROTOCOL_VERSIONS] }
);

await client.connect(new StreamableHTTPClientTransport(new URL(url)));

console.log(client.getNegotiatedProtocolVersion());
```

An opted-in `connect()` probes with `server/discover` instead of `initialize`. When the server shares a per-request revision, the connection is established without any handshake: the discover result supplies the server's capabilities, info, and instructions, and every subsequent request carries its own `_meta` envelope (protocol version, client info, client capabilities) plus, on HTTP, the matching `MCP-Protocol-Version` header. If the server rejects the probed version with error `-32004`, the client retries once with a mutually supported version from the error's `supported` list.

When the server does not speak a per-request revision — it answers the probe with `-32601`, rejects it at the HTTP layer, or reports only initialize-era versions — the client falls back to the regular initialize handshake, so an opted-in client still connects to today's servers. Per-request negotiation applies to the Streamable HTTP and stdio transports; the legacy SSE transport always negotiates via initialize.

## 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 Expand Up @@ -487,6 +517,26 @@ client.setRequestHandler('roots/list', async () => {

When the available roots change, notify the server with {@linkcode @modelcontextprotocol/client!client/client.Client#sendRootsListChanged | client.sendRootsListChanged()}.

> [!NOTE]
> The `notifications/roots/list_changed` notification exists only on initialize-era (2025-11-25 and earlier) connections. The draft per-request revision removes it: servers there receive the client's current context with each request instead, so there is no standing roots state to invalidate. A server speaking a per-request revision ignores the notification.

### Request context

Handlers receive the request context (`ctx`) as their second argument. `ctx.mcpReq.protocolVersion` (from {@linkcode @modelcontextprotocol/client!index.BaseContext | BaseContext}) is the protocol version governing the request:

```ts source="../examples/client/src/clientGuide.examples.ts#requestContext_handler"
client.setRequestHandler('sampling/createMessage', async (request, ctx) => {
console.log(`Sampling request under MCP ${ctx.mcpReq.protocolVersion}:`, request.params.messages.at(-1));

// In production, send messages to your LLM here
return {
model: 'my-model',
role: 'assistant' as const,
content: { type: 'text' as const, text: 'Response from the model' }
};
});
```

## Error handling

### Tool errors vs protocol errors
Expand Down
10 changes: 10 additions & 0 deletions docs/migration-SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,14 @@ Request/notification params remain fully typed. Remove unused schema imports aft
| `ctx.mcpReq.elicitInput(params, options?)` | Elicit user input (form or URL) | `server.elicitInput(...)` from within handler |
| `ctx.mcpReq.requestSampling(params, options?)` | Request LLM sampling from client | `server.createMessage(...)` from within handler |

Per-request facts on the context (new in v2):

| Field | Description | v1 near-equivalent |
| ---------------------------- | ------------------------------------------------------------- | --------------------------------------------------------- |
| `ctx.mcpReq.protocolVersion` | Protocol version governing the request (both roles) | `server.getNegotiatedProtocolVersion()` from within handler |
| `ctx.client.capabilities` | Calling client's declared capabilities (only `ServerContext`) | `server.getClientCapabilities()` from within handler |
| `ctx.client.info` | Calling client's implementation info (only `ServerContext`) | `server.getClientVersion()` 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.
Expand Down Expand Up @@ -507,6 +515,8 @@ NOT removed (wire surface, kept for 2025-11-25 interop): task Zod schemas + infe

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

`initialize` version negotiation is restricted to `STATEFUL_PROTOCOL_VERSIONS` (released versions ≤ 2025-11-25): unknown/future strings in `supportedProtocolVersions` are ignored by the handshake, a list containing none makes `connect()` throw, and the server's fallback picks its first stateful entry. Revisions after 2025-11-25 negotiate per-request instead (arriving with a later release).

## 14. Runtime-Specific JSON Schema Validators (Enhancement)

The SDK now auto-selects the appropriate JSON Schema validator based on runtime:
Expand Down
10 changes: 8 additions & 2 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -632,11 +632,12 @@ server.setRequestHandler('tools/call', async (request, ctx) => {
});
```

Context fields are organized into 3 groups:
Context fields are organized into groups:

- **`mcpReq`** — request-level concerns: `id`, `method`, `_meta`, `signal`, `send()`, `notify()`, plus server-only `log()`, `elicitInput()`, and `requestSampling()`
- **`mcpReq`** — request-level concerns: `id`, `method`, `protocolVersion`, `_meta`, `signal`, `send()`, `notify()`, plus server-only `log()`, `elicitInput()`, and `requestSampling()`
- **`http?`** — HTTP transport concerns (undefined for stdio): `authInfo`, plus server-only `req`, `closeSSE`, `closeStandaloneSSE`
- **`sessionId?`** — transport session identifier (top-level)
- **`client`** — server-only: the calling client's declared `capabilities` and implementation `info`

`BaseContext` is the common base type shared by both `ServerContext` and `ClientContext`. `ServerContext` extends each group with server-specific additions via type intersection.

Expand Down Expand Up @@ -898,6 +899,11 @@ The 2025-11 experimental tasks side-channel woven through `Protocol` has been re

There is no migration path for the removed surface; it was always `@experimental`. Task support is planned to return as an opt-in extension plugin per SEP-2663.

### `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
Loading
Loading