@daloyjs/core 1.0.0-rc.6 → 1.0.0-rc.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @since 0.19.0
9
9
  */
10
- import type { BaseContext, Hooks } from "./types.js";
10
+ import type { Hooks, IdentityGateContext } from "./types.js";
11
11
  /**
12
12
  * Options for {@link ipRestriction}. At least one of `allow` or `deny` must
13
13
  * be provided; supplying both runs deny-first then allow-otherwise (deny
@@ -36,14 +36,28 @@ export interface IpRestrictionOptions {
36
36
  * Provide a function to read adapter connection metadata or a trusted
37
37
  * custom header (e.g. a CDN-specific identifier).
38
38
  */
39
- resolveIp?: (ctx: BaseContext<any, any>) => string | undefined;
39
+ resolveIp?: (ctx: IdentityGateContext) => string | undefined;
40
40
  /**
41
41
  * Read `X-Forwarded-For` / `X-Real-IP` in the default resolver. Defaults
42
42
  * to `false` because those headers are client-spoofable unless every
43
43
  * request reaches Daloy through a proxy chain you control. Pair with
44
44
  * `new App({ trustProxy: true })` in production.
45
+ *
46
+ * When enabled, the resolver reads the **rightmost** `X-Forwarded-For`
47
+ * entry — the one your immediate proxy appended — never the
48
+ * attacker-influenceable leftmost one, so a spoofed left entry cannot
49
+ * bypass an allow-list or dodge a deny. Behind more than one proxy hop,
50
+ * set {@link trustedHops} instead.
45
51
  */
46
52
  trustProxyHeaders?: boolean;
53
+ /**
54
+ * Declare exactly how many proxy hops sit between Daloy and the public
55
+ * internet. Implies proxy-header trust and reads the client IP that many
56
+ * entries from the right of `X-Forwarded-For` via
57
+ * {@link "./conn-info.js".resolveForwardedClientIp}. Must be an integer in
58
+ * [1, 64]; validated at construction.
59
+ */
60
+ trustedHops?: number;
47
61
  /**
48
62
  * Response message when a request is rejected. Defaults to
49
63
  * `"IP address not permitted"`. Avoid echoing the client IP back —
@@ -87,7 +101,7 @@ export interface IpMatcher {
87
101
  *
88
102
  * @param opts Allow/deny lists plus IP-resolution options; see
89
103
  * {@link IpRestrictionOptions}. Deny matches always win over allow.
90
- * @returns A {@link Hooks} object whose `beforeHandle` enforces the lists,
104
+ * @returns A {@link Hooks} object whose `preBody` hook enforces the lists,
91
105
  * failing closed (403) when the client IP cannot be resolved or parsed.
92
106
  * @throws Error at setup time when neither `allow` nor `deny` is provided,
93
107
  * or when a pattern is not a valid IP/CIDR.
@@ -8,6 +8,7 @@
8
8
  * @since 0.19.0
9
9
  */
10
10
  import { ForbiddenError } from "./errors.js";
