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.
- package/README.md +41 -3
- package/dist/conformance.d.ts +67 -2
- package/dist/conformance.d.ts.map +1 -1
- package/dist/conformance.js +218 -9
- package/dist/conformance.js.map +1 -1
- package/dist/crawlers.d.ts.map +1 -1
- package/dist/crawlers.js +9 -2
- package/dist/crawlers.js.map +1 -1
- package/dist/http.d.ts +53 -2
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +84 -4
- package/dist/http.js.map +1 -1
- package/dist/mcp.d.ts +106 -5
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +138 -19
- package/dist/mcp.js.map +1 -1
- package/dist/pagination.d.ts +21 -0
- package/dist/pagination.d.ts.map +1 -1
- package/dist/pagination.js +25 -0
- package/dist/pagination.js.map +1 -1
- package/dist/rate-limit.d.ts +25 -0
- package/dist/rate-limit.d.ts.map +1 -1
- package/dist/rate-limit.js +45 -0
- package/dist/rate-limit.js.map +1 -1
- package/dist/well-known.d.ts +9 -1
- package/dist/well-known.d.ts.map +1 -1
- package/dist/well-known.js +21 -4
- package/dist/well-known.js.map +1 -1
- package/package.json +2 -2
package/dist/http.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
/**
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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":"AAAA,gEAAgE;AAChE,EAAE;AACF,4EAA4E;AAC5E,6EAA6E;AAC7E,4EAA4E;AAC5E,wEAAwE;AAExE,iFAAiF;AACjF,MAAM,UAAU,UAAU,CAAC,KAAuD;IAChF,MAAM,IAAI,GAAG,KAAK;SACf,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;SACjE,IAAI,CAAC,GAAG,CAAC,CAAC;IACb,2EAA2E;IAC3E,6EAA6E;IAC7E,IAAI,IAAI,GAAG,UAAU,CAAC;IACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAC3B,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,MAAM,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,CAAC;AAChE,CAAC;AAED,
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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":"AAeA,eAAO,MAAM,oBAAoB,eAAe,CAAC;
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
432
|
+
return new Response(null, { status: 202, headers });
|
|
314
433
|
}
|
|
315
|
-
return Response.json(result, { headers
|
|
434
|
+
return Response.json(result, { headers });
|
|
316
435
|
}
|
|
317
436
|
//# sourceMappingURL=mcp.js.map
|