wiki-formant 0.11.0 → 0.13.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAOA,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,MAAM,CAY1F;AAED,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAMjG;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,MAAM,SAAO,GACZ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOxB;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,EAC5B,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAO,GAC7D,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQxB;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,SAAM,GAAG,MAAM,CAQ5D;AAED,kFAAkF;AAClF,wBAAgB,QAAQ,CAAC,IAAI,EAAE;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAC;CAChC,GAAG,MAAM,CAMT"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAOA,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,MAAM,CAY1F;AAKD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,MAAM,EACZ,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAC3B,QAAQ,GAAG,IAAI,CAuBjB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACnC,YAAY,SAAuB,GAClC,QAAQ,CAKV;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,MAAM,SAAO,GACZ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOxB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAC7B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,EAC5B,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAO,GAC5E,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASxB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAO,GAC7D,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQxB;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,OAAO,EACb,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAO,GAC7D,QAAQ,CAIV;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,SAAM,GAAG,MAAM,CAQ5D;AAED,kFAAkF;AAClF,wBAAgB,QAAQ,CAAC,IAAI,EAAE;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAC;CAChC,GAAG,MAAM,CAMT"}
package/dist/http.js CHANGED
@@ -18,14 +18,57 @@ export function corpusEtag(parts) {
18
18
  }
19
19
  return `W/"${hash.toString(36)}-${seed.length.toString(36)}"`;
20
20
  }