11
+ import { resolveForwardedClientIp, resolveForwardedTrust } from "./conn-info.js";
11
12
  /**
12
13
  * Block or allow requests by source IP / CIDR range. In direct Web-standard
13
14
  * runtimes, pass `resolveIp` from the adapter-specific connection metadata.
@@ -28,7 +29,7 @@ import { ForbiddenError } from "./errors.js";
28
29
  *
29
30
  * @param opts Allow/deny lists plus IP-resolution options; see
30
31
  * {@link IpRestrictionOptions}. Deny matches always win over allow.
31
- * @returns A {@link Hooks} object whose `beforeHandle` enforces the lists,
32
+ * @returns A {@link Hooks} object whose `preBody` hook enforces the lists,
32
33
  * failing closed (403) when the client IP cannot be resolved or parsed.
33
34
  * @throws Error at setup time when neither `allow` nor `deny` is provided,
34
35
  * or when a pattern is not a valid IP/CIDR.
@@ -40,10 +41,16 @@ export function ipRestriction(opts) {
40
41
  }
41
42
  const allow = (opts.allow ?? []).map(compileCidrMatcher);
42
43
  const deny = (opts.deny ?? []).map(compileCidrMatcher);
43
- const resolveIp = opts.resolveIp ?? (opts.trustProxyHeaders ? forwardedIpResolver : noIpResolver);
44
+ const hops = resolveForwardedTrust("ipRestriction()", opts);
45
+ const resolveIp = opts.resolveIp ?? (hops !== undefined ? forwardedIpResolver(hops) : noIpResolver);
44
46
  const message = opts.message ?? "IP address not permitted";
45
47
  return {
46
- beforeHandle(ctx) {
48
+ // `preBody`, not `beforeHandle`: a gate that returns a Response from
49
+ // `beforeHandle` can be preempted by any earlier `beforeHandle` middleware
50
+ // that short-circuits first — a `responseCache()` HIT mounted above it would
51
+ // serve a deny-listed address the cached body. `preBody` always runs first,
52
+ // so the allow/deny lists hold regardless of mount order.
53
+ preBody(ctx) {
47
54
  const raw = resolveIp(ctx);
48
55
  if (!raw)
49
56
  throw new ForbiddenError(message);
@@ -62,12 +69,8 @@ export function ipRestriction(opts) {
62
69
  function noIpResolver(_ctx) {
63
70
  return undefined;
64
71
  }
65
- function forwardedIpResolver(ctx) {
66
- const headers = ctx.request.headers;
67
- const forwarded = headers.get("x-forwarded-for");
68
- if (forwarded)
69
- return forwarded.split(",")[0]?.trim();
70
- return headers.get("x-real-ip") ?? undefined;
72
+ function forwardedIpResolver(hops) {
73
+ return (ctx) => resolveForwardedClientIp(ctx.request, hops);
71
74
  }
72
75
  /**
73
76
  * Test whether a parsed IP falls inside a compiled CIDR matcher, comparing
package/dist/mcp.d.ts CHANGED
@@ -2,17 +2,57 @@ import type { PathString, RouteDefinition } from "./types.js";
2
2
  /**
3
3
  * Latest MCP protocol version DaloyJS negotiates by default.
4
4
  *
5
- * @see https://modelcontextprotocol.io/specification/2025-11-25
5
+ * @see https://modelcontextprotocol.io/specification/2026-07-28
6
6
  * @since 1.0.0
7
7
  */
8
- export declare const MCP_PROTOCOL_VERSION = "2025-11-25";
8
+ export declare const MCP_PROTOCOL_VERSION = "2026-07-28";
9
+ /**
10
+ * First MCP revision of the *stateless* ("modern") era: version, client
11
+ * identity, and client capabilities travel in each request's `_meta` instead
12
+ * of being established once by an `initialize` handshake.
13
+ *
14
+ * Revisions are `YYYY-MM-DD` strings, so a lexicographic comparison against
15
+ * this constant correctly classifies every past and future revision.
16
+ *
17
+ * @since 1.0.0
18
+ */
19
+ export declare const MCP_MODERN_ERA_MIN_VERSION = "2026-07-28";
9
20
  /**
10
21
  * Protocol revisions accepted by {@link createMcpHandler} unless the caller
11
22
  * provides an explicit `protocolVersions` list.
12
23
  *
24
+ * The list spans both protocol eras: the stateless `2026-07-28` revision and
25
+ * the older handshake-based revisions, so one endpoint serves modern and
26
+ * legacy MCP clients at the same time.
27
+ *
13
28
  * @since 1.0.0
14
29
  */
15
30
  export declare const MCP_PROTOCOL_VERSIONS: readonly string[];
