Skip to content

feat(core,server): SEP-2577 type-stack @deprecated sweep; ResourceNotFoundError -32602; 2026-era + entry-hosting e2e cells#2333

Open
felixweinberger wants to merge 6 commits into
fweinberger/on-codec-conformancefrom
fweinberger/on-m12
Open

feat(core,server): SEP-2577 type-stack @deprecated sweep; ResourceNotFoundError -32602; 2026-era + entry-hosting e2e cells#2333
felixweinberger wants to merge 6 commits into
fweinberger/on-codec-conformancefrom
fweinberger/on-m12

Conversation

@felixweinberger

@felixweinberger felixweinberger commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

SEP-2577 @deprecated sweep on the public type stacks, the resources/read not-found error-code flip to −32602 with a typed ResourceNotFoundError, small additive verifications, and two batches of new e2e cells (2026-era sibling rows + dedicated entry-hosting cells). Stacked on fweinberger/on-codec-conformance.

Motivation and Context

SEP-2577 @deprecated markers (SEP-2596 Tier-1)

Completes the @deprecated JSDoc sweep on the public runtime type stacks in schemas.ts + types.ts (the runtime API methods landed in #2268; the per-revision reference types in #2252). Covered: logging (LoggingLevel, SetLevelRequest, LoggingMessageNotification and params/schemas), sampling (ModelHint/ModelPreferences/ToolChoice/ToolUseContent/ToolResultContent/SamplingContent/SamplingMessage/CreateMessageRequest/CreateMessageResult and variants), includeContext upgraded soft → @deprecated, roots (Root/ListRootsRequest/ListRootsResult/RootsListChangedNotification), and registerClient (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/read miss → −32602 + typed error

New typed ResourceNotFoundError (data.uri, code −32602) is the one neutral domain error McpServer.resources/read throws on a miss. New WireCodec.encodeErrorCode is 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.fromError reconstructs the typed class for either code only when error.data carries uri (a bare −32002 without data.uri stays a generic ProtocolError). HTTP status: in-band 200 both eras. The −32000 "Unsupported protocol version" literal is verified untouched; encodeErrorCode unit test pins pass-through for −32000/−32001/−32003/−32004/−32042.

Small additives + doc closeout

  • tools/list / prompts/list / resources/list deterministic registration-order verified at the wire (test pins existing behavior)
  • empty-string cursor pass-through verified
  • migration.md "Specification clarifications adopted (no SDK behavior change)" section
  • 5 pre-existing {@link} warnings resolved → docs:check 0 warnings

e2e: 2026-era sibling rows

14 new addedInSpecVersion:'2026-07-28' rows on entryModern, 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 genuine entryModern cells, −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 via wired.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.http via self-hosted createMcpHandler (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?

  • new unit tests: errorSurfacePins.test.ts (accept-both data-parse contract), encodeContract.test.ts (flat-map + pass-through table both eras)
  • existing-test modifications (all strengthened or relocated, none weakened): mcp.test.ts:2731 expectation ResourceNotFoundInvalidParams + data.uri; e2e resources:read:unknown-uri body rewritten (asserts −32602 + data.uri + accept-both via fromError + importability + MUST-NOT-empty rider); eraParityErrorShapes.test.ts:202 and perRequestTransport.test.ts:174 expected −32_002−32_602
  • conformance: sep-2164-resource-not-found burned down on both yaml files (3p/0f at this pin)
  • Full gates at tip: typecheck:all, lint:all, docs:check 0 warnings, build:all; package suites core 1210 / codemod 350 / server-legacy 162 / server 334 / client 535 / hono 12 / fastify 22 / express 40 / node 88 / shared 2; integration 348; e2e 2490p/205xf/2695 cells, 0 unexpected; conformance client / client:all / client:2026 / server / server:draft / server:2026 all rc=0

Breaking Changes

None at the API surface. Behavior change: resources/read on an unknown URI now answers −32602 (was −32002) on every era. Receive-side accepts both; changeset + migration.md / migration-SKILL.md entries included.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

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-renumber in this stack flips the spec#2907 error codes; at this tip server:draft is 70p/4f with 3 of the 4 expected-fails being renumber fallout.

@felixweinberger felixweinberger requested a review from a team as a code owner June 19, 2026 14:18
@changeset-bot

changeset-bot Bot commented Jun 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 40670b2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 7 packages
Name Type
@modelcontextprotocol/core Minor
@modelcontextprotocol/server Major
@modelcontextprotocol/client Minor
@modelcontextprotocol/express Major
@modelcontextprotocol/fastify Major
@modelcontextprotocol/hono Major
@modelcontextprotocol/node Major

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

@pkg-pr-new

pkg-pr-new Bot commented Jun 19, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@2333

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/codemod@2333

@modelcontextprotocol/server

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server@2333

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server-legacy@2333

@modelcontextprotocol/express

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/express@2333

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/fastify@2333

@modelcontextprotocol/hono

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/hono@2333

@modelcontextprotocol/node

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/node@2333

commit: 40670b2

Comment thread packages/core/src/types/errors.ts
Comment on lines +32 to +47

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) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread packages/server/src/server/fileUri.ts Outdated
@felixweinberger felixweinberger force-pushed the fweinberger/on-codec-conformance branch from f2a3ec6 to f0848e2 Compare June 19, 2026 14:37
@felixweinberger felixweinberger force-pushed the fweinberger/on-codec-conformance branch from f0848e2 to f4e4654 Compare June 19, 2026 14:50
Comment thread packages/server/src/server/fileUri.ts Outdated
Comment thread .changeset/resource-not-found-32602.md Outdated
@felixweinberger felixweinberger force-pushed the fweinberger/on-codec-conformance branch from f4e4654 to 50ac3ce Compare June 22, 2026 16:54
Comment thread packages/core/src/exports/public/index.ts
Comment thread .changeset/resource-not-found-32602.md Outdated
@felixweinberger felixweinberger force-pushed the fweinberger/on-codec-conformance branch from 50ac3ce to 47f361b Compare June 22, 2026 17:53
@felixweinberger felixweinberger changed the title feat(core,server): SEP-2577 type-stack @deprecated sweep; ResourceNotFoundError -32602; resolveFileUriPath; 2026-era + entry-hosting e2e cells feat(core,server): SEP-2577 type-stack @deprecated sweep; ResourceNotFoundError -32602; 2026-era + entry-hosting e2e cells Jun 22, 2026
@felixweinberger felixweinberger force-pushed the fweinberger/on-codec-conformance branch from 47f361b to 096183a Compare June 22, 2026 20:12
@felixweinberger felixweinberger force-pushed the fweinberger/on-codec-conformance branch from 096183a to 75506c6 Compare June 22, 2026 20:15
@felixweinberger felixweinberger force-pushed the fweinberger/on-codec-conformance branch from 75506c6 to 7e85c8a Compare June 22, 2026 21:16
@felixweinberger felixweinberger force-pushed the fweinberger/on-codec-conformance branch from 7e85c8a to 4cecdc8 Compare June 22, 2026 21:39
Comment thread docs/migration.md

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.
@felixweinberger felixweinberger force-pushed the fweinberger/on-codec-conformance branch from 4cecdc8 to e5f6143 Compare June 22, 2026 21:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant