wiki-formant 0.1.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wiki-formant contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # wiki-formant
2
+
3
+ The portable half of a wiki: derived taxonomy, a spec-correct MCP transport, markdown twins, and the conditional-GET plumbing an agent surface needs.
4
+
5
+ Zero runtime dependencies. Web-standard `Request`/`Response`, so it runs unchanged on Next route handlers, Hono, Bun, Deno and workers.
6
+
7
+ ```bash
8
+ npm install wiki-formant
9
+ ```
10
+
11
+ ## What this is, and what it deliberately is not
12
+
13
+ Every wiki needs a page store, a design system and a set of block types. Those are the parts you must own — they encode your schema, your brand and your content model, and a package that tried to own them would fight you.
14
+
15
+ What every wiki *also* needs, rebuilds by hand, and then lets drift, is the layer above: the second browsing axis derived from metadata you are already storing, the HTML→markdown converter behind every `.md` twin, the JSON-RPC edges that decide whether an MCP client can talk to you at all, and the ETag arithmetic that turns a recrawl into a 304.
16
+
17
+ This package was extracted from three production wikis that had each grown their own copy. By the time it was lifted, the copies had already diverged — one had lost the alphabetical index and the related-page ranking; one had `ToolAnnotations` the others lacked; one had MCP prompts the others lacked. Everything here is the union, with tests pinning the specific bugs that shipped.
18
+
19
+ ## Taxonomy
20
+
21
+ `tagPath` puts a page in exactly one place in a tree. The `select`-typed metadata it already carries is a *cross-cutting* axis — and it is almost always stored, rendered once in an infobox as dead text, and never made pressable. The failure mode is a 141-page category rendered as one flat grid while the data to split it sits unread in a JSON column.
22
+
23
+ The tree is yours; supply `getMetadataKeys` and the rest is derived.
24
+
25
+ ```ts
26
+ import { createTaxonomy } from 'wiki-formant/taxonomy';
27
+
28
+ const taxonomy = createTaxonomy({
29
+ getMetadataKeys: tagPath => TAGS[tagPath]?.metadataKeys ?? [],
30
+ href: (tagPath, { sort, filters, letter }) => /* your URL contract */,
31
+ });
32
+
33
+ const filters = taxonomy.facetFilters('ecosystem', searchParams);
34
+ const pages = taxonomy.filterPages(allPages, filters, letter);
35
+ const facets = taxonomy.buildFacets('ecosystem', allPages, filters, letter);
36
+ const index = taxonomy.needsAlphaIndex(allPages.length)
37
+ ? taxonomy.alphaIndex(allPages, filters) : [];
38
+ ```
39
+
40
+ Four behaviours worth knowing, because each replaces a plausible wrong answer:
41
+
42
+ - **Each facet is counted over the set narrowed by every *other* active filter**, so its own options stay switchable instead of collapsing to the one already chosen.
43
+ - **Values come from the data, not from the declared `options`.** A key that declares four values while the pages hold seven would otherwise hide three behind a bar claiming to cover everything. Render what is there and the drift becomes visible.
44
+ - **A single-valued facet hides — unless it is the active one.** An infobox row can set a filter the chips never offered; without its chip the reader lands on a narrowed list with nothing to press to widen it.
45
+ - **`rankRelated` ranks by shared facet values** and returns the shared axis as `{key, value}`, so the *See also* heading can be the link into the filtered set. The behaviour it replaces — `pages.slice(0, 5)` — shows every page in a large category the same five links.
46
+
47
+ One `href` builder is passed in and used by every chip, letter and sort button. A sort button that drops the active filters is the tell that a project grew a second one.
48
+
49
+ ## MCP
50
+
51
+ A minimal [Model Context Protocol](https://modelcontextprotocol.io) server over Streamable HTTP, with the transport edges most implementations get wrong.
52
+
53
+ ```ts
54
+ import { mcpResponse, mcpGet, mcpOptions, McpToolError } from 'wiki-formant/mcp';
55
+
56
+ const config = {
57
+ serverInfo: { name: 'my-wiki', version: '1.4.0' },
58
+ instructions: 'Call search_pages first; get_page needs a full path.',
59
+ docsUrl: 'https://example.com/llms.txt',
60
+ tools: [{
61
+ name: 'search_pages',
62
+ description: 'Full-text search across the wiki.',
63
+ inputSchema: {
64
+ type: 'object',
65
+ properties: { q: { type: 'string', description: 'query' } },
66
+ required: ['q'],
67
+ },
68
+ annotations: { readOnlyHint: true },
69
+ handler: async ({ q }) => search(String(q)),
70
+ }],
71
+ onCall: (req, body) => track(req, body),
72
+ };
73
+
74
+ export const POST = (req: Request) => mcpResponse(req, config);
75
+ export const GET = () => mcpGet(config.docsUrl);
76
+ export const OPTIONS = () => mcpOptions();
77
+ ```
78
+
79
+ What it gets right:
80
+
81
+ - **A caller-fixable mistake is a tool result with `isError`, never `-32603`.** Bad arguments come back naming every bad field at once, quoting the legal values and appending the schema, so one retry can fix all of them.
82
+ - **Capabilities advertise only what the config populates.** An advertised `resources` whose list comes back empty reads as a bug, not as honesty. The `-32601` method list narrows the same way.
83
+ - **A notification-only POST answers a bare `202`**, not a `200` carrying JSON `null`.
84
+ - **Malformed JSON is `-32700` with a `400`**, never a 500.
85
+ - **`GET` is an explicit 405 with CORS headers.** A framework's automatic 405 carries none, so a browser client cannot even read the refusal.
86
+ - **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".
87
+ - **Batches are capped** (default 20) with a teaching error, because the rate limiter charges one token per HTTP request before the body is parsed.
88
+
89
+ ## Markdown twins
90
+
91
+ `htmlToMarkdown` preserves the structure an agent cites by — headings, lists, tables, code, emphasis — rather than flattening to prose. Tables convert first so the generic rules cannot eat their markup, ordered lists number per list, pipes inside cells are escaped, and a headerless table gets a synthesised header because GFM has no other form.
92
+
93
+ ```ts
94
+ import { htmlToMarkdown, markdownDocument } from 'wiki-formant/markdown';
95
+
96
+ return new Response(
97
+ markdownDocument(
98
+ { title: page.title, url, updated: page.updatedAt, lastVerified: page.lastVerifiedAt,
99
+ license: { spdx: 'CC-BY-4.0', url: 'https://creativecommons.org/licenses/by/4.0/' } },
100
+ blocksToMarkdown(page.content), // your block types, your function
101
+ ),
102
+ { headers: markdownHeaders(lastModified) },
103
+ );
104
+ ```
105
+
106
+ Block trees stay in your app — every project owns its own type set. Give this module HTML and it gives you markdown.
107
+
108
+ > A trap worth naming: if you serve twins via a rewrite, Next drops the destination query string. The rewrite must carry the `.md` extension through to the destination path, or the twin silently serves JSON.
109
+
110
+ ## Conditional GET
111
+
112
+ The `llms.txt` / `llms-index.txt` / `llms-full.txt` trio are the most-recrawled URLs a wiki serves and the most expensive to render. Without a corpus-derived ETag, every AI crawler pays full price on every pass, forever.
113
+
114
+ ```ts
115
+ import { corpusEtag, notModified, textHeaders } from 'wiki-formant/http';
116
+
117
+ const etag = corpusEtag([pageCount, newestUpdatedAt]);
118
+ const lastModified = newestUpdatedAt.toUTCString();
119
+
120
+ export async function GET(request: Request) {
121
+ return notModified(request, etag, lastModified)
122
+ ?? new Response(buildCorpus(), { headers: textHeaders(etag, lastModified) });
123
+ }
124
+ ```
125
+
126
+ ## Pagination and versioning
127
+
128
+ `parsePagination` clamps `page ≥ 1` and `pageSize` to 1–100; `paginatedResponse` always carries `totalPages`. Reshaping that response is a breaking change to every client that pages, which is why it lives here rather than being re-typed per repo.
129
+
130
+ `parseVersion` / `bump` / `compareVersions` handle revision semver tolerantly — a page always has a version, even when the column holds `null` or junk.
131
+
132
+ ## API
133
+
134
+ | Export | From |
135
+ |---|---|
136
+ | `createTaxonomy`, `defaultHref`, `firstLetter`, `toggleFilter` | `wiki-formant/taxonomy` |
137
+ | `mcpResponse`, `mcpGet`, `mcpOptions`, `handleMcp`, `withMcpCors`, `McpToolError`, `MCP_CORS`, `MCP_PROTOCOL_VERSION` | `wiki-formant/mcp` |
138
+ | `htmlToMarkdown`, `inlineToMarkdown`, `tableToMarkdown`, `frontmatter`, `markdownDocument`, `decodeEntities` | `wiki-formant/markdown` |
139
+ | `corpusEtag`, `notModified`, `textHeaders`, `markdownHeaders`, `cleanSnippet`, `pageLine` | `wiki-formant/http` |
140
+ | `parsePagination`, `paginatedResponse`, `toOffset` | `wiki-formant/pagination` |
141
+ | `parseVersion`, `formatVersion`, `incrementVersion`, `bump`, `compareVersions` | `wiki-formant/versioning` |
142
+
143
+ All are also re-exported from the package root.
144
+
145
+ ## Licence
146
+
147
+ MIT.
@@ -0,0 +1,3 @@
1
+ export declare const NAMED_ENTITIES: Record<string, string>;
2
+ export declare function decodeEntities(s: string): string;
3
+ //# sourceMappingURL=entities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entities.d.ts","sourceRoot":"","sources":["../src/entities.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAejD,CAAC;AAEF,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAOhD"}
@@ -0,0 +1,30 @@
1
+ // entities.ts — HTML entity decoding for text exports.
2
+ //
3
+ // Rich-text editors store typographic punctuation as named entities, so pages
4
+ // carry raw `&mdash;` / `&ldquo;` / `&rsquo;` into every text export — ampersand
5
+ // soup an agent has to read through. Numeric forms decode generically; the named
6
+ // table covers what wiki corpora actually use (punctuation, arrows, maths,
7
+ // accented Latin, Greek) rather than attempting the full HTML5 list.
8
+ export const NAMED_ENTITIES = {
9
+ amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ',
10
+ mdash: '—', ndash: '–', hellip: '…', middot: '·', bull: '•', sect: '§', para: '¶', dagger: '†',
11
+ ldquo: '“', rdquo: '”', lsquo: '‘', rsquo: '’', laquo: '«', raquo: '»',
12
+ larr: '←', rarr: '→', uarr: '↑', darr: '↓', harr: '↔',
13
+ times: '×', divide: '÷', minus: '−', plusmn: '±', ne: '≠', le: '≤', ge: '≥',
14
+ asymp: '≈', radic: '√', infin: '∞', sum: '∑', prod: '∏', int: '∫', deg: '°', permil: '‰',
15
+ sup2: '²', sup3: '³', frac12: '½', frac14: '¼', frac34: '¾',
16
+ euro: '€', pound: '£', yen: '¥', cent: '¢', copy: '©', reg: '®', trade: '™',
17
+ eacute: 'é', egrave: 'è', ecirc: 'ê', aacute: 'á', agrave: 'à', acirc: 'â', aring: 'å',
18
+ auml: 'ä', ouml: 'ö', uuml: 'ü', iacute: 'í', oacute: 'ó', uacute: 'ú', ntilde: 'ñ',
19
+ ccedil: 'ç', oslash: 'ø', szlig: 'ß', aelig: 'æ',
20
+ alpha: 'α', beta: 'β', gamma: 'γ', delta: 'δ', epsilon: 'ε', theta: 'θ', lambda: 'λ',
21
+ mu: 'μ', nu: 'ν', pi: 'π', rho: 'ρ', sigma: 'σ', tau: 'τ', phi: 'φ', omega: 'ω',
22
+ Delta: 'Δ', Sigma: 'Σ', Omega: 'Ω', Lambda: 'Λ', Phi: 'Φ', Pi: 'Π',
23
+ };
24
+ export function decodeEntities(s) {
25
+ return s
26
+ .replace(/&#x([0-9a-fA-F]{1,6});/g, (_m, hex) => String.fromCodePoint(parseInt(hex, 16)))
27
+ .replace(/&#(\d{1,7});/g, (_m, dec) => String.fromCodePoint(Number(dec)))
28
+ .replace(/&([a-zA-Z][a-zA-Z0-9]{1,9});/g, (m, name) => NAMED_ENTITIES[name] ?? m);
29
+ }
30
+ //# sourceMappingURL=entities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entities.js","sourceRoot":"","sources":["../src/entities.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,EAAE;AACF,8EAA8E;AAC9E,iFAAiF;AACjF,iFAAiF;AACjF,2EAA2E;AAC3E,qEAAqE;AAErE,MAAM,CAAC,MAAM,cAAc,GAA2B;IACpD,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG;IAC3D,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG;IAC9F,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG;IACtE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG;IACrD,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG;IAC3E,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG;IACxF,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG;IAC3D,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG;IAC3E,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG;IACtF,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG;IACnF,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG;IAChD,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG;IACpF,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG;IAC/E,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG;CACnE,CAAC;AAEF,MAAM,UAAU,cAAc,CAAC,CAAS;IACtC,OAAO,CAAC;SACL,OAAO,CAAC,yBAAyB,EAAE,CAAC,EAAE,EAAE,GAAW,EAAE,EAAE,CACtD,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CACxC;SACA,OAAO,CAAC,eAAe,EAAE,CAAC,EAAE,EAAE,GAAW,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;SAChF,OAAO,CAAC,+BAA+B,EAAE,CAAC,CAAC,EAAE,IAAY,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAC9F,CAAC"}
package/dist/http.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ /** A stable ETag from whatever the corpus revision is (count + newest stamp). */
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;
5
+ /**
6
+ * Headers for a plain-text export. `maxAge` is the edge window — a curated
7
+ * corpus can sit on hours, a projection of live data should pass a short one.
8
+ */
9
+ export declare function textHeaders(etag: string, lastModified: string, maxAge?: number): Record<string, string>;
10
+ /**
11
+ * Headers for a markdown twin. Separate from `textHeaders` because the twin is
12
+ * addressed per page and carries its own `Last-Modified`, and because a client
13
+ * that asked for `.md` should not be handed `text/plain`.
14
+ */
15
+ export declare function markdownHeaders(lastModified: string, maxAge?: number): Record<string, string>;
16
+ /** Strip URLs and collapse whitespace so an excerpt stays one readable line. */
17
+ export declare function cleanSnippet(text: string, max?: number): string;
18
+ /** One markdown bullet: linked title, excerpt, and the date an agent diffs on. */
19
+ export declare function pageLine(opts: {
20
+ title: string;
21
+ url: string;
22
+ excerpt?: string;
23
+ updated?: Date | string | null;
24
+ }): string;
25
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +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;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,SAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAM3F;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 ADDED
@@ -0,0 +1,72 @@
1
+ // http.ts — conditional-GET plumbing for text corpus endpoints.
2
+ //
3
+ // The three llms depths (llms.txt / llms-index.txt / llms-full.txt) are the
4
+ // most-recrawled URLs a wiki serves and the most expensive to render. With a
5
+ // corpus-derived ETag a recrawl costs a 304 instead of a full corpus build.
6
+ // Without one, every AI crawler pays full price on every pass, forever.
7
+ /** A stable ETag from whatever the corpus revision is (count + newest stamp). */
8
+ export function corpusEtag(parts) {
9
+ const seed = parts
10
+ .map(p => (p instanceof Date ? p.toISOString() : String(p ?? '')))
11
+ .join('|');
12
+ // FNV-1a: short, stable across processes, and no dependency. Collisions do
13
+ // not matter here — the seed already carries the count and the newest stamp.
14
+ let hash = 0x811c9dc5;
15
+ for (let i = 0; i < seed.length; i++) {
16
+ hash ^= seed.charCodeAt(i);
17
+ hash = Math.imul(hash, 0x01000193) >>> 0;
18
+ }
19
+ return `W/"${hash.toString(36)}-${seed.length.toString(36)}"`;
20
+ }
21
+ /** 304 when the client already holds this revision, else `null` to render. */
22
+ export function notModified(request, etag, lastModified) {
23
+ const inm = request.headers.get('if-none-match');
24
+ const ims = request.headers.get('if-modified-since');
25
+ const fresh = inm ? inm === etag : Boolean(ims && ims === lastModified);
26
+ if (!fresh)
27
+ return null;
28
+ return new Response(null, { status: 304, headers: { ETag: etag, 'Last-Modified': lastModified } });
29
+ }
30
+ /**
31
+ * Headers for a plain-text export. `maxAge` is the edge window — a curated
32
+ * corpus can sit on hours, a projection of live data should pass a short one.
33
+ */
34
+ export function textHeaders(etag, lastModified, maxAge = 3600) {
35
+ return {
36
+ 'Content-Type': 'text/plain; charset=utf-8',
37
+ 'Cache-Control': `public, s-maxage=${maxAge}, stale-while-revalidate=${maxAge * 24}`,
38
+ ETag: etag,
39
+ 'Last-Modified': lastModified,
40
+ };
41
+ }
42
+ /**
43
+ * Headers for a markdown twin. Separate from `textHeaders` because the twin is
44
+ * addressed per page and carries its own `Last-Modified`, and because a client
45
+ * that asked for `.md` should not be handed `text/plain`.
46
+ */
47
+ export function markdownHeaders(lastModified, maxAge = 3600) {
48
+ return {
49
+ 'Content-Type': 'text/markdown; charset=utf-8',
50
+ 'Cache-Control': `public, s-maxage=${maxAge}, stale-while-revalidate=${maxAge * 24}`,
51
+ 'Last-Modified': lastModified,
52
+ };
53
+ }
54
+ /** Strip URLs and collapse whitespace so an excerpt stays one readable line. */
55
+ export function cleanSnippet(text, max = 160) {
56
+ return text
57
+ .replace(/\(https?:\/\/[^)]*\)/g, '')
58
+ .replace(/https?:\/\/\S+/g, '')
59
+ .replace(/\(\s*\)/g, '')
60
+ .replace(/\s{2,}/g, ' ')
61
+ .trim()
62
+ .slice(0, max);
63
+ }
64
+ /** One markdown bullet: linked title, excerpt, and the date an agent diffs on. */
65
+ export function pageLine(opts) {
66
+ const excerpt = opts.excerpt ? `: ${cleanSnippet(opts.excerpt)}` : '';
67
+ const stamp = opts.updated
68
+ ? (typeof opts.updated === 'string' ? opts.updated : opts.updated.toISOString()).split('T')[0]
69
+ : '';
70
+ return `- [${opts.title}](${opts.url})${excerpt}${stamp ? ` _(updated ${stamp})_` : ''}`;
71
+ }
72
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,EAAE;AACF,4EAA4E;AAC5E,6EAA6E;AAC7E,4EAA4E;AAC5E,wEAAwE;AAExE,iFAAiF;AACjF,MAAM,UAAU,UAAU,CAAC,KAAuD;IAChF,MAAM,IAAI,GAAG,KAAK;SACf,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;SACjE,IAAI,CAAC,GAAG,CAAC,CAAC;IACb,2EAA2E;IAC3E,6EAA6E;IAC7E,IAAI,IAAI,GAAG,UAAU,CAAC;IACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAC3B,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,MAAM,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,CAAC;AAChE,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,WAAW,CAAC,OAAgB,EAAE,IAAY,EAAE,YAAoB;IAC9E,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IACjD,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IACrD,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,IAAI,GAAG,KAAK,YAAY,CAAC,CAAC;IACxE,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,eAAe,EAAE,YAAY,EAAE,EAAE,CAAC,CAAC;AACrG,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CACzB,IAAY,EACZ,YAAoB,EACpB,MAAM,GAAG,IAAI;IAEb,OAAO;QACL,cAAc,EAAE,2BAA2B;QAC3C,eAAe,EAAE,oBAAoB,MAAM,4BAA4B,MAAM,GAAG,EAAE,EAAE;QACpF,IAAI,EAAE,IAAI;QACV,eAAe,EAAE,YAAY;KAC9B,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,YAAoB,EAAE,MAAM,GAAG,IAAI;IACjE,OAAO;QACL,cAAc,EAAE,8BAA8B;QAC9C,eAAe,EAAE,oBAAoB,MAAM,4BAA4B,MAAM,GAAG,EAAE,EAAE;QACpF,eAAe,EAAE,YAAY;KAC9B,CAAC;AACJ,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,GAAG,GAAG,GAAG;IAClD,OAAO,IAAI;SACR,OAAO,CAAC,uBAAuB,EAAE,EAAE,CAAC;SACpC,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC;SAC9B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC;SACvB,IAAI,EAAE;SACN,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACnB,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,QAAQ,CAAC,IAKxB;IACC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACtE,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO;QACxB,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAC9F,CAAC,CAAC,EAAE,CAAC;IACP,OAAO,MAAM,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,cAAc,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;AAC3F,CAAC"}
@@ -0,0 +1,8 @@
1
+ export * from './taxonomy.js';
2
+ export * from './mcp.js';
3
+ export * from './markdown.js';
4
+ export * from './entities.js';
5
+ export * from './http.js';
6
+ export * from './pagination.js';
7
+ export * from './versioning.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ // wiki-formant — the portable half of a wiki.
2
+ //
3
+ // Nothing here touches a database, a framework or a design system. Those are
4
+ // the parts every wiki must own; these are the parts every wiki rebuilds and
5
+ // then lets drift.
6
+ export * from './taxonomy.js';
7
+ export * from './mcp.js';
8
+ export * from './markdown.js';
9
+ export * from './entities.js';
10
+ export * from './http.js';
11
+ export * from './pagination.js';
12
+ export * from './versioning.js';
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAC9C,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,mBAAmB;AAEnB,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC"}
@@ -0,0 +1,25 @@
1
+ /** Inline-level HTML → markdown. Applied inside cells, list items, headings. */
2
+ export declare function inlineToMarkdown(html: string): string;
3
+ /** One `<table>` → a GFM table. Falls back to nothing when there are no rows. */
4
+ export declare function tableToMarkdown(html: string): string;
5
+ /** Block-level HTML → markdown. */
6
+ export declare function htmlToMarkdown(html: string): string;
7
+ export interface FrontmatterFields {
8
+ title: string;
9
+ url: string;
10
+ updated?: Date | string | null;
11
+ /** When the page's facts were last checked against sources — the freshness
12
+ * signal an agent actually needs, and the one most twins omit. */
13
+ lastVerified?: Date | string | null;
14
+ license?: {
15
+ spdx: string;
16
+ url: string;
17
+ } | null;
18
+ /** Any additional scalar rows, emitted in insertion order. */
19
+ extra?: Record<string, string | number | undefined>;
20
+ }
21
+ /** YAML frontmatter block, `---` fences included. */
22
+ export declare function frontmatter(fields: FrontmatterFields): string;
23
+ /** A complete markdown document: frontmatter, an H1, and a body you supply. */
24
+ export declare function markdownDocument(fields: FrontmatterFields, body: string): string;
25
+ //# sourceMappingURL=markdown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"AAcA,gFAAgF;AAChF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAmBrD;AAED,iFAAiF;AACjF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoBpD;AAED,mCAAmC;AACnC,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAwDnD;AAKD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAC;IAC/B;uEACmE;IACnE,YAAY,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAC;IACpC,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC/C,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC;CACrD;AAED,qDAAqD;AACrD,wBAAgB,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAgB7D;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,iBAAiB,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhF"}
@@ -0,0 +1,107 @@
1
+ // markdown.ts — HTML → markdown for the `.md` twin of a content page.
2
+ //
3
+ // Distinct from a plain text extractor, which flattens everything to prose.
4
+ // This preserves the structure an agent navigates and cites by — headings,
5
+ // lists, tables, code, emphasis — so a fetched page can be quoted precisely
6
+ // instead of re-summarised.
7
+ //
8
+ // Block trees are NOT handled here: every project owns its own block type set,
9
+ // so `blocksToMarkdown` belongs in the app. Give this module HTML and it gives
10
+ // you markdown; give `frontmatter` the fields and it gives you the document
11
+ // head. That is the whole portable half.
12
+ import { decodeEntities } from './entities.js';
13
+ /** Inline-level HTML → markdown. Applied inside cells, list items, headings. */
14
+ export function inlineToMarkdown(html) {
15
+ return decodeEntities(html
16
+ .replace(/<br\s*\/?>/gi, ' ')
17
+ .replace(/<(?:strong|b)\b[^>]*>(.*?)<\/(?:strong|b)>/gi, '**$1**')
18
+ .replace(/<(?:em|i)\b[^>]*>(.*?)<\/(?:em|i)>/gi, '_$1_')
19
+ .replace(/<code\b[^>]*>(.*?)<\/code>/gi, '`$1`')
20
+ .replace(/<a\b[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)')
21
+ .replace(/<img\b[^>]*alt="([^"]*)"[^>]*src="([^"]*)"[^>]*>/gi, '![$1]($2)')
22
+ .replace(/<img\b[^>]*src="([^"]*)"[^>]*>/gi, '![]($1)')
23
+ .replace(/<[^>]+>/g, ''))
24
+ // HTML collapses whitespace in inline context, so the converter has to as
25
+ // well — including newlines. Editor-produced HTML arrives on one line and
26
+ // never exposes this, but hand-authored HTML is indented, and without the
27
+ // newline in this class every wrapped source line reaches the reader with a
28
+ // leading space and markdown renderers start seeing stray indentation.
29
+ .replace(/\s+/g, ' ')
30
+ .trim();
31
+ }
32
+ /** One `<table>` → a GFM table. Falls back to nothing when there are no rows. */
33
+ export function tableToMarkdown(html) {
34
+ const rows = [...html.matchAll(/<tr\b[^>]*>([\s\S]*?)<\/tr>/gi)].map(m => [...m[1].matchAll(/<(t[hd])\b[^>]*>([\s\S]*?)<\/\1>/gi)].map(c => inlineToMarkdown(c[2]).replace(/\|/g, '\\|')));
35
+ if (!rows.length)
36
+ return '';
37
+ const width = Math.max(...rows.map(r => r.length));
38
+ const pad = (r) => [...r, ...Array(width - r.length).fill('')];
39
+ // A table whose first row is all <th> has a header; otherwise synthesise an
40
+ // empty one, since GFM has no headerless table form.
41
+ const headed = /<th\b/i.test(html.slice(0, html.indexOf('</tr>') + 1));
42
+ const [head, ...body] = headed
43
+ ? [pad(rows[0]), ...rows.slice(1).map(pad)]
44
+ : [Array(width).fill(''), ...rows.map(pad)];
45
+ return [
46
+ `| ${head.join(' | ')} |`,
47
+ `| ${Array(width).fill('---').join(' | ')} |`,
48
+ ...body.map(r => `| ${r.join(' | ')} |`),
49
+ ].join('\n');
50
+ }
51
+ /** Block-level HTML → markdown. */
52
+ export function htmlToMarkdown(html) {
53
+ let out = html;
54
+ // Tables first — their inner markup must not be eaten by the generic rules.
55
+ out = out.replace(/<table\b[^>]*>[\s\S]*?<\/table>/gi, m => `\n\n${tableToMarkdown(m)}\n\n`);
56
+ // Lists. `<ol>` numbering is computed per-list, which is why this cannot be a
57
+ // single regex with a `$1` backreference — inside a replace callback `$1` is
58
+ // a literal, not a substitution. (That exact bug shipped once and rendered
59
+ // every ordered list as a column of `1. $1`.)
60
+ out = out.replace(/<ul\b[^>]*>([\s\S]*?)<\/ul>/gi, (_m, body) => {
61
+ const items = [...body.matchAll(/<li\b[^>]*>([\s\S]*?)<\/li>/gi)].map(i => `- ${inlineToMarkdown(i[1])}`);
62
+ return `\n\n${items.join('\n')}\n\n`;
63
+ });
64
+ out = out.replace(/<ol\b[^>]*>([\s\S]*?)<\/ol>/gi, (_m, body) => {
65
+ const items = [...body.matchAll(/<li\b[^>]*>([\s\S]*?)<\/li>/gi)].map((i, n) => `${n + 1}. ${inlineToMarkdown(i[1])}`);
66
+ return `\n\n${items.join('\n')}\n\n`;
67
+ });
68
+ out = out
69
+ .replace(/<pre\b[^>]*>\s*<code\b[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/gi, (_m, code) => `\n\n\`\`\`\n${decodeEntities(code).trim()}\n\`\`\`\n\n`)
70
+ .replace(/<pre\b[^>]*>([\s\S]*?)<\/pre>/gi, (_m, code) => `\n\n\`\`\`\n${decodeEntities(code).trim()}\n\`\`\`\n\n`)
71
+ .replace(/<blockquote\b[^>]*>([\s\S]*?)<\/blockquote>/gi, (_m, body) => `\n\n${inlineToMarkdown(body)
72
+ .split('\n')
73
+ .map(l => `> ${l}`)
74
+ .join('\n')}\n\n`)
75
+ .replace(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1>/gi, (_m, level, body) => `\n\n${'#'.repeat(Number(level))} ${inlineToMarkdown(body)}\n\n`)
76
+ .replace(/<hr\s*\/?>/gi, '\n\n---\n\n')
77
+ .replace(/<p\b[^>]*>([\s\S]*?)<\/p>/gi, (_m, body) => `\n\n${inlineToMarkdown(body)}\n\n`)
78
+ .replace(/<div\b[^>]*>([\s\S]*?)<\/div>/gi, (_m, body) => `\n\n${inlineToMarkdown(body)}\n\n`);
79
+ return decodeEntities(out.replace(/<[^>]+>/g, ''))
80
+ .replace(/[ \t]+$/gm, '')
81
+ .replace(/\n{3,}/g, '\n\n')
82
+ .trim();
83
+ }
84
+ const isoDate = (d) => (typeof d === 'string' ? d : d.toISOString()).split('T')[0];
85
+ /** YAML frontmatter block, `---` fences included. */
86
+ export function frontmatter(fields) {
87
+ const esc = (s) => s.replace(/"/g, '\\"');
88
+ return [
89
+ '---',
90
+ `title: "${esc(fields.title)}"`,
91
+ `url: "${fields.url}"`,
92
+ ...(fields.updated ? [`updated: ${isoDate(fields.updated)}`] : []),
93
+ ...(fields.lastVerified ? [`last_verified: ${isoDate(fields.lastVerified)}`] : []),
94
+ ...(fields.license
95
+ ? [`license: ${fields.license.spdx}`, `license_url: "${fields.license.url}"`]
96
+ : []),
97
+ ...Object.entries(fields.extra ?? {})
98
+ .filter(([, v]) => v !== undefined && v !== '')
99
+ .map(([k, v]) => (typeof v === 'number' ? `${k}: ${v}` : `${k}: "${esc(String(v))}"`)),
100
+ '---',
101
+ ].join('\n');
102
+ }
103
+ /** A complete markdown document: frontmatter, an H1, and a body you supply. */
104
+ export function markdownDocument(fields, body) {
105
+ return `${frontmatter(fields)}\n\n# ${fields.title}\n\n${body.trim()}\n`;
106
+ }
107
+ //# sourceMappingURL=markdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"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"}
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,123 @@
1
+ export declare const MCP_PROTOCOL_VERSION = "2025-03-26";
2
+ export type ToolParam = {
3
+ type: 'string' | 'number' | 'boolean' | 'array';
4
+ description: string;
5
+ enum?: string[];
6
+ items?: {
7
+ type: 'string' | 'number';
8
+ };
9
+ };
10
+ export type ToolSchema = {
11
+ type: 'object';
12
+ properties: Record<string, ToolParam>;
13
+ required?: string[];
14
+ };
15
+ /**
16
+ * Behavioural hints a client uses to decide what needs a human in the loop.
17
+ * Without them every tool looks alike, so a client that auto-approves read-only
18
+ * calls has no way to tell a lookup from one that reaches a payment processor.
19
+ */
20
+ export type ToolAnnotations = {
21
+ /** Human-facing label for the tool. */
22
+ title?: string;
23
+ /** Does not modify anything. */
24
+ readOnlyHint?: boolean;
25
+ /** May destroy or overwrite state (only meaningful when not read-only). */
26
+ destructiveHint?: boolean;
27
+ /** Repeating the identical call has no additional effect. */
28
+ idempotentHint?: boolean;
29
+ /** Touches systems beyond this server. */
30
+ openWorldHint?: boolean;
31
+ };
32
+ export interface McpTool {
33
+ name: string;
34
+ description: string;
35
+ inputSchema: ToolSchema;
36
+ annotations?: ToolAnnotations;
37
+ /** Surfaced as an A2A skill on the agent card. Not sent over MCP. */
38
+ skill?: {
39
+ id: string;
40
+ tags: string[];
41
+ examples?: string[];
42
+ };
43
+ handler: (args: Record<string, unknown>) => Promise<unknown>;
44
+ }
45
+ export interface McpResource {
46
+ uri: string;
47
+ name: string;
48
+ description: string;
49
+ mimeType: string;
50
+ read: () => Promise<string | null>;
51
+ }
52
+ export interface McpPrompt {
53
+ name: string;
54
+ description: string;
55
+ /**
56
+ * The user message the client inserts when someone picks this prompt. Name
57
+ * the tool sequence outright rather than restating the question: the model
58
+ * reading it has the tool list but no reason to prefer one call order over
59
+ * another, which is the same gap `instructions` exists to close.
60
+ */
61
+ text: string;
62
+ }
63
+ export interface McpServerConfig {
64
+ serverInfo: {
65
+ name: string;
66
+ version: string;
67
+ };
68
+ /**
69
+ * Server-level usage guide returned by `initialize`. Say which tool to reach
70
+ * for first and what NOT to do; it is the only text an agent sees before the
71
+ * tool list.
72
+ */
73
+ instructions: string;
74
+ tools: McpTool[];
75
+ resources?: McpResource[];
76
+ /**
77
+ * The entry point a client offers after install. Tools are what an agent
78
+ * reaches for once it already has a question; prompts are what a person
79
+ * clicks when they do not have one yet.
80
+ */
81
+ prompts?: McpPrompt[];
82
+ /** Appended to the GET refusal so a browser that lands here learns where to go. */
83
+ docsUrl?: string;
84
+ /**
85
+ * Cap on JSON-RPC batch size. The rate limiter charges one token per HTTP
86
+ * request, before the body is parsed — an unbounded batch would let a single
87
+ * token fan out into thousands of concurrent handler executions, each its own
88
+ * query. Keep this aligned with whatever ceiling the batching tools advertise.
89
+ */
90
+ maxBatch?: number;
91
+ /** Per-request analytics hook. Runs before dispatch; never blocks the response. */
92
+ onCall?: (request: Request, body: unknown) => void;
93
+ }
94
+ /**
95
+ * A caller-fixable failure inside a handler (page not found, empty input).
96
+ * Reported as a tool result with `isError` so the model sees the text and can
97
+ * correct itself, per the spec's split between protocol and execution errors.
98
+ */
99
+ export declare class McpToolError extends Error {
100
+ readonly details?: Record<string, unknown> | undefined;
101
+ constructor(message: string, details?: Record<string, unknown> | undefined);
102
+ }
103
+ type RpcRequest = {
104
+ jsonrpc: '2.0';
105
+ id: string | number | null;
106
+ method: string;
107
+ params?: unknown;
108
+ };
109
+ /** 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>;
111
+ export declare const MCP_CORS: Record<string, string>;
112
+ export declare function withMcpCors<T extends Response>(res: T): T;
113
+ export declare function mcpOptions(): Response;
114
+ /**
115
+ * Streamable HTTP lets a server decline the SSE leg by answering GET with 405.
116
+ * A framework's automatic 405 carries no CORS headers, so a browser client
117
+ * could not even read the refusal — answer it here, and say what to do instead.
118
+ */
119
+ export declare function mcpGet(docsUrl?: string): Response;
120
+ /** The whole POST leg: parse, track, dispatch, and answer with the right status. */
121
+ export declare function mcpResponse(request: Request, config: McpServerConfig): Promise<Response>;
122
+ export {};
123
+ //# sourceMappingURL=mcp.d.ts.map