31
+ /**
32
+ * Reserved `_meta` keys defined by MCP `2026-07-28` for per-request protocol
33
+ * metadata. Exported so applications and tests can build spec-compliant
34
+ * requests without hard-coding string literals.
35
+ *
36
+ * @since 1.0.0
37
+ */
38
+ export declare const MCP_META_KEYS: {
39
+ readonly protocolVersion: "io.modelcontextprotocol/protocolVersion";
40
+ readonly clientInfo: "io.modelcontextprotocol/clientInfo";
41
+ readonly clientCapabilities: "io.modelcontextprotocol/clientCapabilities";
42
+ readonly logLevel: "io.modelcontextprotocol/logLevel";
43
+ readonly serverInfo: "io.modelcontextprotocol/serverInfo";
44
+ };
45
+ /**
46
+ * JSON-RPC error codes defined by the MCP specification in its reserved
47
+ * `-32020`..`-32099` sub-range.
48
+ *
49
+ * @since 1.0.0
50
+ */
51
+ export declare const MCP_ERROR_CODES: {
52
+ readonly headerMismatch: -32020;
53
+ readonly missingRequiredClientCapability: -32021;
54
+ readonly unsupportedProtocolVersion: -32022;
55
+ };
16
56
  /**
17
57
  * Default maximum accepted JSON-RPC request body for a DaloyJS MCP endpoint.
18
58
  * The cap is intentionally small because MCP calls should carry parameters,
@@ -21,6 +61,30 @@ export declare const MCP_PROTOCOL_VERSIONS: readonly string[];
21
61
  * @since 1.0.0
22
62
  */
23
63
  export declare const MCP_DEFAULT_MAX_BODY_BYTES: number;
64
+ /**
65
+ * Maximum accepted length of a client-supplied `params.requestState` string.
66
+ *
67
+ * `requestState` is opaque server state that round-trips through an untrusted
68
+ * client during a multi round-trip request, so it is bounded independently of
69
+ * the body cap to keep a hostile client from forcing large state parsing.
70
+ *
71
+ * @since 1.0.0
72
+ */
73
+ export declare const MCP_MAX_REQUEST_STATE_LENGTH = 8192;
74
+ /**
75
+ * Report whether a protocol revision belongs to the stateless ("modern") MCP
76
+ * era introduced by {@link MCP_MODERN_ERA_MIN_VERSION}.
77
+ *
78
+ * Modern requests carry their protocol version, client identity, and client
79
+ * capabilities in `_meta` and are validated against the standard
80
+ * `MCP-Protocol-Version` / `Mcp-Method` / `Mcp-Name` HTTP headers. Older
81
+ * revisions keep the `initialize` handshake instead.
82
+ *
83
+ * @param version - A protocol revision string such as `"2026-07-28"`.
84
+ * @returns `true` when the revision uses per-request metadata.
85
+ * @since 1.0.0
86
+ */
87
+ export declare function isModernProtocolVersion(version: string): boolean;
24
88
  /**
25
89
  * JSON value accepted in MCP schemas, structured results, and metadata.
26
90
  *
@@ -94,6 +158,34 @@ export interface McpServerInfo {
94
158
  /** Optional icons clients may display for this server (MCP 2025-11-25). */
95
159
  icons?: McpIcon[];
96
160
  }
161
+ /**
162
+ * Self-reported implementation identity (`serverInfo` / `clientInfo`).
163
+ *
164
+ * Values are supplied by the peer and are **not** verified by the protocol.
165
+ * Use them for display, logging, and debugging only — never for authorization
166
+ * or any other security decision.
167
+ *
168
+ * @since 1.0.0
169
+ */
170
+ export interface McpImplementation {
171
+ /** Stable machine-readable implementation name. */
172
+ name: string;
173
+ /** Implementation version string. */
174
+ version: string;
175
+ /** Optional human-readable display title. */
176
+ title?: string;
177
+ }
178
+ /**
179
+ * Protocol era a request was served under.
180
+ *
181
+ * - `"modern"`: MCP `2026-07-28` and later. Stateless; version, identity, and
182
+ * capabilities arrive in `_meta` and are mirrored into HTTP headers.
183
+ * - `"legacy"`: MCP `2025-11-25` and earlier. Established by an `initialize`
184
+ * handshake.
185
+ *
186
+ * @since 1.0.0
187
+ */
188
+ export type McpProtocolEra = "modern" | "legacy";
97
189
  /**
98
190
  * Per-request context passed to tool, resource, and prompt handlers.
99
191
  *
@@ -103,16 +195,57 @@ export interface McpRequestContext {
103
195
  /** The original HTTP request received by the DaloyJS route. */
104
196
  request: Request;
