wiki-formant 0.12.0 → 0.15.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 +32 -26
- package/bin/check-classes.mjs +2 -3
- package/dist/analytics.d.ts +0 -1
- package/dist/analytics.js +0 -1
- package/dist/block-views.d.ts +97 -0
- package/dist/block-views.js +119 -0
- package/dist/blocks.d.ts +0 -1
- package/dist/blocks.js +1 -2
- package/dist/combobox.d.ts +0 -1
- package/dist/combobox.js +0 -1
- package/dist/conformance.d.ts +10 -3
- package/dist/conformance.js +73 -36
- package/dist/crawlers.d.ts +0 -1
- package/dist/crawlers.js +0 -1
- package/dist/dom.d.ts +0 -1
- package/dist/dom.js +0 -1
- package/dist/editor.d.ts +101 -0
- package/dist/editor.js +218 -0
- package/dist/feed.d.ts +0 -1
- package/dist/feed.js +0 -1
- package/dist/freshness.d.ts +0 -1
- package/dist/freshness.js +2 -2
- package/dist/headings.d.ts +0 -1
- package/dist/headings.js +1 -3
- package/dist/html.d.ts +16 -0
- package/dist/html.js +23 -0
- package/dist/http.d.ts +32 -1
- package/dist/http.js +40 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/license.d.ts +0 -1
- package/dist/license.js +0 -1
- package/dist/link-check.d.ts +92 -0
- package/dist/link-check.js +249 -0
- package/dist/links.d.ts +0 -1
- package/dist/links.js +1 -6
- package/dist/maps.d.ts +0 -1
- package/dist/maps.js +2 -2
- package/dist/markdown.d.ts +1 -1
- package/dist/markdown.js +34 -3
- package/dist/mcp.d.ts +43 -3
- package/dist/mcp.js +37 -11
- package/dist/pagination.d.ts +21 -2
- package/dist/pagination.js +32 -3
- package/dist/rate-limit.d.ts +32 -1
- package/dist/rate-limit.js +51 -1
- package/dist/react-server.d.ts +91 -9
- package/dist/react-server.js +68 -16
- package/dist/react.d.ts +37 -48
- package/dist/react.js +44 -23
- package/dist/revisions.d.ts +0 -1
- package/dist/revisions.js +0 -1
- package/dist/rola.d.ts +0 -1
- package/dist/rola.js +0 -1
- package/dist/seeded.d.ts +11 -0
- package/dist/seeded.js +32 -0
- package/dist/sidebar.d.ts +0 -1
- package/dist/sidebar.js +0 -1
- package/dist/taxonomy.d.ts +0 -1
- package/dist/taxonomy.js +0 -1
- package/dist/text.d.ts +14 -1
- package/dist/text.js +15 -2
- package/dist/tiptap.d.ts +1 -1
- package/dist/tiptap.js +1 -2
- package/dist/validation.d.ts +2 -3
- package/dist/validation.js +4 -5
- package/dist/versioning.d.ts +0 -1
- package/dist/versioning.js +0 -1
- package/dist/well-known.d.ts +19 -2
- package/dist/well-known.js +28 -2
- package/dist/x402.d.ts +2 -11
- package/dist/x402.js +4 -16
- package/package.json +52 -27
- package/dist/analytics.d.ts.map +0 -1
- package/dist/analytics.js.map +0 -1
- package/dist/blocks.d.ts.map +0 -1
- package/dist/blocks.js.map +0 -1
- package/dist/combobox.d.ts.map +0 -1
- package/dist/combobox.js.map +0 -1
- package/dist/conformance.d.ts.map +0 -1
- package/dist/conformance.js.map +0 -1
- package/dist/crawlers.d.ts.map +0 -1
- package/dist/crawlers.js.map +0 -1
- package/dist/dom.d.ts.map +0 -1
- package/dist/dom.js.map +0 -1
- package/dist/entities.d.ts +0 -3
- package/dist/entities.d.ts.map +0 -1
- package/dist/entities.js +0 -30
- package/dist/entities.js.map +0 -1
- package/dist/feed.d.ts.map +0 -1
- package/dist/feed.js.map +0 -1
- package/dist/freshness.d.ts.map +0 -1
- package/dist/freshness.js.map +0 -1
- package/dist/headings.d.ts.map +0 -1
- package/dist/headings.js.map +0 -1
- package/dist/http.d.ts.map +0 -1
- package/dist/http.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/license.d.ts.map +0 -1
- package/dist/license.js.map +0 -1
- package/dist/links.d.ts.map +0 -1
- package/dist/links.js.map +0 -1
- package/dist/maps.d.ts.map +0 -1
- package/dist/maps.js.map +0 -1
- package/dist/markdown.d.ts.map +0 -1
- package/dist/markdown.js.map +0 -1
- package/dist/mcp.d.ts.map +0 -1
- package/dist/mcp.js.map +0 -1
- package/dist/pagination.d.ts.map +0 -1
- package/dist/pagination.js.map +0 -1
- package/dist/rate-limit.d.ts.map +0 -1
- package/dist/rate-limit.js.map +0 -1
- package/dist/react-server.d.ts.map +0 -1
- package/dist/react-server.js.map +0 -1
- package/dist/react.d.ts.map +0 -1
- package/dist/react.js.map +0 -1
- package/dist/revisions.d.ts.map +0 -1
- package/dist/revisions.js.map +0 -1
- package/dist/rola.d.ts.map +0 -1
- package/dist/rola.js.map +0 -1
- package/dist/sidebar.d.ts.map +0 -1
- package/dist/sidebar.js.map +0 -1
- package/dist/taxonomy.d.ts.map +0 -1
- package/dist/taxonomy.js.map +0 -1
- package/dist/text.d.ts.map +0 -1
- package/dist/text.js.map +0 -1
- package/dist/tiptap.d.ts.map +0 -1
- package/dist/tiptap.js.map +0 -1
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js.map +0 -1
- package/dist/versioning.d.ts.map +0 -1
- package/dist/versioning.js.map +0 -1
- package/dist/well-known.d.ts.map +0 -1
- package/dist/well-known.js.map +0 -1
- package/dist/x402.d.ts.map +0 -1
- package/dist/x402.js.map +0 -1
package/README.md
CHANGED
|
@@ -123,11 +123,16 @@ What it gets right:
|
|
|
123
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. Under 2025-06-18 they are refused outright, because the revision removed them.
|
|
124
124
|
- **The protocol version is negotiated, not asserted.** `initialize` echoes what the client asked for when it is one of `2025-06-18`, `2025-03-26` or `2024-11-05`, and offers the newest otherwise; every response carries the negotiated version 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
125
|
- **`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
|
+
- **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.
|
|
126
127
|
- **`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.
|
|
127
128
|
|
|
128
129
|
### Structured output, `_meta`, and the envelope gate
|
|
129
130
|
|
|
130
|
-
|
|
131
|
+
**Every object a handler returns comes back as `structuredContent` beside the text block**, so a client reads the answer rather than scraping prose for it. This used to be gated on declaring an `outputSchema`, and the result was that across four live servers and thirty-two tools — every one of them answering in JSON — not a single response ever carried it. `outputSchema` remains the stronger contract, because a client validates against it; it is no longer the price of admission. A handler returning a string is left alone.
|
|
132
|
+
|
|
133
|
+
`inputSchema.requireOneOf` names a set of which at least one must be present. `required` cannot express "query or popular", so the one tool needing it checked in its handler and the caller learned at execution time — the single class of argument mistake this module was otherwise catching before dispatch.
|
|
134
|
+
|
|
135
|
+
A handler's second argument is its context:
|
|
131
136
|
|
|
132
137
|
```ts
|
|
133
138
|
handler: async (args, ctx) => {
|
|
@@ -139,6 +144,20 @@ handler: async (args, ctx) => {
|
|
|
139
144
|
|
|
140
145
|
`config.gate` is envelope-level middleware: it may withhold entries before dispatch and merge its own responses back afterwards. A payment gate has to sit there rather than in a handler, because the demand *replaces* the call and the receipt rides on the envelope.
|
|
141
146
|
|
|
147
|
+
## Conformance
|
|
148
|
+
|
|
149
|
+
`wiki-formant/conformance` is the half of an MCP conformance run that is not about any one server's tools: a JSON-RPC client that backs off on a 429, the transport assertions (CORS preflight, `GET`→405, a notification answering 202 with no body, `-32700`, the batch cap, honest `capabilities`, version negotiation), version coherence across every descriptor a surface publishes, A2A card parity, conditional-GET and `robots` checks, and a pass/fail table with an exit code.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const t = createTester({ base: 'https://example.com', clientName: 'my-mcp-test' });
|
|
153
|
+
await transportChecks(t, 'my-mcp-test');
|
|
154
|
+
await annotationChecks(t, { writes: ['create_page'] });
|
|
155
|
+
await payloadBudget(t, [{ name: 'search', args: { query: 'x' } }]);
|
|
156
|
+
process.exit(t.summary());
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
What stays in your repo is fixtures: which tools you expect, what a good answer from each looks like, and which text surfaces you publish. `payloadBudget` weighs every listed call **and** every read-only tool that takes no required arguments, because the one tool nobody thought to list is the one that answers with 3.3 MB; pass a per-call `maxBytes` for a bulk-export tool that is deliberately large. It also asserts that a JSON answer arrived with `structuredContent`.
|
|
160
|
+
|
|
142
161
|
## Markdown twins
|
|
143
162
|
|
|
144
163
|
Pass an `etag` and answer `notModified` before rendering: a twin is the single most recrawled URL a page has, so a twin with no validator is a full render on every pass, forever — the same arithmetic that justifies the corpus ETag, applied per page.
|
|
@@ -245,7 +264,9 @@ are unit-tested without a DOM.
|
|
|
245
264
|
|
|
246
265
|
`FacetBar` renders the rows `createTaxonomy` already produces. This is why the taxonomy exports a rows model rather than markup: the rows could always cross the boundary and, until this subpath existed, the markup could not, so all three wikis hand-rendered it and two put `aria-pressed` on an `<a>`. `Breadcrumbs` renders the trail and its `BreadcrumbList` JSON-LD together, because a trail whose structured data is written somewhere else is a trail that will one day disagree with its own markup — which is the case Google penalises. It takes a `base` origin: structured-data URLs must be absolute and a package cannot know the site.
|
|
247
266
|
|
|
248
|
-
`
|
|
267
|
+
`PageNav` is the previous/next pair at the foot of an article — the sequential read the infobox rail's lateral links do not cover. Ordering is the caller's, because it is the one part that is never portable: a wiki's sequence is its section's configured sort, a knowledge base's is a taxonomy walk. Pair it with `adjacentPages` from `wiki-formant/pagination` over a list you already hold — neither wiki needs a query for it, and the two indexed lookups the neighbours used to cost were the reason one of them dropped the control.
|
|
268
|
+
|
|
269
|
+
`RailShell` stays in `wiki-formant/react`, because it calls `useSidebar` and genuinely is a client component. It renders the rail's landmark, its scroll wrapper and the three collapse states — including the `--instant` class that keeps a remembered-closed rail from animating shut on first paint, and the close-on-tap that a mobile rail needs. Compose your own rail inside it and mark the active link with `isRailLinkActive`; all three wikis do, and the pre-composed component that used to sit here had one consumer and had already lost both of those behaviours.
|
|
249
270
|
|
|
250
271
|
All three take the router's link component as a prop:
|
|
251
272
|
|
|
@@ -400,30 +421,15 @@ they name a gap in the corpus in the reader's own words.
|
|
|
400
421
|
|
|
401
422
|
## API
|
|
402
423
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
| `parseVersion`, `formatVersion`, `incrementVersion`, `bump`, `compareVersions` | `wiki-formant/versioning` |
|
|
413
|
-
| `plausibleEvent`, `mcpCallProps`, `searchQueryProps`, `plausibleDomain` | `wiki-formant/analytics` |
|
|
414
|
-
| `comboboxAria`, `listId`, `optionId` | `wiki-formant/combobox` |
|
|
415
|
-
| `computeRevisionDiff`, `diffBlocks`, `extractBlocks`, `classifyChanges`, `changeSummary` | `wiki-formant/revisions` |
|
|
416
|
-
| `renderFeed`, `renderItem`, `escXml`, `cdata`, `clampWords`, `absolutise`, `FEED_HEADERS` | `wiki-formant/feed` |
|
|
417
|
-
| `ccBy40`, `licenseBlock`, `licenseLines`, `licenseNote` | `wiki-formant/license` |
|
|
418
|
-
| `AI_CRAWLERS`, `detectAiBot`, `aiCrawlerTokens`, `aiCrawlerRules` | `wiki-formant/crawlers` |
|
|
419
|
-
| `useCollapsibleSidebar`, `SidebarProvider`, `useSidebar`, `TableOfContents`, `useTypeahead`, `useLinkPreview`, `useClickOutside`, `useTableSort`, `useCopy`, `ErrorBoundary`, `WikiRail` | `wiki-formant/react` |
|
|
420
|
-
| `FacetBar`, `Breadcrumbs` | `wiki-formant/react-server` |
|
|
421
|
-
| `gatePaidCalls` | `wiki-formant/x402` |
|
|
422
|
-
| `resolveSidebarOpen`, `sidebarBootScript`, `SIDEBAR_ATTRIBUTE` | `wiki-formant/sidebar` |
|
|
423
|
-
| `addCopyButtons`, `activateTabGroups`, `tweetEmbedSrc`, `onTweetResize`, `hydrateTweetEmbeds`, `sizeTweetEmbeds`, `TWITTER_ORIGIN` | `wiki-formant/dom` |
|
|
424
|
-
| `Iframe`, `YouTube`, `TwitterEmbed`, `createMapEmbed`, `createCodeBlock`, `createTabs` | `wiki-formant/tiptap` |
|
|
425
|
-
|
|
426
|
-
Everything above `wiki-formant/react` is also re-exported from the package root. The React, sidebar, DOM, tiptap, react-server and x402 subpaths are not: they carry `'use client'`, reach for a browser global, or need a peer the root must not assume, and the root has to stay importable from a route handler.
|
|
424
|
+
Every module has a subpath — `wiki-formant/taxonomy`, `wiki-formant/mcp`, and so on.
|
|
425
|
+
The emitted `.d.ts` files are the reference. There is no hand-maintained symbol list
|
|
426
|
+
here, because the one that used to be here drifted from them.
|
|
427
|
+
|
|
428
|
+
The package root re-exports the runtime modules that need no peer dependency and no
|
|
429
|
+
client boundary, so `import … from 'wiki-formant'` stays importable from a route
|
|
430
|
+
handler with nothing else installed. Everything that reaches for React, a browser
|
|
431
|
+
global, tiptap or a wallet — and the tooling modules, which no route imports — is
|
|
432
|
+
subpath-only.
|
|
427
433
|
|
|
428
434
|
## A bin
|
|
429
435
|
|
package/bin/check-classes.mjs
CHANGED
|
@@ -113,7 +113,6 @@ for (const [tok, at] of [...dead].sort((a, b) => b[1].length - a[1].length)) {
|
|
|
113
113
|
console.error(` ${tok.padEnd(24)} ${String(at.length).padStart(3)}x ${at.slice(0, 4).join(', ')}${at.length > 4 ? ', …' : ''}`);
|
|
114
114
|
}
|
|
115
115
|
// Derived, not hardcoded: this runs in every sibling project and their
|
|
116
|
-
// globals.css does not sit at the same path.
|
|
117
|
-
|
|
118
|
-
console.error(`\nEither define it in ${globals} or delete the usage.`);
|
|
116
|
+
// globals.css does not sit at the same path. Found once, above.
|
|
117
|
+
console.error(`\nEither define it in ${globalsFile ?? 'your globals.css'} or delete the usage.`);
|
|
119
118
|
process.exit(WARN_ONLY ? 0 : 1);
|
package/dist/analytics.d.ts
CHANGED
|
@@ -62,4 +62,3 @@ export interface SearchQueryInput {
|
|
|
62
62
|
export declare function searchQueryProps({ query, results, surface, }: SearchQueryInput): Record<string, string> | null;
|
|
63
63
|
/** The hostname of a site URL, for the `domain` a Plausible property is filed under. */
|
|
64
64
|
export declare function plausibleDomain(siteUrl: string | undefined, fallback: string): string;
|
|
65
|
-
//# sourceMappingURL=analytics.d.ts.map
|
package/dist/analytics.js
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { CodeTab, LinkGridGroup, ReferenceItem, StatItem } from './blocks.js';
|
|
3
|
+
import type { WikiLinkComponent } from './react-server.js';
|
|
4
|
+
/**
|
|
5
|
+
* Tabbed code samples. Every tab's body stays mounted and the inactive ones are
|
|
6
|
+
* hidden rather than unmounted: the panels carry pre-highlighted markup, and
|
|
7
|
+
* remounting one would re-run whatever the consumer's highlighter attached to
|
|
8
|
+
* it on every tab press.
|
|
9
|
+
*/
|
|
10
|
+
export declare function CodeTabsView({ tabs }: {
|
|
11
|
+
tabs: readonly CodeTab[];
|
|
12
|
+
}): import("react").JSX.Element | null;
|
|
13
|
+
export interface ColumnsViewProps<B extends {
|
|
14
|
+
id: string;
|
|
15
|
+
}> {
|
|
16
|
+
columns: ReadonlyArray<{
|
|
17
|
+
id: string;
|
|
18
|
+
blocks?: readonly B[];
|
|
19
|
+
}>;
|
|
20
|
+
gap?: 'sm' | 'md' | 'lg';
|
|
21
|
+
align?: 'start' | 'center' | 'end' | 'stretch';
|
|
22
|
+
/** The caller's own dispatch, because the block union is the caller's. */
|
|
23
|
+
render: (block: B) => ReactNode;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A row of columns, each holding blocks the caller renders.
|
|
27
|
+
*
|
|
28
|
+
* Children are wrapped in a `Fragment`, not a `<div>`. One repo used a div and
|
|
29
|
+
* it was a layout bug rather than a style: `.column-view` is the flow context
|
|
30
|
+
* its stylesheet spaces children in, and an extra element between them means
|
|
31
|
+
* every gap rule inside a column silently matches nothing.
|
|
32
|
+
*/
|
|
33
|
+
export declare function ColumnsView<B extends {
|
|
34
|
+
id: string;
|
|
35
|
+
}>({ columns, gap, align, render, }: ColumnsViewProps<B>): import("react").JSX.Element;
|
|
36
|
+
/**
|
|
37
|
+
* Banded groups of link pills.
|
|
38
|
+
*
|
|
39
|
+
* A link whose href fails `safeLinkHref` is DROPPED, not rendered inert. These
|
|
40
|
+
* hrefs are author-supplied, and a `javascript:` URL fails the http test above
|
|
41
|
+
* — so without the guard it falls through to the router's link component, which
|
|
42
|
+
* renders it looking like an ordinary pill and executes it on click.
|
|
43
|
+
*/
|
|
44
|
+
export declare function LinkGridView({ groups, intro, link: Link, }: {
|
|
45
|
+
groups?: readonly (LinkGridGroup & {
|
|
46
|
+
id?: string;
|
|
47
|
+
})[];
|
|
48
|
+
intro?: string;
|
|
49
|
+
link?: WikiLinkComponent;
|
|
50
|
+
}): import("react").JSX.Element;
|
|
51
|
+
/**
|
|
52
|
+
* The numbered citation list, with the `#cite-n` back-links the inline markers
|
|
53
|
+
* point at. `processHtml` is a prop because only one wiki runs reference text
|
|
54
|
+
* through its HTML pipeline, and running it in the other would rewrite anchors
|
|
55
|
+
* that were minted under different rules.
|
|
56
|
+
*/
|
|
57
|
+
export declare function ReferencesView({ items, title, processHtml, }: {
|
|
58
|
+
items?: readonly (ReferenceItem & {
|
|
59
|
+
id?: string;
|
|
60
|
+
})[];
|
|
61
|
+
title?: string;
|
|
62
|
+
processHtml?: (html: string) => string;
|
|
63
|
+
}): import("react").JSX.Element | null;
|
|
64
|
+
/**
|
|
65
|
+
* A row of measured figures.
|
|
66
|
+
*
|
|
67
|
+
* The column count is a class (`stat-grid-4`), never an inline
|
|
68
|
+
* `gridTemplateColumns`. One repo did it inline, which put a layout decision
|
|
69
|
+
* outside the stylesheet that owns every other one — invisible to the dead-class
|
|
70
|
+
* check, unreachable from a media query, and so a four-up row that could not
|
|
71
|
+
* become a two-up row on a phone.
|
|
72
|
+
*/
|
|
73
|
+
export declare function StatsView({ items, columns, }: {
|
|
74
|
+
items?: readonly (StatItem & {
|
|
75
|
+
id?: string;
|
|
76
|
+
})[];
|
|
77
|
+
columns?: number;
|
|
78
|
+
}): import("react").JSX.Element | null;
|
|
79
|
+
/**
|
|
80
|
+
* A maintenance notice.
|
|
81
|
+
*
|
|
82
|
+
* The label and the fallback message arrive as `meta` because they are this
|
|
83
|
+
* wiki's editorial voice — "You can help RADIX Wiki by expanding it" has a name
|
|
84
|
+
* in it. The markup, the `role="note"` and the variant class are shared, and
|
|
85
|
+
* were identical in both repos. `icon` is optional: one wiki sets one, and a
|
|
86
|
+
* component library that hardcoded an icon set would make its consumers install
|
|
87
|
+
* that icon set.
|
|
88
|
+
*/
|
|
89
|
+
export declare function BannerView({ variant, text, meta, icon, }: {
|
|
90
|
+
variant: string;
|
|
91
|
+
text?: string | null;
|
|
92
|
+
meta: {
|
|
93
|
+
label: string;
|
|
94
|
+
message: string;
|
|
95
|
+
};
|
|
96
|
+
icon?: ReactNode;
|
|
97
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
// block-views.tsx — the block renderers both wikis had written twice.
|
|
4
|
+
//
|
|
5
|
+
// Behind its own subpath for the reason `react.tsx` is: React is an OPTIONAL
|
|
6
|
+
// peer, so a consumer that only wants the taxonomy or the MCP transport still
|
|
7
|
+
// installs a package with no runtime dependencies.
|
|
8
|
+
//
|
|
9
|
+
// WHY THESE FIVE AND NOT THE SWITCH. The block union is per-repo and staying
|
|
10
|
+
// that way (see the Block Model note in the workspace CLAUDE.md) — a closed
|
|
11
|
+
// union is what makes `switch (block.type)` exhaustive, so a new block type is
|
|
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: those were
|
|
14
|
+
// byte-identical in both repos, down to the class names. So the dispatch stays
|
|
15
|
+
// with the caller and the leaves move here.
|
|
16
|
+
//
|
|
17
|
+
// The class names are NOT props. Like the rail's `wiki-rail__*` tree they are
|
|
18
|
+
// the shared convention both stylesheets already implement, and making them
|
|
19
|
+
// configurable would only let that convention fork again. Everything that
|
|
20
|
+
// genuinely differs — the prose of a banner, whether references run through an
|
|
21
|
+
// HTML processor, the router's link — arrives as a prop.
|
|
22
|
+
import { Fragment, useState } from 'react';
|
|
23
|
+
import { Anchor } from './react-server.js';
|
|
24
|
+
import { safeLinkHref } from './validation.js';
|
|
25
|
+
import { cx } from './html.js';
|
|
26
|
+
// ---- codeTabs ---------------------------------------------------------------
|
|
27
|
+
/**
|
|
28
|
+
* Tabbed code samples. Every tab's body stays mounted and the inactive ones are
|
|
29
|
+
* hidden rather than unmounted: the panels carry pre-highlighted markup, and
|
|
30
|
+
* remounting one would re-run whatever the consumer's highlighter attached to
|
|
31
|
+
* it on every tab press.
|
|
32
|
+
*/
|
|
33
|
+
export function CodeTabsView({ tabs }) {
|
|
34
|
+
const [activeTab, setActiveTab] = useState(0);
|
|
35
|
+
if (!tabs.length)
|
|
36
|
+
return null;
|
|
37
|
+
return (_jsxs("div", { className: "code-tabs", children: [_jsx("div", { className: "code-tabs-list", children: tabs.map((tab, i) => (_jsx("button", { type: "button", className: cx('code-tabs-btn', i === activeTab && 'code-tabs-btn-active'), onClick: () => setActiveTab(i), children: tab.label }, i))) }), tabs.map((tab, i) => (_jsx("div", { className: i === activeTab ? 'block' : 'hidden', dangerouslySetInnerHTML: { __html: tab.code } }, i)))] }));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A row of columns, each holding blocks the caller renders.
|
|
41
|
+
*
|
|
42
|
+
* Children are wrapped in a `Fragment`, not a `<div>`. One repo used a div and
|
|
43
|
+
* it was a layout bug rather than a style: `.column-view` is the flow context
|
|
44
|
+
* its stylesheet spaces children in, and an extra element between them means
|
|
45
|
+
* every gap rule inside a column silently matches nothing.
|
|
46
|
+
*/
|
|
47
|
+
export function ColumnsView({ columns, gap = 'md', align = 'start', render, }) {
|
|
48
|
+
const gapClass = { sm: 'gap-2', md: 'gap-4', lg: 'gap-6' }[gap];
|
|
49
|
+
const alignClass = {
|
|
50
|
+
start: 'items-start',
|
|
51
|
+
center: 'items-center',
|
|
52
|
+
end: 'items-end',
|
|
53
|
+
stretch: 'items-stretch',
|
|
54
|
+
}[align];
|
|
55
|
+
return (_jsx("div", { className: cx('columns-layout', gapClass, alignClass), children: columns.map(col => (_jsx("div", { className: "column-view", children: (col.blocks ?? []).map(bl => (_jsx(Fragment, { children: render(bl) }, bl.id))) }, col.id))) }));
|
|
56
|
+
}
|
|
57
|
+
// ---- linkGrid ---------------------------------------------------------------
|
|
58
|
+
/**
|
|
59
|
+
* Banded groups of link pills.
|
|
60
|
+
*
|
|
61
|
+
* A link whose href fails `safeLinkHref` is DROPPED, not rendered inert. These
|
|
62
|
+
* hrefs are author-supplied, and a `javascript:` URL fails the http test above
|
|
63
|
+
* — so without the guard it falls through to the router's link component, which
|
|
64
|
+
* renders it looking like an ordinary pill and executes it on click.
|
|
65
|
+
*/
|
|
66
|
+
export function LinkGridView({ groups, intro, link: Link = Anchor, }) {
|
|
67
|
+
return (_jsxs("div", { className: "link-grid", children: [intro && _jsx("p", { children: intro }), (groups ?? []).map((group, gi) => (_jsxs("section", { className: "link-grid-group", children: [_jsx("h3", { children: group.heading }), group.description && (_jsx("div", { className: "link-grid-group-description", dangerouslySetInnerHTML: { __html: group.description } })), _jsx("div", { className: "link-grid-pills", children: (group.links ?? []).map((item, i) => {
|
|
68
|
+
const href = safeLinkHref(item.href);
|
|
69
|
+
if (!href)
|
|
70
|
+
return null;
|
|
71
|
+
return /^https?:\/\//.test(href) ? (_jsx("a", { href: href, target: "_blank", rel: "noopener", children: item.label }, i)) : (_jsx(Link, { href: href, children: item.label }, i));
|
|
72
|
+
}) })] }, group.id ?? gi)))] }));
|
|
73
|
+
}
|
|
74
|
+
// ---- references -------------------------------------------------------------
|
|
75
|
+
/**
|
|
76
|
+
* The numbered citation list, with the `#cite-n` back-links the inline markers
|
|
77
|
+
* point at. `processHtml` is a prop because only one wiki runs reference text
|
|
78
|
+
* through its HTML pipeline, and running it in the other would rewrite anchors
|
|
79
|
+
* that were minted under different rules.
|
|
80
|
+
*/
|
|
81
|
+
export function ReferencesView({ items, title = 'References', processHtml = (html) => html, }) {
|
|
82
|
+
const rows = items ?? [];
|
|
83
|
+
if (!rows.length)
|
|
84
|
+
return null;
|
|
85
|
+
return (_jsxs("section", { className: "references-block", "aria-labelledby": "references-heading", children: [_jsx("h2", { id: "references-heading", children: title }), _jsx("ol", { className: "references-list", children: rows.map((item, i) => {
|
|
86
|
+
const url = safeLinkHref(item.url);
|
|
87
|
+
return (_jsxs("li", { id: `ref-${i + 1}`, className: "reference-item", children: [_jsx("a", { href: `#cite-${i + 1}`, className: "ref-backlink", "aria-label": "Back to citation", children: "\u2191" }), ' ', _jsx("span", { dangerouslySetInnerHTML: { __html: processHtml(item.text) } }), url && (_jsxs(_Fragment, { children: [' ', _jsx("a", { href: url, target: "_blank", rel: "noopener", className: "reference-link", "aria-label": "Open source", children: "\u2197" })] }))] }, item.id ?? i));
|
|
88
|
+
}) })] }));
|
|
89
|
+
}
|
|
90
|
+
// ---- stats ------------------------------------------------------------------
|
|
91
|
+
/**
|
|
92
|
+
* A row of measured figures.
|
|
93
|
+
*
|
|
94
|
+
* The column count is a class (`stat-grid-4`), never an inline
|
|
95
|
+
* `gridTemplateColumns`. One repo did it inline, which put a layout decision
|
|
96
|
+
* outside the stylesheet that owns every other one — invisible to the dead-class
|
|
97
|
+
* check, unreachable from a media query, and so a four-up row that could not
|
|
98
|
+
* become a two-up row on a phone.
|
|
99
|
+
*/
|
|
100
|
+
export function StatsView({ items, columns = 4, }) {
|
|
101
|
+
const rows = items ?? [];
|
|
102
|
+
if (!rows.length)
|
|
103
|
+
return null;
|
|
104
|
+
return (_jsx("div", { className: cx('stat-grid', `stat-grid-${columns}`), children: rows.map((item, i) => (_jsxs("div", { className: "stat-card", children: [_jsxs("span", { className: "stat-value", children: [item.value, item.suffix && _jsx("span", { className: "stat-suffix", children: item.suffix })] }), _jsx("span", { className: "stat-label", children: item.label })] }, item.id ?? i))) }));
|
|
105
|
+
}
|
|
106
|
+
// ---- banner -----------------------------------------------------------------
|
|
107
|
+
/**
|
|
108
|
+
* A maintenance notice.
|
|
109
|
+
*
|
|
110
|
+
* The label and the fallback message arrive as `meta` because they are this
|
|
111
|
+
* wiki's editorial voice — "You can help RADIX Wiki by expanding it" has a name
|
|
112
|
+
* in it. The markup, the `role="note"` and the variant class are shared, and
|
|
113
|
+
* were identical in both repos. `icon` is optional: one wiki sets one, and a
|
|
114
|
+
* component library that hardcoded an icon set would make its consumers install
|
|
115
|
+
* that icon set.
|
|
116
|
+
*/
|
|
117
|
+
export function BannerView({ variant, text, meta, icon, }) {
|
|
118
|
+
return (_jsxs("div", { className: cx('editorial-banner', `editorial-banner-${variant}`), role: "note", children: [icon, _jsxs("p", { className: "editorial-banner-body", children: [_jsxs("strong", { children: [meta.label, "."] }), " ", text?.trim() || meta.message] })] }));
|
|
119
|
+
}
|
package/dist/blocks.d.ts
CHANGED
|
@@ -94,4 +94,3 @@ export declare function statsToMarkdown(items: readonly StatItem[]): string;
|
|
|
94
94
|
export declare function linkGridToMarkdown(groups: readonly LinkGridGroup[], intro?: string | null): string;
|
|
95
95
|
/** A flat bullet list of links — resolved page lists, feed items, link rails. */
|
|
96
96
|
export declare function linkList(items: readonly LinkGridLink[]): string;
|
|
97
|
-
//# sourceMappingURL=blocks.d.ts.map
|
package/dist/blocks.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// new block type is a compile error until it is handled) and the bodies come
|
|
11
11
|
// from here. Each renderer takes the block's DATA rather than the block, which
|
|
12
12
|
// keeps this file free of any one repo's type union.
|
|
13
|
-
import { decodeEntities } from './
|
|
13
|
+
import { decodeEntities } from './markdown.js';
|
|
14
14
|
import { htmlToMarkdown, inlineToMarkdown } from './markdown.js';
|
|
15
15
|
/**
|
|
16
16
|
* The whole tree as markdown. Containers flatten in document order, because a
|
|
@@ -121,4 +121,3 @@ export function linkGridToMarkdown(groups, intro) {
|
|
|
121
121
|
export function linkList(items) {
|
|
122
122
|
return items.map(l => `- [${l.label}](${l.href})`).join('\n');
|
|
123
123
|
}
|
|
124
|
-
//# sourceMappingURL=blocks.js.map
|
package/dist/combobox.d.ts
CHANGED
package/dist/combobox.js
CHANGED
package/dist/conformance.d.ts
CHANGED
|
@@ -7,12 +7,21 @@ export interface Rpc {
|
|
|
7
7
|
isError?: boolean;
|
|
8
8
|
tools?: Array<{
|
|
9
9
|
name: string;
|
|
10
|
+
title?: string;
|
|
10
11
|
description?: string;
|
|
12
|
+
inputSchema?: {
|
|
13
|
+
required?: string[];
|
|
14
|
+
requireOneOf?: string[];
|
|
15
|
+
};
|
|
11
16
|
annotations?: Record<string, unknown>;
|
|
12
17
|
}>;
|
|
13
18
|
resources?: Array<{
|
|
14
19
|
uri: string;
|
|
15
20
|
}>;
|
|
21
|
+
prompts?: Array<{
|
|
22
|
+
name: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
}>;
|
|
16
25
|
resourceTemplates?: Array<{
|
|
17
26
|
uriTemplate?: string;
|
|
18
27
|
}>;
|
|
@@ -82,8 +91,6 @@ export declare function transportChecks(t: Tester, clientName: string,
|
|
|
82
91
|
* business, so it is a parameter rather than one surface's set hardcoded here.
|
|
83
92
|
*/
|
|
84
93
|
expectedCapabilities?: readonly string[]): Promise<Rpc>;
|
|
85
|
-
/** The newest protocol revision `wiki-formant/mcp` speaks. */
|
|
86
|
-
export declare const CURRENT_PROTOCOL = "2025-06-18";
|
|
87
94
|
/**
|
|
88
95
|
* One service, many descriptors — server.json, the two agent-card paths, the
|
|
89
96
|
* OpenAPI document, the MCP server card, and `initialize` — should never
|
|
@@ -142,6 +149,7 @@ export declare function annotationChecks(t: Tester, opts?: {
|
|
|
142
149
|
export declare function payloadBudget(t: Tester, calls: ReadonlyArray<{
|
|
143
150
|
name: string;
|
|
144
151
|
args?: Record<string, unknown>;
|
|
152
|
+
maxBytes?: number;
|
|
145
153
|
}>, maxBytes?: number): Promise<void>;
|
|
146
154
|
/**
|
|
147
155
|
* The default `User-agent: *` group may reach everything the descriptors
|
|
@@ -151,4 +159,3 @@ export declare function payloadBudget(t: Tester, calls: ReadonlyArray<{
|
|
|
151
159
|
* every unnamed crawler is telling two different stories to the same caller.
|
|
152
160
|
*/
|
|
153
161
|
export declare function robotsChecks(t: Tester, paths: readonly string[]): Promise<void>;
|
|
154
|
-
//# sourceMappingURL=conformance.d.ts.map
|
package/dist/conformance.js
CHANGED
|
@@ -11,6 +11,31 @@
|
|
|
11
11
|
// same package — so the suite that checks it belongs next to it. What each app
|
|
12
12
|
// keeps is its own fixtures: which tools it expects, what a good answer from each
|
|
13
13
|
// looks like, and which text surfaces it publishes.
|
|
14
|
+
//
|
|
15
|
+
// Every value the transport assertions compare against is imported from `mcp.ts`,
|
|
16
|
+
// not restated here. A suite that carries its own copy of the contract stops
|
|
17
|
+
// testing the boundary the moment the contract moves and says nothing about it.
|
|
18
|
+
import { DEFAULT_MAX_BATCH, MCP_CORS, MCP_PROTOCOL_VERSION } from './mcp.js';
|
|
19
|
+
/**
|
|
20
|
+
* Does the live preflight carry every token `MCP_CORS` declares for this header?
|
|
21
|
+
*
|
|
22
|
+
* Derived rather than listed: the assertion used to name three headers by hand,
|
|
23
|
+
* so a fourth added to `MCP_CORS` was tested by nobody.
|
|
24
|
+
*/
|
|
25
|
+
const corsCovers = (res, header) => {
|
|
26
|
+
const live = res.headers.get(header) ?? '';
|
|
27
|
+
return (MCP_CORS[header] ?? '').split(',').every(tok => live.includes(tok.trim()));
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* A POST that `Tester.rpc` cannot make: one whose raw status and headers are the
|
|
31
|
+
* thing under test, and which must not take `rpc`'s 429 retry. A string body is
|
|
32
|
+
* sent as-is, so the malformed-JSON case uses the same door as the rest.
|
|
33
|
+
*/
|
|
34
|
+
const rawPost = (endpoint, body, headers = {}) => fetch(endpoint, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
37
|
+
body: typeof body === 'string' ? body : JSON.stringify(body),
|
|
38
|
+
});
|
|
14
39
|
export function createTester(opts) {
|
|
15
40
|
const base = opts.base.replace(/\/$/, '');
|
|
16
41
|
const endpoint = opts.endpoint ?? `${base}/api/mcp`;
|
|
@@ -87,42 +112,42 @@ expectedCapabilities = ['tools']) {
|
|
|
87
112
|
// 2025-03-26 — forfeiting structured output, tool titles and `_meta` — while
|
|
88
113
|
// every suite reported green.
|
|
89
114
|
const echoed = await t.rpc('initialize', {
|
|
90
|
-
protocolVersion:
|
|
115
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
91
116
|
capabilities: {},
|
|
92
117
|
clientInfo: { name: clientName, version: '1' },
|
|
93
118
|
});
|
|
94
|
-
t.check('protocol negotiated', echoed.result?.protocolVersion ===
|
|
119
|
+
t.check('protocol negotiated', echoed.result?.protocolVersion === MCP_PROTOCOL_VERSION, `asked ${MCP_PROTOCOL_VERSION}, got ${echoed.result?.protocolVersion}`);
|
|
95
120
|
t.check('downgrades gracefully', (init.result?.protocolVersion ?? '').length > 0, `asked 2024-11-05, got ${init.result?.protocolVersion}`);
|
|
96
121
|
console.log(`\n=== transport ===`);
|
|
97
122
|
const opt = await fetch(t.endpoint, { method: 'OPTIONS' });
|
|
98
123
|
t.check('OPTIONS preflight', opt.status === 204 &&
|
|
99
124
|
opt.headers.get('access-control-allow-origin') === '*' &&
|
|
100
|
-
(opt
|
|
125
|
+
corsCovers(opt, 'Access-Control-Allow-Headers'), `${opt.status} ACAO=${opt.headers.get('access-control-allow-origin')}`);
|
|
101
126
|
// Allow-Headers governs what a browser may send; Expose-Headers what it may
|
|
102
127
|
// read. Without the second, a browser client cannot see `Retry-After` on a
|
|
103
128
|
// 429 and a rate limit reads to it as a hang.
|
|
104
|
-
t.check('CORS exposes response headers',
|
|
129
|
+
t.check('CORS exposes response headers', corsCovers(opt, 'Access-Control-Expose-Headers'), `expose=${opt.headers.get('access-control-expose-headers')}`);
|
|
130
|
+
// A budget stated only in the 429 can be discovered only by exceeding it —
|
|
131
|
+
// the one moment an agent is least able to act on it. This suite is the
|
|
132
|
+
// proof: with no header to read, its own client blind-sleeps five seconds on
|
|
133
|
+
// a 429 and hopes.
|
|
134
|
+
const headroom = await rawPost(t.endpoint, { jsonrpc: '2.0', id: 'headroom', method: 'ping' });
|
|
135
|
+
t.recordCall();
|
|
136
|
+
const remaining = headroom.headers.get('ratelimit-remaining');
|
|
137
|
+
t.check('rate limit states headroom', !!headroom.headers.get('ratelimit-limit') && remaining !== null, `RateLimit-Limit=${headroom.headers.get('ratelimit-limit')} Remaining=${remaining} Reset=${headroom.headers.get('ratelimit-reset')}`);
|
|
105
138
|
const get = await fetch(t.endpoint);
|
|
106
139
|
t.check('GET→405', get.status === 405 && get.headers.get('access-control-allow-origin') === '*', `${get.status} Allow=${get.headers.get('allow')} ACAO=${get.headers.get('access-control-allow-origin')}`);
|
|
107
140
|
// A notification has no id, so it must be acknowledged with no body at all —
|
|
108
141
|
// a JSON-RPC response to one is a protocol error.
|
|
109
|
-
const notif = await
|
|
110
|
-
method: 'POST',
|
|
111
|
-
headers: { 'Content-Type': 'application/json' },
|
|
112
|
-
body: JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }),
|
|
113
|
-
});
|
|
142
|
+
const notif = await rawPost(t.endpoint, { jsonrpc: '2.0', method: 'notifications/initialized' });
|
|
114
143
|
const notifBody = await notif.text();
|
|
115
144
|
t.check('notification→202', notif.status === 202 && notifBody === '', `${notif.status} body="${notifBody}"`);
|
|
116
|
-
const bad = await
|
|
145
|
+
const bad = await rawPost(t.endpoint, 'not json');
|
|
117
146
|
const badJson = (await bad.json());
|
|
118
147
|
t.check('parse error→400', bad.status === 400 && badJson.error?.code === -32700, `${bad.status} code=${badJson.error?.code}`);
|
|
119
|
-
const over = await
|
|
120
|
-
method: 'POST',
|
|
121
|
-
headers: { 'Content-Type': 'application/json' },
|
|
122
|
-
body: JSON.stringify(Array.from({ length: 21 }, (_, i) => ({ jsonrpc: '2.0', id: i, method: 'ping' }))),
|
|
123
|
-
});
|
|
148
|
+
const over = await rawPost(t.endpoint, Array.from({ length: DEFAULT_MAX_BATCH + 1 }, (_, i) => ({ jsonrpc: '2.0', id: i, method: 'ping' })));
|
|
124
149
|
const overJson = (await over.json());
|
|
125
|
-
t.check(
|
|
150
|
+
t.check(`batch cap (${DEFAULT_MAX_BATCH + 1})`, overJson.error?.code === -32600, `code=${overJson.error?.code}: ${(overJson.error?.message ?? '').slice(0, 80)}`);
|
|
126
151
|
const caps = init.result?.capabilities ?? {};
|
|
127
152
|
t.check('capabilities honest', expectedCapabilities.every(k => k in caps), `${JSON.stringify(caps)} expected=[${expectedCapabilities.join(', ')}]`);
|
|
128
153
|
// Declaring `resources` makes a client ask for templates. -32601 to a method
|
|
@@ -131,26 +156,20 @@ expectedCapabilities = ['tools']) {
|
|
|
131
156
|
const templates = await t.rpc('resources/templates/list');
|
|
132
157
|
t.check('resources/templates/list', Array.isArray(templates.result?.resourceTemplates), templates.error ? `-${templates.error.code}` : `${templates.result?.resourceTemplates?.length} templates`);
|
|
133
158
|
}
|
|
134
|
-
const versioned = await
|
|
135
|
-
|
|
136
|
-
headers: { 'Content-Type': 'application/json', 'MCP-Protocol-Version': CURRENT_PROTOCOL },
|
|
137
|
-
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'ping' }),
|
|
159
|
+
const versioned = await rawPost(t.endpoint, { jsonrpc: '2.0', id: 1, method: 'ping' }, {
|
|
160
|
+
'MCP-Protocol-Version': MCP_PROTOCOL_VERSION,
|
|
138
161
|
});
|
|
139
162
|
t.recordCall();
|
|
140
|
-
t.check('MCP-Protocol-Version echoed', versioned.headers.get('mcp-protocol-version') ===
|
|
163
|
+
t.check('MCP-Protocol-Version echoed', versioned.headers.get('mcp-protocol-version') === MCP_PROTOCOL_VERSION, `sent ${MCP_PROTOCOL_VERSION}, got ${versioned.headers.get('mcp-protocol-version')}`);
|
|
141
164
|
// Batching was removed in 2025-06-18. A server that keeps honouring it under
|
|
142
165
|
// a version that forbids it is telling the client something untrue.
|
|
143
|
-
const batched = await
|
|
144
|
-
|
|
145
|
-
headers: { 'Content-Type': 'application/json', 'MCP-Protocol-Version': CURRENT_PROTOCOL },
|
|
146
|
-
body: JSON.stringify([{ jsonrpc: '2.0', id: 1, method: 'ping' }]),
|
|
166
|
+
const batched = await rawPost(t.endpoint, [{ jsonrpc: '2.0', id: 1, method: 'ping' }], {
|
|
167
|
+
'MCP-Protocol-Version': MCP_PROTOCOL_VERSION,
|
|
147
168
|
});
|
|
148
169
|
t.recordCall();
|
|
149
170
|
t.check('batch refused at 2025-06-18', batched.status === 400, `${batched.status}`);
|
|
150
171
|
return init;
|
|
151
172
|
}
|
|
152
|
-
/** The newest protocol revision `wiki-formant/mcp` speaks. */
|
|
153
|
-
export const CURRENT_PROTOCOL = '2025-06-18';
|
|
154
173
|
/**
|
|
155
174
|
* One service, many descriptors — server.json, the two agent-card paths, the
|
|
156
175
|
* OpenAPI document, the MCP server card, and `initialize` — should never
|
|
@@ -200,11 +219,7 @@ export async function agentCardParity(t) {
|
|
|
200
219
|
// `url` is where a client sends its first call. Pointed at the homepage it
|
|
201
220
|
// gets HTML back, which is the failure that looks like a broken agent.
|
|
202
221
|
const endpoint = String(a.url ?? '');
|
|
203
|
-
const probe = await
|
|
204
|
-
method: 'POST',
|
|
205
|
-
headers: { 'Content-Type': 'application/json' },
|
|
206
|
-
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'ping' }),
|
|
207
|
-
});
|
|
222
|
+
const probe = await rawPost(endpoint, { jsonrpc: '2.0', id: 1, method: 'ping' });
|
|
208
223
|
t.recordCall();
|
|
209
224
|
t.check('agent card url is callable', (probe.headers.get('content-type') ?? '').includes('json'), `POST ${endpoint} -> ${probe.status} ${probe.headers.get('content-type')}`);
|
|
210
225
|
}
|
|
@@ -290,7 +305,21 @@ export async function annotationChecks(t, opts = {}) {
|
|
|
290
305
|
* answer, so this weighs it.
|
|
291
306
|
*/
|
|
292
307
|
export async function payloadBudget(t, calls, maxBytes = 120_000) {
|
|
293
|
-
|
|
308
|
+
// Every read-only tool a caller can invoke with no arguments at all, whether
|
|
309
|
+
// or not the suite thought to name it. This check used to weigh only the
|
|
310
|
+
// listed calls, which is how a tool taking no parameters and answering with
|
|
311
|
+
// 3.3 MB — 27× the budget it was exempt from — passed a suite that measured
|
|
312
|
+
// the four tools beside it. A tool with required arguments still has to be
|
|
313
|
+
// listed: the suite is the only thing that knows a valid pair.
|
|
314
|
+
const listed = new Set(calls.map(c => c.name));
|
|
315
|
+
const bare = ((await t.rpc('tools/list')).result?.tools ?? [])
|
|
316
|
+
.filter(x => x.annotations?.readOnlyHint === true && !listed.has(x.name))
|
|
317
|
+
// `requireOneOf` is a required argument too, just one this cannot pick for
|
|
318
|
+
// the caller — such a tool has to be listed with args like any other.
|
|
319
|
+
.filter(x => !(x.inputSchema?.required ?? []).length && !(x.inputSchema?.requireOneOf ?? []).length)
|
|
320
|
+
.map(x => ({ name: x.name, args: undefined, maxBytes: undefined }));
|
|
321
|
+
for (const { name, args, maxBytes: own } of [...calls, ...bare]) {
|
|
322
|
+
const budget = own ?? maxBytes;
|
|
294
323
|
const answer = await t.call(name, args ?? {});
|
|
295
324
|
const text = answer.result?.content?.[0]?.text ?? '';
|
|
296
325
|
// An empty answer is not a small one. The first draft compared only the
|
|
@@ -299,9 +328,18 @@ export async function payloadBudget(t, calls, maxBytes = 120_000) {
|
|
|
299
328
|
// measured nothing. A budget that green-lights a call that never returned
|
|
300
329
|
// is the exact failure it exists to catch.
|
|
301
330
|
const answered = text.length > 0 && !answer.error && !answer.result?.isError;
|
|
302
|
-
t.check(`${name} within budget`, answered && text.length <=
|
|
303
|
-
? `${text.length.toLocaleString()} chars (max ${
|
|
331
|
+
t.check(`${name} within budget`, answered && text.length <= budget, answered
|
|
332
|
+
? `${text.length.toLocaleString()} chars (max ${budget.toLocaleString()})`
|
|
304
333
|
: `no answer to weigh: ${answer.error ? `-${answer.error.code} ${answer.error.message}` : answer.result?.isError ? `isError: ${text.slice(0, 80)}` : 'empty result'}`);
|
|
334
|
+
// A JSON answer that arrives only as prose costs every client a parse it
|
|
335
|
+
// should never have had to write. Measured on the answer already in hand,
|
|
336
|
+
// so it is free — and it stays quiet for a tool that genuinely returns
|
|
337
|
+
// prose, which is the only reason a read tool may skip it.
|
|
338
|
+
if (answered && /^\s*\{/.test(text)) {
|
|
339
|
+
t.check(`${name} structured`, answer.result?.structuredContent !== undefined, answer.result?.structuredContent !== undefined
|
|
340
|
+
? 'structuredContent beside the text'
|
|
341
|
+
: 'JSON answer with no structuredContent — the client has to parse prose');
|
|
342
|
+
}
|
|
305
343
|
}
|
|
306
344
|
}
|
|
307
345
|
/**
|
|
@@ -337,4 +375,3 @@ export async function robotsChecks(t, paths) {
|
|
|
337
375
|
t.check(`robots allows ${path}`, !match || match.allow, match ? `${match.allow ? 'Allow' : 'Disallow'}: ${match.path}` : 'no matching rule');
|
|
338
376
|
}
|
|
339
377
|
}
|
|
340
|
-
//# sourceMappingURL=conformance.js.map
|
package/dist/crawlers.d.ts
CHANGED
package/dist/crawlers.js
CHANGED
package/dist/dom.d.ts
CHANGED
package/dist/dom.js
CHANGED