Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changeset/era-gate-explicit-schema-handlers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@modelcontextprotocol/client': patch
'@modelcontextprotocol/server': patch
---

A server can now serve, and a client can now call, `tasks/get` and `tasks/cancel` of the Tasks extension (SEP-2663) on a 2026-07-28 connection, when the handler is registered and the request is sent with an explicit schema. Every other method that a protocol revision removed is still refused. If one server factory serves both eras and such a handler is meant for 2025-era clients only, register it only when `ctx.era === 'legacy'`.
5 changes: 5 additions & 0 deletions .changeset/origin-scheme-entries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/server': minor
---

`allowedOrigins` and `validateOriginHeader` accept lowercase entries of the form `<scheme>://*`, such as `moz-extension://*` or `chrome-extension://*`, which admit every origin of that scheme. This lets a server admit MCP clients that run as a browser extension when the extension ID cannot be listed, as on Firefox, where it differs on every install. `http://*` and `https://*` are not honoured, and the defaults are unchanged.
13 changes: 13 additions & 0 deletions .changeset/package-license-field.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@modelcontextprotocol/client': patch
'@modelcontextprotocol/codemod': patch
'@modelcontextprotocol/core': patch
'@modelcontextprotocol/server': patch
'@modelcontextprotocol/server-legacy': patch
'@modelcontextprotocol/node': patch
'@modelcontextprotocol/express': patch
'@modelcontextprotocol/fastify': patch
'@modelcontextprotocol/hono': patch
---

The `license` field of the package manifests is now `Apache-2.0`; the `LICENSE` file shipped in each package carries the full terms, including the MIT text for earlier contributions. No code change.
9 changes: 9 additions & 0 deletions .changeset/probe-unusable-2xx-reply-message.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@modelcontextprotocol/client': patch
---

With `versionNegotiation` in `'auto'` or pin mode, a `server/discover` probe answered with a 2xx that carries no usable reply (a body that
is not JSON under `application/json`, a bare `204`, a missing or unaccepted content type) still rejects `connect()` with
`EraNegotiationFailed`; an empty SSE stream or a `202` surfaces as the probe timeout instead. The message now says
`the server answered with an unusable reply (...)` instead of reading like a network failure. To connect to a 2025 server behind a front that
answers the probe this way, pass `connect(transport, { prior: { kind: 'legacy' } })` or use `mode: 'legacy'`.
5 changes: 5 additions & 0 deletions .changeset/register-prompt-context-only-overload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/server': patch
---

`McpServer.registerPrompt()` now types the callback correctly when no `argsSchema` is given: its one parameter is the server context. Before, reading `ctx.mcpReq` there was a type error although it worked at runtime. Prompts registered with an `argsSchema` are unchanged.
5 changes: 5 additions & 0 deletions .changeset/sse-connect-retry-once.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/client': patch
---

`SSEClientTransport` now retries the SSE connection once after `onUnauthorized()` resolves, as documented. If the retry is also answered with 401, `start()` rejects with `SdkHttpError` (`ClientHttpAuthentication`) instead of calling `onUnauthorized()` again. A 401 on a later reconnect of a stream that had opened still gets one refresh.
95 changes: 0 additions & 95 deletions .github/workflows/update-spec-types.yml

