wiki-formant 0.17.1 → 0.19.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 +3 -2
- package/bin/check-classes.mjs +44 -7
- package/dist/block-views.js +6 -7
- package/dist/combobox.js +6 -15
- package/dist/conformance.d.ts +7 -0
- package/dist/conformance.js +59 -13
- package/dist/crawlers.js +6 -12
- package/dist/dom.d.ts +1 -2
- package/dist/dom.js +1 -2
- package/dist/editor.d.ts +1 -24
- package/dist/editor.js +10 -19
- package/dist/headings.js +1 -8
- package/dist/link-check.js +6 -9
- package/dist/maps.d.ts +0 -2
- package/dist/maps.js +1 -1
- package/dist/mcp.d.ts +43 -12
- package/dist/mcp.js +265 -65
- package/dist/pagination.d.ts +2 -6
- package/dist/pagination.js +4 -6
- package/dist/rate-limit.d.ts +3 -4
- package/dist/rate-limit.js +3 -4
- package/dist/react-server.d.ts +7 -11
- package/dist/react-server.js +7 -11
- package/dist/react.d.ts +1 -10
- package/dist/react.js +1 -1
- package/dist/revisions.d.ts +0 -9
- package/dist/revisions.js +4 -12
- package/dist/rola.js +5 -11
- package/dist/taxonomy.d.ts +5 -7
- package/dist/taxonomy.js +1 -1
- package/dist/text.d.ts +0 -9
- package/dist/text.js +1 -1
- package/dist/tiptap.js +2 -7
- package/dist/versioning.js +1 -1
- package/dist/well-known.d.ts +0 -6
- package/dist/well-known.js +1 -1
- package/package.json +1 -1
package/dist/mcp.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// mcp.ts — a minimal Model Context Protocol server over Streamable HTTP
|
|
2
|
-
// (JSON-RPC)
|
|
2
|
+
// (JSON-RPC), serving both protocol eras from one endpoint.
|
|
3
|
+
// Spec: https://modelcontextprotocol.io/specification/2026-07-28
|
|
3
4
|
//
|
|
4
5
|
// Web-standard `Request`/`Response` only, so this runs unchanged on Next route
|
|
5
6
|
// handlers (NextResponse extends Response), Hono, Bun, Deno and workers.
|
|
@@ -13,31 +14,58 @@
|
|
|
13
14
|
// A caller-fixable mistake is a tool result with `isError`, never a -32603.
|
|
14
15
|
// That split is the one most implementations get wrong.
|
|
15
16
|
/**
|
|
16
|
-
* The
|
|
17
|
+
* The modern era: every request carries its own version and client
|
|
18
|
+
* capabilities in `params._meta`. No handshake, no session, no `ping`, no batch.
|
|
19
|
+
*/
|
|
20
|
+
export const MCP_MODERN_VERSIONS = ['2026-07-28'];
|
|
21
|
+
/**
|
|
22
|
+
* The legacy era, negotiated by an `initialize` handshake.
|
|
17
23
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
* Served beside the modern era rather than replaced by it. The spec lets one
|
|
25
|
+
* endpoint speak both, and a legacy client meeting a dual-era server works;
|
|
26
|
+
* going modern-only would fail the handshake of every client in the field.
|
|
27
|
+
*/
|
|
28
|
+
export const MCP_LEGACY_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'];
|
|
29
|
+
/**
|
|
30
|
+
* Every version this transport speaks, newest first. It is the `supported` list
|
|
31
|
+
* a `-32022` names and the `supportedVersions` of `server/discover`, so a modern
|
|
32
|
+
* client that cannot use the newest still learns a legacy fallback exists.
|
|
23
33
|
*/
|
|
24
|
-
export const MCP_PROTOCOL_VERSIONS = [
|
|
25
|
-
/** The newest version spoken here
|
|
34
|
+
export const MCP_PROTOCOL_VERSIONS = [...MCP_MODERN_VERSIONS, ...MCP_LEGACY_VERSIONS];
|
|
35
|
+
/** The newest version spoken here. */
|
|
26
36
|
export const MCP_PROTOCOL_VERSION = MCP_PROTOCOL_VERSIONS[0];
|
|
27
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* The newest version `initialize` can negotiate, and what an unrecognised ask is
|
|
39
|
+
* offered. A handshake cannot land on a modern version — the modern era has no
|
|
40
|
+
* handshake — so this, not `MCP_PROTOCOL_VERSION`, is what a legacy client gets.
|
|
41
|
+
*/
|
|
42
|
+
export const MCP_LEGACY_PROTOCOL_VERSION = MCP_LEGACY_VERSIONS[0];
|
|
43
|
+
/** The reserved `_meta` keys the modern era is spelled in. */
|
|
44
|
+
export const MCP_META = {
|
|
45
|
+
protocolVersion: 'io.modelcontextprotocol/protocolVersion',
|
|
46
|
+
clientCapabilities: 'io.modelcontextprotocol/clientCapabilities',
|
|
47
|
+
serverInfo: 'io.modelcontextprotocol/serverInfo',
|
|
48
|
+
};
|
|
49
|
+
/** What a legacy request carrying no `MCP-Protocol-Version` header means, per the spec. */
|
|
28
50
|
const ASSUMED_VERSION = '2025-03-26';
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
51
|
+
const member = (list) => (v) => list.includes(v);
|
|
52
|
+
const speaksLegacy = member(MCP_LEGACY_VERSIONS);
|
|
53
|
+
const speaksModern = member(MCP_MODERN_VERSIONS);
|
|
54
|
+
/**
|
|
55
|
+
* Echo the client's version when it is one we speak, else offer the newest.
|
|
56
|
+
* Answering a constant instead is legal and still wrong: it silently held every
|
|
57
|
+
* caller below the version that carries structured output and `_meta`.
|
|
58
|
+
*/
|
|
59
|
+
function negotiateProtocol(requested) {
|
|
60
|
+
return speaksLegacy(requested) ? requested : MCP_LEGACY_PROTOCOL_VERSION;
|
|
33
61
|
}
|
|
34
62
|
/** The version a post-initialize request is operating under. */
|
|
35
|
-
|
|
63
|
+
function requestProtocol(request) {
|
|
36
64
|
const header = request.headers.get('mcp-protocol-version');
|
|
37
|
-
return
|
|
65
|
+
return speaksLegacy(header) ? header : ASSUMED_VERSION;
|
|
38
66
|
}
|
|
39
|
-
/** JSON-RPC batching was removed in 2025-06-18; it
|
|
40
|
-
const allowsBatch = (v) => v
|
|
67
|
+
/** JSON-RPC batching was removed in 2025-06-18 and stays removed; it is legal only below that. */
|
|
68
|
+
const allowsBatch = (v) => v === '2025-03-26' || v === '2024-11-05';
|
|
41
69
|
import { clientKey, rateLimit, rateLimitHeaders, withRateLimit } from './rate-limit.js';
|
|
42
70
|
/**
|
|
43
71
|
* A caller-fixable failure inside a handler (page not found, empty input).
|
|
@@ -137,15 +165,19 @@ function validateArgs(tool, args) {
|
|
|
137
165
|
`Expected schema: ${JSON.stringify(tool.inputSchema)}`,
|
|
138
166
|
].join('\n');
|
|
139
167
|
}
|
|
140
|
-
|
|
141
|
-
|
|
168
|
+
/** The methods one era has and the other does not. Everything else is shared. */
|
|
169
|
+
const ERA_METHODS = {
|
|
170
|
+
legacy: ['initialize', 'ping'],
|
|
171
|
+
modern: ['server/discover'],
|
|
172
|
+
};
|
|
173
|
+
function methodsFor(config, era) {
|
|
142
174
|
return [
|
|
143
|
-
...
|
|
175
|
+
...ERA_METHODS[era],
|
|
176
|
+
'tools/list',
|
|
177
|
+
'tools/call',
|
|
144
178
|
// The list methods answer whether or not anything is registered: an empty
|
|
145
179
|
// list is a better answer to a client that asked than a -32601 it has to
|
|
146
|
-
// interpret.
|
|
147
|
-
// answered them, so the error message contradicted the server describing
|
|
148
|
-
// itself.
|
|
180
|
+
// interpret.
|
|
149
181
|
'resources/list',
|
|
150
182
|
'resources/templates/list',
|
|
151
183
|
'prompts/list',
|
|
@@ -153,11 +185,30 @@ function methodsFor(config) {
|
|
|
153
185
|
...(config.prompts?.length ? ['prompts/get'] : []),
|
|
154
186
|
];
|
|
155
187
|
}
|
|
188
|
+
const methodNotFound = (id, method, config, era) => {
|
|
189
|
+
const methods = methodsFor(config, era);
|
|
190
|
+
return rpcError(id, -32601, `Method not found: "${method}". This server implements: ${quote(methods)}.`, { supportedMethods: methods });
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Only the capabilities the config actually populates — an advertised
|
|
194
|
+
* `resources` whose list comes back empty reads as a bug to a client, not as
|
|
195
|
+
* honesty. One function, so `initialize` and `server/discover` cannot disagree.
|
|
196
|
+
*/
|
|
197
|
+
const capabilitiesOf = (config) => ({
|
|
198
|
+
tools: {},
|
|
199
|
+
...(config.resources?.length ? { resources: {} } : {}),
|
|
200
|
+
...(config.prompts?.length ? { prompts: {} } : {}),
|
|
201
|
+
});
|
|
156
202
|
async function handleRpc(req, config, dispatch) {
|
|
157
203
|
const { id, method, params } = req;
|
|
158
204
|
const p = (params ?? {});
|
|
159
205
|
const resources = config.resources ?? [];
|
|
160
206
|
const prompts = config.prompts ?? [];
|
|
207
|
+
// A method the other era owns is not found in this one, however well the
|
|
208
|
+
// server knows it: a modern `ping` is a removed method, not a pong.
|
|
209
|
+
const foreign = ERA_METHODS[dispatch.era === 'modern' ? 'legacy' : 'modern'];
|
|
210
|
+
if (foreign.includes(method))
|
|
211
|
+
return methodNotFound(id, method, config, dispatch.era);
|
|
161
212
|
try {
|
|
162
213
|
switch (method) {
|
|
163
214
|
case 'initialize':
|
|
@@ -165,21 +216,24 @@ async function handleRpc(req, config, dispatch) {
|
|
|
165
216
|
jsonrpc: '2.0',
|
|
166
217
|
id,
|
|
167
218
|
result: {
|
|
168
|
-
// Echo what the client asked for when we speak it. Answering a
|
|
169
|
-
// constant is what silently held every caller at 2025-03-26.
|
|
170
219
|
protocolVersion: negotiateProtocol(p.protocolVersion),
|
|
171
|
-
|
|
172
|
-
// advertised `resources` whose list comes back empty reads as a bug
|
|
173
|
-
// to a client, not as honesty.
|
|
174
|
-
capabilities: {
|
|
175
|
-
tools: {},
|
|
176
|
-
...(resources.length ? { resources: {} } : {}),
|
|
177
|
-
...(prompts.length ? { prompts: {} } : {}),
|
|
178
|
-
},
|
|
220
|
+
capabilities: capabilitiesOf(config),
|
|
179
221
|
serverInfo: config.serverInfo,
|
|
180
222
|
instructions: config.instructions,
|
|
181
223
|
},
|
|
182
224
|
};
|
|
225
|
+
// What `initialize` tells a legacy client, asked the modern way. The
|
|
226
|
+
// transport adds `resultType`, the cache hints and `_meta` serverInfo.
|
|
227
|
+
case 'server/discover':
|
|
228
|
+
return {
|
|
229
|
+
jsonrpc: '2.0',
|
|
230
|
+
id,
|
|
231
|
+
result: {
|
|
232
|
+
supportedVersions: [...MCP_PROTOCOL_VERSIONS],
|
|
233
|
+
capabilities: capabilitiesOf(config),
|
|
234
|
+
instructions: config.instructions,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
183
237
|
// Notifications have no response at all.
|
|
184
238
|
case 'notifications/initialized':
|
|
185
239
|
case 'notifications/cancelled':
|
|
@@ -286,13 +340,12 @@ async function handleRpc(req, config, dispatch) {
|
|
|
286
340
|
try {
|
|
287
341
|
const data = await tool.handler(args, ctx);
|
|
288
342
|
const text = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
|
289
|
-
// Every object answer, not only the schema-bearing ones.
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
// no longer the price of admission. The text block stays regardless:
|
|
343
|
+
// Every object answer, not only the schema-bearing ones. Gated on
|
|
344
|
+
// `outputSchema`, a JSON-returning tool without one would carry no
|
|
345
|
+
// `structuredContent`, and agents would parse prose to reach data the
|
|
346
|
+
// server has in hand. A declared `outputSchema` is the stronger
|
|
347
|
+
// contract (a client validates against it), not the price of
|
|
348
|
+
// admission. The text block stays regardless:
|
|
296
349
|
// the spec asks for the serialised twin, and a client that reads only
|
|
297
350
|
// content still has to be able to read the answer.
|
|
298
351
|
const structured = data !== null && typeof data === 'object' && !Array.isArray(data)
|
|
@@ -309,10 +362,8 @@ async function handleRpc(req, config, dispatch) {
|
|
|
309
362
|
return toolText(id, JSON.stringify({ error: err.message, ...err.details }, null, 2), true);
|
|
310
363
|
}
|
|
311
364
|
}
|
|
312
|
-
default:
|
|
313
|
-
|
|
314
|
-
return rpcError(id, -32601, `Method not found: "${method}". This server implements: ${quote(methods)}.`, { supportedMethods: methods });
|
|
315
|
-
}
|
|
365
|
+
default:
|
|
366
|
+
return methodNotFound(id, method, config, dispatch.era);
|
|
316
367
|
}
|
|
317
368
|
}
|
|
318
369
|
catch (err) {
|
|
@@ -321,7 +372,7 @@ async function handleRpc(req, config, dispatch) {
|
|
|
321
372
|
}
|
|
322
373
|
}
|
|
323
374
|
/** Dispatch a parsed body. `null` means notification-only — answer 202, not 200. */
|
|
324
|
-
export async function handleMcp(body, config, dispatch = { protocolVersion: ASSUMED_VERSION }) {
|
|
375
|
+
export async function handleMcp(body, config, dispatch = { protocolVersion: ASSUMED_VERSION, era: 'legacy' }) {
|
|
325
376
|
const maxBatch = config.maxBatch ?? DEFAULT_MAX_BATCH;
|
|
326
377
|
const isBatch = Array.isArray(body);
|
|
327
378
|
if (isBatch && body.length > maxBatch) {
|
|
@@ -340,12 +391,13 @@ export async function handleMcp(body, config, dispatch = { protocolVersion: ASSU
|
|
|
340
391
|
// the spec's Origin-validation MUST exists to protect localhost servers from
|
|
341
392
|
// DNS rebinding, which is the opposite situation. The allow-headers list
|
|
342
393
|
// matters more than it looks — MCP clients preflight with `Accept` and
|
|
343
|
-
// `Mcp-Protocol-Version
|
|
344
|
-
//
|
|
394
|
+
// `Mcp-Protocol-Version` — and a modern client adds `Mcp-Method` and `Mcp-Name`
|
|
395
|
+
// on every call — and one missing entry fails the preflight, not the POST,
|
|
396
|
+
// which reads as "the server is down".
|
|
345
397
|
export const MCP_CORS = {
|
|
346
398
|
'Access-Control-Allow-Origin': '*',
|
|
347
399
|
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
348
|
-
'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID',
|
|
400
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization, Mcp-Session-Id, Mcp-Protocol-Version, Mcp-Method, Mcp-Name, Last-Event-ID',
|
|
349
401
|
// Allow-Headers governs what a browser may send; without Expose-Headers it
|
|
350
402
|
// may read none of what comes back. A browser client could not see the
|
|
351
403
|
// negotiated version, and could not see `Retry-After` on the 429 telling it
|
|
@@ -389,7 +441,6 @@ export function mcpRateLimited(retryAfterSec, message) {
|
|
|
389
441
|
}
|
|
390
442
|
/** The whole POST leg: parse, gate, track, dispatch, and answer with the right status. */
|
|
391
443
|
export async function mcpResponse(request, config) {
|
|
392
|
-
const protocolVersion = requestProtocol(request);
|
|
393
444
|
// Before `request.json()`: an unparsed body must not cost a query, which is
|
|
394
445
|
// also why the refusal carries a null id.
|
|
395
446
|
let headroom = {};
|
|
@@ -400,12 +451,10 @@ export async function mcpResponse(request, config) {
|
|
|
400
451
|
}
|
|
401
452
|
headroom = rateLimitHeaders(verdict, config.rateLimit);
|
|
402
453
|
}
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
|
|
407
|
-
// meets when it is least able to act on it.
|
|
408
|
-
const headers = { ...MCP_CORS, 'MCP-Protocol-Version': protocolVersion, ...headroom };
|
|
454
|
+
// The headroom rides on every answer, not only on the 429 — a budget
|
|
455
|
+
// discoverable only by exceeding it is one an agent meets when it is least
|
|
456
|
+
// able to act on it.
|
|
457
|
+
const base = { ...MCP_CORS, ...headroom };
|
|
409
458
|
let body;
|
|
410
459
|
try {
|
|
411
460
|
body = await request.json();
|
|
@@ -413,20 +462,49 @@ export async function mcpResponse(request, config) {
|
|
|
413
462
|
catch {
|
|
414
463
|
return Response.json(rpcError(null, -32700, 'Parse error: request body is not valid JSON.'), {
|
|
415
464
|
status: 400,
|
|
416
|
-
headers,
|
|
465
|
+
headers: { ...base, 'MCP-Protocol-Version': requestProtocol(request) },
|
|
417
466
|
});
|
|
418
467
|
}
|
|
468
|
+
// Tracked before either era refuses anything: a probe the server turns away,
|
|
469
|
+
// and a call an agent walked away from behind a gate, are exactly the ones
|
|
470
|
+
// worth counting.
|
|
471
|
+
config.onCall?.(request, body);
|
|
472
|
+
return isModern(request, body)
|
|
473
|
+
? modernResponse(request, body, config, base)
|
|
474
|
+
: legacyResponse(request, body, config, base);
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Which era a POST belongs to. The spec's rule: modern `_meta` selects the modern
|
|
478
|
+
* era and `initialize` selects the legacy one. A modern version header and
|
|
479
|
+
* `server/discover` can only mean one thing too. Anything else is legacy,
|
|
480
|
+
* because a body without `_meta` is exactly what every client in the field sends.
|
|
481
|
+
*/
|
|
482
|
+
function isModern(request, body) {
|
|
483
|
+
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
484
|
+
return false;
|
|
485
|
+
const { method, params } = body;
|
|
486
|
+
if (method === 'initialize')
|
|
487
|
+
return false;
|
|
488
|
+
const meta = params?._meta;
|
|
489
|
+
return ((!!meta && typeof meta === 'object' && MCP_META.protocolVersion in meta) ||
|
|
490
|
+
method === 'server/discover' ||
|
|
491
|
+
speaksModern(request.headers.get('mcp-protocol-version')));
|
|
492
|
+
}
|
|
493
|
+
async function dispatchThroughGate(body, config, dispatch) {
|
|
494
|
+
const gate = await config.gate?.(body);
|
|
495
|
+
const dispatched = gate?.empty ? null : await handleMcp(gate ? gate.body : body, config, dispatch);
|
|
496
|
+
return gate ? gate.finish(dispatched) : dispatched;
|
|
497
|
+
}
|
|
498
|
+
async function legacyResponse(request, body, config, base) {
|
|
499
|
+
const protocolVersion = requestProtocol(request);
|
|
500
|
+
// Echoed on every response so a client can see which version it is actually
|
|
501
|
+
// being answered under, rather than inferring it from the initialize it sent
|
|
502
|
+
// some requests ago.
|
|
503
|
+
const headers = { ...base, 'MCP-Protocol-Version': protocolVersion };
|
|
419
504
|
if (Array.isArray(body) && !allowsBatch(protocolVersion)) {
|
|
420
505
|
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 });
|
|
421
506
|
}
|
|
422
|
-
|
|
423
|
-
// agent walked away from is exactly the one worth counting.
|
|
424
|
-
config.onCall?.(request, body);
|
|
425
|
-
const gate = await config.gate?.(body);
|
|
426
|
-
const dispatched = gate?.empty
|
|
427
|
-
? null
|
|
428
|
-
: await handleMcp(gate ? gate.body : body, config, { protocolVersion, request });
|
|
429
|
-
const result = gate ? await gate.finish(dispatched) : dispatched;
|
|
507
|
+
const result = await dispatchThroughGate(body, config, { protocolVersion, era: 'legacy', request });
|
|
430
508
|
// Notification-only input produces no response bodies; the spec requires a
|
|
431
509
|
// bare 202 there, not a 200 carrying a JSON `null`.
|
|
432
510
|
if (result == null || (Array.isArray(result) && !result.length)) {
|
|
@@ -434,3 +512,125 @@ export async function mcpResponse(request, config) {
|
|
|
434
512
|
}
|
|
435
513
|
return Response.json(result, { headers });
|
|
436
514
|
}
|
|
515
|
+
/** The modern era maps a protocol error to an HTTP status; the legacy era answered 200. */
|
|
516
|
+
const MODERN_STATUS = {
|
|
517
|
+
[-32601]: 404,
|
|
518
|
+
[-32602]: 400,
|
|
519
|
+
[-32020]: 400,
|
|
520
|
+
[-32021]: 400,
|
|
521
|
+
[-32022]: 400,
|
|
522
|
+
};
|
|
523
|
+
/** The results a modern client may cache. A call or a prompt is never among them. */
|
|
524
|
+
const CACHEABLE = new Set([
|
|
525
|
+
'server/discover',
|
|
526
|
+
'tools/list',
|
|
527
|
+
'prompts/list',
|
|
528
|
+
'resources/list',
|
|
529
|
+
'resources/templates/list',
|
|
530
|
+
'resources/read',
|
|
531
|
+
]);
|
|
532
|
+
const DEFAULT_CACHE_TTL_MS = 300_000;
|
|
533
|
+
/** `Mcp-Name` carries the target of the three calls that have one, for a gateway to route on. */
|
|
534
|
+
const NAME_PARAM = {
|
|
535
|
+
'tools/call': 'name',
|
|
536
|
+
'prompts/get': 'name',
|
|
537
|
+
'resources/read': 'uri',
|
|
538
|
+
};
|
|
539
|
+
/** A header value, decoded from the `=?base64?…?=` form a client uses for non-ASCII. */
|
|
540
|
+
function headerValue(raw) {
|
|
541
|
+
const encoded = raw?.match(/^=\?base64\?(.*)\?=$/i)?.[1];
|
|
542
|
+
if (encoded === undefined)
|
|
543
|
+
return raw;
|
|
544
|
+
try {
|
|
545
|
+
return new TextDecoder().decode(Uint8Array.from(atob(encoded), c => c.charCodeAt(0)));
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/** The first routing header that disagrees with the body, described. */
|
|
552
|
+
function headerMismatch(request, method, params, version) {
|
|
553
|
+
const expected = [
|
|
554
|
+
['MCP-Protocol-Version', version],
|
|
555
|
+
['Mcp-Method', method],
|
|
556
|
+
];
|
|
557
|
+
const nameKey = NAME_PARAM[method];
|
|
558
|
+
// A call missing its name is reported by the dispatcher, which can list the names.
|
|
559
|
+
if (nameKey && typeof params[nameKey] === 'string')
|
|
560
|
+
expected.push(['Mcp-Name', params[nameKey]]);
|
|
561
|
+
for (const [name, want] of expected) {
|
|
562
|
+
const got = headerValue(request.headers.get(name));
|
|
563
|
+
if (got !== want) {
|
|
564
|
+
return `Header ${name} must equal ${JSON.stringify(want)}; received ${got === null ? 'none' : JSON.stringify(got)}.`;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* A modern result: `resultType` on every one, cache hints on the cacheable ones,
|
|
571
|
+
* and serverInfo in `_meta` now that no handshake carries it. Applied after the
|
|
572
|
+
* gate, so an answer a gate builds in place of a call is shaped the same way.
|
|
573
|
+
*/
|
|
574
|
+
function modernResult(response, method, config) {
|
|
575
|
+
if (!('result' in response))
|
|
576
|
+
return response;
|
|
577
|
+
const result = response.result;
|
|
578
|
+
return {
|
|
579
|
+
...response,
|
|
580
|
+
result: {
|
|
581
|
+
resultType: 'complete',
|
|
582
|
+
...result,
|
|
583
|
+
...(CACHEABLE.has(method)
|
|
584
|
+
? {
|
|
585
|
+
ttlMs: config.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS,
|
|
586
|
+
// Nothing here varies by caller, except a read a gate may have
|
|
587
|
+
// charged for, which a shared cache must not hand to the next one.
|
|
588
|
+
cacheScope: method === 'resources/read' ? 'private' : 'public',
|
|
589
|
+
}
|
|
590
|
+
: {}),
|
|
591
|
+
_meta: { ...result._meta, [MCP_META.serverInfo]: config.serverInfo },
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
async function modernResponse(request, body, config, base) {
|
|
596
|
+
const params = (body.params ?? {});
|
|
597
|
+
const meta = (params._meta ?? {});
|
|
598
|
+
const requested = meta[MCP_META.protocolVersion];
|
|
599
|
+
const headers = {
|
|
600
|
+
...base,
|
|
601
|
+
'MCP-Protocol-Version': speaksModern(requested) ? requested : MCP_MODERN_VERSIONS[0],
|
|
602
|
+
};
|
|
603
|
+
const refuse = (code, message, data) => Response.json(rpcError(body.id ?? null, code, message, data), {
|
|
604
|
+
status: MODERN_STATUS[code] ?? 400,
|
|
605
|
+
headers,
|
|
606
|
+
});
|
|
607
|
+
if (typeof requested !== 'string') {
|
|
608
|
+
return refuse(-32602, `Missing params._meta["${MCP_META.protocolVersion}"]. A ${MCP_MODERN_VERSIONS[0]} request carries its version and client capabilities on every call; to use ${MCP_LEGACY_PROTOCOL_VERSION} instead, send "initialize".`);
|
|
609
|
+
}
|
|
610
|
+
if (!speaksModern(requested)) {
|
|
611
|
+
return refuse(-32022, 'Unsupported protocol version', {
|
|
612
|
+
supported: [...MCP_PROTOCOL_VERSIONS],
|
|
613
|
+
requested,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
const capabilities = meta[MCP_META.clientCapabilities];
|
|
617
|
+
if (!capabilities || typeof capabilities !== 'object' || Array.isArray(capabilities)) {
|
|
618
|
+
return refuse(-32602, `Missing params._meta["${MCP_META.clientCapabilities}"]. Send {} when the client declares none.`);
|
|
619
|
+
}
|
|
620
|
+
const mismatch = headerMismatch(request, body.method, params, requested);
|
|
621
|
+
if (mismatch)
|
|
622
|
+
return refuse(-32020, mismatch);
|
|
623
|
+
const result = await dispatchThroughGate(body, config, {
|
|
624
|
+
protocolVersion: requested,
|
|
625
|
+
era: 'modern',
|
|
626
|
+
request,
|
|
627
|
+
});
|
|
628
|
+
if (result == null)
|
|
629
|
+
return new Response(null, { status: 202, headers });
|
|
630
|
+
const response = modernResult(result, body.method, config);
|
|
631
|
+
const code = response.error?.code;
|
|
632
|
+
return Response.json(response, {
|
|
633
|
+
status: (code !== undefined && MODERN_STATUS[code]) || 200,
|
|
634
|
+
headers,
|
|
635
|
+
});
|
|
636
|
+
}
|
package/dist/pagination.d.ts
CHANGED
|
@@ -7,8 +7,6 @@ export interface PaginatedResponse<T> extends Pagination {
|
|
|
7
7
|
total: number;
|
|
8
8
|
totalPages: number;
|
|
9
9
|
}
|
|
10
|
-
export declare const MAX_PAGE_SIZE = 100;
|
|
11
|
-
export declare const DEFAULT_PAGE_SIZE = 20;
|
|
12
10
|
/**
|
|
13
11
|
* `page` clamped to ≥1, `pageSize` clamped to 1–`max`. Never trust either.
|
|
14
12
|
*
|
|
@@ -55,14 +53,12 @@ export declare function toOffset({ page, pageSize }: Pagination): {
|
|
|
55
53
|
* The ordering is the caller's, deliberately — it is the one thing here that is
|
|
56
54
|
* never portable. A wiki's sequence is its section's configured sort, a
|
|
57
55
|
* knowledge base's is a taxonomy walk, and a company log's is `updatedAt` desc.
|
|
58
|
-
* What both wikis had written twice is this scan, not the sort.
|
|
59
56
|
*
|
|
60
57
|
* `null` on both sides when the page is not in the list, so a page reached by a
|
|
61
58
|
* URL its own section does not list renders no nav rather than a wrong one.
|
|
62
59
|
*
|
|
63
|
-
* No query.
|
|
64
|
-
*
|
|
65
|
-
* neighbours used to cost were the reason one wiki dropped the control.
|
|
60
|
+
* No query. Callers already hold the ordered siblings for something else, a
|
|
61
|
+
* related-pages panel or a section listing, so the neighbours cost no lookups.
|
|
66
62
|
*/
|
|
67
63
|
export declare function adjacentPages<T>(ordered: readonly T[], isCurrent: (page: T) => boolean): {
|
|
68
64
|
prev: T | null;
|
package/dist/pagination.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// Forked three ways once: a re-typed clamp that dropped `totalPages`, and an
|
|
4
4
|
// offset redone by hand in raw SQL. The response shape is the contract a client
|
|
5
5
|
// codes against, so reshaping it per repo is a breaking change nobody declared.
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
const MAX_PAGE_SIZE = 100;
|
|
7
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
8
8
|
/**
|
|
9
9
|
* `page` clamped to ≥1, `pageSize` clamped to 1–`max`. Never trust either.
|
|
10
10
|
*
|
|
@@ -62,14 +62,12 @@ export function toOffset({ page, pageSize }) {
|
|
|
62
62
|
* The ordering is the caller's, deliberately — it is the one thing here that is
|
|
63
63
|
* never portable. A wiki's sequence is its section's configured sort, a
|
|
64
64
|
* knowledge base's is a taxonomy walk, and a company log's is `updatedAt` desc.
|
|
65
|
-
* What both wikis had written twice is this scan, not the sort.
|
|
66
65
|
*
|
|
67
66
|
* `null` on both sides when the page is not in the list, so a page reached by a
|
|
68
67
|
* URL its own section does not list renders no nav rather than a wrong one.
|
|
69
68
|
*
|
|
70
|
-
* No query.
|
|
71
|
-
*
|
|
72
|
-
* neighbours used to cost were the reason one wiki dropped the control.
|
|
69
|
+
* No query. Callers already hold the ordered siblings for something else, a
|
|
70
|
+
* related-pages panel or a section listing, so the neighbours cost no lookups.
|
|
73
71
|
*/
|
|
74
72
|
export function adjacentPages(ordered, isCurrent) {
|
|
75
73
|
const i = ordered.findIndex(isCurrent);
|
package/dist/rate-limit.d.ts
CHANGED
|
@@ -69,10 +69,9 @@ export declare function resetRateLimits(): void;
|
|
|
69
69
|
* `.well-known/mcp.json`, the OpenAPI spec, agents.md, llms.txt and the
|
|
70
70
|
* `initialize` instructions all quote this, so the endpoint enforces exactly
|
|
71
71
|
* what the documents claim. It is a cross-surface contract, which is why it
|
|
72
|
-
* lives here rather than three times over
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* difference from a stale one.
|
|
72
|
+
* lives here rather than three times over. A second copy is how a number
|
|
73
|
+
* becomes impossible to change safely: nobody can tell a considered difference
|
|
74
|
+
* from a stale one.
|
|
76
75
|
*
|
|
77
76
|
* A surface with a genuine reason to differ passes its own `RateLimitOptions`.
|
|
78
77
|
* What it must not do is restate this one.
|
package/dist/rate-limit.js
CHANGED
|
@@ -117,10 +117,9 @@ export function resetRateLimits() {
|
|
|
117
117
|
* `.well-known/mcp.json`, the OpenAPI spec, agents.md, llms.txt and the
|
|
118
118
|
* `initialize` instructions all quote this, so the endpoint enforces exactly
|
|
119
119
|
* what the documents claim. It is a cross-surface contract, which is why it
|
|
120
|
-
* lives here rather than three times over
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* difference from a stale one.
|
|
120
|
+
* lives here rather than three times over. A second copy is how a number
|
|
121
|
+
* becomes impossible to change safely: nobody can tell a considered difference
|
|
122
|
+
* from a stale one.
|
|
124
123
|
*
|
|
125
124
|
* A surface with a genuine reason to differ passes its own `RateLimitOptions`.
|
|
126
125
|
* What it must not do is restate this one.
|
package/dist/react-server.d.ts
CHANGED
|
@@ -54,7 +54,7 @@ export interface FacetBarProps {
|
|
|
54
54
|
* dropping the active facets — and the A–Z row leads with the reset control.
|
|
55
55
|
*
|
|
56
56
|
* `aria-current`, not `aria-pressed`: a link is not a toggle button and does not
|
|
57
|
-
* take that attribute.
|
|
57
|
+
* take that attribute.
|
|
58
58
|
*/
|
|
59
59
|
export declare function FacetBar({ link: Link, facets, letters, alphaLabel, classNames, }: FacetBarProps): import("react").JSX.Element | null;
|
|
60
60
|
export interface BreadcrumbItem {
|
|
@@ -114,7 +114,7 @@ export declare function Breadcrumbs({ items, base, className, link: Link, lastAs
|
|
|
114
114
|
* The standard page-top row: the trail, plus optional right-aligned actions.
|
|
115
115
|
*
|
|
116
116
|
* The column it sits in is the one its route declares — the row reads the
|
|
117
|
-
* page's own max-width by inheritance, so a trail
|
|
117
|
+
* page's own max-width by inheritance, so a trail cannot disagree with
|
|
118
118
|
* the content it titles.
|
|
119
119
|
*/
|
|
120
120
|
export declare function BreadcrumbsRow({ actions, className, ...props }: BreadcrumbsProps & {
|
|
@@ -146,15 +146,11 @@ export interface PageNavProps {
|
|
|
146
146
|
* lateral links do not cover.
|
|
147
147
|
*
|
|
148
148
|
* Ordering is entirely the caller's; pair this with `adjacentPages` from
|
|
149
|
-
* `wiki-formant/pagination` over a list you already hold.
|
|
150
|
-
* markup and had already drifted on the parts that matter rather than the parts
|
|
151
|
-
* that show: one carried `rel="prev"`/`rel="next"` and an `aria-label` and the
|
|
152
|
-
* other carried neither, and the one without expressed its right-hand alignment
|
|
153
|
-
* as two inline utilities instead of the modifier its own stylesheet defines.
|
|
149
|
+
* `wiki-formant/pagination` over a list you already hold.
|
|
154
150
|
*
|
|
155
|
-
* The `page-nav__*` class names are NOT props
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
151
|
+
* The `page-nav__*` class names are NOT props: they are the convention every
|
|
152
|
+
* stylesheet implements, and making them configurable is how a convention
|
|
153
|
+
* forks. The empty `<div>` holds the first article's left column so the next
|
|
154
|
+
* link stays in the right one.
|
|
159
155
|
*/
|
|
160
156
|
export declare function PageNav({ prev, next, link: Link, label, prevLabel, nextLabel, prevGlyph, nextGlyph, }: PageNavProps): import("react").JSX.Element | null;
|
package/dist/react-server.js
CHANGED
|
@@ -34,7 +34,7 @@ export const Anchor = ({ href, className, children }) => _jsx("a", { href: href,
|
|
|
34
34
|
* dropping the active facets — and the A–Z row leads with the reset control.
|
|
35
35
|
*
|
|
36
36
|
* `aria-current`, not `aria-pressed`: a link is not a toggle button and does not
|
|
37
|
-
* take that attribute.
|
|
37
|
+
* take that attribute.
|
|
38
38
|
*/
|
|
39
39
|
export function FacetBar({ link: Link, facets, letters, alphaLabel = 'A–Z', classNames = {}, }) {
|
|
40
40
|
if (!facets.length && !letters.length)
|
|
@@ -85,7 +85,7 @@ export function Breadcrumbs({ items, base, className = '', link: Link = Anchor,
|
|
|
85
85
|
* The standard page-top row: the trail, plus optional right-aligned actions.
|
|
86
86
|
*
|
|
87
87
|
* The column it sits in is the one its route declares — the row reads the
|
|
88
|
-
* page's own max-width by inheritance, so a trail
|
|
88
|
+
* page's own max-width by inheritance, so a trail cannot disagree with
|
|
89
89
|
* the content it titles.
|
|
90
90
|
*/
|
|
91
91
|
export function BreadcrumbsRow({ actions, className = '', ...props }) {
|
|
@@ -96,16 +96,12 @@ export function BreadcrumbsRow({ actions, className = '', ...props }) {
|
|
|
96
96
|
* lateral links do not cover.
|
|
97
97
|
*
|
|
98
98
|
* Ordering is entirely the caller's; pair this with `adjacentPages` from
|
|
99
|
-
* `wiki-formant/pagination` over a list you already hold.
|
|
100
|
-
* markup and had already drifted on the parts that matter rather than the parts
|
|
101
|
-
* that show: one carried `rel="prev"`/`rel="next"` and an `aria-label` and the
|
|
102
|
-
* other carried neither, and the one without expressed its right-hand alignment
|
|
103
|
-
* as two inline utilities instead of the modifier its own stylesheet defines.
|
|
99
|
+
* `wiki-formant/pagination` over a list you already hold.
|
|
104
100
|
*
|
|
105
|
-
* The `page-nav__*` class names are NOT props
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
101
|
+
* The `page-nav__*` class names are NOT props: they are the convention every
|
|
102
|
+
* stylesheet implements, and making them configurable is how a convention
|
|
103
|
+
* forks. The empty `<div>` holds the first article's left column so the next
|
|
104
|
+
* link stays in the right one.
|
|
109
105
|
*/
|
|
110
106
|
export function PageNav({ prev, next, link: Link = Anchor, label = 'Article navigation', prevLabel = 'Previous', nextLabel = 'Next', prevGlyph = '\u2190', nextGlyph = '\u2192', }) {
|
|
111
107
|
if (!prev && !next)
|
package/dist/react.d.ts
CHANGED
|
@@ -22,14 +22,6 @@ export interface SidebarState {
|
|
|
22
22
|
*/
|
|
23
23
|
ready: boolean;
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
26
|
-
* Collapse state for a wiki rail: remembered across loads, defaulted from the
|
|
27
|
-
* viewport only when the reader has never chosen.
|
|
28
|
-
*
|
|
29
|
-
* The reader's choice outranks the breakpoint. Someone who collapses the rail
|
|
30
|
-
* on a laptop and then narrows the window has still collapsed the rail.
|
|
31
|
-
*/
|
|
32
|
-
export declare function useCollapsibleSidebar(options?: SidebarOptions): SidebarState;
|
|
33
25
|
export interface SidebarProviderProps extends SidebarOptions {
|
|
34
26
|
children: ReactNode;
|
|
35
27
|
}
|
|
@@ -286,8 +278,7 @@ export declare class ErrorBoundary extends Component<{
|
|
|
286
278
|
children: ReactNode;
|
|
287
279
|
/**
|
|
288
280
|
* `error` is passed as well as `retry` because a fallback that cannot see
|
|
289
|
-
* what failed can only say "something went wrong"
|
|
290
|
-
* the copies this replaced said, while the other had the message and used it.
|
|
281
|
+
* what failed can only say "something went wrong".
|
|
291
282
|
*/
|
|
292
283
|
fallback: (retry: () => void, error: unknown) => ReactNode;
|
|
293
284
|
onError?: (error: unknown) => void;
|
package/dist/react.js
CHANGED
|
@@ -47,7 +47,7 @@ const readStored = (key) => {
|
|
|
47
47
|
* The reader's choice outranks the breakpoint. Someone who collapses the rail
|
|
48
48
|
* on a laptop and then narrows the window has still collapsed the rail.
|
|
49
49
|
*/
|
|
50
|
-
|
|
50
|
+
function useCollapsibleSidebar(options = {}) {
|
|
51
51
|
const { storageKey = 'wiki:sidebar', breakpoint = 1024 } = options;
|
|
52
52
|
// Start open on the server and on the first client paint. SSR has no viewport
|
|
53
53
|
// and no storage, so any other guess is a guaranteed mismatch on some loads.
|