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/cacheable-result-cache-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@modelcontextprotocol/core': minor
'@modelcontextprotocol/server': minor
---

Results of the cacheable 2026-07-28 operations (`tools/list`, `prompts/list`, `resources/list`, `resources/templates/list`, `resources/read`, `server/discover`) now always carry the revision's required `ttlMs`/`cacheScope` fields when served on that revision, defaulting to `ttlMs: 0` / `cacheScope: 'private'`. Servers can configure the emitted values with the new `ServerOptions.cacheHints` option (per operation) and the new `cacheHint` member of the `registerResource` config (per resource); resolution is per field, most specific author first: cache fields returned by a handler win over the per-resource hint, which wins over the per-operation hint, and configured hints are validated at construction/registration time (`RangeError` on invalid values). Responses on 2025-era connections are unchanged and never carry these fields. Note for untyped callers: `registerResource` now interprets a `cacheHint` key in its config object — it is validated and kept out of the resource's list metadata, where it was previously passed through as ordinary metadata.
7 changes: 7 additions & 0 deletions .changeset/missing-client-capability-error.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 `MissingRequiredClientCapabilityError`, the typed error class for the 2026-07-28 `-32003` protocol error (processing a request requires a capability the client did not declare). Its `data.requiredCapabilities` lists the missing capabilities and `ProtocolError.fromError` recognizes the code/data shape. The 2026-07-28 HTTP entry gains a pre-dispatch gate that refuses a request requiring an undeclared client capability with this error and HTTP status `400`; no method served on the 2026-07-28 registry currently carries such a requirement, so observable behavior is unchanged until methods with capability requirements exist.
30 changes: 30 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,36 @@ can still send them to the 2025-era clients it serves via `initialize`. On a `'d
Declaring `eraSupport: 'dual-era'` is also an assertion that your handlers are ready to serve modern-era requests (for example, that they read per-request client identity from `ctx.mcpReq.envelope` rather than the connection-scoped accessors — see the next section). A
future release may add per-handler era declarations as the basis for a safe automatic default; for now the connection-level `eraSupport` option is the whole opt-in surface.

### Cache fields and cache hints for cacheable 2026-07-28 results

The 2026-07-28 revision requires `ttlMs` and `cacheScope` on the cacheable results (`tools/list`, `prompts/list`, `resources/list`, `resources/templates/list`, `resources/read`, `server/discover`). When serving that revision, the SDK now always emits both fields,
defaulting to `ttlMs: 0` and `cacheScope: 'private'` — the most conservative policy, equivalent to "do not cache". To advertise a real cache policy:

```typescript
const server = new McpServer(
{ name: 'my-server', version: '1.0.0' },
{
capabilities: { tools: {}, resources: {} },
// per-operation hints, used when a result does not carry its own values
cacheHints: { 'tools/list': { ttlMs: 60_000, cacheScope: 'public' } }
}
);