105
197
  /**
106
- * Protocol version selected for this call. `initialize` negotiates it from
107
- * `params.protocolVersion`; other calls take the `MCP-Protocol-Version`
108
- * header, falling back to `2025-03-26` (the spec's assumption for
109
- * headerless requests) when supported, otherwise the preferred version.
198
+ * Protocol version selected for this call. On a modern request this is the
199
+ * verified `io.modelcontextprotocol/protocolVersion` from `_meta`. On a
200
+ * legacy request, `initialize` negotiates it from `params.protocolVersion`
201
+ * and other calls take the `MCP-Protocol-Version` header, falling back to
202
+ * `2025-03-26` (the spec's assumption for headerless requests) when
203
+ * supported, otherwise the preferred version.
110
204
  */
111
205
  protocolVersion: string;
206
+ /**
207
+ * Protocol era this request was served under. Handlers that emit multi
208
+ * round-trip results must check this: `input_required` only exists in the
209
+ * `"modern"` era.
210
+ */
211
+ era: McpProtocolEra;
112
212
  /** JSON-RPC id for request/response correlation. */
113
213
  id: McpJsonRpcId;
114
214
  /** Raw MCP method name, such as `"tools/call"` or `"resources/read"`. */
115
215
  method: string;
216
+ /**
217
+ * Capabilities the client declared for this request
218
+ * (`io.modelcontextprotocol/clientCapabilities`). Empty on legacy requests,
219
+ * which declare capabilities once during `initialize` instead.
220
+ */
221
+ clientCapabilities: McpJsonObject;
222
+ /**
223
+ * Self-reported client identity (`io.modelcontextprotocol/clientInfo`), when
224
+ * the client sent one. Advisory metadata — never a security input.
225
+ */
226
+ clientInfo?: McpImplementation;
227
+ /**
228
+ * Minimum log level the client asked the server to emit for this request
229
+ * (`io.modelcontextprotocol/logLevel`), when supplied.
230
+ */
231
+ logLevel?: string;
232
+ /**
233
+ * Client answers to a previous {@link McpInputRequiredResult}, keyed by the
234
+ * identifiers the server assigned in `inputRequests`. Present only on a
235
+ * multi round-trip retry.
236
+ */
237
+ inputResponses?: McpInputResponses;
238
+ /**
239
+ * Opaque state the server emitted on a previous
240
+ * {@link McpInputRequiredResult} and the client echoed back.
241
+ *
242
+ * Security: this value round-trips through an untrusted client. If it
243
+ * influences authorization, resource access, or business logic, integrity-
244
+ * protect it (HMAC or AEAD), bind it to the authenticated principal and the
245
+ * originating request, give it a short expiry, and reject anything that
246
+ * fails verification.
247
+ */
248
+ requestState?: string;
116
249
  }
117
250
  /**
118
251
  * Text content block returned from an MCP tool, resource, or prompt.
@@ -179,6 +312,69 @@ export interface McpToolResult {
179
312
  /** Set to `true` for domain/tool errors the model may recover from. */
180
313
  isError?: boolean;
181
314
  }
