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
10 changes: 10 additions & 0 deletions .changeset/add-version-negotiation-option.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@modelcontextprotocol/client': minor
'@modelcontextprotocol/core': minor
---

Add opt-in protocol version negotiation on `ClientOptions.versionNegotiation`. The default is unchanged: without the option (or with `mode: 'legacy'`) the client performs today's 2025 connect sequence byte-identically. `mode: 'auto'` probes the server with `server/discover` at
connect time and conservatively falls back to the plain legacy `initialize` handshake on the same connection unless the outcome is definitive modern evidence; a network outage rejects with a typed connect error, and a probe timeout is transport-aware — on stdio it indicates
a legacy server and falls back to `initialize` on the same stream, on HTTP it rejects with a typed timeout error.
`mode: { pin: '<version>' }` negotiates exactly the pinned modern revision with no fallback. Probe policy lives under `probe: { timeoutMs? }` — the probe inherits the standard request timeout. The probe's `MCP-Protocol-Version`/`Mcp-Method` headers derive from the probe
message body; the transport version slot is never touched during negotiation, so legacy-era traffic carries zero 2026 headers by construction. Adds the `SdkErrorCode.EraNegotiationFailed` code for negotiation-phase connect failures.
7 changes: 7 additions & 0 deletions .changeset/codec-era-gates.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 `SdkErrorCode.MethodNotSupportedByProtocolVersion`: a typed local error raised before anything reaches the transport when a spec method is sent toward a peer whose negotiated protocol version's wire era does not define it (for example `tasks/get` toward a 2026-07-28 peer). The protocol layer now resolves a per-era wire codec from the connection's negotiated protocol version (instance state on `Client`/`Server`, with the legacy era as the pre-negotiation default) and resolves per-method schemas at dispatch time instead of registration time; an edge classification on an inbound message is validated against that instance era, and a mismatch is rejected as an entry/routing error. Behavior on existing (2025-era) connections is unchanged.
15 changes: 15 additions & 0 deletions .changeset/codec-split-wire-break.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@modelcontextprotocol/core': major
'@modelcontextprotocol/client': major
'@modelcontextprotocol/server': major
---

Split the wire layer into per-era codecs and make protocol-revision deletions physical. Deliberate wire/schema behavior changes (see docs/migration.md "Per-era wire codecs"):

- `resultType` is no longer modeled by any neutral wire schema: `EmptyResultSchema` (strict) now rejects `{resultType}` bodies; on 2025-era connections a foreign `resultType` is stripped before validation instead of rejected; the member exists only inside the 2026-era codec, which requires it.
- `CallToolResult.content` / `ToolResultContent.content` are required at the wire boundary (`content.default([])` removed): handler results without `content` are rejected with `-32602` instead of silently defaulted, and content-less wire results fail the client parse loudly.
- Custom (3-arg) handlers now receive `_meta` minus the reserved envelope keys instead of having it deleted before params validation.
- `specTypeSchemas` re-scoped to the neutral model: result validators no longer accept `resultType`; task message-type validators and `RequestMetaEnvelope` left the public set (`SpecTypeName` narrowed).
- Role aggregate types/schemas (`ClientRequest`, `ServerResult`, …) no longer carry task vocabulary; the deprecated `Task*` types remain importable unchanged.
- Era-mismatched spec methods fail physically: inbound era-deleted methods get `-32601` even with a handler registered; outbound sends throw `SdkErrorCode.MethodNotSupportedByProtocolVersion` locally.
- Value guards (`isCallToolResult`, …) are documented as neutral-shape consumer checks, not wire validators.
5 changes: 5 additions & 0 deletions .changeset/codemod-flag-removed-task-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/codemod': patch
---

The v1→v2 codemod no longer rewrites `taskStore`/`taskMessageQueue` McpServer constructor options into `capabilities.tasks` — that target does not exist in v2 (the experimental tasks runtime was removed, SEP-2663). The codemod now leaves the code untouched and emits an action-required diagnostic telling migrators to remove the option, matching the removal guidance already given for `experimental/tasks` imports and the migration guide.
10 changes: 0 additions & 10 deletions .changeset/extract-task-manager.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/fix-task-session-isolation.md

This file was deleted.

7 changes: 7 additions & 0 deletions .changeset/hide-wire-only-members.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@modelcontextprotocol/core': major
'@modelcontextprotocol/client': major
'@modelcontextprotocol/server': major
---