This file was deleted.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
> [Issues](https://github.com/modelcontextprotocol/typescript-sdk/issues/new?template=v2-feedback.yml) are the most useful feedback right now — we'll reopen PRs as v2 stabilizes.

[![NPM Version - Server](https://img.shields.io/npm/v/%40modelcontextprotocol%2Fserver?label=%40modelcontextprotocol%2Fserver)](https://www.npmjs.com/package/@modelcontextprotocol/server)
[![NPM Version - Client](https://img.shields.io/npm/v/%40modelcontextprotocol%2Fclient?label=%40modelcontextprotocol%2Fclient)](https://www.npmjs.com/package/@modelcontextprotocol/client) ![MIT licensed](https://img.shields.io/npm/l/%40modelcontextprotocol%2Fserver)
[![NPM Version - Client](https://img.shields.io/npm/v/%40modelcontextprotocol%2Fclient?label=%40modelcontextprotocol%2Fclient)](https://www.npmjs.com/package/@modelcontextprotocol/client) ![Apache-2.0 licensed](https://img.shields.io/npm/l/%40modelcontextprotocol%2Fserver)

<details>
<summary>Table of Contents</summary>
Expand Down
4 changes: 2 additions & 2 deletions REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ review rounds and grows over time.
## Checklist

**Protocol & spec**
- Types match [`schema.ts`](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/draft/schema.ts) exactly (optional vs required fields)
- Types match the schema for the protocol revision being changed (`schema/<revision>/schema.ts` for released revisions; `schema/draft/schema.ts` only for unreleased work)
- Correct `ProtocolError` codes (enum `ProtocolErrorCode`); HTTP status codes match spec (e.g., 404 vs 410)
- Works for both stdio and Streamable HTTP transports — no transport-specific assumptions
- Cross-SDK consistency: check what `python-sdk` does for the same feature
Expand Down Expand Up @@ -61,7 +61,7 @@ When verifying spec compliance, consult the spec directly rather than relying on

- MCP documentation server: `https://modelcontextprotocol.io/mcp`
- Full spec text (single file, LLM-friendly): `https://modelcontextprotocol.io/llms-full.txt` — fetch to a temp file and grep for the relevant section
- Schema source of truth: [`schema.ts`](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/draft/schema.ts)
- Schema source of truth: the revision-matched `schema.ts` in `modelcontextprotocol/modelcontextprotocol` (`schema/<revision>/schema.ts` for released revisions; `schema/draft/schema.ts` only for unreleased work)

## Recurring Catches

Expand Down
27 changes: 20 additions & 7 deletions docs/migration/support-2026-07-28.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ falling back — see [Protocol versions](../protocol-versions.md). This holds ev
a front that answers the probe `403` but would pass `initialize`: relying on a legacy
fallback there is a deliberate non-goal — fix the auth wall (or pass credentials),
because auth status never selects an era. A `5xx` on the probe is a server failure,
also never era evidence: `connect()` rejects with `SdkHttpError(EraNegotiationFailed)`. Probe timeouts are **transport-aware**: on **stdio** a server that does not
also never era evidence: `connect()` rejects with `SdkHttpError(EraNegotiationFailed)`. A `2xx` probe answer that carries no usable reply (a body that is not JSON under `application/json`, a bare `204`, a missing or unaccepted content type) is not era evidence either: `connect()` rejects with `SdkError(EraNegotiationFailed)`; an empty SSE stream or a `202` surfaces as the probe timeout instead. 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`
(some legacy servers never respond to unknown pre-`initialize`
requests at all); on **HTTP** a probe timeout rejects with `SdkError(RequestTimeout)` —
Expand Down Expand Up @@ -349,11 +349,20 @@ mismatch is rejected as an entry/routing error (`-32022 Unsupported protocol ver
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
registry: an inbound `ping` or `tasks/result` 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 `ping` toward a 2026-era peer) throws
`SdkError(MethodNotSupportedByProtocolVersion)` before anything reaches the transport.

The one exception is the Tasks extension (SEP-2663), which keeps two names the
2025-11-25 revision had in core: `tasks/get` and `tasks/cancel`. A handler registered
for them with an explicit schema (`setRequestHandler('tasks/get', { params, result },
handler)`) is served, and a request sent with an explicit result schema
(`request({ method: 'tasks/get', params }, resultSchema)`) is sent, on every era. With
no handler, or with a method-keyed handler, they still get `-32601`. If one factory
serves both eras and such a handler is meant for 2025-era clients only, register it only
when `ctx.era === 'legacy'`; otherwise it answers 2026-07-28 requests too.

If you were on a v2 alpha and consumed wire schemas directly:

| v2-alpha pattern | Mechanical fix |
Expand Down Expand Up @@ -700,9 +709,13 @@ methods at compile time. `ResultTypeMap['tools/call']` is plain `CallToolResult`
maps still carry the `tasks/*` entries and the `CreateTaskResult` unions; narrow with
the `isCallToolResult` guard if you are pinned to one of those alphas. `2.0.0-alpha.4`
and later include the exclusion.) Where
task interop is genuinely required, use the explicit-schema custom-method form
(`request({ method: 'tasks/get', params }, GetTaskResultSchema)`). Inbound `tasks/*`
requests → `-32601`.
task interop is genuinely required, use the explicit-schema custom-method form on both
sides: `request({ method: 'tasks/get', params }, resultSchema)` to send and
`setRequestHandler('tasks/get', { params, result }, handler)` to serve, with the schemas
of the revision you are talking to (`GetTaskResultSchema` is the 2025-11-25 shape). On a
2026-era connection only `tasks/get` and `tasks/cancel`, the two names the Tasks
extension keeps, are served and sent this way; `tasks/result` and `tasks/list` stay
refused there. With no handler registered, inbound `tasks/*` requests get `-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).
Expand Down
2 changes: 2 additions & 0 deletions docs/protocol-versions.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ const cli = new Client(
);
```

A `2xx` answer that carries no usable reply (a body that is not JSON under `application/json`, a bare `204`, a missing or unaccepted content type) is not era evidence: `connect()` rejects with `SdkError(EraNegotiationFailed)` and the message says `the server answered with an unusable reply`. An empty SSE stream or a `202` surfaces as the probe timeout instead. To connect to a 2025 server behind a front that answers the probe this way, pass `connect(transport, { prior: { kind: 'legacy' } })`.

A probe timeout is transport-aware. On stdio a silent server is a legacy server, so `connect()` falls back to `initialize`; on HTTP silence is an outage, so `connect()` rejects with `SdkError(RequestTimeout)` instead of misreporting a dead server as legacy. One browser exception: an opaque CORS `TypeError` during the probe falls back to the legacy era, because deployed 2025 servers commonly have allow-lists that predate the 2026 headers.

Auth statuses are not era evidence either. An HTTP `401` or `403` rejecting the probe surfaces as a typed authorization failure, never the legacy fallback — and never as `EraNegotiationFailed`, so era-recovery flows keyed on that code (the [gateway guide](./advanced/gateway.md) recipe) cannot consume an auth wall. A plain `401` or `403` — with or without an `authProvider` — rejects `connect()` with an `SdkHttpError` carrying the status: code `ClientHttpAuthentication` for `401`, `ClientHttpForbidden` for `403`. One 403 shape is different: a `WWW-Authenticate` challenge with `error="insufficient_scope"` enters the Streamable HTTP transport's step-up flow regardless of provider, and with none it rejects with the flow's typed `InsufficientScopeError`. With a provider, a `401` (and a `403` `insufficient_scope` challenge) runs the auth flow first, and whatever escapes it reaches you as thrown, identity intact — the transport stamps errors at its auth seams (the `token()` read, `onUnauthorized` including custom callbacks, step-up), so `UnauthorizedError` for `finishAuth()`, the flow's typed failures (`OAuthError`, `InsufficientScopeError`, the 401-after-re-authentication diagnostic), and even an untyped crash inside a callback all propagate unchanged. A `5xx` rejecting the probe is a server failure, not era evidence: `connect()` rejects with `SdkHttpError(EraNegotiationFailed)` naming the status. These status-keyed rows read the transport's typed HTTP rejections — the SDK's Streamable HTTP transport surfaces them with the status attached; the legacy SSE client transport reports a non-2xx POST as a generic error, so over SSE a probe rejection surfaces as the generic `Version negotiation probe failed` connect error instead. Auth settles first, era second: a `401` never decides the era — the auth wall answers before the MCP layer ever sees `server/discover` — and the post-auth re-probe supplies the real era evidence.
Expand Down
21 changes: 16 additions & 5 deletions docs/servers/input-required.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,20 +174,23 @@ Sampling and roots are deprecated as of protocol revision 2026-07-28 (SEP-2577)
To run rounds in sequence, return an opaque `requestState` string alongside the requests. The client echoes it back byte-for-byte on the retry, and `ctx.mcpReq.requestState<State>()` reads its decoded payload on re-entry. Mint it with the codec from the next section.

```ts source="../../examples/guides/servers/input-required.examples.ts#requestState_mint"
const cacheConfirmationSchema = z.object({ confirm: z.boolean() });
const cacheScopeSchema = z.object({ scope: z.string() });

server.registerTool(
'wipe-cache',
{ description: 'Confirm, then pick a scope, then wipe', inputSchema: z.object({}) },
async (_args, ctx): Promise<CallToolResult | InputRequiredResult> => {
const state = ctx.mcpReq.requestState<{ step: string }>();

if (state?.step !== 'confirmed') {
const confirmed = acceptedContent<{ confirm: boolean }>(ctx.mcpReq.inputResponses, 'confirm');
const confirmed = acceptedContent(ctx.mcpReq.inputResponses, 'confirm', cacheConfirmationSchema);
if (confirmed?.confirm !== true) {
return inputRequired({
inputRequests: {
confirm: inputRequired.elicit({
message: 'Really wipe the cache?',
requestedSchema: { type: 'object', properties: { confirm: { type: 'boolean' } }, required: ['confirm'] }
requestedSchema: cacheConfirmationSchema
})
}
});
Expand All @@ -197,15 +200,23 @@ server.registerTool(
inputRequests: {
scope: inputRequired.elicit({
message: 'Which scope?',
requestedSchema: { type: 'object', properties: { scope: { type: 'string' } }, required: ['scope'] }
requestedSchema: cacheScopeSchema
})
},
requestState: await stateCodec.mint({ step: 'confirmed' })
});
}

const scope = acceptedContent<{ scope: string }>(ctx.mcpReq.inputResponses, 'scope');
return { content: [{ type: 'text', text: `Wiped ${scope?.scope ?? 'all'}` }] };
const scope = acceptedContent(ctx.mcpReq.inputResponses, 'scope', cacheScopeSchema);
if (scope === undefined) {
return inputRequired({
inputRequests: {
scope: inputRequired.elicit({ message: 'Which scope?', requestedSchema: cacheScopeSchema })
},
requestState: await stateCodec.mint({ step: 'confirmed' })
});
}
return { content: [{ type: 'text', text: `Wiped ${scope.scope}` }] };
}
);
```
Expand Down
Loading
Loading