21
- /** 304 when the client already holds this revision, else `null` to render. */
21
+ /** Weak comparison per RFC 9110 8.8.3.2: the `W/` prefix never affects a match. */
22
+ const bareTag = (tag) => tag.trim().replace(/^W\//, '');
23
+ /**
24
+ * 304 when the client already holds this revision, else `null` to render.
25
+ *
26
+ * Both validators are matched the way RFC 9110 defines them rather than by
27
+ * string equality, because equality is wrong in the cases that actually occur:
28
+ * a client sends every tag it holds as a list, a proxy adds or strips the weak
29
+ * prefix, and a crawler reformats the date. Each of those took a full render
30
+ * from a response that was already fresh.
31
+ */
22
32
  export function notModified(request, etag, lastModified) {
33
+ const headers = {
34
+ ETag: etag,
35
+ ...(lastModified ? { 'Last-Modified': lastModified } : {}),
36
+ };
23
37
  const inm = request.headers.get('if-none-match');
38
+ if (inm) {
39
+ const fresh = inm.trim() === '*' || inm.split(',').some(t => bareTag(t) === bareTag(etag));
40
+ // When If-None-Match is present If-Modified-Since must be ignored entirely.
41
+ return fresh ? new Response(null, { status: 304, headers }) : null;
42
+ }
24
43
  const ims = request.headers.get('if-modified-since');
25
- const fresh = inm ? inm === etag : Boolean(ims && ims === lastModified);
26
- if (!fresh)
44
+ if (!ims || !lastModified)
45
+ return null;
46
+ const held = Date.parse(ims);
47
+ const current = Date.parse(lastModified);
48
+ if (!Number.isFinite(held) || !Number.isFinite(current))
27
49
  return null;
28
- return new Response(null, { status: 304, headers: { ETag: etag, 'Last-Modified': lastModified } });
50
+ // Second precision: the header carries no sub-second part, so a stamp that
51
+ // rounds down would otherwise read as newer than the copy it was sent for.
52
+ return Math.floor(current / 1000) <= Math.floor(held / 1000)
53
+ ? new Response(null, { status: 304, headers })
54
+ : null;
55
+ }
56
+ /**
57
+ * A 404 that teaches, for the plain-GET half of an agent surface.
58
+ *
59
+ * The MCP half of every server here answers a wrong identifier by naming the
60
+ * tools that find a right one. The GET half, reached by exactly the agents that
61
+ * guessed a URL, answered the same mistake with `Page not found`, `Not found`,
62
+ * and in one case a 26 KB HTML error page — nothing to retry from. `hints` are
63
+ * merged into the body, so an index URL or a nearest match rides along. A
64
+ * missing page is a hot crawler path, so it is cacheable by default; pass
65
+ * `cacheControl` where the surface has its own edge posture.
66
+ */
67
+ export function teachingNotFound(message, hints = {}, cacheControl = 'public, max-age=60') {
68
+ return Response.json({ error: message, ...hints }, {
69
+ status: 404,
70
+ headers: { 'Cache-Control': cacheControl },
71
+ });
29
72
  }
30
73
  /**
31
74
  * Headers for a plain-text export. `maxAge` is the edge window — a curated
@@ -48,16 +91,53 @@ export function textHeaders(etag, lastModified, maxAge = 3600) {
48
91
  * offer, and a twin that stamps the epoch is worse than one that stamps
49
92
  * nothing. `extra` carries whatever the mount needs on top — an `X-Robots-Tag`
50
93
  * where the twin is a second public URL with no canonical of its own.
94
+ *
95
+ * Pass `etag`. The twin is the single most recrawled URL a page has, and a
96
+ * response with no validator is a full render on every pass forever — the same
97
+ * arithmetic that justifies the corpus ETag, applied per page. Three wikis
98
+ * shipped twins with neither validator and only 304'd where a proxy happened to
99
+ * synthesise one; that is why it is spelled out here rather than left optional
100
+ * in spirit.
51
101
  */
52
102
  export function markdownHeaders(lastModified, opts = {}) {
53
103
  const maxAge = opts.maxAge ?? 3600;
54
104
  return {
55
105
  'Content-Type': 'text/markdown; charset=utf-8',
56
106
  'Cache-Control': `public, s-maxage=${maxAge}, stale-while-revalidate=${maxAge * 24}`,
107
+ ...(opts.etag ? { ETag: opts.etag } : {}),
57
108
  ...(lastModified ? { 'Last-Modified': lastModified } : {}),
58
109
  ...opts.extra,
59
110
  };
60
111
  }
112
+ /**
113
+ * Headers for a JSON descriptor — an agent card, an OpenAPI document, a
114
+ * registry manifest. These are the documents a client refetches most and the
115
+ * ones that had no validator at all: served as `Cache-Control: public` with no
116
+ * `max-age`, a caller falls back to heuristic freshness and can never
117
+ * revalidate, so a corrected card takes an unbounded time to reach anyone.
118
+ */
119
+ export function descriptorHeaders(etag, opts = {}) {
120
+ const maxAge = opts.maxAge ?? 86400;
121
+ return {
122
+ 'Content-Type': 'application/json; charset=utf-8',
123
+ 'Cache-Control': `public, max-age=300, s-maxage=${maxAge}, stale-while-revalidate=${maxAge * 7}`,
124
+ ETag: etag,
125
+ ...opts.extra,
126
+ };
127
+ }
128
+ /**
129
+ * A JSON descriptor served with a validator, answering a conditional GET.
130
+ *
131
+ * The body is its own ETag source, so the tag moves exactly when the document
132
+ * does and never otherwise. Every descriptor route in this workspace was a
133
+ * hand-rolled `NextResponse.json` with a Cache-Control and no validator at all,
134
+ * which is why a corrected agent card took an unbounded time to reach anyone.
135
+ */
136
+ export function descriptorResponse(request, body, opts = {}) {
137
+ const text = JSON.stringify(body);
138
+ const etag = corpusEtag([text]);
139
+ return notModified(request, etag) ?? new Response(text, { headers: descriptorHeaders(etag, opts) });
140
+ }
61
141
  /** Strip URLs and collapse whitespace so an excerpt stays one readable line. */
62
142
  export function cleanSnippet(text, max = 160) {
63
143
  return text
package/dist/http.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
package/dist/mcp.d.ts CHANGED
@@ -1,4 +1,21 @@
1
- export declare const MCP_PROTOCOL_VERSION = "2025-03-26";
1
+ /**
2
+ * The versions this transport speaks, newest first.
3
+ *
4
+ * This was a constant answered unconditionally, which is legal and still wrong:
5
+ * a client asking for 2025-06-18 was told 2025-03-26 and silently gave up tool
6
+ * titles, structured output and `_meta`. The last one is where a payment
7
+ * receipt rides, so the one surface that needed it rebuilt this module's HTTP
8
+ * shell by hand to reach it.
9
+ */
10
+ export declare const MCP_PROTOCOL_VERSIONS: readonly ["2025-06-18", "2025-03-26", "2024-11-05"];
11
+ export type McpProtocolVersion = (typeof MCP_PROTOCOL_VERSIONS)[number];
12
+ /** The newest version spoken here, and what an unrecognised ask falls back to. */
13
+ export declare const MCP_PROTOCOL_VERSION: McpProtocolVersion;
14
+ /** Echo the client's version when it is one we speak, else offer the newest. */
15
+ export declare function negotiateProtocol(requested: unknown): McpProtocolVersion;
16
+ /** The version a post-initialize request is operating under. */
17
+ export declare function requestProtocol(request: Request): McpProtocolVersion;
18
+ import { type RateLimitOptions } from './rate-limit.js';
2
19
  export type ToolParam = {
3
20
  type: 'string' | 'number' | 'boolean' | 'array' | 'object';
4
21
  description: string;
@@ -11,6 +28,13 @@ export type ToolSchema = {
11
28
  type: 'object';
12
29
  properties: Record<string, ToolParam>;
13
30
  required?: string[];
31
+ /**
32
+ * At least one of these must be present. `required` cannot say "query or
33
+ * popular", so a tool needing that checked it in its handler and the caller
34
+ * only learned at execution time — the one class of argument mistake this
35
+ * module was otherwise catching before dispatch.
36
+ */
37
+ requireOneOf?: string[];
14
38
  };
15
39
  /**
16
40
  * Behavioural hints a client uses to decide what needs a human in the loop.
@@ -18,7 +42,11 @@ export type ToolSchema = {
18
42
  * calls has no way to tell a lookup from one that reaches a payment processor.
19
43
  */
20
44
  export type ToolAnnotations = {
21
- /** Human-facing label for the tool. */
45
+ /**
46
+ * Human-facing label — the pre-2025-06-18 slot. `tools/list` hoists it to
47
+ * the top-level `title` when that is absent, so a manifest written against
48
+ * either revision renders the same in a client. Prefer `McpTool.title`.
49
+ */
22
50
  title?: string;
23
51
  /** Does not modify anything. */
24
52
  readOnlyHint?: boolean;
@@ -29,10 +57,33 @@ export type ToolAnnotations = {
29
57
  /** Touches systems beyond this server. */
30
58
  openWorldHint?: boolean;
31
59
  };
60
+ /**
61
+ * What a handler can see of the call beyond its arguments, and its one channel
62
+ * back out. `_meta` carries everything that is *about* a call rather than in it
63
+ * — a payment receipt, a progress token — and a handler returning a bare value
64
+ * could reach none of it.
65
+ */
66
+ export interface ToolContext {
67
+ /** `params._meta` as it arrived. */
68
+ meta: Record<string, unknown>;
69
+ /** Set a key on this result's `_meta`. */
70
+ setMeta: (key: string, value: unknown) => void;
71
+ /** The version negotiated for this request. */
72
+ protocolVersion: McpProtocolVersion;
73
+ /** The HTTP request, where the mount had one to give. */
74
+ request?: Request;
75
+ }
32
76
  export interface McpTool {
33
77
  name: string;
78
+ /** Human-facing label. 2025-06-18 promoted this out of `annotations`. */
79
+ title?: string;
34
80
  description: string;
35
81
  inputSchema: ToolSchema;
82
+ /**
83
+ * Declare it and the result carries `structuredContent` beside the text, so a
84
+ * client reads the answer instead of scraping prose for it.
85
+ */
86
+ outputSchema?: ToolSchema;
36
87
  annotations?: ToolAnnotations;
37
88
  /** Surfaced as an A2A skill on the agent card. Not sent over MCP. */
38
89
  skill?: {
@@ -40,7 +91,7 @@ export interface McpTool {
40
91
  tags: string[];
41
92
  examples?: string[];
42
93
  };
43
- handler: (args: Record<string, unknown>) => Promise<unknown>;
94
+ handler: (args: Record<string, unknown>, ctx: ToolContext) => Promise<unknown>;
44
95
  }
45
96
  export interface McpResource {
46
97
  uri: string;
@@ -88,8 +139,41 @@ export interface McpServerConfig {
88
139
  * query. Keep this aligned with whatever ceiling the batching tools advertise.
89
140
  */
90
141
  maxBatch?: number;
142
+ /**
143
+ * Per-IP budget for this endpoint.
144
+ *
145
+ * Declared rather than wired: the four routes in this workspace each spelled
146
+ * out the same verdict → refuse → answer → attach-headroom dance through
147
+ * three differently-named local helpers, two of them taking an async detour
148
+ * through the framework's `headers()` to reach a `Request` that was already
149
+ * in hand. A surface that states a budget owes the same four things every
150
+ * time, so stating the budget is now the whole of it — and the headroom
151
+ * header cannot be the part a new surface forgets.
152
+ *
153
+ * Enforced before the body is parsed, deliberately: an unparsed body must not
154
+ * cost a query.
155
+ */
156
+ rateLimit?: RateLimitOptions & {
157
+ /** Bucket namespace. Defaults to `mcp`; endpoints sharing it share a bucket. */
158
+ prefix?: string;
159
+ };
91
160
  /** Per-request analytics hook. Runs before dispatch; never blocks the response. */
92
161
  onCall?: (request: Request, body: unknown) => void;
162
+ /**
163
+ * Envelope-level middleware: it may withhold entries before dispatch and
164
+ * merge its own responses back in afterwards. A payment gate has to sit here
165
+ * rather than in a handler, because the demand *replaces* the call and the
166
+ * receipt rides on the envelope. Without this hook one surface had rebuilt
167
+ * the whole of `mcpResponse` by hand to wrap it.
168
+ */
169
+ gate?: (body: unknown) => Promise<EnvelopeGate>;
170
+ }
171
+ /** What a `gate` hands back: the body to dispatch, and how to finish. */
172
+ export interface EnvelopeGate {
173
+ body: RpcRequest | RpcRequest[];
174
+ /** Every entry withheld — dispatch nothing rather than an empty batch. */
175
+ empty: boolean;
176
+ finish(dispatched: object | object[] | null): Promise<object | object[] | null>;
93
177
  }
94
178
  /**
95
179
  * A caller-fixable failure inside a handler (page not found, empty input).
@@ -108,8 +192,13 @@ export type RpcRequest = {
108
192
  method: string;
109
193
  params?: unknown;
110
194
  };
195
+ /** Everything the dispatcher needs that is not in the JSON-RPC entry itself. */
196
+ interface Dispatch {
197
+ protocolVersion: McpProtocolVersion;
198
+ request?: Request;
199
+ }
111
200
  /** Dispatch a parsed body. `null` means notification-only — answer 202, not 200. */
112
- export declare function handleMcp(body: RpcRequest | RpcRequest[], config: McpServerConfig): Promise<object | object[] | null>;
201
+ export declare function handleMcp(body: RpcRequest | RpcRequest[], config: McpServerConfig, dispatch?: Dispatch): Promise<object | object[] | null>;
113
202
  export declare const MCP_CORS: Record<string, string>;
114
203
  export declare function withMcpCors<T extends Response>(res: T): T;
115
204
  export declare function mcpOptions(): Response;
@@ -119,6 +208,18 @@ export declare function mcpOptions(): Response;
119
208
  * could not even read the refusal — answer it here, and say what to do instead.
120
209
  */
121
210
  export declare function mcpGet(docsUrl?: string): Response;
122
- /** The whole POST leg: parse, track, dispatch, and answer with the right status. */
211
+ /**
212
+ * The 429 an MCP endpoint owes a caller that is over budget.
213
+ *
214
+ * Every surface here answered with a bare `{error: "..."}` — a string where the
215
+ * client's parser expects `{code, message}` — on the one response an agent
216
+ * meets precisely when it is working hard, and the one it most needs to read to
217
+ * back off correctly. It needs the CORS headers for the same reason the GET
218
+ * refusal does. The id is null because the limiter runs before the body is
219
+ * parsed, which is deliberate: an unparsed body cannot cost a query.
220
+ */
221
+ export declare function mcpRateLimited(retryAfterSec: number, message?: string): Response;
222
+ /** The whole POST leg: parse, gate, track, dispatch, and answer with the right status. */
123
223
  export declare function mcpResponse(request: Request, config: McpServerConfig): Promise<Response>;
224
+ export {};
124
225
  //# sourceMappingURL=mcp.d.ts.map
package/dist/mcp.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"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"}
package/dist/mcp.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // mcp.ts — a minimal Model Context Protocol server over Streamable HTTP
2
- // (JSON-RPC). Spec: https://modelcontextprotocol.io/specification/2025-03-26
2
+ // (JSON-RPC). Spec: https://modelcontextprotocol.io/specification/2025-06-18
3
3
  //
4
4
  // Web-standard `Request`/`Response` only, so this runs unchanged on Next route
5
5
  // handlers (NextResponse extends Response), Hono, Bun, Deno and workers.
@@ -12,7 +12,33 @@
12
12
  //
13
13
  // A caller-fixable mistake is a tool result with `isError`, never a -32603.
14
14
  // That split is the one most implementations get wrong.
15
- export const MCP_PROTOCOL_VERSION = '2025-03-26';
15
+ /**
16
+ * The versions this transport speaks, newest first.
17
+ *
18
+ * This was a constant answered unconditionally, which is legal and still wrong:
19
+ * a client asking for 2025-06-18 was told 2025-03-26 and silently gave up tool
20
+ * titles, structured output and `_meta`. The last one is where a payment
21
+ * receipt rides, so the one surface that needed it rebuilt this module's HTTP
22
+ * shell by hand to reach it.
23
+ */
24
+ export const MCP_PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'];
25
+ /** The newest version spoken here, and what an unrecognised ask falls back to. */
26
+ export const MCP_PROTOCOL_VERSION = MCP_PROTOCOL_VERSIONS[0];
27
+ /** What a request carrying no `MCP-Protocol-Version` header means, per the spec. */
28
+ const ASSUMED_VERSION = '2025-03-26';
29
+ const speaks = (v) => MCP_PROTOCOL_VERSIONS.includes(v);
30
+ /** Echo the client's version when it is one we speak, else offer the newest. */
31
+ export function negotiateProtocol(requested) {
32
+ return speaks(requested) ? requested : MCP_PROTOCOL_VERSION;
33
+ }
34
+ /** The version a post-initialize request is operating under. */
35
+ export function requestProtocol(request) {
36
+ const header = request.headers.get('mcp-protocol-version');
37
+ return speaks(header) ? header : ASSUMED_VERSION;
38
+ }
39
+ /** JSON-RPC batching was removed in 2025-06-18; it stays legal below that. */
40
+ const allowsBatch = (v) => v !== '2025-06-18';
41
+ import { clientKey, rateLimit, rateLimitHeaders, withRateLimit } from './rate-limit.js';
16
42
  /**
17
43
  * A caller-fixable failure inside a handler (page not found, empty input).
18
44
  * Reported as a tool result with `isError` so the model sees the text and can
@@ -40,17 +66,17 @@ const rpcError = (id, code, message, data) => ({
40
66
  id,
41
67
  error: { code, message, ...(data ? { data } : {}) },
42
68
  });
43
- const toolText = (id, text, isError = false) => ({
69
+ const toolText = (id, text, isError = false, extra = {}) => ({
44
70
  jsonrpc: '2.0',
45
71
  id,
46
- result: { content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) },
72
+ result: { content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}), ...extra },
47
73
  });
48
74
  /**
49
75
  * Every problem with the call at once, each naming the field and its legal
50
76
  * values, plus the schema — one retry should be able to fix all of them.
51
77
  */
52
78
  function validateArgs(tool, args) {
53
- const { properties, required = [] } = tool.inputSchema;
79
+ const { properties, required = [], requireOneOf } = tool.inputSchema;
54
80
  const allowed = Object.keys(properties);
55
81
  const problems = [];
56
82
  for (const key of Object.keys(args)) {
@@ -65,6 +91,9 @@ function validateArgs(tool, args) {
65
91
  problems.push(`Missing required parameter "${key}". Required: ${quote(required)}.`);
66
92
  }
67
93
  }
94
+ if (requireOneOf?.length && !requireOneOf.some(k => args[k] !== undefined && args[k] !== null)) {
95
+ problems.push(`Provide at least one of ${quote(requireOneOf)}.`);
96
+ }
68
97
  for (const [key, spec] of Object.entries(properties)) {
69
98
  const value = args[key];
70
99
  if (value === undefined || value === null)
@@ -107,11 +136,19 @@ const BASE_METHODS = ['initialize', 'ping', 'tools/list', 'tools/call'];
107
136
  function methodsFor(config) {
108
137
  return [
109
138
  ...BASE_METHODS,
110
- ...(config.resources?.length ? ['resources/list', 'resources/read'] : []),
111
- ...(config.prompts?.length ? ['prompts/list', 'prompts/get'] : []),
139
+ // The list methods answer whether or not anything is registered: an empty
140
+ // list is a better answer to a client that asked than a -32601 it has to
141
+ // interpret. This enumeration used to omit them while the dispatch below
142
+ // answered them, so the error message contradicted the server describing
143
+ // itself.
144
+ 'resources/list',
145
+ 'resources/templates/list',
146
+ 'prompts/list',
147
+ ...(config.resources?.length ? ['resources/read'] : []),
148
+ ...(config.prompts?.length ? ['prompts/get'] : []),
112
149
  ];
113
150
  }
114
- async function handleRpc(req, config) {
151
+ async function handleRpc(req, config, dispatch) {
115
152
  const { id, method, params } = req;
116
153
  const p = (params ?? {});
117
154
  const resources = config.resources ?? [];
@@ -123,7 +160,9 @@ async function handleRpc(req, config) {
123
160
  jsonrpc: '2.0',
124
161
  id,
125
162
  result: {
126
- protocolVersion: MCP_PROTOCOL_VERSION,
163
+ // Echo what the client asked for when we speak it. Answering a
164
+ // constant is what silently held every caller at 2025-03-26.
165
+ protocolVersion: negotiateProtocol(p.protocolVersion),
127
166
  // Only advertise capabilities the config actually populates — an
128
167
  // advertised `resources` whose list comes back empty reads as a bug
129
168
  // to a client, not as honesty.
@@ -147,10 +186,16 @@ async function handleRpc(req, config) {
147
186
  jsonrpc: '2.0',
148
187
  id,
149
188
  result: {
150
- tools: config.tools.map(({ name, description, inputSchema, annotations }) => ({
189
+ tools: config.tools.map(({ name, title, description, inputSchema, outputSchema, annotations }) => ({
151
190
  name,
191
+ // 2025-06-18 promoted `title` out of `annotations`. Hoisting
192
+ // rather than requiring every manifest to be rewritten: a
193
+ // client reading only the new slot showed a raw tool name for
194
+ // every surface that had not moved its label yet.
195
+ ...(title ?? annotations?.title ? { title: title ?? annotations.title } : {}),
152
196
  description,
153
197
  inputSchema,
198
+ ...(outputSchema ? { outputSchema } : {}),
154
199
  ...(annotations ? { annotations } : {}),
155
200
  })),
156
201
  },
@@ -168,6 +213,11 @@ async function handleRpc(req, config) {
168
213
  })),
169
214
  },
170
215
  };
216
+ // Declared `resources` makes a client ask for templates too. There are
217
+ // none to give — every resource here is a fixed URI — but an empty list
218
+ // is the answer to that question, where -32601 reads as a broken server.
219
+ case 'resources/templates/list':
220
+ return { jsonrpc: '2.0', id, result: { resourceTemplates: [] } };
171
221
  case 'resources/read': {
172
222
  const { uri } = p;
173
223
  const resource = uri ? resources.find(r => r.uri === uri) : undefined;
@@ -223,9 +273,34 @@ async function handleRpc(req, config) {
223
273
  const invalid = validateArgs(tool, args);
224
274
  if (invalid)
225
275
  return toolText(id, invalid, true);
276
+ const out = {};
277
+ const ctx = {
278
+ meta: (p._meta ?? {}),
279
+ setMeta: (key, value) => {
280
+ out[key] = value;
281
+ },
282
+ protocolVersion: dispatch.protocolVersion,
283
+ request: dispatch.request,
284
+ };
226
285
  try {
227
- const data = await tool.handler(args);
228
- return toolText(id, typeof data === 'string' ? data : JSON.stringify(data, null, 2));
286
+ const data = await tool.handler(args, ctx);
287
+ const text = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
288
+ // Every object answer, not only the schema-bearing ones. Gating this
289
+ // on `outputSchema` meant that across four live servers and thirty-two
290
+ // tools — every one of them returning JSON — not a single result ever
291
+ // carried `structuredContent`, and every agent parsed prose to reach
292
+ // data the server already had in hand. A declared `outputSchema` is
293
+ // still the stronger contract (a client validates against it); it is
294
+ // no longer the price of admission. The text block stays regardless:
295
+ // the spec asks for the serialised twin, and a client that reads only
296
+ // content still has to be able to read the answer.
297
+ const structured = data !== null && typeof data === 'object' && !Array.isArray(data)
298
+ ? { structuredContent: data }
299
+ : {};
300
+ return toolText(id, text, false, {
301
+ ...structured,
302
+ ...(Object.keys(out).length ? { _meta: out } : {}),
303
+ });
229
304
  }
230
305
  catch (err) {
231
306
  if (!(err instanceof McpToolError))
@@ -245,7 +320,7 @@ async function handleRpc(req, config) {
245
320
  }
246
321
  }
247
322
  /** Dispatch a parsed body. `null` means notification-only — answer 202, not 200. */
248
- export async function handleMcp(body, config) {
323
+ export async function handleMcp(body, config, dispatch = { protocolVersion: ASSUMED_VERSION }) {
249
324
  const maxBatch = config.maxBatch ?? DEFAULT_MAX_BATCH;
250
325
  const isBatch = Array.isArray(body);
251
326
  if (isBatch && body.length > maxBatch) {
@@ -254,7 +329,7 @@ export async function handleMcp(body, config) {
254
329
  if (isBatch && body.length === 0) {
255
330
  return rpcError(null, -32600, 'Batch is empty. Send at least one JSON-RPC request.');
256
331
  }
257
- const responses = (await Promise.all((isBatch ? body : [body]).map(r => handleRpc(r, config)))).filter(Boolean);
332
+ const responses = (await Promise.all((isBatch ? body : [body]).map(r => handleRpc(r, config, dispatch)))).filter(Boolean);
258
333
  return isBatch ? responses : (responses[0] ?? null);
259
334
  }
260
335
  // ---------------------------------------------------------------------------
@@ -270,6 +345,11 @@ export const MCP_CORS = {
270
345
  'Access-Control-Allow-Origin': '*',
271
346
  'Access-Control-Allow-Methods': 'POST, OPTIONS',
272
347
  'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID',
348
+ // Allow-Headers governs what a browser may send; without Expose-Headers it
349
+ // may read none of what comes back. A browser client could not see the
350
+ // negotiated version, and could not see `Retry-After` on the 429 telling it
351
+ // how long to wait — which reads as a hang, not as a limit.
352
+ 'Access-Control-Expose-Headers': 'Mcp-Session-Id, Mcp-Protocol-Version, Retry-After, RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset',
273
353
  'Access-Control-Max-Age': '86400',
274
354
  };
275
355
  export function withMcpCors(res) {
@@ -293,8 +373,38 @@ export function mcpGet(docsUrl) {
293
373
  headers: { ...MCP_CORS, Allow: 'POST, OPTIONS' },
294
374
  });
295
375
  }
296
- /** The whole POST leg: parse, track, dispatch, and answer with the right status. */
376
+ /**
377
+ * The 429 an MCP endpoint owes a caller that is over budget.
378
+ *
379
+ * Every surface here answered with a bare `{error: "..."}` — a string where the
380
+ * client's parser expects `{code, message}` — on the one response an agent
381
+ * meets precisely when it is working hard, and the one it most needs to read to
382
+ * back off correctly. It needs the CORS headers for the same reason the GET
383
+ * refusal does. The id is null because the limiter runs before the body is
384
+ * parsed, which is deliberate: an unparsed body cannot cost a query.
385
+ */
386
+ export function mcpRateLimited(retryAfterSec, message) {
387
+ return Response.json(rpcError(null, -32000, message ?? `Rate limit exceeded. Retry in ${retryAfterSec} seconds.`, { retryAfterSec }), { status: 429, headers: { ...MCP_CORS, 'Retry-After': String(retryAfterSec) } });
388
+ }
389
+ /** The whole POST leg: parse, gate, track, dispatch, and answer with the right status. */
297
390
  export async function mcpResponse(request, config) {
391
+ const protocolVersion = requestProtocol(request);
392
+ // Before `request.json()`: an unparsed body must not cost a query, which is
393
+ // also why the refusal carries a null id.
394
+ let headroom = {};
395
+ if (config.rateLimit) {
396
+ const verdict = rateLimit(clientKey(config.rateLimit.prefix ?? 'mcp', request.headers), config.rateLimit);
397
+ if (!verdict.ok) {
398
+ return withRateLimit(mcpRateLimited(verdict.retryAfterSec), verdict, config.rateLimit);
399
+ }
400
+ headroom = rateLimitHeaders(verdict, config.rateLimit);
401
+ }
402
+ // Echoed on every response so a client can see which version it is actually
403
+ // being answered under, rather than inferring it from the initialize it sent
404
+ // some requests ago. The headroom rides alongside on every answer, not only
405
+ // on the 429 — a budget discoverable only by exceeding it is one an agent
406
+ // meets when it is least able to act on it.
407
+ const headers = { ...MCP_CORS, 'MCP-Protocol-Version': protocolVersion, ...headroom };
298
408
  let body;
299
409
  try {
300
410
  body = await request.json();
@@ -302,16 +412,25 @@ export async function mcpResponse(request, config) {
302
412
  catch {
303
413
  return Response.json(rpcError(null, -32700, 'Parse error: request body is not valid JSON.'), {
304
414
  status: 400,
305
- headers: MCP_CORS,
415
+ headers,
306
416
  });
307
417
  }
418
+ if (Array.isArray(body) && !allowsBatch(protocolVersion)) {
419
+ return Response.json(rpcError(null, -32600, `JSON-RPC batching was removed in MCP ${protocolVersion}. Send one request per POST, or negotiate ${ASSUMED_VERSION} to keep batching.`), { status: 400, headers });
420
+ }
421
+ // Tracked on the original body, before a gate withholds anything: a call an
422
+ // agent walked away from is exactly the one worth counting.
308
423
  config.onCall?.(request, body);
309
- const result = await handleMcp(body, config);
424
+ const gate = await config.gate?.(body);
425
+ const dispatched = gate?.empty
426
+ ? null
427
+ : await handleMcp(gate ? gate.body : body, config, { protocolVersion, request });
428
+ const result = gate ? await gate.finish(dispatched) : dispatched;
310
429
  // Notification-only input produces no response bodies; the spec requires a
311
430
  // bare 202 there, not a 200 carrying a JSON `null`.
312
431
  if (result == null || (Array.isArray(result) && !result.length)) {
313
- return new Response(null, { status: 202, headers: MCP_CORS });
432
+ return new Response(null, { status: 202, headers });
314
433
  }
315
- return Response.json(result, { headers: MCP_CORS });
434
+ return Response.json(result, { headers });
316
435
  }
317
436
  //# sourceMappingURL=mcp.js.map