Hide wire-only protocol members from the public surface, at the type level and at runtime. `resultType` (the 2026-07-28 result discrimination field) is no longer declared on any public result type — the wire schemas keep parsing it, and the client funnel now consumes it raw-first: `'complete'` results are stripped to the public shape and any other kind (e.g. `input_required`) rejects with the new `SdkErrorCode.UnsupportedResultType` instead of masking into an empty success. The reserved `_meta` envelope keys are lifted out of inbound requests and notifications before handlers run, and the multi-round-trip retry fields (`inputResponses`, `requestState`) out of inbound requests only (the spec reserves those names on client-initiated requests; notification params keep them), so handler params keep the 2025-era shape; for requests the lifted material surfaces at `ctx.mcpReq.envelope`, `ctx.mcpReq.inputResponses`, and `ctx.mcpReq.requestState` (notifications have no ctx — their lifted envelope keys are not surfaced). High-level client/server methods now return the named public result types (`Promise<CallToolResult>` etc.). Task wire vocabulary stays importable but is `@deprecated` and excluded from the typed method maps (`RequestMethod`/`RequestTypeMap`/`ResultTypeMap`/`NotificationTypeMap`), and `callTool` is typed as plain `CallToolResult`. See docs/migration.md "Wire-only protocol members hidden from the public types".
6 changes: 6 additions & 0 deletions .changeset/node-forward-supported-versions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@modelcontextprotocol/node': patch
---

Forward `setSupportedProtocolVersions` from `NodeStreamableHTTPServerTransport` to the wrapped Web Standard transport. Previously a server's `supportedProtocolVersions` option never reached the Node adapter's `MCP-Protocol-Version` header validation, which silently kept
validating against the default version list.
5 changes: 5 additions & 0 deletions .changeset/spec-corpus-and-leak-net.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/core': patch
---

