wiki-formant 0.10.0 → 0.12.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/dist/crawlers.js CHANGED
@@ -68,11 +68,18 @@ export function aiCrawlerTokens() {
68
68
  * agent everything, because it stops matching `*` the moment it matches itself.
69
69
  */
70
70
  export function aiCrawlerRules(opts) {
71
+ const aiAllow = [opts.aiAllow].flat();
71
72
  return [
72
- { userAgent: '*', allow: opts.allow, disallow: opts.disallow },
73
+ // `aiAllow` rides on the default group too, not only on the named roster.
74
+ // The agent surface an origin advertises has to be reachable by a caller it
75
+ // has never heard of: two origins here disallowed `/api/` under `*` while
76
+ // their own agent card named `/api/mcp`, so an MCP client that identified
77
+ // honestly as itself was told the endpoint was off limits. The roster is
78
+ // for indexing policy, not for hiding a documented endpoint.
79
+ { userAgent: '*', allow: [...new Set([...[opts.allow].flat(), ...aiAllow])], disallow: opts.disallow },
73
80
  ...aiCrawlerTokens().map(userAgent => ({
74
81
  userAgent,
75
- allow: opts.aiAllow,
82
+ allow: aiAllow,
76
83
  disallow: opts.disallow,
77
84
  })),
78
85
  ];
@@ -1 +1 @@
1
- {"version":3,"file":"crawlers.js","sourceRoot":"","sources":["../src/crawlers.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,EAAE;AACF,gFAAgF;AAChF,gFAAgF;AAChF,uEAAuE;AACvE,yEAAyE;AACzE,EAAE;AACF,4EAA4E;AAC5E,6EAA6E;AAC7E,gFAAgF;AAChF,0EAA0E;AAC1E,EAAE;AACF,wEAAwE;AACxE,+EAA+E;AAC/E,gFAAgF;AAChF,wEAAwE;AACxE,8EAA8E;AAC9E,+EAA+E;AAC/E,6BAA6B;AAe7B;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAyB;IAC/C,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE;IACnD,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE;IAC1D,EAAE,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE;IAChE,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE;IACzD,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE;IAC1D,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE;IAC5D,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,IAAI,EAAE;IACtE,EAAE,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,IAAI,EAAE;IACjE,EAAE,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,IAAI,EAAE;IACpE,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE;IACzD,6EAA6E;IAC7E,+EAA+E;IAC/E,8EAA8E;IAC9E,EAAE,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,IAAI,EAAE;IACpE,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE;IAC3D,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE;IACjD,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE;IACzD,EAAE,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAE,IAAI,EAAE;IAC1E,EAAE,KAAK,EAAE,sBAAsB,EAAE,KAAK,EAAE,qBAAqB,EAAE,OAAO,EAAE,IAAI,EAAE;IAC9E,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE;IAC9D,EAAE,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,IAAI,EAAE;IACjE,EAAE,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,KAAK,EAAE;CACvE,CAAC;AAEF,gFAAgF;AAChF,MAAM,UAAU,WAAW,CAAC,SAAoC;IAC9D,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IAC5B,KAAK,MAAM,OAAO,IAAI,WAAW,EAAE,CAAC;QAClC,IAAI,OAAO,CAAC,OAAO,IAAI,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,OAAO,OAAO,CAAC,KAAK,CAAC;IACjF,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,eAAe;IAC7B,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AACvC,CAAC;AAaD;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,IAI9B;IACC,OAAO;QACL,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE;QAC9D,GAAG,eAAe,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;YACrC,SAAS;YACT,KAAK,EAAE,IAAI,CAAC,OAAO;YACnB,QAAQ,EAAE,IAAI,CAAC,QAAQ;SACxB,CAAC,CAAC;KACJ,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"crawlers.js","sourceRoot":"","sources":["../src/crawlers.ts"],"names":[],"mappings":"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"}
package/dist/http.d.ts CHANGED
@@ -1,7 +1,15 @@
1
1
  /** A stable ETag from whatever the corpus revision is (count + newest stamp). */
2
2
  export declare function corpusEtag(parts: Array<string | number | Date | null | undefined>): string;
3
- /** 304 when the client already holds this revision, else `null` to render. */
4
- export declare function notModified(request: Request, etag: string, lastModified: string): Response | null;
3
+ /**
4
+ * 304 when the client already holds this revision, else `null` to render.
5
+ *
6
+ * Both validators are matched the way RFC 9110 defines them rather than by
7
+ * string equality, because equality is wrong in the cases that actually occur:
8
+ * a client sends every tag it holds as a list, a proxy adds or strips the weak
9
+ * prefix, and a crawler reformats the date. Each of those took a full render
10
+ * from a response that was already fresh.
11
+ */
12
+ export declare function notModified(request: Request, etag: string, lastModified?: string | null): Response | null;
5
13
  /**
6
14
  * Headers for a plain-text export. `maxAge` is the edge window — a curated
7
15
  * corpus can sit on hours, a projection of live data should pass a short one.
@@ -16,11 +24,42 @@ export declare function textHeaders(etag: string, lastModified: string, maxAge?:
16
24
  * offer, and a twin that stamps the epoch is worse than one that stamps
17
25
  * nothing. `extra` carries whatever the mount needs on top — an `X-Robots-Tag`
18
26
  * where the twin is a second public URL with no canonical of its own.
27
+ *
28
+ * Pass `etag`. The twin is the single most recrawled URL a page has, and a
29
+ * response with no validator is a full render on every pass forever — the same
30
+ * arithmetic that justifies the corpus ETag, applied per page. Three wikis
31
+ * shipped twins with neither validator and only 304'd where a proxy happened to
32
+ * synthesise one; that is why it is spelled out here rather than left optional
33
+ * in spirit.
19
34
  */
20
35
  export declare function markdownHeaders(lastModified?: string | null, opts?: {
36
+ maxAge?: number;
37
+ etag?: string;
38
+ extra?: Record<string, string>;
39
+ }): Record<string, string>;
40
+ /**
41
+ * Headers for a JSON descriptor — an agent card, an OpenAPI document, a
42
+ * registry manifest. These are the documents a client refetches most and the
43
+ * ones that had no validator at all: served as `Cache-Control: public` with no
44
+ * `max-age`, a caller falls back to heuristic freshness and can never
45
+ * revalidate, so a corrected card takes an unbounded time to reach anyone.
46
+ */
47
+ export declare function descriptorHeaders(etag: string, opts?: {
21
48
  maxAge?: number;
22
49
  extra?: Record<string, string>;
23
50
  }): Record<string, string>;
51
+ /**
52
+ * A JSON descriptor served with a validator, answering a conditional GET.
53
+ *
54
+ * The body is its own ETag source, so the tag moves exactly when the document
55
+ * does and never otherwise. Every descriptor route in this workspace was a
56
+ * hand-rolled `NextResponse.json` with a Cache-Control and no validator at all,
57
+ * which is why a corrected agent card took an unbounded time to reach anyone.
58
+ */
59
+ export declare function descriptorResponse(request: Request, body: unknown, opts?: {
60
+ maxAge?: number;
61
+ extra?: Record<string, string>;
62
+ }): Response;
24
63
  /** Strip URLs and collapse whitespace so an excerpt stays one readable line. */
25
64
  export declare function cleanSnippet(text: string, max?: number): string;
26
65
  /** One markdown bullet: linked title, excerpt, and the date an agent diffs on. */
@@ -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;;;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,40 @@ 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;
29
55
  }
30
56
  /**
31
57
  * Headers for a plain-text export. `maxAge` is the edge window — a curated
@@ -48,16 +74,53 @@ export function textHeaders(etag, lastModified, maxAge = 3600) {
48
74
  * offer, and a twin that stamps the epoch is worse than one that stamps
49
75
  * nothing. `extra` carries whatever the mount needs on top — an `X-Robots-Tag`
50
76
  * where the twin is a second public URL with no canonical of its own.
77
+ *
78
+ * Pass `etag`. The twin is the single most recrawled URL a page has, and a
79
+ * response with no validator is a full render on every pass forever — the same
80
+ * arithmetic that justifies the corpus ETag, applied per page. Three wikis
81
+ * shipped twins with neither validator and only 304'd where a proxy happened to
82
+ * synthesise one; that is why it is spelled out here rather than left optional
83
+ * in spirit.
51
84
  */
52
85
  export function markdownHeaders(lastModified, opts = {}) {
53
86
  const maxAge = opts.maxAge ?? 3600;
54
87
  return {
55
88
  'Content-Type': 'text/markdown; charset=utf-8',
56
89
  'Cache-Control': `public, s-maxage=${maxAge}, stale-while-revalidate=${maxAge * 24}`,
90
+ ...(opts.etag ? { ETag: opts.etag } : {}),
57
91
  ...(lastModified ? { 'Last-Modified': lastModified } : {}),
58
92
  ...opts.extra,
59
93
  };
60
94
  }
95
+ /**
96
+ * Headers for a JSON descriptor — an agent card, an OpenAPI document, a
97
+ * registry manifest. These are the documents a client refetches most and the
98
+ * ones that had no validator at all: served as `Cache-Control: public` with no
99
+ * `max-age`, a caller falls back to heuristic freshness and can never
100
+ * revalidate, so a corrected card takes an unbounded time to reach anyone.
101
+ */
102
+ export function descriptorHeaders(etag, opts = {}) {
103
+ const maxAge = opts.maxAge ?? 86400;
104
+ return {
105
+ 'Content-Type': 'application/json; charset=utf-8',
106
+ 'Cache-Control': `public, max-age=300, s-maxage=${maxAge}, stale-while-revalidate=${maxAge * 7}`,
107
+ ETag: etag,
108
+ ...opts.extra,
109
+ };
110
+ }
111
+ /**
112
+ * A JSON descriptor served with a validator, answering a conditional GET.
113
+ *
114
+ * The body is its own ETag source, so the tag moves exactly when the document
115
+ * does and never otherwise. Every descriptor route in this workspace was a
116
+ * hand-rolled `NextResponse.json` with a Cache-Control and no validator at all,
117
+ * which is why a corrected agent card took an unbounded time to reach anyone.
118
+ */
119
+ export function descriptorResponse(request, body, opts = {}) {
120
+ const text = JSON.stringify(body);
121
+ const etag = corpusEtag([text]);
122
+ return notModified(request, etag) ?? new Response(text, { headers: descriptorHeaders(etag, opts) });
123
+ }
61
124
  /** Strip URLs and collapse whitespace so an excerpt stays one readable line. */
62
125
  export function cleanSnippet(text, max = 160) {
63
126
  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,20 @@
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;
2
18
  export type ToolParam = {
3
19
  type: 'string' | 'number' | 'boolean' | 'array' | 'object';
4
20
  description: string;
@@ -29,10 +45,33 @@ export type ToolAnnotations = {
29
45
  /** Touches systems beyond this server. */
30
46
  openWorldHint?: boolean;
31
47
  };
48
+ /**
49
+ * What a handler can see of the call beyond its arguments, and its one channel
50
+ * back out. `_meta` carries everything that is *about* a call rather than in it
51
+ * — a payment receipt, a progress token — and a handler returning a bare value
52
+ * could reach none of it.
53
+ */
54
+ export interface ToolContext {
55
+ /** `params._meta` as it arrived. */
56
+ meta: Record<string, unknown>;
57
+ /** Set a key on this result's `_meta`. */
58
+ setMeta: (key: string, value: unknown) => void;
59
+ /** The version negotiated for this request. */
60
+ protocolVersion: McpProtocolVersion;
61
+ /** The HTTP request, where the mount had one to give. */
62
+ request?: Request;
63
+ }
32
64
  export interface McpTool {
33
65
  name: string;
66
+ /** Human-facing label. 2025-06-18 promoted this out of `annotations`. */
67
+ title?: string;
34
68
  description: string;
35
69
  inputSchema: ToolSchema;
70
+ /**
71
+ * Declare it and the result carries `structuredContent` beside the text, so a
72
+ * client reads the answer instead of scraping prose for it.
73
+ */
74
+ outputSchema?: ToolSchema;
36
75
  annotations?: ToolAnnotations;
37
76
  /** Surfaced as an A2A skill on the agent card. Not sent over MCP. */
38
77
  skill?: {
@@ -40,7 +79,7 @@ export interface McpTool {
40
79
  tags: string[];
41
80
  examples?: string[];
42
81
  };
43
- handler: (args: Record<string, unknown>) => Promise<unknown>;
82
+ handler: (args: Record<string, unknown>, ctx: ToolContext) => Promise<unknown>;
44
83
  }
45
84
  export interface McpResource {
46
85
  uri: string;
@@ -90,6 +129,21 @@ export interface McpServerConfig {
90
129
  maxBatch?: number;
91
130
  /** Per-request analytics hook. Runs before dispatch; never blocks the response. */
92
131
  onCall?: (request: Request, body: unknown) => void;
132
+ /**
133
+ * Envelope-level middleware: it may withhold entries before dispatch and
134
+ * merge its own responses back in afterwards. A payment gate has to sit here
135
+ * rather than in a handler, because the demand *replaces* the call and the
136
+ * receipt rides on the envelope. Without this hook one surface had rebuilt
137
+ * the whole of `mcpResponse` by hand to wrap it.
138
+ */
139
+ gate?: (body: unknown) => Promise<EnvelopeGate>;
140
+ }
141
+ /** What a `gate` hands back: the body to dispatch, and how to finish. */
142
+ export interface EnvelopeGate {
143
+ body: RpcRequest | RpcRequest[];
144
+ /** Every entry withheld — dispatch nothing rather than an empty batch. */
145
+ empty: boolean;
146
+ finish(dispatched: object | object[] | null): Promise<object | object[] | null>;
93
147
  }
94
148
  /**
95
149
  * A caller-fixable failure inside a handler (page not found, empty input).
@@ -100,14 +154,21 @@ export declare class McpToolError extends Error {
100
154
  readonly details?: Record<string, unknown> | undefined;
101
155
  constructor(message: string, details?: Record<string, unknown> | undefined);
102
156
  }
103
- type RpcRequest = {
157
+ /** One entry in a JSON-RPC envelope. Exported because `wiki-formant/x402`
158
+ * gates the envelope before `handleMcp` ever sees it. */
159
+ export type RpcRequest = {
104
160
  jsonrpc: '2.0';
105
161
  id: string | number | null;
106
162
  method: string;
107
163
  params?: unknown;
108
164
  };
165
+ /** Everything the dispatcher needs that is not in the JSON-RPC entry itself. */
166
+ interface Dispatch {
167
+ protocolVersion: McpProtocolVersion;
168
+ request?: Request;
169
+ }
109
170
  /** Dispatch a parsed body. `null` means notification-only — answer 202, not 200. */
110
- export declare function handleMcp(body: RpcRequest | RpcRequest[], config: McpServerConfig): Promise<object | object[] | null>;
171
+ export declare function handleMcp(body: RpcRequest | RpcRequest[], config: McpServerConfig, dispatch?: Dispatch): Promise<object | object[] | null>;
111
172
  export declare const MCP_CORS: Record<string, string>;
112
173
  export declare function withMcpCors<T extends Response>(res: T): T;
113
174
  export declare function mcpOptions(): Response;
@@ -117,7 +178,18 @@ export declare function mcpOptions(): Response;
117
178
  * could not even read the refusal — answer it here, and say what to do instead.
118
179
  */
119
180
  export declare function mcpGet(docsUrl?: string): Response;
120
- /** The whole POST leg: parse, track, dispatch, and answer with the right status. */
181
+ /**
182
+ * The 429 an MCP endpoint owes a caller that is over budget.
183
+ *
184
+ * Every surface here answered with a bare `{error: "..."}` — a string where the
185
+ * client's parser expects `{code, message}` — on the one response an agent
186
+ * meets precisely when it is working hard, and the one it most needs to read to
187
+ * back off correctly. It needs the CORS headers for the same reason the GET
188
+ * refusal does. The id is null because the limiter runs before the body is
189
+ * parsed, which is deliberate: an unparsed body cannot cost a query.
190
+ */
191
+ export declare function mcpRateLimited(retryAfterSec: number, message?: string): Response;
192
+ /** The whole POST leg: parse, gate, track, dispatch, and answer with the right status. */
121
193
  export declare function mcpResponse(request: Request, config: McpServerConfig): Promise<Response>;
122
194
  export {};
123
195
  //# 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,32 @@
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';
16
41
  /**
17
42
  * A caller-fixable failure inside a handler (page not found, empty input).
18
43
  * Reported as a tool result with `isError` so the model sees the text and can
@@ -40,10 +65,10 @@ const rpcError = (id, code, message, data) => ({
40
65
  id,
41
66
  error: { code, message, ...(data ? { data } : {}) },
42
67
  });
43
- const toolText = (id, text, isError = false) => ({
68
+ const toolText = (id, text, isError = false, extra = {}) => ({
44
69
  jsonrpc: '2.0',
45
70
  id,
46
- result: { content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) },
71
+ result: { content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}), ...extra },
47
72
  });
48
73
  /**
49
74
  * Every problem with the call at once, each naming the field and its legal
@@ -107,11 +132,19 @@ const BASE_METHODS = ['initialize', 'ping', 'tools/list', 'tools/call'];
107
132
  function methodsFor(config) {
108
133
  return [
109
134
  ...BASE_METHODS,
110
- ...(config.resources?.length ? ['resources/list', 'resources/read'] : []),
111
- ...(config.prompts?.length ? ['prompts/list', 'prompts/get'] : []),
135
+ // The list methods answer whether or not anything is registered: an empty
136
+ // list is a better answer to a client that asked than a -32601 it has to
137
+ // interpret. This enumeration used to omit them while the dispatch below
138
+ // answered them, so the error message contradicted the server describing
139
+ // itself.
140
+ 'resources/list',
141
+ 'resources/templates/list',
142
+ 'prompts/list',
143
+ ...(config.resources?.length ? ['resources/read'] : []),
144
+ ...(config.prompts?.length ? ['prompts/get'] : []),
112
145
  ];
113
146
  }
114
- async function handleRpc(req, config) {
147
+ async function handleRpc(req, config, dispatch) {
115
148
  const { id, method, params } = req;
116
149
  const p = (params ?? {});
117
150
  const resources = config.resources ?? [];
@@ -123,7 +156,9 @@ async function handleRpc(req, config) {
123
156
  jsonrpc: '2.0',
124
157
  id,
125
158
  result: {
126
- protocolVersion: MCP_PROTOCOL_VERSION,
159
+ // Echo what the client asked for when we speak it. Answering a
160
+ // constant is what silently held every caller at 2025-03-26.
161
+ protocolVersion: negotiateProtocol(p.protocolVersion),
127
162
  // Only advertise capabilities the config actually populates — an
128
163
  // advertised `resources` whose list comes back empty reads as a bug
129
164
  // to a client, not as honesty.
@@ -147,10 +182,12 @@ async function handleRpc(req, config) {
147
182
  jsonrpc: '2.0',
148
183
  id,
149
184
  result: {
150
- tools: config.tools.map(({ name, description, inputSchema, annotations }) => ({
185
+ tools: config.tools.map(({ name, title, description, inputSchema, outputSchema, annotations }) => ({
151
186
  name,
187
+ ...(title ? { title } : {}),
152
188
  description,
153
189
  inputSchema,
190
+ ...(outputSchema ? { outputSchema } : {}),
154
191
  ...(annotations ? { annotations } : {}),
155
192
  })),
156
193
  },
@@ -168,6 +205,11 @@ async function handleRpc(req, config) {
168
205
  })),
169
206
  },
170
207
  };
208
+ // Declared `resources` makes a client ask for templates too. There are
209
+ // none to give — every resource here is a fixed URI — but an empty list
210
+ // is the answer to that question, where -32601 reads as a broken server.
211
+ case 'resources/templates/list':
212
+ return { jsonrpc: '2.0', id, result: { resourceTemplates: [] } };
171
213
  case 'resources/read': {
172
214
  const { uri } = p;
173
215
  const resource = uri ? resources.find(r => r.uri === uri) : undefined;
@@ -223,9 +265,28 @@ async function handleRpc(req, config) {
223
265
  const invalid = validateArgs(tool, args);
224
266
  if (invalid)
225
267
  return toolText(id, invalid, true);
268
+ const out = {};
269
+ const ctx = {
270
+ meta: (p._meta ?? {}),
271
+ setMeta: (key, value) => {
272
+ out[key] = value;
273
+ },
274
+ protocolVersion: dispatch.protocolVersion,
275
+ request: dispatch.request,
276
+ };
226
277
  try {
227
- const data = await tool.handler(args);
228
- return toolText(id, typeof data === 'string' ? data : JSON.stringify(data, null, 2));
278
+ const data = await tool.handler(args, ctx);
279
+ const text = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
280
+ // `structuredContent` only where a schema says what its shape is; the
281
+ // text block stays either way, because a client that never looked at
282
+ // outputSchema still has to be able to read the answer.
283
+ const structured = tool.outputSchema && data !== null && typeof data === 'object' && !Array.isArray(data)
284
+ ? { structuredContent: data }
285
+ : {};
286
+ return toolText(id, text, false, {
287
+ ...structured,
288
+ ...(Object.keys(out).length ? { _meta: out } : {}),
289
+ });
229
290
  }
230
291
  catch (err) {
231
292
  if (!(err instanceof McpToolError))
@@ -245,7 +306,7 @@ async function handleRpc(req, config) {
245
306
  }
246
307
  }
247
308
  /** Dispatch a parsed body. `null` means notification-only — answer 202, not 200. */
248
- export async function handleMcp(body, config) {
309
+ export async function handleMcp(body, config, dispatch = { protocolVersion: ASSUMED_VERSION }) {
249
310
  const maxBatch = config.maxBatch ?? DEFAULT_MAX_BATCH;
250
311
  const isBatch = Array.isArray(body);
251
312
  if (isBatch && body.length > maxBatch) {
@@ -254,7 +315,7 @@ export async function handleMcp(body, config) {
254
315
  if (isBatch && body.length === 0) {
255
316
  return rpcError(null, -32600, 'Batch is empty. Send at least one JSON-RPC request.');
256
317
  }
257
- const responses = (await Promise.all((isBatch ? body : [body]).map(r => handleRpc(r, config)))).filter(Boolean);
318
+ const responses = (await Promise.all((isBatch ? body : [body]).map(r => handleRpc(r, config, dispatch)))).filter(Boolean);
258
319
  return isBatch ? responses : (responses[0] ?? null);
259
320
  }
260
321
  // ---------------------------------------------------------------------------
@@ -270,6 +331,11 @@ export const MCP_CORS = {
270
331
  'Access-Control-Allow-Origin': '*',
271
332
  'Access-Control-Allow-Methods': 'POST, OPTIONS',
272
333
  'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID',
334
+ // Allow-Headers governs what a browser may send; without Expose-Headers it
335
+ // may read none of what comes back. A browser client could not see the
336
+ // negotiated version, and could not see `Retry-After` on the 429 telling it
337
+ // how long to wait — which reads as a hang, not as a limit.
338
+ 'Access-Control-Expose-Headers': 'Mcp-Session-Id, Mcp-Protocol-Version, Retry-After',
273
339
  'Access-Control-Max-Age': '86400',
274
340
  };
275
341
  export function withMcpCors(res) {
@@ -293,8 +359,26 @@ export function mcpGet(docsUrl) {
293
359
  headers: { ...MCP_CORS, Allow: 'POST, OPTIONS' },
294
360
  });
295
361
  }
296
- /** The whole POST leg: parse, track, dispatch, and answer with the right status. */
362
+ /**
363
+ * The 429 an MCP endpoint owes a caller that is over budget.
364
+ *
365
+ * Every surface here answered with a bare `{error: "..."}` — a string where the
366
+ * client's parser expects `{code, message}` — on the one response an agent
367
+ * meets precisely when it is working hard, and the one it most needs to read to
368
+ * back off correctly. It needs the CORS headers for the same reason the GET
369
+ * refusal does. The id is null because the limiter runs before the body is
370
+ * parsed, which is deliberate: an unparsed body cannot cost a query.
371
+ */
372
+ export function mcpRateLimited(retryAfterSec, message) {
373
+ return Response.json(rpcError(null, -32000, message ?? `Rate limit exceeded. Retry in ${retryAfterSec} seconds.`, { retryAfterSec }), { status: 429, headers: { ...MCP_CORS, 'Retry-After': String(retryAfterSec) } });
374
+ }
375
+ /** The whole POST leg: parse, gate, track, dispatch, and answer with the right status. */
297
376
  export async function mcpResponse(request, config) {
377
+ const protocolVersion = requestProtocol(request);
378
+ // Echoed on every response so a client can see which version it is actually
379
+ // being answered under, rather than inferring it from the initialize it sent
380
+ // some requests ago.
381
+ const headers = { ...MCP_CORS, 'MCP-Protocol-Version': protocolVersion };
298
382
  let body;
299
383
  try {
300
384
  body = await request.json();
@@ -302,16 +386,25 @@ export async function mcpResponse(request, config) {
302
386
  catch {
303
387
  return Response.json(rpcError(null, -32700, 'Parse error: request body is not valid JSON.'), {
304
388
  status: 400,
305
- headers: MCP_CORS,
389
+ headers,
306
390
  });
307
391
  }
392
+ if (Array.isArray(body) && !allowsBatch(protocolVersion)) {
393
+ 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 });
394
+ }
395
+ // Tracked on the original body, before a gate withholds anything: a call an
396
+ // agent walked away from is exactly the one worth counting.
308
397
  config.onCall?.(request, body);
309
- const result = await handleMcp(body, config);
398
+ const gate = await config.gate?.(body);
399
+ const dispatched = gate?.empty
400
+ ? null
401
+ : await handleMcp(gate ? gate.body : body, config, { protocolVersion, request });
402
+ const result = gate ? await gate.finish(dispatched) : dispatched;
310
403
  // Notification-only input produces no response bodies; the spec requires a
311
404
  // bare 202 there, not a 200 carrying a JSON `null`.
312
405
  if (result == null || (Array.isArray(result) && !result.length)) {
313
- return new Response(null, { status: 202, headers: MCP_CORS });
406
+ return new Response(null, { status: 202, headers });
314
407
  }
315
- return Response.json(result, { headers: MCP_CORS });
408
+ return Response.json(result, { headers });
316
409
  }
317
410
  //# sourceMappingURL=mcp.js.map