Skip to content

feat(server): complete the stateless call — result stamping, cache hints, and the error status matrix#2306

Merged
felixweinberger merged 8 commits into
v2-2026-07-28from
fweinberger/stateless-call-completion
Jun 16, 2026
Merged

feat(server): complete the stateless call — result stamping, cache hints, and the error status matrix#2306
felixweinberger merged 8 commits into
v2-2026-07-28from
fweinberger/stateless-call-completion

Conversation

@felixweinberger

Copy link
Copy Markdown
Contributor

Completes the modern (2026-07-28) stateless call on top of the per-request serving entry: results of the cacheable operations are stamped and filled with the revision's required cache fields, servers can configure those values through new cache-hint options, the error→HTTP status table gains its remaining rows, and the -32003 missing-client-capability error ships as a typed class with a pre-dispatch gate at the HTTP entry.

Motivation and Context

The 2026-07-28 draft requires every result to carry a resultType discriminator and requires ttlMs/cacheScope on the cacheable results (tools/list, prompts/list, resources/list, resources/templates/list, resources/read, server/discover). The serving core landed in the previous PRs of this stack; this PR makes the responses it produces spec-complete and gives servers a way to express a real cache policy, while keeping 2025-era responses byte-identical.

  • Result encode contract (2026 era): the era codec's encodeResult now stamps resultType (handler-provided values pass through only for the multi round-trip methods, whose results may be input_required; a stray non-complete value elsewhere fails loudly as an internal error) and then fills ttlMs/cacheScope on complete results of the cacheable operations — valid handler-returned values first, then the configured hint, then ttlMs: 0 / cacheScope: 'private'. The 2025-era codec remains the identity; a suppression test suite pins everything that must never be stamped (legacy traffic, input_required results, non-cacheable operations, era-removed methods, client-emitted requests, error responses), with consumer-authored cache fields on 2025-era results passing through unchanged.
  • Cache hints: ServerOptions.cacheHints (per operation, for the results the SDK builds itself) and registerResource(..., { cacheHint }) (per resource). Optional, validated at configuration time (RangeError), and carried to the encode seam on a never-serialized property so 2025-era responses cannot change.
  • Error→HTTP status table: explicit parse-error and invalid-request rows join the origin-keyed table; handler-originated errors keep answering in-band on HTTP 200 (no blanket 500), invalid-params stays unmapped (the envelope validation rejection carries its own 400), and the disputed header/body-mismatch cells remain parameterized.
  • MissingRequiredClientCapabilityError (-32003): typed error class with data.requiredCapabilities, recognized from the error code and data shape. createMcpHandler refuses a request that requires a client capability missing from the request's declared clientCapabilities before dispatch, with HTTP status 400. No method served on the 2026-07-28 registry has a static requirement yet, so production behavior is unchanged until such methods exist; the gate, error shape and status are pinned by tests.

How Has This Been Tested?

  • New unit suites for the encode contract (stamp/fill ordering, pass-through and loud-failure rows, the validity gate, hint precedence), the suppression rules, the cache-hint configuration (validation, precedence, list non-leak, 2025-era unaffected), the status table, the typed error (data-parse recognition), and the entry-level capability gate.
  • Full repo gates: typecheck, lint, build, docs build, every package suite, integration, the full e2e matrix (2025-era cells byte-identical), and the conformance suites against the published referee with no expected-failures changes.

Breaking Changes

None. All new options and exports are additive and optional; 2026-07-28 responses gain the fields the draft requires, and 2025-era responses are unchanged (verified by the negative-vocabulary suites and the unchanged e2e/conformance baselines).

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

  • The error-code assignments for the header/body mismatch rejections remain provisional upstream; the table keeps those cells parameterized, unchanged from the previous PRs in this stack.
  • The cache-hint values travel from the (era-blind) configuration layer to the (era-aware) encode step on a symbol-keyed property that JSON serialization never emits; this is what keeps 2025-era responses byte-identical even for fully configured servers.

@felixweinberger felixweinberger requested a review from a team as a code owner June 16, 2026 06:46
@changeset-bot

changeset-bot Bot commented Jun 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cefb94b

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 Minor
@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 16, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

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

@modelcontextprotocol/codemod

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

@modelcontextprotocol/server

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

@modelcontextprotocol/server-legacy

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

@modelcontextprotocol/express

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

@modelcontextprotocol/fastify

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

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: cefb94b

@pkg-pr-new

pkg-pr-new Bot commented Jun 16, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

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

@modelcontextprotocol/codemod

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

@modelcontextprotocol/server

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

@modelcontextprotocol/server-legacy

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

@modelcontextprotocol/express

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

@modelcontextprotocol/fastify

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

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: b9cd16e

@felixweinberger felixweinberger force-pushed the fweinberger/create-mcp-handler branch from 37c10ad to 7a1300e Compare June 16, 2026 13:19
@felixweinberger felixweinberger force-pushed the fweinberger/create-mcp-handler branch from 7a1300e to fbb552a Compare June 16, 2026 14:02
Base automatically changed from fweinberger/create-mcp-handler to v2-2026-07-28 June 16, 2026 14:07
…e the ladder status table

Adds MissingRequiredClientCapabilityError (-32003) with data.requiredCapabilities,
recognized by ProtocolError.fromError from the error code and data shape, plus the
shared helpers for computing which required client capabilities a request's declared
capabilities are missing (with a method-keyed requirement table for the pre-dispatch
gate, currently empty). The ladder status table gains explicit parse-error and
invalid-request rows; handler-originated codes keep answering in-band on HTTP 200 and
invalid params stays unmapped (the envelope rung carries its own 400).
…ode seam

