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/README.md
CHANGED
|
@@ -120,8 +120,9 @@ What it gets right:
|
|
|
120
120
|
- **Malformed JSON is `-32700` with a `400`**, never a 500.
|
|
121
121
|
- **`GET` is an explicit 405 with CORS headers.** A framework's automatic 405 carries none, so a browser client cannot even read the refusal.
|
|
122
122
|
- **The preflight allow-list includes `Accept` and `Mcp-Protocol-Version`.** One missing entry fails the preflight rather than the POST, which presents as "the server is down".
|
|
123
|
-
- **Batches are capped** (default 20) with a teaching error, because the rate limiter charges one token per HTTP request before the body is parsed.
|
|
124
|
-
- **
|
|
123
|
+
- **Batches are capped** (default 20) with a teaching error, because the rate limiter charges one token per HTTP request before the body is parsed. From 2025-06-18 on they are refused outright, because that revision removed them.
|
|
124
|
+
- **Both protocol eras, one endpoint.** A request carrying `io.modelcontextprotocol/protocolVersion` in `params._meta` is served as `2026-07-28`: `server/discover`, per-request version and header validation (`-32022` naming every version spoken, `-32020` on a routing header that disagrees with the body), `resultType` and cache hints on results, and 404 for the methods that revision removed. Everything else is the legacy era, where `initialize` echoes the client's version when it is one of `2025-11-25`, `2025-06-18`, `2025-03-26` or `2024-11-05` and offers the newest otherwise. The spec permits a dual-era server, and it is the only way to adopt the new revision without failing the handshake of every client already in the field.
|
|
125
|
+
- **The version is negotiated, not asserted.** Every response carries it back in `MCP-Protocol-Version`. Answering a constant is legal and still costs the caller structured output, tool titles and `_meta` without ever saying so.
|
|
125
126
|
- **`Access-Control-Expose-Headers` is set.** Allow-Headers governs what a browser may send, Expose-Headers what it may read — without the second a browser client cannot see `Retry-After` on a 429, and a rate limit presents as a hang.
|
|
126
127
|
- **The rate limit is declared, not wired.** Put `rateLimit: {capacity, refillPerSec}` on the config and `mcpResponse` enforces it before parsing the body, refuses with a JSON-RPC envelope, and states `RateLimit-Limit`/`-Remaining`/`-Reset` on every answer. Four routes had each transcribed that by hand through three differently-named local helpers, which is how a headroom header gets added to one surface and forgotten on the next.
|
|
127
128
|
- **`mcpRateLimited(retryAfterSec)` is a JSON-RPC envelope.** A 429 whose body is `{"error": "..."}` is a string where the client's parser expects `{code, message}`, on the one response an agent meets exactly when it is working hard.
|
package/bin/check-classes.mjs
CHANGED
|
@@ -18,11 +18,20 @@
|
|
|
18
18
|
// are utilities is derived, not listed — anything the build emits that globals.css
|
|
19
19
|
// does not define is Tailwind's.
|
|
20
20
|
//
|
|
21
|
+
// And the reverse: a selector globals.css defines that nothing the build ships
|
|
22
|
+
// references. The shared blocks and chrome render their markup inside
|
|
23
|
+
// wiki-formant while each app styles it, so references are read from the build
|
|
24
|
+
// output, which holds this repo's code, the package's components and bundled
|
|
25
|
+
// libraries alike. A class renamed in the package surfaces here as its old
|
|
26
|
+
// selector going unreferenced. HTML stored in a database is not in the build,
|
|
27
|
+
// so a selector only stored content uses is reported too.
|
|
28
|
+
//
|
|
21
29
|
// npx check-classes # exit 1 on any dead token
|
|
22
30
|
// npx check-classes --warn # report and exit 0
|
|
23
31
|
// npx check-classes --compositions # also fail on 3+ inline utilities
|
|
32
|
+
// npx check-classes --unused # also fail on unreferenced selectors
|
|
24
33
|
//
|
|
25
|
-
// Run it from the repo root: `.next
|
|
34
|
+
// Run it from the repo root: `.next` and `src` are resolved against cwd.
|
|
26
35
|
//
|
|
27
36
|
// Needs a build first (npm run build): without one there is nothing to check
|
|
28
37
|
// against, and the script says so and exits 0 rather than failing blind.
|
|
@@ -30,7 +39,7 @@ import fs from 'node:fs';
|
|
|
30
39
|
import path from 'node:path';
|
|
31
40
|
|
|
32
41
|
const WARN_ONLY = process.argv.includes('--warn');
|
|
33
|
-
const
|
|
42
|
+
const STATIC_DIR = '.next/static';
|
|
34
43
|
const SRC_DIR = 'src';
|
|
35
44
|
|
|
36
45
|
const walk = (dir, ext, out = []) => {
|
|
@@ -43,7 +52,7 @@ const walk = (dir, ext, out = []) => {
|
|
|
43
52
|
return out;
|
|
44
53
|
};
|
|
45
54
|
|
|
46
|
-
const cssFiles = walk(
|
|
55
|
+
const cssFiles = walk(STATIC_DIR, '.css');
|
|
47
56
|
if (!cssFiles.length) {
|
|
48
57
|
console.log('check-classes: no built CSS under .next/static — run `npm run build` first. Skipping.');
|
|
49
58
|
process.exit(0);
|
|
@@ -59,12 +68,18 @@ for (const f of cssFiles) {
|
|
|
59
68
|
|
|
60
69
|
// Classes this project defines itself. Everything else the build emitted is a
|
|
61
70
|
// Tailwind utility, which is what makes the composition count derivable rather
|
|
62
|
-
// than a maintained prefix list.
|
|
71
|
+
// than a maintained prefix list. Read from rule preludes only, so a decimal in a
|
|
72
|
+
// value or a class named in a comment is not taken for a selector.
|
|
63
73
|
const globalsFile = walk(SRC_DIR, '.css').find(f => f.endsWith('globals.css'));
|
|
64
74
|
const named = new Set();
|
|
65
75
|
if (globalsFile) {
|
|
66
|
-
|
|
67
|
-
|
|
76
|
+
const css = fs.readFileSync(globalsFile, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
77
|
+
for (const [, prelude] of css.matchAll(/([^{};]*)\{/g)) {
|
|
78
|
+
if (prelude.trim().startsWith('@')) continue;
|
|
79
|
+
for (const m of prelude.matchAll(/\.((?:\\.|[-\w])+)/g)) {
|
|
80
|
+
const name = m[1].replace(/\\/g, '');
|
|
81
|
+
if (/^[A-Za-z_-]/.test(name)) named.add(name);
|
|
82
|
+
}
|
|
68
83
|
}
|
|
69
84
|
}
|
|
70
85
|
|
|
@@ -92,6 +107,7 @@ for (const f of walk(SRC_DIR, '.tsx')) {
|
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
const CHECK_COMPOSITIONS = process.argv.includes('--compositions');
|
|
110
|
+
const CHECK_UNUSED = process.argv.includes('--unused');
|
|
95
111
|
|
|
96
112
|
if (compositions.length) {
|
|
97
113
|
const verb = CHECK_COMPOSITIONS ? 'must be named' : 'should be named (advisory)';
|
|
@@ -103,9 +119,30 @@ if (compositions.length) {
|
|
|
103
119
|
console.error('');
|
|
104
120
|
}
|
|
105
121
|
|
|
122
|
+
// Every token in the JavaScript the build ships, server and client. A selector
|
|
123
|
+
// is referenced when its name appears whole, or when a prefix of it ending in `-`
|
|
124
|
+
// or `_` does, which is how `editorial-banner-${variant}` is written.
|
|
125
|
+
const shipped = new Set();
|
|
126
|
+
for (const f of [...walk(STATIC_DIR, '.js'), ...walk('.next/server', '.js')]) {
|
|
127
|
+
for (const [tok] of fs.readFileSync(f, 'utf8').matchAll(/[\w-]+/g)) shipped.add(tok);
|
|
128
|
+
}
|
|
129
|
+
const referenced = name =>
|
|
130
|
+
shipped.has(name) ||
|
|
131
|
+
[...name].some((c, i) => i >= 3 && (c === '-' || c === '_') && shipped.has(name.slice(0, i + 1)));
|
|
132
|
+
const unused = [...named].filter(n => !referenced(n)).sort();
|
|
133
|
+
|
|
134
|
+
if (unused.length) {
|
|
135
|
+
const verb = CHECK_UNUSED ? 'must be deleted' : 'advisory';
|
|
136
|
+
console.error(`check-classes: ${unused.length} selector(s) in ${globalsFile} referenced by nothing the build ships (${verb}):\n`);
|
|
137
|
+
console.error(` ${unused.join(', ')}\n`);
|
|
138
|
+
console.error(' Class names inside HTML stored in a database do not count, so check stored content before deleting one.\n');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const advisoryFailed = (CHECK_COMPOSITIONS && compositions.length > 0) || (CHECK_UNUSED && unused.length > 0);
|
|
142
|
+
|
|
106
143
|
if (!dead.size) {
|
|
107
144
|
console.log(`check-classes: clean — every className token resolves against ${emitted.size} emitted selectors.`);
|
|
108
|
-
process.exit(
|
|
145
|
+
process.exit(advisoryFailed && !WARN_ONLY ? 1 : 0);
|
|
109
146
|
}
|
|
110
147
|
|
|
111
148
|
console.error(`check-classes: ${dead.size} className token(s) style nothing:\n`);
|
package/dist/block-views.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
// block-views.tsx — the block
|
|
3
|
+
// block-views.tsx — the block leaves the wikis share.
|
|
4
4
|
//
|
|
5
5
|
// Behind its own subpath for the reason `react.tsx` is: React is an OPTIONAL
|
|
6
6
|
// peer, so a consumer that only wants the taxonomy or the MCP transport still
|
|
@@ -10,13 +10,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
10
10
|
// that way (see the Block Model note in the workspace CLAUDE.md) — a closed
|
|
11
11
|
// union is what makes `switch (block.type)` exhaustive, so a new block type is
|
|
12
12
|
// a compile error rather than a silent blank. What is NOT per-repo is what a
|
|
13
|
-
// codeTabs or a linkGrid LOOKS like once you have dispatched to it
|
|
14
|
-
//
|
|
15
|
-
// with the caller and the leaves move here.
|
|
13
|
+
// codeTabs or a linkGrid LOOKS like once you have dispatched to it, down to the
|
|
14
|
+
// class names. So the dispatch stays with the caller and the leaves live here.
|
|
16
15
|
//
|
|
17
|
-
// The class names are NOT props.
|
|
18
|
-
//
|
|
19
|
-
//
|
|
16
|
+
// The class names are NOT props. They are the shared convention every
|
|
17
|
+
// stylesheet implements, and making them configurable would let that convention
|
|
18
|
+
// fork. Everything that
|
|
20
19
|
// genuinely differs — the prose of a banner, whether references run through an
|
|
21
20
|
// HTML processor, the router's link — arrives as a prop.
|
|
22
21
|
import { Fragment, useState } from 'react';
|
package/dist/combobox.js
CHANGED
|
@@ -4,21 +4,12 @@
|
|
|
4
4
|
// pure, so it can be tested without a DOM, and `react.tsx` is left holding only
|
|
5
5
|
// the hook that calls it.
|
|
6
6
|
//
|
|
7
|
-
// `useTypeahead`
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// - one gave the rows `role="option"` but still no listbox, so the options
|
|
14
|
-
// had no owner;
|
|
15
|
-
// - two had no roles whatsoever;
|
|
16
|
-
// - one used a `data-highlighted` attribute, which no assistive technology
|
|
17
|
-
// reads.
|
|
18
|
-
//
|
|
19
|
-
// None of the five set `aria-activedescendant`, which is the attribute that
|
|
20
|
-
// actually announces the highlighted row as the reader arrows through it. A
|
|
21
|
-
// combobox without it is a text field that silently changes what Enter does.
|
|
7
|
+
// `useTypeahead` shares the state machine; this is the ARIA that goes with it:
|
|
8
|
+
// a `role="listbox"` container that owns `role="option"` rows, `aria-selected`
|
|
9
|
+
// on the option rather than on a plain `<button>`, and `aria-activedescendant`,
|
|
10
|
+
// the attribute that announces the highlighted row as the reader arrows
|
|
11
|
+
// through it. A combobox without it is a text field that silently changes what
|
|
12
|
+
// Enter does.
|
|
22
13
|
/** The id of one option row. Exported because a caller that scrolls the
|
|
23
14
|
* highlighted row into view has to find it by the same id the input points at. */
|
|
24
15
|
export function optionId(baseId, listKey, index) {
|
package/dist/conformance.d.ts
CHANGED
|
@@ -34,12 +34,19 @@ export interface Rpc {
|
|
|
34
34
|
protocolVersion?: string;
|
|
35
35
|
capabilities?: Record<string, unknown>;
|
|
36
36
|
instructions?: string;
|
|
37
|
+
resultType?: string;
|
|
38
|
+
supportedVersions?: string[];
|
|
39
|
+
ttlMs?: number;
|
|
40
|
+
cacheScope?: string;
|
|
41
|
+
_meta?: Record<string, unknown>;
|
|
37
42
|
};
|
|
38
43
|
error?: {
|
|
39
44
|
code: number;
|
|
40
45
|
message: string;
|
|
41
46
|
data?: {
|
|
42
47
|
availableTools?: string[];
|
|
48
|
+
supported?: string[];
|
|
49
|
+
requested?: string;
|
|
43
50
|
};
|
|
44
51
|
};
|
|
45
52
|
}
|
package/dist/conformance.js
CHANGED
|
@@ -15,12 +15,11 @@
|
|
|
15
15
|
// Every value the transport assertions compare against is imported from `mcp.ts`,
|
|
16
16
|
// not restated here. A suite that carries its own copy of the contract stops
|
|
17
17
|
// testing the boundary the moment the contract moves and says nothing about it.
|
|
18
|
-
import { DEFAULT_MAX_BATCH, MCP_CORS,
|
|
18
|
+
import { DEFAULT_MAX_BATCH, MCP_CORS, MCP_LEGACY_PROTOCOL_VERSION, MCP_META, MCP_MODERN_VERSIONS, MCP_PROTOCOL_VERSIONS, } from './mcp.js';
|
|
19
19
|
/**
|
|
20
20
|
* Does the live preflight carry every token `MCP_CORS` declares for this header?
|
|
21
21
|
*
|
|
22
|
-
* Derived rather than listed
|
|
23
|
-
* so a fourth added to `MCP_CORS` was tested by nobody.
|
|
22
|
+
* Derived rather than listed, so a header added to `MCP_CORS` is tested too.
|
|
24
23
|
*/
|
|
25
24
|
const corsCovers = (res, header) => {
|
|
26
25
|
const live = res.headers.get(header) ?? '';
|
|
@@ -112,11 +111,11 @@ expectedCapabilities = ['tools']) {
|
|
|
112
111
|
// 2025-03-26 — forfeiting structured output, tool titles and `_meta` — while
|
|
113
112
|
// every suite reported green.
|
|
114
113
|
const echoed = await t.rpc('initialize', {
|
|
115
|
-
protocolVersion:
|
|
114
|
+
protocolVersion: MCP_LEGACY_PROTOCOL_VERSION,
|
|
116
115
|
capabilities: {},
|
|
117
116
|
clientInfo: { name: clientName, version: '1' },
|
|
118
117
|
});
|
|
119
|
-
t.check('protocol negotiated', echoed.result?.protocolVersion ===
|
|
118
|
+
t.check('protocol negotiated', echoed.result?.protocolVersion === MCP_LEGACY_PROTOCOL_VERSION, `asked ${MCP_LEGACY_PROTOCOL_VERSION}, got ${echoed.result?.protocolVersion}`);
|
|
120
119
|
t.check('downgrades gracefully', (init.result?.protocolVersion ?? '').length > 0, `asked 2024-11-05, got ${init.result?.protocolVersion}`);
|
|
121
120
|
console.log(`\n=== transport ===`);
|
|
122
121
|
const opt = await fetch(t.endpoint, { method: 'OPTIONS' });
|
|
@@ -157,19 +156,67 @@ expectedCapabilities = ['tools']) {
|
|
|
157
156
|
t.check('resources/templates/list', Array.isArray(templates.result?.resourceTemplates), templates.error ? `-${templates.error.code}` : `${templates.result?.resourceTemplates?.length} templates`);
|
|
158
157
|
}
|
|
159
158
|
const versioned = await rawPost(t.endpoint, { jsonrpc: '2.0', id: 1, method: 'ping' }, {
|
|
160
|
-
'MCP-Protocol-Version':
|
|
159
|
+
'MCP-Protocol-Version': MCP_LEGACY_PROTOCOL_VERSION,
|
|
161
160
|
});
|
|
162
161
|
t.recordCall();
|
|
163
|
-
t.check('MCP-Protocol-Version echoed', versioned.headers.get('mcp-protocol-version') ===
|
|
162
|
+
t.check('MCP-Protocol-Version echoed', versioned.headers.get('mcp-protocol-version') === MCP_LEGACY_PROTOCOL_VERSION, `sent ${MCP_LEGACY_PROTOCOL_VERSION}, got ${versioned.headers.get('mcp-protocol-version')}`);
|
|
164
163
|
// Batching was removed in 2025-06-18. A server that keeps honouring it under
|
|
165
164
|
// a version that forbids it is telling the client something untrue.
|
|
166
165
|
const batched = await rawPost(t.endpoint, [{ jsonrpc: '2.0', id: 1, method: 'ping' }], {
|
|
167
|
-
'MCP-Protocol-Version':
|
|
166
|
+
'MCP-Protocol-Version': MCP_LEGACY_PROTOCOL_VERSION,
|
|
168
167
|
});
|
|
169
168
|
t.recordCall();
|
|
170
|
-
t.check(
|
|
169
|
+
t.check(`batch refused at ${MCP_LEGACY_PROTOCOL_VERSION}`, batched.status === 400, `${batched.status}`);
|
|
170
|
+
await modernChecks(t, caps);
|
|
171
171
|
return init;
|
|
172
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* A modern request exactly as a `2026-07-28` client sends it: version and
|
|
175
|
+
* capabilities in `_meta`, echoed by the headers a gateway routes on.
|
|
176
|
+
*/
|
|
177
|
+
const modernPost = async (t, method, opts = {}) => {
|
|
178
|
+
const version = opts.version ?? MCP_MODERN_VERSIONS[0];
|
|
179
|
+
const res = await rawPost(t.endpoint, {
|
|
180
|
+
jsonrpc: '2.0',
|
|
181
|
+
id: method,
|
|
182
|
+
method,
|
|
183
|
+
params: { _meta: { [MCP_META.protocolVersion]: version, [MCP_META.clientCapabilities]: {} } },
|
|
184
|
+
}, { 'MCP-Protocol-Version': version, 'Mcp-Method': method, ...opts.headers });
|
|
185
|
+
t.recordCall();
|
|
186
|
+
return { status: res.status, json: (await res.json()) };
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* The modern era, served beside the legacy one. Every probe that graded these
|
|
190
|
+
* servers on `server/discover` saw a -32601 for a month while every legacy check
|
|
191
|
+
* here stayed green, because nothing asked the modern question.
|
|
192
|
+
*/
|
|
193
|
+
async function modernChecks(t, legacyCapabilities) {
|
|
194
|
+
console.log(`\n=== modern era (${MCP_MODERN_VERSIONS[0]}) ===`);
|
|
195
|
+
const discover = await modernPost(t, 'server/discover');
|
|
196
|
+
const d = discover.json.result;
|
|
197
|
+
t.check('server/discover', discover.status === 200 &&
|
|
198
|
+
d?.resultType === 'complete' &&
|
|
199
|
+
MCP_PROTOCOL_VERSIONS.every(v => d.supportedVersions?.includes(v)) &&
|
|
200
|
+
!!d.instructions &&
|
|
201
|
+
typeof d.ttlMs === 'number' &&
|
|
202
|
+
!!d.cacheScope &&
|
|
203
|
+
!!d._meta?.[MCP_META.serverInfo], `${discover.status} ${JSON.stringify(d?.supportedVersions ?? discover.json.error)}`);
|
|
204
|
+
// One server, two ways to ask what it can do. They must give one answer.
|
|
205
|
+
t.check('discover agrees with initialize', JSON.stringify(d?.capabilities) === JSON.stringify(legacyCapabilities), `discover=${JSON.stringify(d?.capabilities)} initialize=${JSON.stringify(legacyCapabilities)}`);
|
|
206
|
+
const list = await modernPost(t, 'tools/list');
|
|
207
|
+
t.check('modern tools/list', list.status === 200 &&
|
|
208
|
+
list.json.result?.resultType === 'complete' &&
|
|
209
|
+
(list.json.result?.tools?.length ?? 0) > 0 &&
|
|
210
|
+
typeof list.json.result?.ttlMs === 'number', `${list.status} tools=${list.json.result?.tools?.length ?? JSON.stringify(list.json.error)} ttlMs=${list.json.result?.ttlMs}`);
|
|
211
|
+
const unsupported = await modernPost(t, 'tools/list', { version: '1999-01-01' });
|
|
212
|
+
t.check('unsupported version→-32022', unsupported.status === 400 &&
|
|
213
|
+
unsupported.json.error?.code === -32022 &&
|
|
214
|
+
JSON.stringify(unsupported.json.error?.data?.supported) === JSON.stringify(MCP_PROTOCOL_VERSIONS), `${unsupported.status} code=${unsupported.json.error?.code} supported=${JSON.stringify(unsupported.json.error?.data?.supported)}`);
|
|
215
|
+
const ping = await modernPost(t, 'ping');
|
|
216
|
+
t.check('removed method→404', ping.status === 404 && ping.json.error?.code === -32601, `ping: ${ping.status} code=${ping.json.error?.code}`);
|
|
217
|
+
const mismatch = await modernPost(t, 'tools/list', { headers: { 'Mcp-Method': 'prompts/list' } });
|
|
218
|
+
t.check('header mismatch→-32020', mismatch.status === 400 && mismatch.json.error?.code === -32020, `${mismatch.status} code=${mismatch.json.error?.code}`);
|
|
219
|
+
}
|
|
173
220
|
/**
|
|
174
221
|
* One service, many descriptors — server.json, the two agent-card paths, the
|
|
175
222
|
* OpenAPI document, the MCP server card, and `initialize` — should never
|
|
@@ -306,10 +353,9 @@ export async function annotationChecks(t, opts = {}) {
|
|
|
306
353
|
*/
|
|
307
354
|
export async function payloadBudget(t, calls, maxBytes = 120_000) {
|
|
308
355
|
// Every read-only tool a caller can invoke with no arguments at all, whether
|
|
309
|
-
// or not the suite thought to name it
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
// the four tools beside it. A tool with required arguments still has to be
|
|
356
|
+
// or not the suite thought to name it, so a no-argument tool answering with
|
|
357
|
+
// megabytes cannot pass by going unlisted. A tool with required arguments
|
|
358
|
+
// still has to be
|
|
313
359
|
// listed: the suite is the only thing that knows a valid pair.
|
|
314
360
|
const listed = new Set(calls.map(c => c.name));
|
|
315
361
|
const bare = ((await t.rpc('tools/list')).result?.tools ?? [])
|
package/dist/crawlers.js
CHANGED
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
// crawlers.ts — one roster of AI crawler tokens, for both surfaces that need it.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// the set of agents that get their own group. The two copies were each
|
|
6
|
-
// byte-identical across all three repos — and they were different lists.
|
|
3
|
+
// The proxy counts an "AI Bot Visit" by user-agent substring, and `robots.ts`
|
|
4
|
+
// gives each agent its own group. Both read this one roster.
|
|
7
5
|
//
|
|
8
|
-
//
|
|
6
|
+
// One difference between the two uses is deliberate. `Applebot-Extended`
|
|
9
7
|
// never fetches a page: it is a robots.txt-only token that Applebot consults
|
|
10
8
|
// before using already-crawled data for AI, so counting it would count nothing.
|
|
11
9
|
// That belongs in robots and not in the matcher, and it is declared here.
|
|
12
10
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// an agent with no group of its own falls through to `*` and is granted
|
|
17
|
-
// whatever that grants. The wikis were measuring five crawlers they had never
|
|
18
|
-
// addressed. One roster makes that a property of the data rather than of which
|
|
19
|
-
// file you happened to edit.
|
|
11
|
+
// Every other token appears in both, because a crawler obeys only its
|
|
12
|
+
// most-specific matching group: an agent with no group of its own falls through
|
|
13
|
+
// to `*` and is granted whatever that grants.
|
|
20
14
|
/**
|
|
21
15
|
* Order is significant: `detectAiBot` returns the first token the user agent
|
|
22
16
|
* contains, so a token that is a substring of another must come after it.
|
package/dist/dom.d.ts
CHANGED
|
@@ -19,8 +19,7 @@ export interface CopyButtonOptions {
|
|
|
19
19
|
export declare function addCopyButtons(root: ParentNode, options?: CopyButtonOptions): number;
|
|
20
20
|
/**
|
|
21
21
|
* The one place this origin is written down. It is both the embed host and the
|
|
22
|
-
* allow-list the resize listener checks
|
|
23
|
-
* four call sites across the two wikis.
|
|
22
|
+
* allow-list the resize listener checks.
|
|
24
23
|
*/
|
|
25
24
|
export declare const TWITTER_ORIGIN = "https://platform.twitter.com";
|
|
26
25
|
/** The embed iframe's src. `dnt=true` opts the embed out of Twitter's tracking. */
|
package/dist/dom.js
CHANGED
|
@@ -55,8 +55,7 @@ export function addCopyButtons(root, options = {}) {
|
|
|
55
55
|
// ---- twitter embeds ---------------------------------------------------------
|
|
56
56
|
/**
|
|
57
57
|
* The one place this origin is written down. It is both the embed host and the
|
|
58
|
-
* allow-list the resize listener checks
|
|
59
|
-
* four call sites across the two wikis.
|
|
58
|
+
* allow-list the resize listener checks.
|
|
60
59
|
*/
|
|
61
60
|
export const TWITTER_ORIGIN = 'https://platform.twitter.com';
|
|
62
61
|
/** The embed iframe's src. `dnt=true` opts the embed out of Twitter's tracking. */
|
package/dist/editor.d.ts
CHANGED
|
@@ -5,8 +5,7 @@ import { type ChangeEvent, type RefObject } from 'react';
|
|
|
5
5
|
*
|
|
6
6
|
* Each of these packages ships a `declare module '@tiptap/core'` block that adds
|
|
7
7
|
* its commands to `ChainedCommands` — `toggleBold`, `insertTable`, `setLink`,
|
|
8
|
-
* `setImage`.
|
|
9
|
-
* a side effect of the import; now that this module owns them, it has to carry
|
|
8
|
+
* `setImage`. Because this module owns the extensions, it has to carry
|
|
10
9
|
* the augmentation across the package boundary, and a re-exported type is what
|
|
11
10
|
* makes the emitted `.d.ts` load the module that declares it. Without these
|
|
12
11
|
* four lines every `editor.chain().focus().toggleBold()` in every consumer
|
|
@@ -16,19 +15,6 @@ export type { StarterKitOptions } from '@tiptap/starter-kit';
|
|
|
16
15
|
export type { LinkOptions } from '@tiptap/extension-link';
|
|
17
16
|
export type { ImageOptions } from '@tiptap/extension-image';
|
|
18
17
|
export type { TableOptions } from '@tiptap/extension-table';
|
|
19
|
-
/**
|
|
20
|
-
* Reduce pasted HTML to structure plus the three attributes that carry meaning.
|
|
21
|
-
*
|
|
22
|
-
* A paste from a word processor or a web page arrives carrying its whole
|
|
23
|
-
* stylesheet inline. Keeping any of it means the wiki's own typography loses to
|
|
24
|
-
* whatever the author copied from, per paragraph, invisibly — and `style` on a
|
|
25
|
-
* pasted node is also the cheapest way to smuggle a full-bleed overlay into a
|
|
26
|
-
* page body.
|
|
27
|
-
*
|
|
28
|
-
* Browser-only: it parses with `DOMParser`. Called from `transformPastedHTML`,
|
|
29
|
-
* which only ever runs in response to a paste.
|
|
30
|
-
*/
|
|
31
|
-
export declare function cleanPastedHtml(html: string): string;
|
|
32
18
|
export interface WikiEditorExtensionOptions {
|
|
33
19
|
/** Empty-document prompt. */
|
|
34
20
|
placeholder?: string;
|
|
@@ -42,15 +28,6 @@ export interface WikiEditorExtensionOptions {
|
|
|
42
28
|
*/
|
|
43
29
|
nodes?: readonly AnyExtension[];
|
|
44
30
|
}
|
|
45
|
-
/**
|
|
46
|
-
* The editor's extension set.
|
|
47
|
-
*
|
|
48
|
-
* `codeBlock: false` on StarterKit is load-bearing: the consumer registers its
|
|
49
|
-
* own via `createCodeBlock`, and leaving StarterKit's in place would give the
|
|
50
|
-
* schema two nodes claiming the same name. Headings stop at h2 — the page title
|
|
51
|
-
* is the only h1 a wiki page has.
|
|
52
|
-
*/
|
|
53
|
-
export declare function wikiEditorExtensions({ placeholder, nodes, }?: WikiEditorExtensionOptions): AnyExtension[];
|
|
54
31
|
/**
|
|
55
32
|
* Turn a pasted URL into the richest node that fits it, falling back to a bare
|
|
56
33
|
* iframe. Order matters: a YouTube URL is also a valid iframe source, so the
|
package/dist/editor.js
CHANGED
|
@@ -4,25 +4,16 @@
|
|
|
4
4
|
// `tiptap.tsx` holds the custom NODES both wikis needed (iframe, tweet, map,
|
|
5
5
|
// code block, tabs). This holds what sits around them: which extensions are
|
|
6
6
|
// configured how, what a paste is scrubbed down to, how a pasted URL becomes
|
|
7
|
-
// the right embed, and the state a toolbar reads.
|
|
8
|
-
// both repos — the same twelve-entry extension array, the same paste scrubber,
|
|
9
|
-
// the same embed dispatch including the shortened-map async swap.
|
|
7
|
+
// the right embed, and the state a toolbar reads.
|
|
10
8
|
//
|
|
11
|
-
// TWO
|
|
12
|
-
// exists at all is that each repo had exactly one of them:
|
|
9
|
+
// TWO GUARDS HERE ARE LOAD-BEARING:
|
|
13
10
|
//
|
|
14
|
-
// 1. `onChangeRef.current = onChange`
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// keystroke of every edit that ended in a click was dropped. Blur flushes
|
|
21
|
-
// the pending debounce.
|
|
22
|
-
//
|
|
23
|
-
// Neither repo was "behind": each had shipped the fix the other lacked, which
|
|
24
|
-
// is the drift that costs the most, because neither file looks like the one to
|
|
25
|
-
// fix.
|
|
11
|
+
// 1. `onChangeRef.current = onChange` is written in an effect, never during
|
|
12
|
+
// render: a ref mutated mid-render can tear under concurrent rendering.
|
|
13
|
+
// It is only read from events and timeouts, which run later.
|
|
14
|
+
// 2. Blur flushes the pending debounce. The change is debounced 150ms, and
|
|
15
|
+
// clicking Save blurs the editor before the click lands, so without the
|
|
16
|
+
// flush the final keystroke of an edit that ends in a click is dropped.
|
|
26
17
|
//
|
|
27
18
|
// Every @tiptap package here is an OPTIONAL PEER. A consumer that only wants
|
|
28
19
|
// the taxonomy or the MCP transport installs none of them.
|
|
@@ -50,7 +41,7 @@ import { toMapEmbedUrl } from './maps.js';
|
|
|
50
41
|
* Browser-only: it parses with `DOMParser`. Called from `transformPastedHTML`,
|
|
51
42
|
* which only ever runs in response to a paste.
|
|
52
43
|
*/
|
|
53
|
-
|
|
44
|
+
function cleanPastedHtml(html) {
|
|
54
45
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
55
46
|
doc.querySelectorAll('style, script, meta, link, svg, canvas, noscript').forEach(el => el.remove());
|
|
56
47
|
doc.querySelectorAll('*').forEach(el => {
|
|
@@ -72,7 +63,7 @@ export function cleanPastedHtml(html) {
|
|
|
72
63
|
* schema two nodes claiming the same name. Headings stop at h2 — the page title
|
|
73
64
|
* is the only h1 a wiki page has.
|
|
74
65
|
*/
|
|
75
|
-
|
|
66
|
+
function wikiEditorExtensions({ placeholder = '', nodes = [], } = {}) {
|
|
76
67
|
return [
|
|
77
68
|
StarterKit.configure({ heading: { levels: [2, 3, 4] }, codeBlock: false }),
|
|
78
69
|
TiptapLink.configure({ openOnClick: false, HTMLAttributes: { class: 'link' } }),
|
package/dist/headings.js
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
// headings.ts — stable ids on a wiki page's headings, and the list a table of
|
|
2
2
|
// contents is built from.
|
|
3
3
|
//
|
|
4
|
-
// Two of the three wikis had already written this out (caper's `injectHeadingIds`,
|
|
5
|
-
// radix-wiki's heading branch in `processHtml`), down to byte-identical
|
|
6
|
-
// `stripTags` and `getAttr` helpers — caper's file says "ported in spirit from
|
|
7
|
-
// radix-wiki" at the top, which is the drift admitting itself. What they had
|
|
8
|
-
// drifted on is below.
|
|
9
|
-
//
|
|
10
4
|
// The slug rule is a parameter, not a decision this module makes. A heading id
|
|
11
5
|
// is a live URL: readers link to `#the-shape-of-a-code`, and so does the page's
|
|
12
6
|
// own permalink anchor. Unifying two slug rules would silently move every
|
|
@@ -15,8 +9,7 @@
|
|
|
15
9
|
//
|
|
16
10
|
// Deduping, by contrast, is not a choice: two headings with the same text
|
|
17
11
|
// otherwise mint the same id twice and every link to the second one lands on
|
|
18
|
-
// the first
|
|
19
|
-
// dedupes.
|
|
12
|
+
// the first, so this always dedupes.
|
|
20
13
|
import { getAttr, stripTags } from './html.js';
|
|
21
14
|
/** The default slug rule: lowercase words joined by hyphens. */
|
|
22
15
|
export function slugifyHeading(text) {
|
package/dist/link-check.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
// link-check.ts — the dead-link probe
|
|
1
|
+
// link-check.ts — the dead-link probe wiki sweep scripts share.
|
|
2
2
|
//
|
|
3
3
|
// Node-only (fetch, AbortSignal, URL). No framework, no database: what a
|
|
4
4
|
// checker does with the verdicts — which pages to walk, what counts as an
|
|
5
5
|
// internal path, how to report — stays with the caller, because that is the
|
|
6
6
|
// half that is genuinely per-wiki.
|
|
7
7
|
//
|
|
8
|
-
// EVERY GUARD BELOW WAS PAID FOR BY A FALSE POSITIVE
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// serialising per hostname is what fixes it. Neither copy was behind. A sweep
|
|
14
|
-
// running either one alone strips good citations for reasons the other repo had
|
|
15
|
-
// already written down — which is the entire argument for this file.
|
|
8
|
+
// EVERY GUARD BELOW WAS PAID FOR BY A FALSE POSITIVE. npmjs.com 403s scripted
|
|
9
|
+
// requests, an expired certificate is not a dead host, a YouTube /embed/ URL
|
|
10
|
+
// answers 200 for a deleted video, and a connect refusal is usually concurrency
|
|
11
|
+
// rather than death, which serialising per hostname fixes. A sweep missing any
|
|
12
|
+
// one of these strips good citations.
|
|
16
13
|
import { stripTags } from './html.js';
|
|
17
14
|
const DEFAULTS = { timeoutMs: 12_000, slowTimeoutMs: 40_000 };
|
|
18
15
|
/**
|
package/dist/maps.d.ts
CHANGED
|
@@ -3,8 +3,6 @@ export interface MapCoords {
|
|
|
3
3
|
lon: number;
|
|
4
4
|
zoom?: number | undefined;
|
|
5
5
|
}
|
|
6
|
-
/** A plain embed URL for a coordinate pair. */
|
|
7
|
-
export declare function mapsEmbedUrl(lat: number, lon: number, zoom?: number): string;
|
|
8
6
|
/**
|
|
9
7
|
* Dig a coordinate pair out of a maps URL. Four shapes in descending
|
|
10
8
|
* specificity: the `@lat,lon,zoom` path segment, a `/search/lat,lon`, the
|
package/dist/maps.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// pure string work over Google and Apple Maps URL shapes, with no framework or
|
|
5
5
|
// database in it, which is why neither copy had a reason to diverge.
|
|
6
6
|
/** A plain embed URL for a coordinate pair. */
|
|
7
|
-
|
|
7
|
+
function mapsEmbedUrl(lat, lon, zoom = 15) {
|
|
8
8
|
return `https://maps.google.com/maps?q=${lat},${lon}&z=${zoom}&output=embed`;
|
|
9
9
|
}
|
|
10
10
|
/**
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,20 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The modern era: every request carries its own version and client
|
|
3
|
+
* capabilities in `params._meta`. No handshake, no session, no `ping`, no batch.
|
|
4
|
+
*/
|
|
5
|
+
export declare const MCP_MODERN_VERSIONS: readonly ["2026-07-28"];
|
|
6
|
+
/**
|
|
7
|
+
* The legacy era, negotiated by an `initialize` handshake.
|
|
3
8
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* receipt rides, so the one surface that needed it rebuilt this module's HTTP
|
|
8
|
-
* shell by hand to reach it.
|
|
9
|
+
* Served beside the modern era rather than replaced by it. The spec lets one
|
|
10
|
+
* endpoint speak both, and a legacy client meeting a dual-era server works;
|
|
11
|
+
* going modern-only would fail the handshake of every client in the field.
|
|
9
12
|
*/
|
|
10
|
-
export declare const
|
|
13
|
+
export declare const MCP_LEGACY_VERSIONS: readonly ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
14
|
+
/**
|
|
15
|
+
* Every version this transport speaks, newest first. It is the `supported` list
|
|
16
|
+
* a `-32022` names and the `supportedVersions` of `server/discover`, so a modern
|
|
17
|
+
* client that cannot use the newest still learns a legacy fallback exists.
|
|
18
|
+
*/
|
|
19
|
+
export declare const MCP_PROTOCOL_VERSIONS: readonly ["2026-07-28", "2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
11
20
|
export type McpProtocolVersion = (typeof MCP_PROTOCOL_VERSIONS)[number];
|
|
12
|
-
|
|
21
|
+
type LegacyVersion = (typeof MCP_LEGACY_VERSIONS)[number];
|
|
22
|
+
/** The newest version spoken here. */
|
|
13
23
|
export declare const MCP_PROTOCOL_VERSION: McpProtocolVersion;
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The newest version `initialize` can negotiate, and what an unrecognised ask is
|
|
26
|
+
* offered. A handshake cannot land on a modern version — the modern era has no
|
|
27
|
+
* handshake — so this, not `MCP_PROTOCOL_VERSION`, is what a legacy client gets.
|
|
28
|
+
*/
|
|
29
|
+
export declare const MCP_LEGACY_PROTOCOL_VERSION: LegacyVersion;
|
|
30
|
+
/** The reserved `_meta` keys the modern era is spelled in. */
|
|
31
|
+
export declare const MCP_META: {
|
|
32
|
+
readonly protocolVersion: "io.modelcontextprotocol/protocolVersion";
|
|
33
|
+
readonly clientCapabilities: "io.modelcontextprotocol/clientCapabilities";
|
|
34
|
+
readonly serverInfo: "io.modelcontextprotocol/serverInfo";
|
|
35
|
+
};
|
|
18
36
|
import { type RateLimitOptions } from './rate-limit.js';
|
|
19
37
|
export type ToolParam = {
|
|
20
38
|
type: 'string' | 'number' | 'boolean' | 'array' | 'object';
|
|
@@ -126,6 +144,12 @@ export interface McpServerConfig {
|
|
|
126
144
|
prompts?: McpPrompt[];
|
|
127
145
|
/** Appended to the GET refusal so a browser that lands here learns where to go. */
|
|
128
146
|
docsUrl?: string;
|
|
147
|
+
/**
|
|
148
|
+
* How long, in ms, a modern client may reuse `server/discover`, a list or a
|
|
149
|
+
* read. The modern era requires the hint on each of those. Defaults to five
|
|
150
|
+
* minutes: tool and prompt lists only change on deploy.
|
|
151
|
+
*/
|
|
152
|
+
cacheTtlMs?: number;
|
|
129
153
|
/**
|
|
130
154
|
* Cap on JSON-RPC batch size. The rate limiter charges one token per HTTP
|
|
131
155
|
* request, before the body is parsed — an unbounded batch would let a single
|
|
@@ -203,9 +227,16 @@ export declare const toolText: (id: RpcRequest["id"], text: string, isError?: bo
|
|
|
203
227
|
}[];
|
|
204
228
|
};
|
|
205
229
|
};
|
|
230
|
+
/** The methods one era has and the other does not. Everything else is shared. */
|
|
231
|
+
declare const ERA_METHODS: {
|
|
232
|
+
readonly legacy: readonly ["initialize", "ping"];
|
|
233
|
+
readonly modern: readonly ["server/discover"];
|
|
234
|
+
};
|
|
235
|
+
type Era = keyof typeof ERA_METHODS;
|
|
206
236
|
/** Everything the dispatcher needs that is not in the JSON-RPC entry itself. */
|
|
207
237
|
interface Dispatch {
|
|
208
238
|
protocolVersion: McpProtocolVersion;
|
|
239
|
+
era: Era;
|
|
209
240
|
request?: Request;
|
|
210
241
|
}
|
|
211
242
|
/** Dispatch a parsed body. `null` means notification-only — answer 202, not 200. */
|