// per-resource hint for that resource's resources/read results
server.registerResource('config', 'config://app', { cacheHint: { ttlMs: 5_000 } }, async uri => ({
contents: [{ uri: uri.href, text: '…' }]
}));
```

Resolution is per field, most specific author first: for each of `ttlMs` and `cacheScope`, a value returned by the handler itself (when valid) wins over the per-resource `cacheHint`, which wins over `ServerOptions.cacheHints[operation]`, which wins over the default — so a
per-resource hint that sets only one field never suppresses the other field configured at the operation level. Configured hints are validated when they are configured — an invalid `ttlMs` (negative or non-integer) or `cacheScope` throws a `RangeError`. Responses on
2025-era connections never carry these fields, with or without configuration.

### Typed `-32003` missing-client-capability error

`MissingRequiredClientCapabilityError` is the typed error class for the 2026-07-28 `-32003` protocol error: processing a request requires a capability the client did not declare in the request's `clientCapabilities`. Its `data.requiredCapabilities` lists the missing
capabilities, and `ProtocolError.fromError` recognizes the code/data shape (recognize peers' errors by their code and `error.data`, not by `instanceof`). When the HTTP entry refuses such a request, the response uses HTTP status `400` as the specification requires.

### Client identity accessors deprecated in favor of per-request context

`Server.getClientCapabilities()`, `Server.getClientVersion()` and `Server.getNegotiatedProtocolVersion()` are deprecated (they remain functional). On 2026-07-28 requests the client's identity travels with each request in the validated `_meta` envelope and is available to
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/exports/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,12 @@ export {
export { ProtocolErrorCode } from '../../types/enums.js';

// Error classes
export { ProtocolError, UnsupportedProtocolVersionError, UrlElicitationRequiredError } from '../../types/errors.js';
export {
MissingRequiredClientCapabilityError,
ProtocolError,
UnsupportedProtocolVersionError,
UrlElicitationRequiredError
} from '../../types/errors.js';

// Type guards and message parsing
export {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ export * from './auth/errors.js';
export * from './errors/sdkErrors.js';
export * from './shared/auth.js';
export * from './shared/authUtils.js';
export * from './shared/clientCapabilityRequirements.js';
export * from './shared/envelope.js';
export * from './shared/inboundClassification.js';
export * from './shared/metadataUtils.js';
export * from './shared/protocol.js';
export * from './shared/protocolEras.js';
export * from './shared/resultCacheHints.js';
export * from './shared/stdio.js';
export * from './shared/toolNameValidation.js';
export * from './shared/transport.js';
Expand Down
99 changes: 99 additions & 0 deletions packages/core/src/shared/clientCapabilityRequirements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* Client-capability requirements for inbound requests (protocol revision
* 2026-07-28).
*
* The 2026-07-28 revision carries the client's declared capabilities on every
* request (`io.modelcontextprotocol/clientCapabilities`), and a server MUST
* NOT rely on capabilities the client did not declare: when processing a
* request requires an undeclared capability, the server answers
* `MissingRequiredClientCapabilityError` (`-32003`) with
* `data.requiredCapabilities` listing what is missing — HTTP status `400` on
* HTTP transports.
*
* This module is the shared, pure half of that rule. It is written for three
* call sites:
*
* 1. the pre-dispatch feature gate at the HTTP entry (a request to a method
* whose processing structurally requires a client capability is refused
* before dispatch),
* 2. the outbound input-request leg of multi round-trip requests (a server
* must not embed an input request the client cannot satisfy) — lands with
* the input-request engine,
* 3. the legacy-session pre-check before bridging input requests onto a
* 2025-era session — lands with that bridge.
*
* All three share {@linkcode missingClientCapabilities}; the per-method
* requirement table below feeds call site 1 only.
*/
import type { ClientCapabilities } from '../types/types.js';

/**
* Inbound request methods whose processing structurally requires a client
* capability, keyed by method, valued by the capabilities required.
*
* Currently empty: none of the request methods served on the 2026-07-28
* registry unconditionally requires a client capability. Entries appear here
* when such methods exist — for example requests whose handling embeds
* elicitation or sampling input requests (the input-request engine), or
* opt-in subscription delivery. Handler-conditional requirements (a specific
* tool that needs sampling) are not expressible as a static method table and
* are enforced at the point the requirement arises instead.
*/
export const REQUIRED_CLIENT_CAPABILITIES_BY_METHOD: Readonly<Record<string, ClientCapabilities>> = {};

/**
* The client capabilities a request method structurally requires, or
* `undefined` when the method has no static requirement.
*/
export function requiredClientCapabilitiesForRequest(method: string): ClientCapabilities | undefined {
return Object.hasOwn(REQUIRED_CLIENT_CAPABILITIES_BY_METHOD, method) ? REQUIRED_CLIENT_CAPABILITIES_BY_METHOD[method] : undefined;
}

function isPlainObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}

/**
* Computes the subset of `required` client capabilities the client did not
* declare. Returns `undefined` when every required capability is declared;
* otherwise returns an object in the `ClientCapabilities` shape containing
* exactly the missing capabilities (suitable for
* `data.requiredCapabilities` on the `-32003` error).
*
* A capability counts as declared when its top-level key is present on the
* declared capabilities; when the requirement names nested members (for
* example `elicitation: { url: {} }`), each named member must also be present
* under the declared capability. An absent or empty `declared` value means
* nothing is declared — every required capability is missing (the structural
* clean-refusal posture for sessions with no per-request capability view).
*/
export function missingClientCapabilities(
required: ClientCapabilities,
declared: ClientCapabilities | undefined
): ClientCapabilities | undefined {
const missing: Record<string, unknown> = {};

for (const [capability, requirement] of Object.entries(required)) {
if (requirement === undefined) {
continue;
}
const declaredValue = declared === undefined ? undefined : (declared as Record<string, unknown>)[capability];
if (declaredValue === undefined) {
missing[capability] = requirement;
continue;
}
if (isPlainObject(requirement) && isPlainObject(declaredValue)) {
const missingMembers: Record<string, unknown> = {};
for (const [member, memberRequirement] of Object.entries(requirement)) {
if (memberRequirement !== undefined && declaredValue[member] === undefined) {
missingMembers[member] = memberRequirement;
}
}
if (Object.keys(missingMembers).length > 0) {
missing[capability] = missingMembers;
}
}
}

return Object.keys(missing).length > 0 ? (missing as ClientCapabilities) : undefined;
}
29 changes: 21 additions & 8 deletions packages/core/src/shared/inboundClassification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,13 @@ export interface InboundValidationRungDescriptor {
rung: InboundValidationRung;
/** Evaluation order: lower runs first; an earlier rung's outcome wins over a later rung's. */
order: number;
/** Where the rung is evaluated: at the HTTP entry edge or at protocol dispatch. */
evaluatedAt: 'edge' | 'dispatch';
/**
* Where the rung is evaluated: at the HTTP entry edge by
* {@linkcode classifyInboundRequest} (`edge`), by the HTTP entry after
* classification but before dispatch (`pre-dispatch`), or by the protocol
* layer at dispatch (`dispatch`).
*/
evaluatedAt: 'edge' | 'pre-dispatch' | 'dispatch';
/** The JSON-RPC error codes this rung can produce (empty when the rung only routes). */
codes: readonly number[];
/** Conformance scenarios that exercise this rung (where one exists). */
Expand All @@ -183,9 +188,11 @@ export interface InboundValidationRungDescriptor {
* The edge rungs are evaluated by {@linkcode classifyInboundRequest}; the
* dispatch rungs are evaluated by the protocol layer once the classified
* message is injected into a per-request server instance (the era registry
* gate, the envelope requiredness check, per-method params validation, and
* the client-capability check). The order is the precedence: a request that
* fails several rungs is answered by the earliest one.
* gate, the envelope requiredness check, and per-method params validation).
* The client-capability rung is evaluated by the HTTP entry itself,
* pre-dispatch, on the validated envelope the classifier produced — see that
* rung's rationale for the ordering caveat. The order is the precedence: a
* request that fails several rungs is answered by the earliest one.
*/
export const INBOUND_VALIDATION_LADDER: readonly InboundValidationRungDescriptor[] = [
{
Expand Down Expand Up @@ -249,12 +256,16 @@ export const INBOUND_VALIDATION_LADDER: readonly InboundValidationRungDescriptor
{
rung: 'client-capabilities',
order: 7,
evaluatedAt: 'dispatch',
evaluatedAt: 'pre-dispatch',
codes: [ProtocolErrorCode.MissingRequiredClientCapability],
conformance: ['server-stateless'],
rationale:
'Capability assertion runs after envelope validation and method resolution, immediately before the handler; the ' +
'emission itself ships with the capability-policy work and is recorded here for ordering only.'
'The capability requirement is checked by the HTTP entry, pre-dispatch, against the validated envelope the ' +
'classifier produced — pinning the spec-mandated HTTP 400 independently of how dispatch- and handler-produced ' +
'errors are mapped. The documented order (after method resolution and params validation) is preserved observably ' +
'only while the requirement table is empty: once a served method gains a requirement entry, a request that is ' +
'missing the capability and would also fail a dispatch rung is answered by this gate first, so the entry must ' +
'consult the method registry before the gate if the documented precedence is to stay observable.'
}
];

Expand All @@ -277,6 +288,8 @@ export const INBOUND_VALIDATION_LADDER: readonly InboundValidationRungDescriptor
* handler-produced invalid-params error is always in-band.
*/
export const LADDER_ERROR_HTTP_STATUS: Readonly<Record<number, number>> = {
[ProtocolErrorCode.ParseError]: 400,
[ProtocolErrorCode.InvalidRequest]: 400,
[ProtocolErrorCode.MethodNotFound]: 404,
[ProtocolErrorCode.UnsupportedProtocolVersion]: 400,
[ProtocolErrorCode.MissingRequiredClientCapability]: 400,
Expand Down
Loading
Loading