feat(core,server): SEP-2577 type-stack @deprecated sweep; ResourceNotFoundError -32602; 2026-era + entry-hosting e2e cells#2333
Conversation
🦋 Changeset detectedLatest commit: 40670b2 The changes in this PR will be included in the next version bump. This PR includes changesets to release 7 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@modelcontextprotocol/client
@modelcontextprotocol/codemod
@modelcontextprotocol/server
@modelcontextprotocol/server-legacy
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
|
|
||
| verifies('typescript:hosting:entry:method-405', async ({ transport }: TestArgs) => { | ||
| const client = new Client({ name: 'method-405-client', version: '1.0.0' }); | ||
| await using wired = await wire(transport, echoFactory, client, { entry: { legacy: 'stateless' } }); | ||
|
|
||
| for (const method of ['PUT', 'PATCH']) { | ||
| const response = await wired.fetch!(wired.url!, { method }); | ||
| expect(response.status).toBe(405); | ||
| const body = (await response.json()) as { jsonrpc: string; error: { code: number; message: string } }; | ||
| expect(body.jsonrpc).toBe('2.0'); | ||
| expect(body.error.code).toBe(-32_000); | ||
| expect(body.error.message).toMatch(/method not allowed/i); | ||
| } | ||
| }); | ||
|
|
||
| verifies('typescript:hosting:entry:parse-error-400', async ({ transport }: TestArgs) => { |
There was a problem hiding this comment.
🟡 These three test bodies (method-405, parse-error-400, no-session-id) pass { entry: { legacy: 'stateless' } } unconditionally, and wire() spreads the caller-supplied entry option after the arm default — so on the entryModern arm this overrides the arm's legacy: 'reject' posture and both arms end up hosting the entry in the same stateless-legacy configuration. The new requirement rows claim these cells exercise the modern-only strict path, but that path is never actually hit; either select the posture per arm (as hosting-entry-auth.test.ts in this PR does: legacy: transport === 'entryStateless' ? 'stateless' : 'reject') or drop the override and rely on the arm defaults.
Extended reasoning...
What the bug is. The new entry-hosting HTTP-mechanics cells in test/e2e/scenarios/hosting-entry-http.test.ts are registered on both entry arms (entryStateless and entryModern), and the file header plus the new requirement rows in test/e2e/requirements.ts (~2405–2440) say the entryModern leg exercises the modern-only strict path of createMcpHandler (legacy: 'reject'). However, the bodies for typescript:hosting:entry:method-405 (line ~35), parse-error-400 (line ~49), and no-session-id (line ~141) call wire(transport, echoFactory, client, { entry: { legacy: 'stateless' } }) unconditionally.
The code path that triggers it. In test/e2e/helpers/index.ts (~line 151) the entry options are built as transport === 'entryStateless' ? { legacy: 'stateless', ...sniff.entry } : { legacy: 'reject', ...sniff.entry } — the caller-supplied entry option is spread after the arm default. So the explicit { legacy: 'stateless' } is a no-op on the entryStateless arm (it's already the default) and its only effect is to replace the entryModern arm's legacy: 'reject' with legacy: 'stateless'. Both arms then host the entry in the identical stateless-legacy posture.
Why it matters / step-by-step. Take parse-error-400 on the entryModern arm: (1) the requirement row says "the entry classifier reads no envelope claim from a non-JSON body, so the stateless legacy fallback delegates the parse error and the modern-only strict path emits it itself"; (2) with the override, the handler is built with legacy: 'stateless', so the no-JSON-body branch always delegates to legacyStatelessFallback — the strict path that emits the −32700 itself (the branch in createMcpHandler.ts taken when no legacy handler exists) never runs; (3) the assertion still passes because the fallback also answers 400/−32700 — so the cell is green while verifying the same configuration as the entryStateless cell. The same applies to method-405: with legacy: 'reject' the non-POST would be answered by the modern-only-method-not-allowed rejection, which the override prevents from ever running. no-session-id is the least affected (the wired 2026-pinned client still rides the modern per-request path; the override only leaves an unused fallback enabled), but it carries the same misleading override.
Why existing checks don't catch it. The probes assert HTTP status and JSON-RPC error shape, which happen to be the same on both legs, so the matrix shows two green cells per row even though they exercise one configuration. Nothing in the harness flags that an arm's documented posture was overridden.
How to fix. Either select the posture per arm exactly as the sibling file added in this same PR does — hosting-entry-auth.test.ts uses legacy: transport === 'entryStateless' ? 'stateless' : 'reject' — or simply drop the { entry: { legacy: 'stateless' } } override from these three bodies and rely on the wire() arm defaults, which already encode the intended per-arm posture. Either change makes the entryModern cells actually verify the modern-only strict behavior the requirement prose claims.
This is a test-coverage/documentation mismatch introduced by this PR rather than a runtime bug, so it doesn't affect shipped behavior — but the requirement rows added here record coverage that doesn't exist, which is exactly what the requirements registry is meant to prevent.
6f302eb to
e579ca8
Compare
f2a3ec6 to
f0848e2
Compare
e579ca8 to
ae54743
Compare
f0848e2 to
f4e4654
Compare
f4e4654 to
50ac3ce
Compare
ae54743 to
7eb29bc
Compare
50ac3ce to
47f361b
Compare
7eb29bc to
d0bbf34
Compare
47f361b to
096183a
Compare
d0bbf34 to
463350c
Compare
096183a to
75506c6
Compare
463350c to
109bc4f
Compare
75506c6 to
7e85c8a
Compare
109bc4f to
11a0e06
Compare
7e85c8a to
4cecdc8
Compare
11a0e06 to
8ef9239
Compare
|
|
||
| - **Timeouts** — the specification's per-operation timeout guidance section was removed; the SDK's `RequestOptions.timeout` and `DEFAULT_REQUEST_TIMEOUT_MSEC` are unchanged. | ||
| - **stdio shutdown** — the specification clarifies stdio shutdown/termination wording; `StdioServerTransport`/`StdioClientTransport` close semantics are unchanged. | ||
| - **Transports as bindings** — the specification reframes transports as bindings of one protocol; the SDK's `Transport` interface is unchanged. |
There was a problem hiding this comment.
🟡 The PR description's 'Small additives + doc closeout' section claims 'empty-string cursor pass-through verified', but no test in this diff or anywhere in the repo at the PR head sends an empty-string cursor — the only new test file (listOrdering.test.ts) never uses a cursor, and the pre-existing pagination tests only exercise non-empty cursors. The migration.md bullet itself is accurate (CursorSchema is z.string(), passed through verbatim), so this is a PR-description-accuracy nit: either add the small empty-cursor pass-through pin or drop the 'verified' wording so reviewers/changelog readers don't assume coverage that doesn't exist.
Extended reasoning...
What the issue is. The PR description's "Small additives + doc closeout" section lists two parallel bullets: "tools/list / prompts/list / resources/list deterministic registration-order verified at the wire (test pins existing behavior)" and "empty-string cursor pass-through verified". The first bullet is backed by a real deliverable in this diff (packages/server/test/server/listOrdering.test.ts). The second is not backed by anything: no test in this PR or anywhere in the repo at the PR head sends an empty-string cursor.
Verification. A repo-wide search at the PR head for an empty-string cursor assertion (cursor: '', \"cursor\": \"\", "empty cursor") across packages/server/test, packages/core/test, packages/client/test, test/integration, and test/e2e returns nothing. The existing cursor coverage (test/e2e/scenarios/pagination.test.ts and related cells) only exercises non-empty/opaque cursors plus an invalid-cursor rejection, and is untouched by this PR. The only new test file in this PR, listOrdering.test.ts, pins list ordering and never sends a cursor at all. The .changeset mention of empty-string cursors elsewhere in the repo belongs to a different PR.
Step-by-step reading a reviewer would make. (1) The sibling bullet says "verified at the wire (test pins existing behavior)" and ships a test. (2) The cursor bullet, in the same list, says "empty-string cursor pass-through verified". (3) The natural reading is that a similar small pin was added for the cursor case. (4) Searching the diff for any cursor test finds none — the claimed verification was not added. A future contributor or changelog reader relying on the description would believe coverage exists that doesn't, and a later refactor of cursor handling (e.g. a well-meaning cursor || undefined normalization) would not be caught by any pin.
Why the shipped artifacts are fine (and why this is only a nit). The new docs/migration.md bullet at this location is accurate: CursorSchema is z.string(), PaginatedRequestParamsSchema uses CursorSchema.optional(), and the client only gates on cursor !== undefined, so an empty-string cursor genuinely passes through verbatim and is not treated as end-of-results. Nothing shipped in the diff is wrong, and there is no runtime impact.
Addressing the counter-argument. One reviewer noted the bullet does not literally say "test pins" the way its sibling does, so "verified" could be read as the doc-closeout conclusion recorded in migration.md (schema/code inspection rather than a new test cell). That reading is possible, but the bullet sits in a list whose other verification item explicitly means a test pin, and "How Has This Been Tested" doesn't disambiguate — so at minimum the wording is ambiguous in a way this repo's conventions on prose-vs-diff accuracy try to avoid. Either resolution is cheap.
How to fix. Prose-only, pick one: (a) drop or reword the bullet (e.g. "empty-string cursor pass-through confirmed by inspection — CursorSchema is z.string(), no normalization on the path"), or (b) add the small pin the wording implies — a list request with cursor: '' reaching the handler verbatim (and/or a PaginatedRequestParamsSchema.parse({ cursor: '' }) round-trip), a few lines next to the new listOrdering test. Not blocking either way.
… stacks (SEP-2596 Tier-1) Finishes the runtime-surface @deprecated sweep started by #2268, mirroring the markers already present on the per-revision reference types (spec.types.2026-07-28.ts). JSDoc only — no wire/behavior change; everything remains fully functional during the deprecation window. - Logging type stack: LoggingLevel, SetLevelRequest(+Params), LoggingMessageNotification(+Params) — schemas + type aliases - Sampling type stack: ModelHint, ModelPreferences, ToolChoice, ToolUseContent, ToolResultContent, SamplingContent, SamplingMessage(+ContentBlock), CreateMessageRequest(+Params/+Result/ +ResultWithTools/+ParamsBase/+ParamsWithTools) — schemas + type aliases - includeContext: mark thisServer/allServers values @deprecated (SEP-2596) - Roots type stack: Root, ListRootsRequest/Result, RootsListChangedNotification — schemas + type aliases - registerClient: DCR @deprecated in favor of Client ID Metadata Documents (SEP-991); marker only, mechanics unchanged Push-style server-to-client request APIs (Server.ping/createMessage/ elicitInput/listRoots, ServerContext.mcpReq.elicitInput/requestSampling) intentionally untouched — handled separately on impl/deprecated-push-apis.
…sourceNotFoundError typed class
The 2026-07-28 spec requires -32602 (Invalid Params) for an unknown
resources/read URI; the v1.x SDK already emitted -32602 on earlier
revisions. The interim -32002 emission is reverted.
- Domain layer throws one neutral ResourceNotFoundError (data.uri).
- WireCodec.encodeErrorCode is the era-aware seam selecting the wire
code; both era codecs map -32002 -> -32602 (flat — no era branch
preserves -32002), wired at the protocol-layer handler-error path.
- ProtocolErrorCode.ResourceNotFound (-32002) stays importable as
receive-tolerated vocabulary; ProtocolError.fromError recognises BOTH
-32602 and -32002 by the data.uri shape (cross-bundle data-parse).
- Supersedes the pending fix-unknown-tool-protocol-error changeset's
-32002 wording; new changeset + migration.md/-SKILL.md entries.
- Conformance: sep-2164-resource-not-found burned down on both legs;
expected-failures.yaml NOTE updated to reflect the renumber-pending
state for the spec#2907 / conformance#353 codes.
- Docs: 5 pre-existing typedoc {@link} warnings resolved
(requestStateCodec/ServerOptions cross-module refs, RangeError
external, toNodeHandler McpHttpHandler) — docs:check is 0 warnings.
…; doc-only spec clarifications SF-02: pin tools/list, prompts/list, resources/list deterministic ordering (registration/insertion order) at the wire — verification was the open item; behavior already in place. migration.md: "Specification clarifications adopted" section folding the doc-only delta set so an audit of the revision's changelog against the guide is complete. The file:// path-sanitization MUST is recorded as server-author guidance only (no SDK helper — none of the peer SDKs ship one).
…tation/roots and list_changed publish
14 new addedInSpecVersion:'2026-07-28' rows on entryModern, supersedes-linked
to 19 retired 2025-shape source rows (removedInSpecVersion + supersededBy):
MRTR siblings (inputRequired() bodies):
sampling:mrtr:create:{basic,model-preferences,system-prompt,include-context}
elicitation:mrtr:form:{basic,action:decline,action:cancel,schema:primitives}
roots:mrtr:list:{basic,empty}
listen siblings (handler.notify.* bodies):
{tools,resources,prompts}:listen:list-changed
client:listen:auto-refresh
The 2025 source rows keep their bodies and stateful-transport cells on the
2025-11-25 axis; the 2026-07-28 axis is now covered by the dedicated sibling
bodies that exercise the era's actual API shape (inputRequired() / handler.notify)
instead of duplicate-registering the 2025 push-API body under a 2026 label.
…uth on createMcpHandler Entry-side siblings of the hosting:http / hosting:stateless / hosting:auth / hosting:context rows whose bodies hand-host their own server transport and so never reach createMcpHandler when given an entry arm. Ten new typescript:hosting:entry:* requirements + bodies, no edits to existing rows: HTTP mechanics through the harness-hosted entry (wired.fetch probes): - method-405 (both arms), parse-error-400 (both arms), no-session-id (both arms) - legacy-accept-406, legacy-content-type-415, legacy-protocol-version-header-400, legacy-protocol-version-default (entryStateless only — the legacy fallback delegates to the streamable HTTP server transport whose validation is unchanged; the modern per-request path does not apply Accept/Content-Type negotiation) Per-request HTTP context and bearer-auth composition (self-hosted createMcpHandler, matrix arm selects legacy posture + client pin): - ctx-http-req-headers (both arms): custom client header reaches ctx.http.req - auth:missing-401 (both arms): user-composed bearer gate answers 401 + WWW-Authenticate before the entry; factory never runs - auth:authinfo-propagates (both arms): authInfo handed to handler.fetch reaches ctx.http.authInfo and the factory's McpRequestContext.authInfo unchanged Test-only; no src/ changes; no existing scenario-body or knownFailure edits.
…ix cross-bundle example - JSDoc / changeset / migration.md now state that ProtocolError.fromError reconstructs ResourceNotFoundError for either code only when error.data carries `uri`; a bare -32002 without data.uri stays a generic ProtocolError. - migration.md example: drop the outer `instanceof ProtocolError` gate (it defeats the cross-bundle case the prose describes); the example now feeds code+data straight into fromError and tests the locally-created result.
8ef9239 to
40670b2
Compare
4cecdc8 to
e5f6143
Compare
SEP-2577
@deprecatedsweep on the public type stacks, theresources/readnot-found error-code flip to −32602 with a typedResourceNotFoundError, small additive verifications, and two batches of new e2e cells (2026-era sibling rows + dedicated entry-hosting cells). Stacked onfweinberger/on-codec-conformance.Motivation and Context
SEP-2577
@deprecatedmarkers (SEP-2596 Tier-1)Completes the
@deprecatedJSDoc sweep on the public runtime type stacks inschemas.ts+types.ts(the runtime API methods landed in #2268; the per-revision reference types in #2252). Covered: logging (LoggingLevel,SetLevelRequest,LoggingMessageNotificationand params/schemas), sampling (ModelHint/ModelPreferences/ToolChoice/ToolUseContent/ToolResultContent/SamplingContent/SamplingMessage/CreateMessageRequest/CreateMessageResultand variants),includeContextupgraded soft →@deprecated, roots (Root/ListRootsRequest/ListRootsResult/RootsListChangedNotification), andregisterClient(DCR → CIMD/SEP-991 marker only). JSDoc-only — zero wire/behavior diffs, no Zod constraint changes. Runtime warnings (SHOULD) are not in this PR; they ride a separate decision on timing.resources/readmiss → −32602 + typed errorNew typed
ResourceNotFoundError(data.uri, code −32602) is the one neutral domain errorMcpServer.resources/readthrows on a miss. NewWireCodec.encodeErrorCodeis the era-aware seam wired at the protocol-layer handler-error path; both era codecs flat-map −32002 → −32602 (no era branch preserves −32002).ProtocolErrorCode.ResourceNotFound(−32002) stays importable with receive-tolerated JSDoc;ProtocolError.fromErrorreconstructs the typed class for either code only whenerror.datacarriesuri(a bare −32002 withoutdata.uristays a genericProtocolError). HTTP status: in-band 200 both eras. The −32000 "Unsupported protocol version" literal is verified untouched;encodeErrorCodeunit test pins pass-through for −32000/−32001/−32003/−32004/−32042.Small additives + doc closeout
tools/list/prompts/list/resources/listdeterministic registration-order verified at the wire (test pins existing behavior)migration.md"Specification clarifications adopted (no SDK behavior change)" section{@link}warnings resolved →docs:check0 warningse2e: 2026-era sibling rows
14 new
addedInSpecVersion:'2026-07-28'rows onentryModern, supersedes-linked to 19 retired 2025-shape source rows:sampling:mrtr:create:{basic,model-preferences,system-prompt,include-context},elicitation:mrtr:form:{basic,action:decline,action:cancel,schema:primitives},roots:mrtr:list:{basic,empty},{tools,resources,prompts}:listen:list-changed,client:listen:auto-refresh. Cell delta 2740 → 2679 (−61 net = +14 genuineentryModerncells, −75 duplicate 2026-axis cells removed from the retired rows — those were registering identical runs to their 2025-11-25 cells because the body never pinned protocol version on stateful arms).e2e: entry-hosting cells
10 new
typescript:hosting:entry:*rows (no edits to existing rows): HTTP mechanics viawired.fetch(method-405,parse-error-400,no-session-id,legacy-accept-406,legacy-content-type-415,legacy-protocol-version-header-400,legacy-protocol-version-default) and bearer-auth +ctx.httpvia self-hostedcreateMcpHandler(ctx-http-req-headers,auth:missing-401,auth:authinfo-propagates). Cell delta 2679 → 2695 (+16). The full-OAuth-flow cell is deferred (targets a surface still under design).How Has This Been Tested?
errorSurfacePins.test.ts(accept-both data-parse contract),encodeContract.test.ts(flat-map + pass-through table both eras)mcp.test.ts:2731expectationResourceNotFound→InvalidParams+data.uri; e2eresources:read:unknown-uribody rewritten (asserts −32602 +data.uri+ accept-both viafromError+ importability + MUST-NOT-empty rider);eraParityErrorShapes.test.ts:202andperRequestTransport.test.ts:174expected−32_002→−32_602sep-2164-resource-not-foundburned down on both yaml files (3p/0f at this pin)Breaking Changes
None at the API surface. Behavior change:
resources/readon an unknown URI now answers −32602 (was −32002) on every era. Receive-side accepts both; changeset +migration.md/migration-SKILL.mdentries included.Types of changes
Checklist
Additional context
Deferred to follow-up: the client cache-freshness test battery (~100–150 LOC; surfaces exist, only the battery is unbuilt). The conformance-floor exit criterion ("draft suite green except flag-gated auth set") is reached after
fweinberger/on-renumberin this stack flips the spec#2907 error codes; at this tipserver:draftis 70p/4f with 3 of the 4 expected-fails being renumber fallout.