blume 1.4.3 → 1.5.1
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/CHANGELOG.md +32 -0
- package/README.md +16 -12
- package/dist/cli/index.js +1784 -633
- package/dist/cli/index.js.map +111 -106
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/config-input.d.ts +80 -28
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +19 -3
- package/dist/types/core/diagnostics.d.ts +5 -1
- package/dist/types/core/i18n-ui.d.ts +12 -0
- package/dist/types/core/schema.d.ts +112 -15
- package/dist/types/core/sources/types.d.ts +3 -1
- package/dist/types/core/standard-schema.d.ts +7 -3
- package/dist/types/core/types.d.ts +43 -2
- package/dist/types/core/ui-packs/index.d.ts +9 -1
- package/dist/types/openapi/references.d.ts +6 -5
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/dist/types/theme/fonts.d.ts +11 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/advanced/custom-pages.mdx +5 -1
- package/docs/configuration/index.mdx +1 -1
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- package/docs/configuration/theming.mdx +4 -2
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/meta.mdx +2 -1
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +35 -1
- package/docs/content/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +2 -1
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +3 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/agent-readability.ts +37 -10
- package/src/ai/ask-context.ts +5 -1
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +40 -16
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +28 -11
- package/src/ai/mcp/server.ts +183 -38
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +6 -2
- package/src/astro/generate.ts +64 -34
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +16 -9
- package/src/astro/templates.ts +181 -40
- package/src/audit/agent.ts +2 -2
- package/src/audit/checks/content.ts +26 -11
- package/src/audit/checks/dns-aid.ts +3 -0
- package/src/audit/checks/indexability.ts +24 -6
- package/src/audit/checks/llms.ts +9 -4
- package/src/audit/checks/network.ts +2 -0
- package/src/audit/checks/social.ts +18 -10
- package/src/audit/crawl.ts +37 -9
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +5 -2
- package/src/audit/snapshot.ts +2 -4
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +9 -4
- package/src/cli/commands/build.ts +15 -9
- package/src/cli/commands/dev.ts +2 -0
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eval.ts +7 -3
- package/src/cli/commands/init.ts +9 -9
- package/src/cli/commands/mcp-stdio.ts +3 -0
- package/src/cli/commands/translate.ts +14 -3
- package/src/cli/commands/version.ts +85 -0
- package/src/cli/dev-lock.ts +31 -10
- package/src/cli/eject-scripts.ts +17 -2
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +22 -15
- package/src/cli/internal-error.ts +1 -0
- package/src/components/content/auto-type-table.ts +3 -0
- package/src/components/content/diff.ts +9 -5
- package/src/components/content/github-info.ts +2 -0
- package/src/components/islands/ask-ai.tsx +33 -25
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/islands/webmcp.ts +49 -12
- package/src/components/layout/Fonts.astro +23 -3
- package/src/components/layout/Header.astro +25 -1
- package/src/components/layout/NavSelector.astro +11 -2
- package/src/components/layout/NavTree.astro +4 -2
- package/src/components/layout/PageLayout.astro +72 -3
- package/src/components/layout/ReferenceLayout.astro +2 -1
- package/src/components/layout/RootLayout.astro +20 -1
- package/src/components/layout/Search.astro +77 -13
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/hydration-hint.ts +1 -1
- package/src/components/layout/nav-utils.ts +1 -4
- package/src/components/layout/overrides.ts +25 -12
- package/src/components/layout/search/algolia.ts +18 -5
- package/src/components/layout/search/endpoint.ts +3 -0
- package/src/components/layout/search/flexsearch.ts +23 -7
- package/src/components/layout/search/orama-cloud.ts +1 -1
- package/src/components/layout/search/orama.ts +4 -1
- package/src/components/layout/search/pagefind.ts +2 -0
- package/src/components/layout/search/types.ts +13 -1
- package/src/components/layout/search/typesense.ts +19 -3
- package/src/components/openapi/ApiOverview.astro +32 -6
- package/src/components/openapi/AsyncApiOperation.astro +237 -0
- package/src/components/openapi/Bindings.astro +89 -0
- package/src/components/openapi/MethodBadge.astro +3 -0
- package/src/components/openapi/Operation.astro +7 -2
- package/src/components/openapi/PanelTabs.astro +131 -0
- package/src/components/openapi/ParametersTable.astro +2 -0
- package/src/components/openapi/RequestPanel.astro +12 -119
- package/src/components/openapi/async-snippets.ts +174 -0
- package/src/components/openapi/async.ts +348 -0
- package/src/components/openapi/helpers.ts +52 -20
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +89 -28
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +19 -3
- package/src/core/define-components.ts +5 -0
- package/src/core/diagnostics.ts +46 -38
- package/src/core/frontmatter.ts +33 -7
- package/src/core/graph.ts +137 -53
- package/src/core/i18n-ui.ts +15 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/last-modified.ts +49 -0
- package/src/core/load-module.ts +1 -0
- package/src/core/manifest.ts +92 -3
- package/src/core/meta.ts +44 -14
- package/src/core/nav-diagnostics.ts +3 -3
- package/src/core/navigation.ts +247 -67
- package/src/core/project-graph.ts +26 -3
- package/src/core/schema.ts +214 -68
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +6 -0
- package/src/core/sources/github-releases.ts +39 -31
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +67 -20
- package/src/core/sources/notion.ts +49 -17
- package/src/core/sources/portable-text.ts +32 -11
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +4 -0
- package/src/core/sources/watch.ts +1 -1
- package/src/core/standard-schema.ts +9 -3
- package/src/core/text-width.ts +26 -0
- package/src/core/tsconfig-aliases.ts +9 -5
- package/src/core/types.ts +45 -2
- package/src/core/ui-packs/index.ts +9 -1
- package/src/core/version-cut.ts +301 -0
- package/src/core/version.ts +2 -0
- package/src/core/versions.ts +170 -0
- package/src/deploy/adapter-output.ts +5 -2
- package/src/deploy/cloudflare-negotiation.ts +25 -10
- package/src/deploy/sitemap.ts +33 -1
- package/src/deploy/vercel-negotiation.ts +45 -18
- package/src/eval/report.ts +4 -4
- package/src/eval/run.ts +2 -2
- package/src/eval/schema.ts +1 -1
- package/src/markdown/base-links.ts +6 -6
- package/src/markdown/directives.ts +7 -1
- package/src/markdown/heading-anchors.ts +17 -6
- package/src/markdown/index.ts +73 -24
- package/src/markdown/inline-code.ts +14 -2
- package/src/markdown/language-icon.ts +6 -2
- package/src/markdown/mdast.ts +18 -4
- package/src/markdown/package-commands.ts +6 -8
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +33 -12
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +126 -57
- package/src/openapi/parse.ts +97 -5
- package/src/openapi/references.ts +12 -10
- package/src/openapi/render-mdx.ts +73 -34
- package/src/openapi/scalar.ts +6 -8
- package/src/openapi/source.ts +98 -28
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +25 -5
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +66 -20
- package/src/search/popular.ts +10 -5
- package/src/search/providers.ts +2 -2
- package/src/search/sync/index.ts +2 -0
- package/src/search/sync/typesense.ts +4 -2
- package/src/seo/jsonld.ts +24 -6
- package/src/seo/x-handle.ts +8 -3
- package/src/theme/chrome-icons.ts +7 -2
- package/src/theme/entry.ts +24 -2
- package/src/theme/fonts.ts +83 -7
- package/src/theme/icons.ts +4 -2
- package/src/theme/palette.ts +22 -14
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +9 -5
- package/src/translate/run.ts +10 -4
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
package/src/ai/mcp/server.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import type { ServerOptions } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
3
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
3
4
|
import {
|
|
4
5
|
CallToolRequestSchema,
|
|
@@ -33,7 +34,7 @@ const MAX_SEARCH_LIMIT = 20;
|
|
|
33
34
|
/** Excerpt length when a page has no description. */
|
|
34
35
|
const EXCERPT_LENGTH = 200;
|
|
35
36
|
|
|
36
|
-
const CORS_HEADERS
|
|
37
|
+
const CORS_HEADERS = {
|
|
37
38
|
"Access-Control-Allow-Headers":
|
|
38
39
|
"Content-Type, Mcp-Session-Id, Mcp-Protocol-Version",
|
|
39
40
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
@@ -65,11 +66,15 @@ const contentTypesField = z.preprocess((value) => {
|
|
|
65
66
|
* The optional facet filter `search_docs` and `list_pages` share. Only
|
|
66
67
|
* string-valued entries survive; an empty `{}` means "no filter".
|
|
67
68
|
*/
|
|
69
|
+
/** Accepts any plain object, so the string-valued entries can be sifted out. */
|
|
70
|
+
const looseFacetObject = z.record(z.string(), z.unknown());
|
|
71
|
+
|
|
68
72
|
const filtersField = z.preprocess((value) => {
|
|
69
|
-
|
|
73
|
+
const candidate = looseFacetObject.safeParse(value);
|
|
74
|
+
if (!candidate.success) {
|
|
70
75
|
return;
|
|
71
76
|
}
|
|
72
|
-
const entries = Object.entries(
|
|
77
|
+
const entries = Object.entries(candidate.data).filter(
|
|
73
78
|
(entry): entry is [string, string] => typeof entry[1] === "string"
|
|
74
79
|
);
|
|
75
80
|
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
@@ -78,7 +83,9 @@ const filtersField = z.preprocess((value) => {
|
|
|
78
83
|
/** Clamped into range rather than rejected; non-numeric means the default. */
|
|
79
84
|
const limitField = z.preprocess(
|
|
80
85
|
(value) => {
|
|
81
|
-
|
|
86
|
+
// `Number` is the identity on numbers, so one conversion covers both the
|
|
87
|
+
// well-typed call and a numeric string.
|
|
88
|
+
const num = Number(value);
|
|
82
89
|
return Number.isFinite(num)
|
|
83
90
|
? Math.min(Math.max(Math.trunc(num), 1), MAX_SEARCH_LIMIT)
|
|
84
91
|
: undefined;
|
|
@@ -93,26 +100,55 @@ const limitField = z.preprocess(
|
|
|
93
100
|
|
|
94
101
|
/** A required text field; a missing or non-string value coerces to "". */
|
|
95
102
|
const textField = (description: string) =>
|
|
96
|
-
z.preprocess(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
);
|
|
103
|
+
z.preprocess((value) => {
|
|
104
|
+
const parsed = z.string().safeParse(value);
|
|
105
|
+
return parsed.success ? parsed.data : "";
|
|
106
|
+
}, z.string().describe(description));
|
|
107
|
+
|
|
108
|
+
/** An optional trimmed text field; blank or non-string means "absent". */
|
|
109
|
+
const optionalTextField = (description: string) =>
|
|
110
|
+
z.preprocess((value) => {
|
|
111
|
+
const parsed = z.string().safeParse(value);
|
|
112
|
+
const trimmed = parsed.success ? parsed.data.trim() : "";
|
|
113
|
+
return trimmed || undefined;
|
|
114
|
+
}, z.string().optional().describe(description));
|
|
115
|
+
|
|
116
|
+
/** The optional locale filter `search_docs` and `list_pages` share. */
|
|
117
|
+
const localeField = optionalTextField(
|
|
118
|
+
"Only include pages in this locale (e.g. `fr`). Omit for every language."
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
/** The optional docs-version scope `search_docs` and `list_pages` share. */
|
|
122
|
+
const versionField = optionalTextField(
|
|
123
|
+
'Docs version to scope to on a versioned site: `"latest"` (the default — current docs only), `"all"` (every version), or an archived version id (e.g. `"v1.0"`). Ignored when the site is unversioned.'
|
|
124
|
+
);
|
|
100
125
|
|
|
101
126
|
/** Every tool's input schema — the runtime parse and tools/list source. */
|
|
102
127
|
const TOOL_INPUTS = {
|
|
103
|
-
get_navigation: z.object({
|
|
128
|
+
get_navigation: z.object({
|
|
129
|
+
locale: optionalTextField(
|
|
130
|
+
"Locale whose navigation tree to return (defaults to the default locale)."
|
|
131
|
+
),
|
|
132
|
+
version: optionalTextField(
|
|
133
|
+
"Archived version id whose tree to return (defaults to the current docs)."
|
|
134
|
+
),
|
|
135
|
+
}),
|
|
104
136
|
get_page: z.object({
|
|
105
137
|
route: textField("The page route, e.g. `/guides/install`."),
|
|
106
138
|
}),
|
|
107
139
|
list_pages: z.object({
|
|
108
140
|
contentTypes: contentTypesField,
|
|
109
141
|
filters: filtersField,
|
|
142
|
+
locale: localeField,
|
|
143
|
+
version: versionField,
|
|
110
144
|
}),
|
|
111
145
|
search_docs: z.object({
|
|
112
146
|
contentTypes: contentTypesField,
|
|
113
147
|
filters: filtersField,
|
|
114
148
|
limit: limitField,
|
|
149
|
+
locale: localeField,
|
|
115
150
|
query: textField("The search query."),
|
|
151
|
+
version: versionField,
|
|
116
152
|
}),
|
|
117
153
|
};
|
|
118
154
|
|
|
@@ -122,7 +158,7 @@ const TOOL_INPUTS = {
|
|
|
122
158
|
* runtime strips unknown keys rather than rejecting them, and the advertised
|
|
123
159
|
* schema shouldn't promise stricter validation than the server performs.
|
|
124
160
|
*/
|
|
125
|
-
const inputSchemaFor = (schema: z.ZodType)
|
|
161
|
+
const inputSchemaFor = (schema: z.ZodType) => {
|
|
126
162
|
const {
|
|
127
163
|
$schema: _dialect,
|
|
128
164
|
additionalProperties: _closed,
|
|
@@ -136,12 +172,37 @@ const TOOL_DEFINITIONS = MCP_TOOLS.map((tool) => ({
|
|
|
136
172
|
annotations: tool.annotations,
|
|
137
173
|
description: tool.description,
|
|
138
174
|
inputSchema: inputSchemaFor(
|
|
175
|
+
// SAFETY: TOOL_INPUTS declares a schema for every MCP_TOOLS name; the two
|
|
176
|
+
// lists are maintained together so names and descriptions never drift.
|
|
139
177
|
TOOL_INPUTS[tool.name as keyof typeof TOOL_INPUTS]
|
|
140
178
|
),
|
|
141
179
|
name: tool.name,
|
|
142
180
|
title: tool.title,
|
|
143
181
|
}));
|
|
144
182
|
|
|
183
|
+
/** One `search_docs` result entry; `version` only appears on versioned sites. */
|
|
184
|
+
interface SearchHitPayload {
|
|
185
|
+
contentType: string | undefined;
|
|
186
|
+
excerpt: string;
|
|
187
|
+
facets: Record<string, string> | undefined;
|
|
188
|
+
route: string;
|
|
189
|
+
title: string;
|
|
190
|
+
url: string;
|
|
191
|
+
version?: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** One `list_pages` entry; `version` only appears on versioned sites. */
|
|
195
|
+
interface PageListingPayload {
|
|
196
|
+
contentType: string;
|
|
197
|
+
description: string | undefined;
|
|
198
|
+
facets: Record<string, string> | undefined;
|
|
199
|
+
lastModified: string | null;
|
|
200
|
+
route: string;
|
|
201
|
+
title: string;
|
|
202
|
+
url: string;
|
|
203
|
+
version?: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
145
206
|
/** Whether a page's facet values satisfy every requested filter entry. */
|
|
146
207
|
const matchesFacets = (
|
|
147
208
|
facets: Record<string, string> | undefined,
|
|
@@ -149,6 +210,47 @@ const matchesFacets = (
|
|
|
149
210
|
): boolean =>
|
|
150
211
|
Object.entries(filters).every(([key, value]) => facets?.[key] === value);
|
|
151
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Resolve the `version` scope on a versioned site: `undefined` disables the
|
|
215
|
+
* filter (`"all"`), `""` is the current docs (the default — agents almost
|
|
216
|
+
* always want the live documentation), and anything else is an archived id
|
|
217
|
+
* (an unknown id simply matches nothing). On an unversioned site the input is
|
|
218
|
+
* ignored entirely. The input arrives pre-trimmed (blank coerced to absent)
|
|
219
|
+
* from the tool's input schema.
|
|
220
|
+
*/
|
|
221
|
+
const asVersionScope = (
|
|
222
|
+
value: string | undefined,
|
|
223
|
+
data: McpData
|
|
224
|
+
): string | undefined => {
|
|
225
|
+
if (!data.archivedVersions) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (value === "all") {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (value === undefined || value === "latest" || value === "current") {
|
|
232
|
+
return "";
|
|
233
|
+
}
|
|
234
|
+
return value;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Error message for a `get_navigation` version id that isn't a configured
|
|
239
|
+
* archived version, or `null` when the id is valid (or the site is
|
|
240
|
+
* unversioned, where the id is ignored like the other tools' scopes). Unlike
|
|
241
|
+
* `asVersionScope`'s match-nothing filters, a bad id here would otherwise
|
|
242
|
+
* silently return the *current* tree posing as the requested snapshot.
|
|
243
|
+
*/
|
|
244
|
+
const unknownVersionError = (
|
|
245
|
+
versionId: string | undefined,
|
|
246
|
+
data: McpData
|
|
247
|
+
): string | null =>
|
|
248
|
+
versionId &&
|
|
249
|
+
data.archivedVersions &&
|
|
250
|
+
!data.archivedVersions.includes(versionId)
|
|
251
|
+
? `Unknown version "${versionId}". Archived versions: ${data.archivedVersions.join(", ")}.`
|
|
252
|
+
: null;
|
|
253
|
+
|
|
152
254
|
/**
|
|
153
255
|
* Normalize a user-supplied route to a `pages` key (`/`, `/a/b`, no suffix).
|
|
154
256
|
* Accepts a full URL too — `search_docs` hits and llms.txt entries carry
|
|
@@ -196,10 +298,11 @@ const excerptFor = (doc: OramaDoc): string => {
|
|
|
196
298
|
return doc.content.length > EXCERPT_LENGTH ? `${head}…` : head;
|
|
197
299
|
};
|
|
198
300
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
301
|
+
/** A tool call's text result, marked as an error when `isError` is set. */
|
|
302
|
+
const text = (value: string, isError = false) => {
|
|
303
|
+
const content = [{ text: value, type: "text" as const }];
|
|
304
|
+
return isError ? { content, isError: true } : { content };
|
|
305
|
+
};
|
|
203
306
|
|
|
204
307
|
/** Lazily builds the Orama index over a snapshot's documents, once. */
|
|
205
308
|
export type OramaIndexProvider = () => Promise<
|
|
@@ -227,12 +330,12 @@ export const buildServer = (
|
|
|
227
330
|
data: McpData,
|
|
228
331
|
index: OramaIndexProvider
|
|
229
332
|
): Server => {
|
|
333
|
+
const serverOptions: ServerOptions = data.instructions
|
|
334
|
+
? { capabilities: { tools: {} }, instructions: data.instructions }
|
|
335
|
+
: { capabilities: { tools: {} } };
|
|
230
336
|
const server = new Server(
|
|
231
337
|
{ name: data.name, version: data.version },
|
|
232
|
-
|
|
233
|
-
capabilities: { tools: {} },
|
|
234
|
-
...(data.instructions ? { instructions: data.instructions } : {}),
|
|
235
|
-
}
|
|
338
|
+
serverOptions
|
|
236
339
|
);
|
|
237
340
|
|
|
238
341
|
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
@@ -252,18 +355,26 @@ export const buildServer = (
|
|
|
252
355
|
{
|
|
253
356
|
contentTypes: input.contentTypes,
|
|
254
357
|
facets: input.filters,
|
|
358
|
+
locale: input.locale,
|
|
359
|
+
version: asVersionScope(input.version, data),
|
|
255
360
|
}
|
|
256
361
|
);
|
|
257
362
|
// `route` is the key `get_page` takes (the tool descriptions promise
|
|
258
363
|
// it); `url` is where the page is served.
|
|
259
|
-
const results = hits.map((doc: OramaDoc) =>
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
364
|
+
const results = hits.map((doc: OramaDoc) => {
|
|
365
|
+
const hit: SearchHitPayload = {
|
|
366
|
+
contentType: doc.contentType,
|
|
367
|
+
excerpt: excerptFor(doc),
|
|
368
|
+
facets: doc.facets,
|
|
369
|
+
route: doc.route,
|
|
370
|
+
title: doc.title,
|
|
371
|
+
url: urlFor(doc.route, data),
|
|
372
|
+
};
|
|
373
|
+
if (data.archivedVersions) {
|
|
374
|
+
hit.version = doc.version ?? "";
|
|
375
|
+
}
|
|
376
|
+
return hit;
|
|
377
|
+
});
|
|
267
378
|
return text(JSON.stringify(results, null, 2));
|
|
268
379
|
}
|
|
269
380
|
|
|
@@ -281,23 +392,33 @@ export const buildServer = (
|
|
|
281
392
|
}
|
|
282
393
|
|
|
283
394
|
if (name === "list_pages") {
|
|
284
|
-
const
|
|
395
|
+
const input = TOOL_INPUTS.list_pages.parse(args);
|
|
396
|
+
const { contentTypes, filters, locale } = input;
|
|
397
|
+
const versionScope = asVersionScope(input.version, data);
|
|
285
398
|
const routes = data.routes.filter(
|
|
286
399
|
(route) =>
|
|
287
400
|
(!contentTypes || contentTypes.includes(route.contentType)) &&
|
|
288
|
-
(!filters || matchesFacets(route.facets, filters))
|
|
401
|
+
(!filters || matchesFacets(route.facets, filters)) &&
|
|
402
|
+
(!locale || route.locale === locale) &&
|
|
403
|
+
(versionScope === undefined || route.version === versionScope)
|
|
289
404
|
);
|
|
290
405
|
return text(
|
|
291
406
|
JSON.stringify(
|
|
292
|
-
routes.map((route) =>
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
407
|
+
routes.map((route) => {
|
|
408
|
+
const listing: PageListingPayload = {
|
|
409
|
+
contentType: route.contentType,
|
|
410
|
+
description: route.description,
|
|
411
|
+
facets: route.facets,
|
|
412
|
+
lastModified: route.lastModified,
|
|
413
|
+
route: route.route,
|
|
414
|
+
title: route.title,
|
|
415
|
+
url: urlFor(route.route, data),
|
|
416
|
+
};
|
|
417
|
+
if (data.archivedVersions) {
|
|
418
|
+
listing.version = route.version;
|
|
419
|
+
}
|
|
420
|
+
return listing;
|
|
421
|
+
}),
|
|
301
422
|
null,
|
|
302
423
|
2
|
|
303
424
|
)
|
|
@@ -305,7 +426,31 @@ export const buildServer = (
|
|
|
305
426
|
}
|
|
306
427
|
|
|
307
428
|
if (name === "get_navigation") {
|
|
308
|
-
|
|
429
|
+
// A version id selects the snapshot's tree; a locale selects its
|
|
430
|
+
// language (falling back through the default locale to any tree the
|
|
431
|
+
// snapshot has). Without a version, a locale selects the current docs'
|
|
432
|
+
// localized tree. An unknown id on a versioned site is an error — the
|
|
433
|
+
// current tree would silently masquerade as the requested snapshot.
|
|
434
|
+
const { locale, version: versionId } =
|
|
435
|
+
TOOL_INPUTS.get_navigation.parse(args);
|
|
436
|
+
const unknownVersion = unknownVersionError(versionId, data);
|
|
437
|
+
if (unknownVersion) {
|
|
438
|
+
return text(unknownVersion, true);
|
|
439
|
+
}
|
|
440
|
+
let { navigation } = data;
|
|
441
|
+
const byLocale = versionId
|
|
442
|
+
? data.navigationByVersion?.[versionId]
|
|
443
|
+
: undefined;
|
|
444
|
+
if (byLocale) {
|
|
445
|
+
navigation =
|
|
446
|
+
(locale ? byLocale[locale] : undefined) ??
|
|
447
|
+
byLocale[data.defaultLocale ?? ""] ??
|
|
448
|
+
Object.values(byLocale)[0] ??
|
|
449
|
+
navigation;
|
|
450
|
+
} else if (locale && data.navigationByLocale?.[locale]) {
|
|
451
|
+
navigation = data.navigationByLocale[locale];
|
|
452
|
+
}
|
|
453
|
+
return text(JSON.stringify(navigation, null, 2));
|
|
309
454
|
}
|
|
310
455
|
|
|
311
456
|
return text(`Unknown tool: ${name}`, true);
|
package/src/ai/mcp/tools.ts
CHANGED
|
@@ -19,7 +19,7 @@ export const MCP_TOOLS: McpToolMeta[] = [
|
|
|
19
19
|
{
|
|
20
20
|
annotations: READ_ONLY,
|
|
21
21
|
description:
|
|
22
|
-
'Full-text search across the documentation. Returns matching pages with their title, route, content type, and a short excerpt; pass `contentTypes` to search only pages of certain types (e.g. `rfc`, `changelog`),
|
|
22
|
+
'Full-text search across the documentation. Returns matching pages with their title, route, content type, and a short excerpt; pass `contentTypes` to search only pages of certain types (e.g. `rfc`, `changelog`), `filters` to require facet values the site declares per type (e.g. `{"status": "enforced"}`), and `locale` to search one language. On a versioned site results default to the current docs — pass `version` to search an archived version (e.g. `"v1.0"`) or `"all"` for every version. Use this first to discover relevant pages, then `get_page` to read one in full.',
|
|
23
23
|
name: "search_docs",
|
|
24
24
|
title: "Search documentation",
|
|
25
25
|
},
|
|
@@ -33,14 +33,14 @@ export const MCP_TOOLS: McpToolMeta[] = [
|
|
|
33
33
|
{
|
|
34
34
|
annotations: READ_ONLY,
|
|
35
35
|
description:
|
|
36
|
-
|
|
36
|
+
'List every documentation page with its route, title, description, content type, and any declared facet values; pass `contentTypes`, `filters`, and/or `locale` to narrow the list. On a versioned site the list defaults to the current docs — pass `version` for an archived version or `"all"`. Useful for enumerating the docs, discovering the types and facets in use, or finding a page when search is too narrow.',
|
|
37
37
|
name: "list_pages",
|
|
38
38
|
title: "List pages",
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
annotations: READ_ONLY,
|
|
42
42
|
description:
|
|
43
|
-
"Return the documentation navigation tree (header tabs and the sidebar hierarchy), reflecting how the docs are organized for readers.",
|
|
43
|
+
"Return the documentation navigation tree (header tabs and the sidebar hierarchy), reflecting how the docs are organized for readers. Pass `locale` for a language's tree and, on a versioned site, `version` for an archived snapshot's tree.",
|
|
44
44
|
name: "get_navigation",
|
|
45
45
|
title: "Get navigation",
|
|
46
46
|
},
|
package/src/ai/skills.ts
CHANGED
|
@@ -89,23 +89,46 @@ const collectEntries = async (
|
|
|
89
89
|
return entries;
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
+
/** The two SKILL.md frontmatter fields the discovery index publishes. */
|
|
93
|
+
interface SkillMeta {
|
|
94
|
+
description: string;
|
|
95
|
+
name: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface SkillMetaResult {
|
|
99
|
+
meta: SkillMeta | null;
|
|
100
|
+
warning?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* What js-yaml can put in a SKILL.md frontmatter field. Rich scalars (Dates)
|
|
105
|
+
* ride along as the object arm; only strings are accepted below anyway.
|
|
106
|
+
*/
|
|
107
|
+
type FrontmatterField =
|
|
108
|
+
| string
|
|
109
|
+
| number
|
|
110
|
+
| boolean
|
|
111
|
+
| null
|
|
112
|
+
| undefined
|
|
113
|
+
| FrontmatterField[]
|
|
114
|
+
| { [key: string]: FrontmatterField };
|
|
115
|
+
|
|
116
|
+
const isString = (value: FrontmatterField): value is string =>
|
|
117
|
+
typeof value === "string";
|
|
118
|
+
|
|
92
119
|
/** Frontmatter of a SKILL.md, or null with a warning when unusable. */
|
|
93
|
-
const skillMeta = (
|
|
94
|
-
|
|
95
|
-
dirName: string
|
|
96
|
-
): { meta: { description: string; name: string } | null; warning?: string } => {
|
|
97
|
-
let data: Record<string, unknown>;
|
|
120
|
+
const skillMeta = (raw: string, dirName: string): SkillMetaResult => {
|
|
121
|
+
let data: { description?: FrontmatterField; name?: FrontmatterField };
|
|
98
122
|
try {
|
|
99
|
-
({ data } = matter(raw)
|
|
123
|
+
({ data } = matter(raw));
|
|
100
124
|
} catch {
|
|
101
125
|
return {
|
|
102
126
|
meta: null,
|
|
103
127
|
warning: `Skill "${dirName}" has unparsable SKILL.md frontmatter; skipped.`,
|
|
104
128
|
};
|
|
105
129
|
}
|
|
106
|
-
const name =
|
|
107
|
-
const description =
|
|
108
|
-
typeof data.description === "string" ? data.description : "";
|
|
130
|
+
const name = isString(data.name) ? data.name : "";
|
|
131
|
+
const description = isString(data.description) ? data.description : "";
|
|
109
132
|
if (!(name && description)) {
|
|
110
133
|
return {
|
|
111
134
|
meta: null,
|
package/src/ai/visibility.ts
CHANGED
|
@@ -19,10 +19,10 @@ const visibilityBlock = (audience: VisibilityAudience): RegExp =>
|
|
|
19
19
|
"gu"
|
|
20
20
|
);
|
|
21
21
|
|
|
22
|
-
const BLOCKS
|
|
22
|
+
const BLOCKS = {
|
|
23
23
|
agents: visibilityBlock("agents"),
|
|
24
24
|
web: visibilityBlock("web"),
|
|
25
|
-
}
|
|
25
|
+
} satisfies Record<VisibilityAudience, RegExp>;
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Resolve `<Visibility>` blocks for one audience: blocks addressed to the
|
|
@@ -77,6 +77,8 @@ const importClause = (variable: string, name: string, path: string): string =>
|
|
|
77
77
|
|
|
78
78
|
/** A wrapper `.astro` that statically imports a component and hydrates it. */
|
|
79
79
|
const wrapperContent = (override: NormalizedOverride): string => {
|
|
80
|
+
// SAFETY: the only caller guards `if (!source)` and bails before invoking
|
|
81
|
+
// this, so the override always carries a resolved source here.
|
|
80
82
|
const { name, path } = override.source as NonNullable<
|
|
81
83
|
NormalizedOverride["source"]
|
|
82
84
|
>;
|
package/src/astro/examples.ts
CHANGED
|
@@ -33,7 +33,11 @@ export interface ExampleDiscovery {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/** Example extensions mapped to the framework that renders them. */
|
|
36
|
-
|
|
36
|
+
interface FrameworkByExtension {
|
|
37
|
+
[extension: string]: ExampleFramework;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const FRAMEWORK_BY_EXT: FrameworkByExtension = {
|
|
37
41
|
astro: "astro",
|
|
38
42
|
jsx: "react",
|
|
39
43
|
svelte: "svelte",
|
|
@@ -62,7 +66,7 @@ const GLOB_MAGIC = /[!*?[\]{}]/u;
|
|
|
62
66
|
* discovered files can be keyed relative to that prefix (e.g.
|
|
63
67
|
* `registry/x/**\/examples/*` → `{ base: "registry/x", rest: "**\/examples/*" }`).
|
|
64
68
|
*/
|
|
65
|
-
const splitGlobBase = (pattern: string)
|
|
69
|
+
const splitGlobBase = (pattern: string) => {
|
|
66
70
|
const segments = pattern.split("/");
|
|
67
71
|
// Only called when the pattern contains glob magic (see the caller), and `/`
|
|
68
72
|
// is never magic, so the magic char always lands in a segment — `findIndex`
|