Test-only hardening, no runtime changes: a spec example corpus harness (the draft revision's 86 example directories vendored from the specification repository plus a frozen hand-built 2025-11-25 corpus, with rejection-side fixtures routed through real dispatch), a cross-bundle typed-error recognition guard, and extended end-to-end draft-vocabulary leak coverage for hosted transports, SSE streams, and compatibility fallback paths.
7 changes: 7 additions & 0 deletions .changeset/spec-types-2026-repin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@modelcontextprotocol/core': patch
'@modelcontextprotocol/client': patch
'@modelcontextprotocol/server': patch
---

Internal: regenerate the 2026-07-28 spec reference types from the latest draft schema (`DiscoverResult` now extends `CacheableResult`; `ElicitationCompleteNotificationParams` extracted as a named interface) and document the anchor lifecycle policy. Released-revision spec-type generation is now pinned to a fixed spec commit; draft anchors keep floating via the nightly refresh PRs. No public API or runtime behavior changes.
11 changes: 11 additions & 0 deletions .changeset/wire-server-discover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@modelcontextprotocol/core': minor
'@modelcontextprotocol/server': minor
'@modelcontextprotocol/client': minor
---

Wire `server/discover` (protocol revision 2026-07-28) into the typed request funnel and serve it era-aware. The request joins `ClientRequestSchema`/`ServerResultSchema`/`ResultTypeMap` (per-era availability stays with the wire registries: only the 2026-era registry serves
it), and `Client.discover()` issues it as a typed request on 2026-era connections. A `Server` whose `supportedProtocolVersions` list carries a modern (2026-07-28+) revision installs the `server/discover` handler, advertising ONLY its modern revisions and excluding the
listChanged/subscribe-class capabilities until the `subscriptions/listen` flow ships; servers with today's default list are unchanged and keep answering `-32601`. The `initialize` handshake is now era-aware in the other direction: its accept check and counter-offer consult
only the legacy subset of the supported versions — a 2026-era revision is never negotiated via `initialize` — so a 2025-era client can never be offered a 2026 version string; with the default list this is byte-identical to previous behavior. Serving the 2026 revision to
ordinary HTTP/stdio traffic arrives with an upcoming server-side entry point: today the negotiation surface is client-side, and `mode: 'auto'` falls back cleanly against current SDK servers.
2 changes: 1 addition & 1 deletion .github/workflows/conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Conformance Tests

on:
push:
branches: [main]
branches: [main, v2-2026-07-28]
pull_request:
workflow_dispatch:

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ on:
push:
branches:
- main
- v2-2026-07-28
pull_request:
workflow_dispatch:

Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/update-spec-types.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
# Nightly refresh of the draft-tracking spec anchor (2026-07-28).
#
# Anchor lifecycle (see packages/core/src/types/README.md for the full policy):
# - Draft anchors float: this job regenerates the draft-tracking anchor from the
# latest upstream draft schema and, on drift, opens a refresh PR for review.
# It only ever proposes — it never merges.
# - Released anchors are frozen: generation for released revisions is pinned in
# scripts/fetch-spec-types.ts (RELEASED_REVISION_PINS) and is not refreshed by
# this job. Repinning a released revision — including the freeze of a newly
# published revision, when its schema moves out of schema/draft/ — must land
# in the same commit that retargets this workflow.
name: Update Spec Types

on:
Expand Down
8 changes: 8 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ pnpm-lock.yaml
**/src/types/spec.types.2025-11-25.ts
**/src/types/spec.types.2026-07-28.ts

# Spec example corpora: vendored verbatim from the spec repository
# (fetch:spec-examples) or hand-built and frozen - byte-faithful artifacts.
packages/core/test/corpus/fixtures/

# Schema twins: raw upstream schema.json bytes (fetch:schema-twins), locked to
# manifest.json by sha256 in schemaTwinConformance - reformatting breaks the lock.
packages/core/test/corpus/schema-twins/

# Batch test cloned repos and results
packages/codemod/batch-test/repos
packages/codemod/batch-test/results
Expand Down
49 changes: 49 additions & 0 deletions docs/behavior-surface-pins.md
Original file line number Diff line number Diff line change
@@ -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.md` / `docs/migration-SKILL.md` 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/test/types/errorSurfacePins.test.ts` |
| Schema strict/strip/loose boundaries, key existence | `packages/core/test/types/schemaBoundaryPins.test.ts` |
| Published package set, export maps, ESM-only topology | `packages/core/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.
29 changes: 28 additions & 1 deletion docs/migration-SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,34 @@ The 2025-11 task side-channel through `Protocol` is removed (was always `@experi

`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`.
NOT removed (wire surface, kept for 2025-11-25 interop, now `@deprecated`): task Zod schemas + inferred types (`Task`, `TaskStatus`, `TaskMetadata`, `RelatedTaskMetadata`, `CreateTaskResult`, `GetTask*`, `ListTasks*`, `CancelTask*`, `TaskStatusNotification*`, `TaskAugmentedRequestParams`), task members of the request/result/notification union types, the `tasks` capability key, `isTaskAugmentedRequestParams`, `RELATED_TASK_META_KEY`. Inbound `tasks/*` requests → `-32601`.

Task methods are excluded from the typed method maps: `RequestMethod`/`RequestTypeMap`/`ResultTypeMap` have no `tasks/*` entries and `NotificationMethod`/`NotificationTypeMap` have no `notifications/tasks/status`, so the method-keyed overloads of `request()`, `ctx.mcpReq.send()`, `setRequestHandler()`, `setNotificationHandler()` reject task methods at compile time. Mechanical fix where task interop is genuinely required: pass an explicit schema (`request({ method: 'tasks/get', params }, GetTaskResultSchema)`-style custom-method form). `ResultTypeMap['tools/call']` is plain `CallToolResult` (no `| CreateTaskResult`); same for `sampling/createMessage` and `elicitation/create`.

## 12b. Wire-only members hidden from public types

`resultType` (2026-07-28 result discrimination) is no longer declared on any public result type; the SDK parses and consumes it internally. The reserved `_meta` envelope keys (`io.modelcontextprotocol/{protocolVersion,clientInfo,clientCapabilities,logLevel}`) and retry fields (`inputResponses`, `requestState`) appear in no public params/result type. `RequestMetaEnvelope` and the `*_META_KEY` constants remain exported.

| Pattern in v2-alpha code | 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`, …) |

Runtime counterpart: inbound reserved envelope keys are lifted out of `params._meta` before handlers run — on requests they are readable at `ctx.mcpReq.envelope` (typed `Partial<RequestMetaEnvelope>`, keys present only as received); on notifications there is no ctx, so the lifted envelope keys are dropped and NOT surfaced anywhere. Retry fields (`inputResponses`/`requestState`) lift from REQUEST top-level params only, to `ctx.mcpReq.inputResponses` / `ctx.mcpReq.requestState`; notification params are never touched. On a 2026-era exchange a response carrying a non-`complete` `resultType` rejects with `SdkError` code `UNSUPPORTED_RESULT_TYPE` (kind in `error.data.resultType`), while on a 2025-era connection a foreign `resultType` is stripped before validation; the serving wire era is the instance's negotiated protocol version (connection state), and `MessageExtraInfo.classification` is only validated against it at dispatch (a mismatch is rejected as an entry/routing error). Collision note for 2025-era peers: 2025-11-25 reserves the `io.modelcontextprotocol/` `_meta` prefix but NOT the bare names `inputResponses`/`requestState`, so a 2025 peer's custom-method request using those names as ordinary params has them lifted out of `request.params` (recoverable via ctx; everything else passes through untouched).

## 12c. Per-era wire codecs (physical deletions + stricter wire schemas)

The wire layer is split into per-era codecs (2025-era = 2024-10-07 … 2025-11-25; 2026-era = 2026-07-28). Era-mismatched spec methods fail physically: inbound -> `-32601` even with a handler registered; outbound -> `SdkError` code `METHOD_NOT_SUPPORTED_BY_PROTOCOL_VERSION` before the transport.

| Pattern in v2-alpha code | Mechanical fix |
| ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
| tool handler returns without `content` | add `content: []` (or real content) — results without it are rejected `-32602`, no longer defaulted |
| parsing wire bytes with `EmptyResultSchema` that may carry `resultType` | strip `resultType` first (the schema now rejects it as an unknown key) |
| strict custom-handler params schema (3-arg `setRequestHandler`/`setNotification…`) | add optional `_meta` to the schema (or strip it) — `_meta` is now passed through minus reserved keys |
| `specTypeSchemas`/`SpecTypeName` references to task message types or `RequestMetaEnvelope` | remove — these validators left the public set (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 |

## 13. Behavioral Changes

Expand Down
Loading
Loading