315
+ /**
316
+ * Server-to-client request embedded in an {@link McpInputRequiredResult}.
317
+ *
318
+ * MCP `2026-07-28` removed server-initiated JSON-RPC requests. A server that
319
+ * needs elicitation, sampling, or the client's roots returns them here and the
320
+ * client supplies the answers on a retry of the original request.
321
+ *
322
+ * @since 1.0.0
323
+ */
324
+ export interface McpInputRequest {
325
+ /** The client-side method being requested. */
326
+ method: "elicitation/create" | "sampling/createMessage" | "roots/list";
327
+ /** Method parameters, as defined by the MCP client-features specification. */
328
+ params?: McpJsonObject;
329
+ }
330
+ /**
331
+ * Map of server-assigned identifiers to server-to-client requests.
332
+ *
333
+ * @since 1.0.0
334
+ */
335
+ export type McpInputRequests = {
336
+ [id: string]: McpInputRequest;
337
+ };
338
+ /**
339
+ * Map of the same identifiers to the client's answers, echoed back on the
340
+ * retry of the original request.
341
+ *
342
+ * @since 1.0.0
343
+ */
344
+ export type McpInputResponses = {
345
+ [id: string]: McpJsonValue;
346
+ };
347
+ /**
348
+ * Interim result telling the client that more input is required before the
349
+ * call can complete (MCP `2026-07-28` multi round-trip requests).
350
+ *
351
+ * Return this from a tool, resource, or prompt handler instead of a final
352
+ * result. The client gathers the requested input and retries the original
353
+ * request — with a **new** JSON-RPC id — carrying `inputResponses` and, when
354
+ * present, the exact `requestState` string it received.
355
+ *
356
+ * At least one of `inputRequests` or `requestState` must be set. DaloyJS
357
+ * refuses to emit an `inputRequests` entry whose method the client did not
358
+ * declare support for, answering `-32021` instead of leaking a request the
359
+ * client cannot fulfil.
360
+ *
361
+ * @since 1.0.0
362
+ */
363
+ export interface McpInputRequiredResult {
364
+ /** Discriminator literal identifying this as a multi round-trip result. */
365
+ resultType: "input_required";
366
+ /** Requests the client must fulfil before retrying. */
367
+ inputRequests?: McpInputRequests;
368
+ /**
369
+ * Opaque state the client must echo back verbatim on the retry.
370
+ *
371
+ * Security: it passes through an untrusted client. Integrity-protect it
372
+ * (HMAC or AEAD) whenever it influences authorization, resource access, or
373
+ * business logic, bind it to the authenticated principal and originating
374
+ * request, and give it a short expiry.
375
+ */
376
+ requestState?: string;
377
+ }
182
378
  /**
183
379
  * Behavioral hints a tool can advertise to MCP clients. Hints are untrusted
184
380
  * metadata for UX decisions (confirmation prompts, badges); clients must not
@@ -208,13 +404,15 @@ export interface McpToolAnnotations {
208
404
  * declared shape holds at runtime. Constraints expressed only through
209
405
  * unsupported schema keywords (e.g. `pattern`) remain the handler's job.
210
406
  * @param ctx - Request metadata and the original HTTP request.
211
- * @returns Text shorthand or a full {@link McpToolResult}.
407
+ * @returns Text shorthand, a full {@link McpToolResult}, or an
408
+ * {@link McpInputRequiredResult} to ask the client for more input first
409
+ * (modern protocol era only).
212
410
  * @throws {McpToolError} for caller-correctable failures that should be
213
411
  * returned as an MCP tool error result.
214
412
  *
215
413
  * @since 1.0.0
216
414
  */
217
- export type McpToolHandler<TArgs extends Record<string, unknown> = Record<string, unknown>> = (args: TArgs, ctx: McpRequestContext) => string | McpToolResult | Promise<string | McpToolResult>;
415
+ export type McpToolHandler<TArgs extends Record<string, unknown> = Record<string, unknown>> = (args: TArgs, ctx: McpRequestContext) => string | McpToolResult | McpInputRequiredResult | Promise<string | McpToolResult | McpInputRequiredResult>;
218
416
  /**
219
417
  * Definition of a callable MCP tool.
220
418
  *
@@ -301,9 +499,11 @@ export interface McpResourceDefinition extends McpResource {
301
499
  * Read the resource contents for `resources/read`.
302
500
  *
303
501
  * @param ctx - Request metadata and the original HTTP request.
304
- * @returns One or more content entries for this resource.
502
+ * @returns One or more content entries for this resource, or an
503
+ * {@link McpInputRequiredResult} to ask the client for more input first
504
+ * (modern protocol era only).
305
505
  */
306
- read: (ctx: McpRequestContext) => McpResourceContents | McpResourceContents[] | Promise<McpResourceContents | McpResourceContents[]>;
506
+ read: (ctx: McpRequestContext) => McpResourceContents | McpResourceContents[] | McpInputRequiredResult | Promise<McpResourceContents | McpResourceContents[] | McpInputRequiredResult>;
307
507
  }
308
508
  /**
309
509
  * Resource template metadata returned from `resources/templates/list`.
@@ -344,11 +544,13 @@ export interface McpResourceTemplateDefinition extends McpResourceTemplate {
344
544
  * @param uri - The full resource URI requested by the client.
345
545
  * @param variables - Template variable values extracted from `uri`.
346
546
  * @param ctx - Request metadata and the original HTTP request.
347
- * @returns One or more content entries for this resource.
547
+ * @returns One or more content entries for this resource, or an
548
+ * {@link McpInputRequiredResult} to ask the client for more input first
549
+ * (modern protocol era only).
348
550
  * @throws {McpToolError} for caller-correctable failures such as an unknown
349
551
  * record id; these become JSON-RPC invalid-params errors.
350
552
  */