The 2026-era codec's encodeResult now applies the outbound encode contract as two
pure steps: the resultType stamp (handler-provided values pass through only for the
multi round-trip methods whose result vocabulary goes beyond 'complete'; a stray
non-complete value elsewhere fails loudly) followed by the cache fill, which gives
complete results of the six cacheable operations the revision's required ttlMs and
cacheScope fields. Values are resolved most specific author first: valid
handler-returned fields, then a configured cache hint attached by the server layer
through a never-serialized symbol carrier, then the conservative defaults
(ttlMs 0, cacheScope private). The 2025-era codec remains the identity, and a
suppression suite pins what is never stamped: legacy traffic, input_required
results, non-cacheable operations, era-removed methods, client-emitted requests,
and error responses.
…ults

Adds ServerOptions.cacheHints (per-operation hints for the results the SDK builds
itself, including server/discover and the list operations) and an optional cacheHint
member on the registerResource config (per-resource hints for resources/read).
Configured hints are validated when configured (RangeError on an invalid ttlMs or
cacheScope) and are attached to results on a symbol-keyed carrier that only the
2026-era encode seam reads, so cache fields returned by a handler always win and
2025-era responses never change. CacheHint/CacheScope are exported from the server
package.
…fore dispatch

createMcpHandler now checks each modern request against the method-keyed client
capability requirement table before constructing a per-request instance: when the
request's declared clientCapabilities are missing a required capability, the entry
answers the typed -32003 error with data.requiredCapabilities and HTTP 400, echoing
the request id. Emitting at the entry (rather than at handler time) pins the
spec-mandated 400 independently of how dispatch- and handler-produced errors are
mapped to HTTP statuses. No method served on the 2026-07-28 registry has a static
requirement yet, so production behavior is unchanged until such methods exist.
…ng-capability error

Migration-guide sections and changesets for the new ServerOptions.cacheHints /
registerResource cacheHint options, the always-present ttlMs/cacheScope fields on
cacheable 2026-07-28 results, and MissingRequiredClientCapabilityError.
…y-rung metadata

- isValidCacheTtlMs now requires a safe integer: the wire schemas validate
  ttlMs as an integer within the safe range, so a handler-returned value
  like 1e20 was emitted only to fail wire validation downstream. Such values
  now fall through to the configured hint / defaults like other invalid
  values, and the configuration-time RangeError covers them too. The
  validity-gate tests gain the unsafe-integer, NaN and Infinity cases.
- The validation ladder's client-capabilities rung descriptor said
  evaluatedAt: 'dispatch' while the implemented gate runs at the HTTP entry,
  pre-dispatch. The descriptor now uses a dedicated 'pre-dispatch' value and
  its rationale qualifies the documented precedence: with the requirement
  table empty the order is preserved vacuously; once a served method gains a
  requirement entry, the entry must consult the method registry before the
  gate for the documented order to stay observable. Data/doc only — the gate
  itself is unchanged.
- One JSDoc sentence on the resultType pass-through row: non-'complete'
  strings returned by handlers of the multi round-trip methods are forwarded
  verbatim, so their validity is the handler author's responsibility.
- ServerOptions.cacheHints re-spelled the six cacheable operations as a
  string-literal union; it is now keyed by CacheableResultMethod so the
  closed operation list has a single source of truth (the option's JSDoc
  still names the operations). No new exports; the accepted keys are
  unchanged.
- Add per-operation cache-hint coverage for prompts/list, resources/list and
  resources/templates/list, completing the op-level surface (tools/list,
  resources/read and server/discover were already covered).
- Changeset wording: note that registerResource now interprets a cacheHint
  key in its config object (observable for untyped callers), and clarify
  that the -32003 pre-dispatch gate changes no behavior until a served
  method actually carries a capability requirement.
…onfigured authors

- attachCacheHintFallback previously kept an already-attached per-resource
  hint as a whole object, so a partial registerResource cacheHint (say only
  cacheScope) shadowed the per-operation ServerOptions.cacheHints entry
  entirely and its ttlMs fell back to the default 0. The two configured
  hints are now combined per field: for each of ttlMs and cacheScope the
  per-resource value wins when set and the per-operation value fills the
  fields it leaves unset, with the defaults only applying to fields neither
  author sets. Handler-returned values, the encode-time validity gate, the
  configuration-time RangeError and the defaults are unchanged.
- Tests: add the field-mixing cases (per-resource cacheScope with
  per-operation ttlMs and the reverse, both authors setting the same fields,
  a field neither sets, resources/read with no configuration at all) to the
  cache-hint suite, plus a unit-level per-field merge case in the
  encode-contract suite. No existing test cases changed; the cache-hint
  suite's header comment now says the precedence chain resolves per field.
- Docs: the ServerOptions.cacheHints JSDoc, migration.md and the cacheable
  result changeset now state that resolution is per field, most specific
  author first.
@felixweinberger felixweinberger force-pushed the fweinberger/stateless-call-completion branch from b37a565 to cefb94b Compare June 16, 2026 14:21
@felixweinberger felixweinberger merged commit e22980b into v2-2026-07-28 Jun 16, 2026
14 checks passed
@felixweinberger felixweinberger deleted the fweinberger/stateless-call-completion branch June 16, 2026 14:27
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