blume 1.4.2 → 1.5.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/CHANGELOG.md +71 -0
- package/dist/cli/index.js +2260 -1100
- package/dist/cli/index.js.map +123 -117
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/base-path.d.ts +8 -0
- package/dist/types/core/config-input.d.ts +87 -27
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +16 -1
- 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 +116 -15
- package/dist/types/core/sources/types.d.ts +11 -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 +14 -7
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/configuration/ai.mdx +26 -8
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- 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/sources.mdx +1 -1
- package/docs/content/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +1 -0
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +13 -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 +40 -12
- package/src/ai/api-catalog.ts +2 -2
- package/src/ai/ask-context.ts +49 -12
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +42 -17
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +52 -16
- package/src/ai/mcp/server.ts +280 -125
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/tar.ts +29 -70
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +13 -5
- package/src/astro/generate.ts +113 -63
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +23 -12
- package/src/astro/templates.ts +185 -41
- package/src/audit/agent.ts +16 -31
- 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 +78 -25
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +15 -5
- package/src/audit/snapshot.ts +29 -6
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +21 -21
- package/src/cli/commands/build.ts +30 -16
- package/src/cli/commands/dev.ts +15 -15
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eject.ts +4 -4
- package/src/cli/commands/eval.ts +24 -30
- 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/env.ts +13 -30
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +41 -13
- package/src/cli/internal-error.ts +1 -0
- package/src/cli/report-format.ts +22 -0
- package/src/components/content/AccordionItem.astro +2 -9
- package/src/components/content/ColorItem.astro +5 -13
- package/src/components/content/Component.astro +12 -8
- package/src/components/content/Frame.astro +2 -12
- package/src/components/content/Prompt.astro +12 -31
- package/src/components/content/Tab.astro +2 -9
- package/src/components/content/Tooltip.astro +1 -9
- package/src/components/content/Update.astro +2 -9
- 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/content/inline-markdown.ts +28 -0
- package/src/components/copy-feedback.ts +96 -0
- package/src/components/islands/ask-ai.tsx +111 -34
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/islands/webmcp.ts +49 -12
- 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/PageActions.astro +20 -32
- package/src/components/layout/PageLayout.astro +8 -28
- package/src/components/layout/RootLayout.astro +24 -48
- package/src/components/layout/Search.astro +133 -22
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/drawer-inert.ts +31 -0
- 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 +8 -5
- package/src/components/layout/search/types.ts +45 -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/panel.ts +11 -8
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/components/raf-throttle.ts +21 -0
- package/src/components/slug.ts +14 -0
- package/src/core/base-path.ts +18 -1
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +96 -27
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +16 -1
- package/src/core/define-components.ts +5 -0
- package/src/core/diagnostics.ts +46 -38
- package/src/core/frontmatter.ts +74 -4
- 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/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/probe.ts +7 -19
- package/src/core/project-graph.ts +27 -4
- package/src/core/schema.ts +219 -67
- package/src/core/site-url.ts +27 -0
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +16 -8
- package/src/core/sources/github-releases.ts +39 -11
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +93 -22
- package/src/core/sources/notion.ts +76 -22
- package/src/core/sources/portable-text.ts +48 -12
- package/src/core/sources/resolve.ts +1 -0
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +17 -1
- 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 +40 -11
- package/src/deploy/robots.ts +2 -1
- package/src/deploy/rss.ts +2 -1
- package/src/deploy/sitemap.ts +89 -8
- package/src/deploy/vercel-negotiation.ts +11 -4
- package/src/eval/agents.ts +13 -10
- package/src/eval/report.ts +5 -18
- 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 +63 -58
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +50 -33
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +135 -66
- package/src/openapi/parse.ts +166 -33
- package/src/openapi/references.ts +47 -22
- package/src/openapi/render-mdx.ts +137 -59
- package/src/openapi/scalar.ts +8 -10
- package/src/openapi/source.ts +126 -29
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +103 -39
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +117 -32
- 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/fonts.ts +8 -4
- package/src/theme/icons.ts +4 -2
- package/src/theme/palette.ts +27 -15
- package/src/translate/ledger.ts +4 -2
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +10 -19
- package/src/translate/run.ts +29 -38
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
- package/src/cli/coalesce.ts +0 -43
package/src/ai/mcp/server.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
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,
|
|
5
6
|
ListToolsRequestSchema,
|
|
6
7
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { z } from "zod";
|
|
7
9
|
|
|
8
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
normalizeRoute as normalizePageRoute,
|
|
12
|
+
stripBasePath,
|
|
13
|
+
withBasePath,
|
|
14
|
+
} from "../../core/base-path.ts";
|
|
15
|
+
import { absoluteUrl } from "../../core/site-url.ts";
|
|
16
|
+
import { trimEnd } from "../../core/trim.ts";
|
|
9
17
|
import { buildOramaIndex, queryOramaIndex } from "../../search/orama-index.ts";
|
|
10
18
|
import type { OramaDoc } from "../../search/orama-index.ts";
|
|
11
19
|
import type { McpData } from "./data.ts";
|
|
@@ -15,8 +23,9 @@ import { MCP_TOOLS } from "./tools.ts";
|
|
|
15
23
|
* The low-level SDK `Server` is used (rather than the high-level `McpServer`)
|
|
16
24
|
* because the latter's `registerTool` is generic over the caller's Zod instance;
|
|
17
25
|
* Blume's zod and the SDK's may resolve to different copies, whose types don't
|
|
18
|
-
* unify.
|
|
19
|
-
*
|
|
26
|
+
* unify. Each tool's input is defined once in Blume's own zod: the runtime
|
|
27
|
+
* parse and the JSON Schema advertised by `tools/list` (via `z.toJSONSchema`)
|
|
28
|
+
* derive from the same definition, so they cannot drift.
|
|
20
29
|
*/
|
|
21
30
|
|
|
22
31
|
/** Default and maximum number of hits returned by `search_docs`. */
|
|
@@ -25,7 +34,7 @@ const MAX_SEARCH_LIMIT = 20;
|
|
|
25
34
|
/** Excerpt length when a page has no description. */
|
|
26
35
|
const EXCERPT_LENGTH = 200;
|
|
27
36
|
|
|
28
|
-
const CORS_HEADERS
|
|
37
|
+
const CORS_HEADERS = {
|
|
29
38
|
"Access-Control-Allow-Headers":
|
|
30
39
|
"Content-Type, Mcp-Session-Id, Mcp-Protocol-Version",
|
|
31
40
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
@@ -33,97 +42,167 @@ const CORS_HEADERS: Record<string, string> = {
|
|
|
33
42
|
"Access-Control-Expose-Headers": "Mcp-Session-Id",
|
|
34
43
|
};
|
|
35
44
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
/** The optional facet filter `search_docs` and `list_pages` share. */
|
|
45
|
-
const FILTERS_SCHEMA = {
|
|
46
|
-
additionalProperties: { type: "string" },
|
|
47
|
-
description:
|
|
48
|
-
'Only include pages matching every facet, key → required value (e.g. `{"status": "enforced"}`). Facets are metadata the site declares per content type; `list_pages` shows each page\'s facet values. Omit for no facet filtering.',
|
|
49
|
-
type: "object",
|
|
50
|
-
} as const;
|
|
51
|
-
|
|
52
|
-
/** JSON Schema for each tool's input, keyed by tool name. */
|
|
53
|
-
const INPUT_SCHEMAS: Record<string, Record<string, unknown>> = {
|
|
54
|
-
get_navigation: { properties: {}, type: "object" },
|
|
55
|
-
get_page: {
|
|
56
|
-
properties: {
|
|
57
|
-
route: {
|
|
58
|
-
description: "The page route, e.g. `/guides/install`.",
|
|
59
|
-
type: "string",
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
required: ["route"],
|
|
63
|
-
type: "object",
|
|
64
|
-
},
|
|
65
|
-
list_pages: {
|
|
66
|
-
properties: {
|
|
67
|
-
contentTypes: CONTENT_TYPES_SCHEMA,
|
|
68
|
-
filters: FILTERS_SCHEMA,
|
|
69
|
-
},
|
|
70
|
-
type: "object",
|
|
71
|
-
},
|
|
72
|
-
search_docs: {
|
|
73
|
-
properties: {
|
|
74
|
-
contentTypes: CONTENT_TYPES_SCHEMA,
|
|
75
|
-
filters: FILTERS_SCHEMA,
|
|
76
|
-
limit: {
|
|
77
|
-
description: `Maximum hits to return (default ${DEFAULT_SEARCH_LIMIT}).`,
|
|
78
|
-
maximum: MAX_SEARCH_LIMIT,
|
|
79
|
-
minimum: 1,
|
|
80
|
-
type: "integer",
|
|
81
|
-
},
|
|
82
|
-
query: { description: "The search query.", type: "string" },
|
|
83
|
-
},
|
|
84
|
-
required: ["query"],
|
|
85
|
-
type: "object",
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
/** The `tools/list` payload, derived from shared metadata + input schemas. */
|
|
90
|
-
const TOOL_DEFINITIONS = MCP_TOOLS.map((tool) => ({
|
|
91
|
-
annotations: tool.annotations,
|
|
92
|
-
description: tool.description,
|
|
93
|
-
inputSchema: INPUT_SCHEMAS[tool.name],
|
|
94
|
-
name: tool.name,
|
|
95
|
-
title: tool.title,
|
|
96
|
-
}));
|
|
97
|
-
|
|
98
|
-
const asString = (value: unknown): string =>
|
|
99
|
-
typeof value === "string" ? value : "";
|
|
45
|
+
// Each field is a preprocess pipe: the input side accepts the sloppy shapes
|
|
46
|
+
// LLM callers actually send (a bare string for an array field, `[]`/`{}`
|
|
47
|
+
// meaning "no filter", out-of-range limits clamped rather than rejected), and
|
|
48
|
+
// the pipe's *output* side is the clean shape — which is exactly what
|
|
49
|
+
// `z.toJSONSchema` emits for `tools/list`. No coercion can ever fail, so a
|
|
50
|
+
// tool call is never rejected on argument shape, matching the previous
|
|
51
|
+
// hand-rolled coercions.
|
|
100
52
|
|
|
101
53
|
/**
|
|
102
|
-
* The
|
|
103
|
-
*
|
|
54
|
+
* The optional content-type filter `search_docs` and `list_pages` share.
|
|
55
|
+
* `[]` or no usable strings mean "no filter", not "match nothing"; a bare
|
|
104
56
|
* string is accepted as a one-element list.
|
|
105
57
|
*/
|
|
106
|
-
const
|
|
107
|
-
const list = Array.isArray(value)
|
|
108
|
-
|
|
109
|
-
|
|
58
|
+
const contentTypesField = z.preprocess((value) => {
|
|
59
|
+
const list = (Array.isArray(value) ? value : [value]).filter(
|
|
60
|
+
(entry): entry is string => typeof entry === "string"
|
|
61
|
+
);
|
|
110
62
|
return list.length > 0 ? list : undefined;
|
|
111
|
-
};
|
|
63
|
+
}, z.array(z.string()).optional().describe('Only include pages of these content types (frontmatter `type`, e.g. `["doc", "rfc"]`). `list_pages` shows each page\'s type. Omit to include every type.'));
|
|
112
64
|
|
|
113
65
|
/**
|
|
114
|
-
* The
|
|
115
|
-
*
|
|
66
|
+
* The optional facet filter `search_docs` and `list_pages` share. Only
|
|
67
|
+
* string-valued entries survive; an empty `{}` means "no filter".
|
|
116
68
|
*/
|
|
117
|
-
|
|
118
|
-
|
|
69
|
+
/** Accepts any plain object, so the string-valued entries can be sifted out. */
|
|
70
|
+
const looseFacetObject = z.record(z.string(), z.unknown());
|
|
71
|
+
|
|
72
|
+
const filtersField = z.preprocess((value) => {
|
|
73
|
+
const candidate = looseFacetObject.safeParse(value);
|
|
74
|
+
if (!candidate.success) {
|
|
119
75
|
return;
|
|
120
76
|
}
|
|
121
|
-
const entries = Object.entries(
|
|
77
|
+
const entries = Object.entries(candidate.data).filter(
|
|
122
78
|
(entry): entry is [string, string] => typeof entry[1] === "string"
|
|
123
79
|
);
|
|
124
80
|
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
81
|
+
}, z.record(z.string(), z.string()).optional().describe('Only include pages matching every facet, key → required value (e.g. `{"status": "enforced"}`). Facets are metadata the site declares per content type; `list_pages` shows each page\'s facet values. Omit for no facet filtering.'));
|
|
82
|
+
|
|
83
|
+
/** Clamped into range rather than rejected; non-numeric means the default. */
|
|
84
|
+
const limitField = z.preprocess(
|
|
85
|
+
(value) => {
|
|
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);
|
|
89
|
+
return Number.isFinite(num)
|
|
90
|
+
? Math.min(Math.max(Math.trunc(num), 1), MAX_SEARCH_LIMIT)
|
|
91
|
+
: undefined;
|
|
92
|
+
},
|
|
93
|
+
z
|
|
94
|
+
.int()
|
|
95
|
+
.min(1)
|
|
96
|
+
.max(MAX_SEARCH_LIMIT)
|
|
97
|
+
.optional()
|
|
98
|
+
.describe(`Maximum hits to return (default ${DEFAULT_SEARCH_LIMIT}).`)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
/** A required text field; a missing or non-string value coerces to "". */
|
|
102
|
+
const textField = (description: string) =>
|
|
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
|
+
);
|
|
125
|
+
|
|
126
|
+
/** Every tool's input schema — the runtime parse and tools/list source. */
|
|
127
|
+
const TOOL_INPUTS = {
|
|
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
|
+
}),
|
|
136
|
+
get_page: z.object({
|
|
137
|
+
route: textField("The page route, e.g. `/guides/install`."),
|
|
138
|
+
}),
|
|
139
|
+
list_pages: z.object({
|
|
140
|
+
contentTypes: contentTypesField,
|
|
141
|
+
filters: filtersField,
|
|
142
|
+
locale: localeField,
|
|
143
|
+
version: versionField,
|
|
144
|
+
}),
|
|
145
|
+
search_docs: z.object({
|
|
146
|
+
contentTypes: contentTypesField,
|
|
147
|
+
filters: filtersField,
|
|
148
|
+
limit: limitField,
|
|
149
|
+
locale: localeField,
|
|
150
|
+
query: textField("The search query."),
|
|
151
|
+
version: versionField,
|
|
152
|
+
}),
|
|
125
153
|
};
|
|
126
154
|
|
|
155
|
+
/**
|
|
156
|
+
* A tool's advertised JSON Schema. The dialect key is dropped (noise in a
|
|
157
|
+
* tools/list payload), as is the root `additionalProperties: false` — the
|
|
158
|
+
* runtime strips unknown keys rather than rejecting them, and the advertised
|
|
159
|
+
* schema shouldn't promise stricter validation than the server performs.
|
|
160
|
+
*/
|
|
161
|
+
const inputSchemaFor = (schema: z.ZodType) => {
|
|
162
|
+
const {
|
|
163
|
+
$schema: _dialect,
|
|
164
|
+
additionalProperties: _closed,
|
|
165
|
+
...rest
|
|
166
|
+
} = z.toJSONSchema(schema);
|
|
167
|
+
return rest;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/** The `tools/list` payload, derived from shared metadata + input schemas. */
|
|
171
|
+
const TOOL_DEFINITIONS = MCP_TOOLS.map((tool) => ({
|
|
172
|
+
annotations: tool.annotations,
|
|
173
|
+
description: tool.description,
|
|
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.
|
|
177
|
+
TOOL_INPUTS[tool.name as keyof typeof TOOL_INPUTS]
|
|
178
|
+
),
|
|
179
|
+
name: tool.name,
|
|
180
|
+
title: tool.title,
|
|
181
|
+
}));
|
|
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
|
+
|
|
127
206
|
/** Whether a page's facet values satisfy every requested filter entry. */
|
|
128
207
|
const matchesFacets = (
|
|
129
208
|
facets: Record<string, string> | undefined,
|
|
@@ -131,14 +210,47 @@ const matchesFacets = (
|
|
|
131
210
|
): boolean =>
|
|
132
211
|
Object.entries(filters).every(([key, value]) => facets?.[key] === value);
|
|
133
212
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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 "";
|
|
138
233
|
}
|
|
139
|
-
return
|
|
234
|
+
return value;
|
|
140
235
|
};
|
|
141
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
|
+
|
|
142
254
|
/**
|
|
143
255
|
* Normalize a user-supplied route to a `pages` key (`/`, `/a/b`, no suffix).
|
|
144
256
|
* Accepts a full URL too — `search_docs` hits and llms.txt entries carry
|
|
@@ -160,11 +272,10 @@ const normalizeRoute = (input: string, data: McpData): string => {
|
|
|
160
272
|
} catch {
|
|
161
273
|
// Malformed percent sequence — compare it as written.
|
|
162
274
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
return based === "" ? "/" : based;
|
|
275
|
+
// Trailing slashes come off before the suffix so `/a/b.md/` still loses its
|
|
276
|
+
// `.md`; normalizePageRoute then settles the leading slash.
|
|
277
|
+
const noSuffix = trimEnd(value, "/").replace(/\.mdx?$/u, "");
|
|
278
|
+
return stripBasePath(data.base, normalizePageRoute(noSuffix));
|
|
168
279
|
};
|
|
169
280
|
|
|
170
281
|
/** Build the absolute (or root-relative) URL for a route. */
|
|
@@ -174,7 +285,7 @@ const urlFor = (route: string, data: McpData): string => {
|
|
|
174
285
|
const path = withBasePath(data.base, route);
|
|
175
286
|
// Concatenate rather than `new URL(path, site)` — a root-absolute path
|
|
176
287
|
// would drop the base path of a subpath deployment (`acme.com/docs`).
|
|
177
|
-
return data.site ?
|
|
288
|
+
return data.site ? absoluteUrl(data.site, path) : path;
|
|
178
289
|
};
|
|
179
290
|
|
|
180
291
|
/** A hit's excerpt: its description, else the head of its content with an
|
|
@@ -187,10 +298,11 @@ const excerptFor = (doc: OramaDoc): string => {
|
|
|
187
298
|
return doc.content.length > EXCERPT_LENGTH ? `${head}…` : head;
|
|
188
299
|
};
|
|
189
300
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
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
|
+
};
|
|
194
306
|
|
|
195
307
|
/** Lazily builds the Orama index over a snapshot's documents, once. */
|
|
196
308
|
export type OramaIndexProvider = () => Promise<
|
|
@@ -218,12 +330,12 @@ export const buildServer = (
|
|
|
218
330
|
data: McpData,
|
|
219
331
|
index: OramaIndexProvider
|
|
220
332
|
): Server => {
|
|
333
|
+
const serverOptions: ServerOptions = data.instructions
|
|
334
|
+
? { capabilities: { tools: {} }, instructions: data.instructions }
|
|
335
|
+
: { capabilities: { tools: {} } };
|
|
221
336
|
const server = new Server(
|
|
222
337
|
{ name: data.name, version: data.version },
|
|
223
|
-
|
|
224
|
-
capabilities: { tools: {} },
|
|
225
|
-
...(data.instructions ? { instructions: data.instructions } : {}),
|
|
226
|
-
}
|
|
338
|
+
serverOptions
|
|
227
339
|
);
|
|
228
340
|
|
|
229
341
|
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
@@ -234,31 +346,41 @@ export const buildServer = (
|
|
|
234
346
|
const { arguments: args = {}, name } = request.params;
|
|
235
347
|
|
|
236
348
|
if (name === "search_docs") {
|
|
349
|
+
const input = TOOL_INPUTS.search_docs.parse(args);
|
|
237
350
|
const db = await index();
|
|
238
351
|
const hits = await queryOramaIndex(
|
|
239
352
|
db,
|
|
240
|
-
|
|
241
|
-
|
|
353
|
+
input.query,
|
|
354
|
+
input.limit ?? DEFAULT_SEARCH_LIMIT,
|
|
242
355
|
{
|
|
243
|
-
contentTypes:
|
|
244
|
-
facets:
|
|
356
|
+
contentTypes: input.contentTypes,
|
|
357
|
+
facets: input.filters,
|
|
358
|
+
locale: input.locale,
|
|
359
|
+
version: asVersionScope(input.version, data),
|
|
245
360
|
}
|
|
246
361
|
);
|
|
247
362
|
// `route` is the key `get_page` takes (the tool descriptions promise
|
|
248
363
|
// it); `url` is where the page is served.
|
|
249
|
-
const results = hits.map((doc: OramaDoc) =>
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
+
});
|
|
257
378
|
return text(JSON.stringify(results, null, 2));
|
|
258
379
|
}
|
|
259
380
|
|
|
260
381
|
if (name === "get_page") {
|
|
261
|
-
const
|
|
382
|
+
const input = TOOL_INPUTS.get_page.parse(args);
|
|
383
|
+
const key = normalizeRoute(input.route, data);
|
|
262
384
|
const markdown = data.pages[key];
|
|
263
385
|
if (markdown === undefined) {
|
|
264
386
|
return text(
|
|
@@ -270,24 +392,33 @@ export const buildServer = (
|
|
|
270
392
|
}
|
|
271
393
|
|
|
272
394
|
if (name === "list_pages") {
|
|
273
|
-
const
|
|
274
|
-
const filters =
|
|
395
|
+
const input = TOOL_INPUTS.list_pages.parse(args);
|
|
396
|
+
const { contentTypes, filters, locale } = input;
|
|
397
|
+
const versionScope = asVersionScope(input.version, data);
|
|
275
398
|
const routes = data.routes.filter(
|
|
276
399
|
(route) =>
|
|
277
400
|
(!contentTypes || contentTypes.includes(route.contentType)) &&
|
|
278
|
-
(!filters || matchesFacets(route.facets, filters))
|
|
401
|
+
(!filters || matchesFacets(route.facets, filters)) &&
|
|
402
|
+
(!locale || route.locale === locale) &&
|
|
403
|
+
(versionScope === undefined || route.version === versionScope)
|
|
279
404
|
);
|
|
280
405
|
return text(
|
|
281
406
|
JSON.stringify(
|
|
282
|
-
routes.map((route) =>
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
+
}),
|
|
291
422
|
null,
|
|
292
423
|
2
|
|
293
424
|
)
|
|
@@ -295,7 +426,31 @@ export const buildServer = (
|
|
|
295
426
|
}
|
|
296
427
|
|
|
297
428
|
if (name === "get_navigation") {
|
|
298
|
-
|
|
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));
|
|
299
454
|
}
|
|
300
455
|
|
|
301
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,
|