351
- read: (uri: string, variables: Record<string, string>, ctx: McpRequestContext) => McpResourceContents | McpResourceContents[] | Promise<McpResourceContents | McpResourceContents[]>;
553
+ read: (uri: string, variables: Record<string, string>, ctx: McpRequestContext) => McpResourceContents | McpResourceContents[] | McpInputRequiredResult | Promise<McpResourceContents | McpResourceContents[] | McpInputRequiredResult>;
352
554
  }
353
555
  /**
354
556
  * Argument metadata for an MCP prompt.
@@ -417,9 +619,10 @@ export interface McpPromptDefinition extends McpPrompt {
417
619
  *
418
620
  * @param args - Prompt arguments supplied by the MCP client.
419
621
  * @param ctx - Request metadata and the original HTTP request.
420
- * @returns Prompt messages.
622
+ * @returns Prompt messages, or an {@link McpInputRequiredResult} to ask the
623
+ * client for more input first (modern protocol era only).
421
624
  */
422
- get: (args: Record<string, unknown>, ctx: McpRequestContext) => McpPromptResult | Promise<McpPromptResult>;
625
+ get: (args: Record<string, unknown>, ctx: McpRequestContext) => McpPromptResult | McpInputRequiredResult | Promise<McpPromptResult | McpInputRequiredResult>;
423
626
  }
424
627
  /**
425
628
  * Caller-correctable MCP tool/resource/prompt error.
@@ -440,6 +643,31 @@ export declare class McpToolError extends Error {
440
643
  */
441
644
  constructor(message: string);
442
645
  }
646
+ /**
647
+ * Client-side caching hints attached to every cacheable modern result
648
+ * (`server/discover`, the four list methods, and `resources/read`).
649
+ *
650
+ * @since 1.0.0
651
+ */
652
+ export interface McpCacheHints {
653
+ /**
654
+ * Freshness hint in milliseconds. `0` (the default) tells clients to
655
+ * revalidate on every call.
656
+ *
657
+ * @defaultValue 0
658
+ */
659
+ ttlMs?: number;
660
+ /**
661
+ * Whether shared intermediaries may cache the response. DaloyJS defaults to
662
+ * `"private"` because MCP list results legitimately vary by the credential
663
+ * presented on the request — a `"public"` scope on an authorization-scoped
664
+ * tool list would let a proxy serve one caller's tools to another. Only set
665
+ * `"public"` for a server whose results are identical for every caller.
666
+ *
667
+ * @defaultValue "private"
668
+ */
669
+ scope?: "public" | "private";
670
+ }
443
671
  /**
444
672
  * Options for {@link createMcpHandler}.
445
673
  *
@@ -473,6 +701,21 @@ export interface McpHandlerOptions {
473
701
  * other origin is rejected with `403` unless listed here.
474
702
  */
475
703
  allowedOrigins?: readonly string[];
704
+ /**
705
+ * Optional extensions advertised in `capabilities.extensions`, keyed by
706
+ * extension identifier (for example `"io.modelcontextprotocol/tasks"`), with
707
+ * each value the extension's settings object. Identifiers must carry a
708
+ * reverse-DNS prefix, per the `_meta` key naming rules.
709
+ *
710
+ * DaloyJS core implements no extension itself; declaring one here advertises
711
+ * that *your* handlers implement it.
712
+ */
713
+ extensions?: Record<string, McpJsonObject>;
714
+ /**
715
+ * Caching hints returned on cacheable modern results. Defaults to
716
+ * `{ ttlMs: 0, scope: "private" }` — no caching, no sharing.
717
+ */
718
+ cache?: McpCacheHints;
476
719
  /** Accepted MCP protocol versions. Defaults to {@link MCP_PROTOCOL_VERSIONS}. */
477
720
  protocolVersions?: readonly string[];
478
721
  /**
@@ -532,31 +775,51 @@ export declare function validateMcpInput(schema: McpJsonSchema, value: unknown):
532
775
  /**
533
776
  * Create a dependency-free MCP Streamable HTTP endpoint handler.
534
777
  *
535
- * The handler implements the server side of MCP over one HTTP endpoint:
536
- * `initialize`, `ping`, `tools/list`, `tools/call`, `resources/list`,
537
- * `resources/templates/list`, `resources/read`, `prompts/list`, and
538
- * `prompts/get`. It accepts JSON-RPC requests over `POST`, acknowledges
539
- * notifications with `202`, validates the `MCP-Protocol-Version` header,
540
- * bounds request bodies, enforces required prompt arguments, and returns
541
- * JSON-RPC errors for malformed input.
542
- *
543
- * Security: per the Streamable HTTP spec's DNS-rebinding guidance, every
544
- * request bearing an `Origin` header is validated. Same-origin and loopback
545
- * origins pass; anything else is rejected with `403` unless listed in
546
- * {@link McpHandlerOptions.allowedOrigins}.
547
- *
548
- * It intentionally does not spawn stdio servers, manage OAuth metadata, keep
549
- * durable sessions, or open server-initiated SSE streams. Use DaloyJS
550
- * middleware for authentication and authorization, and run this on a dedicated
551
- * Daloy app when your MCP server has a different trust boundary than your REST
552
- * API.
778
+ * The handler serves **both MCP protocol eras** on one endpoint:
779
+ *
780
+ * - **Modern (`2026-07-28`+, stateless).** No handshake. Every request carries
781
+ * its protocol version, client identity, and client capabilities in `_meta`,
782
+ * mirrored into the required `MCP-Protocol-Version`, `Mcp-Method`, and
783
+ * `Mcp-Name` headers. Methods: `server/discover`, `tools/list`,
784
+ * `tools/call`, `resources/list`, `resources/templates/list`,
785
+ * `resources/read`, `prompts/list`, `prompts/get`. Every result carries
786
+ * `resultType`, the server identity in `_meta`, and on cacheable methods —
787
+ * `ttlMs` / `cacheScope`. Handlers may return an
788
+ * {@link McpInputRequiredResult} to run a multi round-trip request.
789
+ * - **Legacy (`2025-11-25` and earlier).** The `initialize` / `ping` handshake
790
+ * protocol, unchanged, so existing clients keep working.
791
+ *
792
+ * A request is served as modern when its `_meta` protocol version (or the
793
+ * `MCP-Protocol-Version` header) is `2026-07-28` or later; otherwise it takes
794
+ * the legacy path.
795
+ *
796
+ * Security, on top of the era-independent body cap, prototype-pollution-safe
797
+ * parsing, and `inputSchema` enforcement:
798
+ *
799
+ * - Per the Streamable HTTP spec's DNS-rebinding guidance, every request
800
+ * bearing an `Origin` header is validated. Loopback origins pass; anything
801
+ * else is rejected with `403` unless listed in
802
+ * {@link McpHandlerOptions.allowedOrigins}.
803
+ * - Modern requests are rejected with `400` and `-32020` (`HeaderMismatch`)
804
+ * when a required standard header is missing or disagrees with the body.
805
+ * This closes the header/body confusion gap that lets a gateway route on one
806
+ * value while the server executes another.
807
+ * - `Mcp-Session-Id` and `Last-Event-ID` are ignored; no session is ever minted
808
+ * or echoed.
809
+ *
810
+ * It intentionally does not spawn stdio servers, manage OAuth metadata, open
811
+ * `subscriptions/listen` notification streams, or implement the tasks
812
+ * extension. Use DaloyJS middleware for authentication and authorization, and
813
+ * run this on a dedicated Daloy app when your MCP server has a different trust
814
+ * boundary than your REST API.
553
815
  *
554
816
  * @param options - Server identity, capabilities, limits, and response headers.
555
817
  * @returns A Fetch-compatible request handler suitable for {@link mcpRoutes}
556
818
  * or for direct use in any web-standard runtime.
557
819
  * @throws {TypeError} at construction for invalid serverInfo, protocol
558
- * versions, body limits, duplicate names/URIs, malformed `allowedOrigins`
559
- * entries, or unsupported URI template expressions.
820
+ * versions, body limits, cache hints, extension identifiers, duplicate
821
+ * names/URIs, malformed `allowedOrigins` entries, invalid `x-mcp-header`
822
+ * annotations, or unsupported URI template expressions.
560
823
  *
561
824
  * @example
562
825
  * ```ts