@hashicorp/design-system-mcp 0.1.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/LICENSE +375 -0
- package/README.md +83 -0
- package/dist/catalog/loader.js +99 -0
- package/dist/catalog/normalize.js +7 -0
- package/dist/catalog/project-root.js +47 -0
- package/dist/index.js +107 -0
- package/dist/prompts/index.js +10 -0
- package/dist/prompts/types.js +5 -0
- package/dist/resources/completions.js +30 -0
- package/dist/resources/components/constants.js +6 -0
- package/dist/resources/components/get-component-by-name.js +54 -0
- package/dist/resources/components/get-components.js +33 -0
- package/dist/resources/components/index.js +11 -0
- package/dist/resources/components/utils.js +39 -0
- package/dist/resources/define-resource.js +35 -0
- package/dist/resources/hds-icons/constants.js +6 -0
- package/dist/resources/hds-icons/get-icon-by-name.js +48 -0
- package/dist/resources/hds-icons/get-icons.js +33 -0
- package/dist/resources/hds-icons/index.js +11 -0
- package/dist/resources/hds-icons/utils.js +26 -0
- package/dist/resources/index.js +23 -0
- package/dist/resources/responses.js +34 -0
- package/dist/resources/tokens/constants.js +2 -0
- package/dist/resources/tokens/get-token-by-key.js +51 -0
- package/dist/resources/tokens/get-tokens.js +33 -0
- package/dist/resources/tokens/index.js +11 -0
- package/dist/resources/tokens/utils.js +15 -0
- package/dist/resources/types.js +5 -0
- package/dist/resources/uri.js +15 -0
- package/dist/stores/components/index.js +114 -0
- package/dist/stores/components/lookup.js +55 -0
- package/dist/stores/components/schema.js +37 -0
- package/dist/stores/docs/constants.js +163 -0
- package/dist/stores/docs/index.js +322 -0
- package/dist/stores/docs/lookup.js +148 -0
- package/dist/stores/docs/schema.js +60 -0
- package/dist/stores/docs/search-index.js +102 -0
- package/dist/stores/docs/tokenize.js +52 -0
- package/dist/stores/hds-icons/index.js +115 -0
- package/dist/stores/hds-icons/lookup.js +43 -0
- package/dist/stores/hds-icons/schema.js +31 -0
- package/dist/stores/rank.js +129 -0
- package/dist/stores/tokens/index.js +105 -0
- package/dist/stores/tokens/lookup.js +65 -0
- package/dist/stores/tokens/schema.js +42 -0
- package/dist/stores/types.js +5 -0
- package/dist/tools/components/constants.js +15 -0
- package/dist/tools/components/get-component.js +98 -0
- package/dist/tools/components/index.js +11 -0
- package/dist/tools/components/search-components.js +75 -0
- package/dist/tools/components/utils.js +42 -0
- package/dist/tools/constants.js +7 -0
- package/dist/tools/define-tool.js +17 -0
- package/dist/tools/docs/constants.js +33 -0
- package/dist/tools/docs/index.js +8 -0
- package/dist/tools/docs/read-doc.js +101 -0
- package/dist/tools/docs/search-docs.js +143 -0
- package/dist/tools/docs/utils.js +82 -0
- package/dist/tools/hds-icons/constants.js +5 -0
- package/dist/tools/hds-icons/index.js +7 -0
- package/dist/tools/hds-icons/search-icons.js +123 -0
- package/dist/tools/index.js +19 -0
- package/dist/tools/output-schema.js +11 -0
- package/dist/tools/responses.js +52 -0
- package/dist/tools/search.js +36 -0
- package/dist/tools/tokens/constants.js +5 -0
- package/dist/tools/tokens/index.js +7 -0
- package/dist/tools/tokens/search-tokens.js +144 -0
- package/dist/tools/types.js +5 -0
- package/dist/types.js +5 -0
- package/docs-catalog.json +39778 -0
- package/package.json +62 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright IBM Corp. 2021, 2026
|
|
3
|
+
* SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { normalizeLookupValue } from "../../catalog/normalize.js";
|
|
6
|
+
import { DEFAULT_CATALOG_SOURCE, createCatalogLoader, } from "../../catalog/loader.js";
|
|
7
|
+
import { getTokenLookupKeys, normalizeTokenLookupKey, toTokenRecord, } from "./lookup.js";
|
|
8
|
+
import { tokenCatalogSchema } from "./schema.js";
|
|
9
|
+
import { searchRanked } from "../rank.js";
|
|
10
|
+
const toSearchBlob = (token) => {
|
|
11
|
+
const path = token.path.join(" ");
|
|
12
|
+
const category = token.category ?? "";
|
|
13
|
+
const value = typeof token.value === "string"
|
|
14
|
+
? token.value
|
|
15
|
+
: (JSON.stringify(token.value) ?? "");
|
|
16
|
+
return [
|
|
17
|
+
token.key,
|
|
18
|
+
token.name,
|
|
19
|
+
token.cssVar ?? "",
|
|
20
|
+
path,
|
|
21
|
+
category,
|
|
22
|
+
value,
|
|
23
|
+
token.comment ?? "",
|
|
24
|
+
token.alias ?? "",
|
|
25
|
+
]
|
|
26
|
+
.join(" ")
|
|
27
|
+
.toLowerCase();
|
|
28
|
+
};
|
|
29
|
+
// the catalog key, the generated name and the CSS variable, all of which a caller may paste;
|
|
30
|
+
// the value stays in the blob so searching a hex code still finds the token that defines it
|
|
31
|
+
const toRankable = (token) => {
|
|
32
|
+
return {
|
|
33
|
+
identities: [
|
|
34
|
+
normalizeTokenLookupKey(token.key),
|
|
35
|
+
normalizeLookupValue(token.name),
|
|
36
|
+
normalizeLookupValue(token.cssVar ?? ""),
|
|
37
|
+
normalizeLookupValue(token.path.join(".")),
|
|
38
|
+
],
|
|
39
|
+
blob: toSearchBlob(token),
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
export const parseTokenCatalog = (value) => {
|
|
43
|
+
return tokenCatalogSchema.parse(value);
|
|
44
|
+
};
|
|
45
|
+
export const createTokenCatalogStore = (rows, source = DEFAULT_CATALOG_SOURCE) => {
|
|
46
|
+
const tokenRecords = rows.map((row) => toTokenRecord(row));
|
|
47
|
+
const tokenLookup = new Map();
|
|
48
|
+
for (const [index, row] of rows.entries()) {
|
|
49
|
+
const token = tokenRecords[index];
|
|
50
|
+
if (token === undefined) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
for (const key of getTokenLookupKeys(row)) {
|
|
54
|
+
tokenLookup.set(key, token);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const categories = [
|
|
58
|
+
...new Set(tokenRecords.map((token) => token.category)),
|
|
59
|
+
].sort((left, right) => left.localeCompare(right));
|
|
60
|
+
return {
|
|
61
|
+
getMeta: () => ({
|
|
62
|
+
totalTokenCount: tokenRecords.length,
|
|
63
|
+
categories,
|
|
64
|
+
source,
|
|
65
|
+
}),
|
|
66
|
+
listTokens: () =>
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
68
|
+
tokenRecords.map(({ original: _original, ...summary }) => summary),
|
|
69
|
+
getTokenByKey: (key) => {
|
|
70
|
+
return tokenLookup.get(normalizeTokenLookupKey(key)) ?? null;
|
|
71
|
+
},
|
|
72
|
+
searchTokens: ({ query, limit, type, category }) => {
|
|
73
|
+
const normalizedCategory = category === undefined ? null : normalizeLookupValue(category);
|
|
74
|
+
const { totalMatches, hits } = searchRanked({
|
|
75
|
+
records: tokenRecords,
|
|
76
|
+
query,
|
|
77
|
+
limit,
|
|
78
|
+
toRankable,
|
|
79
|
+
matches: (token) => {
|
|
80
|
+
if (type !== undefined && token.type !== type) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return (normalizedCategory === null ||
|
|
84
|
+
normalizeLookupValue(token.category ?? "") === normalizedCategory);
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
totalMatches,
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
90
|
+
hits: hits.map(({ original: _original, ...summary }) => summary),
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
const tokenCatalogLoader = createCatalogLoader({
|
|
96
|
+
specifier: "@hashicorp/design-system-tokens/dist/docs/products/tokens.json",
|
|
97
|
+
anchors: ["project-root", "components", "default"],
|
|
98
|
+
create: (value, source) => createTokenCatalogStore(parseTokenCatalog(value), source),
|
|
99
|
+
});
|
|
100
|
+
export const loadTokenCatalog = () => {
|
|
101
|
+
return tokenCatalogLoader.load();
|
|
102
|
+
};
|
|
103
|
+
export const getOrLoadTokenStore = () => {
|
|
104
|
+
return tokenCatalogLoader.getOrLoad();
|
|
105
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright IBM Corp. 2021, 2026
|
|
3
|
+
* SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { TOKEN_TYPES } from "./schema.js";
|
|
6
|
+
import { normalizeLookupValue } from "../../catalog/normalize.js";
|
|
7
|
+
const KNOWN_TOKEN_TYPE_LOOKUP = new Set(TOKEN_TYPES);
|
|
8
|
+
const trimTokenBraces = (value) => {
|
|
9
|
+
return value.replace(/^\{/u, "").replace(/\}$/u, "");
|
|
10
|
+
};
|
|
11
|
+
const trimCssVarPrefix = (value) => {
|
|
12
|
+
return value.replace(/^--/u, "");
|
|
13
|
+
};
|
|
14
|
+
export const normalizeTokenLookupKey = (value) => {
|
|
15
|
+
return trimCssVarPrefix(trimTokenBraces(normalizeLookupValue(value)));
|
|
16
|
+
};
|
|
17
|
+
export const toTokenType = (value) => {
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
return "other";
|
|
20
|
+
}
|
|
21
|
+
const normalized = value.trim();
|
|
22
|
+
if (KNOWN_TOKEN_TYPE_LOOKUP.has(normalized)) {
|
|
23
|
+
return normalized;
|
|
24
|
+
}
|
|
25
|
+
return "other";
|
|
26
|
+
};
|
|
27
|
+
export const toCssVarName = (tokenName) => {
|
|
28
|
+
return `--${tokenName}`;
|
|
29
|
+
};
|
|
30
|
+
const TOKEN_ALIAS_PATTERN = /^\{[^{}]+\}$/u;
|
|
31
|
+
export const toTokenAlias = (original) => {
|
|
32
|
+
const value = original?.$value;
|
|
33
|
+
return typeof value === "string" && TOKEN_ALIAS_PATTERN.test(value)
|
|
34
|
+
? value
|
|
35
|
+
: undefined;
|
|
36
|
+
};
|
|
37
|
+
export const toTokenSummary = (row) => {
|
|
38
|
+
const alias = toTokenAlias(row.original);
|
|
39
|
+
return {
|
|
40
|
+
key: row.key,
|
|
41
|
+
name: row.name,
|
|
42
|
+
attributes: row.attributes,
|
|
43
|
+
type: toTokenType(row.$type),
|
|
44
|
+
value: row.$value,
|
|
45
|
+
cssVar: toCssVarName(row.name),
|
|
46
|
+
category: row.attributes.category,
|
|
47
|
+
path: row.path,
|
|
48
|
+
...(row.comment === undefined ? {} : { comment: row.comment }),
|
|
49
|
+
...(alias === undefined ? {} : { alias }),
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
export const toTokenRecord = (row) => {
|
|
53
|
+
return {
|
|
54
|
+
...toTokenSummary(row),
|
|
55
|
+
...(row.original === undefined ? {} : { original: row.original }),
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
export const getTokenLookupKeys = (row) => {
|
|
59
|
+
const dotPath = row.path.join(".");
|
|
60
|
+
return [
|
|
61
|
+
normalizeTokenLookupKey(row.key),
|
|
62
|
+
normalizeTokenLookupKey(dotPath),
|
|
63
|
+
normalizeTokenLookupKey(row.name),
|
|
64
|
+
];
|
|
65
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright IBM Corp. 2021, 2026
|
|
3
|
+
* SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
export const TOKEN_TYPES = [
|
|
7
|
+
"color",
|
|
8
|
+
"cubicBezier",
|
|
9
|
+
"dimension",
|
|
10
|
+
"duration",
|
|
11
|
+
"font-family",
|
|
12
|
+
"font-weight",
|
|
13
|
+
"font-size",
|
|
14
|
+
"letter-spacing",
|
|
15
|
+
"number",
|
|
16
|
+
"other",
|
|
17
|
+
];
|
|
18
|
+
const tokenAttributesSchema = z
|
|
19
|
+
.object({
|
|
20
|
+
category: z.string().min(1),
|
|
21
|
+
})
|
|
22
|
+
.catchall(z.any());
|
|
23
|
+
const tokenOriginalSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
$type: z.string().min(1).optional(),
|
|
26
|
+
$value: z.json().optional(),
|
|
27
|
+
key: z.string().optional(),
|
|
28
|
+
})
|
|
29
|
+
.catchall(z.json());
|
|
30
|
+
export const tokenCatalogRowSchema = z
|
|
31
|
+
.object({
|
|
32
|
+
key: z.string().min(1),
|
|
33
|
+
$type: z.string().min(1).optional(),
|
|
34
|
+
$value: z.json(),
|
|
35
|
+
name: z.string().min(1),
|
|
36
|
+
comment: z.string().min(1).optional(),
|
|
37
|
+
attributes: tokenAttributesSchema,
|
|
38
|
+
path: z.array(z.string().min(1)),
|
|
39
|
+
original: tokenOriginalSchema.optional(),
|
|
40
|
+
})
|
|
41
|
+
.catchall(z.any());
|
|
42
|
+
export const tokenCatalogSchema = z.array(tokenCatalogRowSchema);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright IBM Corp. 2021, 2026
|
|
3
|
+
* SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
*/
|
|
5
|
+
export const SEARCH_COMPONENTS_TOOL_NAME = "search_hds_components";
|
|
6
|
+
export const GET_COMPONENT_TOOL_NAME = "get_hds_component";
|
|
7
|
+
/**
|
|
8
|
+
* Enum sizes in the catalog are bimodal: 164 arguments carry 15 values or fewer, and the
|
|
9
|
+
* remaining 43 carry either 112 (HTML tag names) or 675 (icon names), because the generator
|
|
10
|
+
* hoists anything over 50 into a shared value set. A cap of 25 therefore never truncates a
|
|
11
|
+
* real enum, and always truncates the two reference lists that would otherwise make a single
|
|
12
|
+
* component's API 30KB.
|
|
13
|
+
*/
|
|
14
|
+
export const MAX_ARG_VALUES = 25;
|
|
15
|
+
export const NOT_FOUND_SUGGESTION_LIMIT = 5;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright IBM Corp. 2021, 2026
|
|
3
|
+
* SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { CATALOG_TOOL_ANNOTATIONS, defineTool } from "../define-tool.js";
|
|
7
|
+
import catalogSourceOutputSchema from "../output-schema.js";
|
|
8
|
+
import { toJsonToolResponse, withSafeToolHandler, } from "../responses.js";
|
|
9
|
+
import { MAX_FILTER_LENGTH, clampFilterValue } from "../search.js";
|
|
10
|
+
import { GET_COMPONENT_TOOL_NAME, MAX_ARG_VALUES, NOT_FOUND_SUGGESTION_LIMIT, SEARCH_COMPONENTS_TOOL_NAME, } from "./constants.js";
|
|
11
|
+
import { SEARCH_ICONS_TOOL_NAME } from "../hds-icons/constants.js";
|
|
12
|
+
import { getOrLoadComponentStore } from "../../stores/components/index.js";
|
|
13
|
+
import { toSerializableComponentApi } from "./utils.js";
|
|
14
|
+
export const getComponentInputShape = {
|
|
15
|
+
name: z
|
|
16
|
+
.string()
|
|
17
|
+
.min(1)
|
|
18
|
+
.max(MAX_FILTER_LENGTH)
|
|
19
|
+
.describe("Component name. Accepts the template invocation ('Hds::Button'), the imported class name ('HdsButton'), the bare name ('button') or the module path ('hds/button'). Case-insensitive, and the Hds:: namespace is optional."),
|
|
20
|
+
};
|
|
21
|
+
const componentArgOutputSchema = z.object({
|
|
22
|
+
// the argument name without its `@` sigil, as it appears in the signature
|
|
23
|
+
name: z.string(),
|
|
24
|
+
type: z.string(),
|
|
25
|
+
required: z.boolean(),
|
|
26
|
+
values: z.array(z.string()).optional(),
|
|
27
|
+
// the real length when `values` was capped, so a caller knows what it is not seeing
|
|
28
|
+
valuesCount: z.number().int().optional(),
|
|
29
|
+
valuesTruncated: z.boolean().optional(),
|
|
30
|
+
// the module an inherited argument actually comes from, e.g. `hds/interactive`
|
|
31
|
+
inheritedFrom: z.string().optional(),
|
|
32
|
+
});
|
|
33
|
+
const componentApiOutputSchema = z.object({
|
|
34
|
+
name: z.string(),
|
|
35
|
+
modulePath: z.string(),
|
|
36
|
+
docsPath: z.string().optional(),
|
|
37
|
+
element: z.string().optional(),
|
|
38
|
+
args: z.array(componentArgOutputSchema),
|
|
39
|
+
blocks: z.array(z.object({
|
|
40
|
+
name: z.string(),
|
|
41
|
+
yields: z.array(z.object({ name: z.string(), type: z.string() })),
|
|
42
|
+
})),
|
|
43
|
+
});
|
|
44
|
+
export const getComponentOutputShape = {
|
|
45
|
+
found: z.boolean(),
|
|
46
|
+
requestedName: z.string(),
|
|
47
|
+
component: componentApiOutputSchema.optional(),
|
|
48
|
+
message: z.string().optional(),
|
|
49
|
+
suggestions: z.array(z.string()).optional(),
|
|
50
|
+
source: catalogSourceOutputSchema,
|
|
51
|
+
};
|
|
52
|
+
export const getComponentOutputSchema = z.object(getComponentOutputShape);
|
|
53
|
+
const DESCRIPTION = [
|
|
54
|
+
"Read the full API of one Helios Design System component: every argument with its type, whether it is required and which values it accepts, plus the blocks it takes and the contextual components it yields.",
|
|
55
|
+
"The data is generated from the TypeScript declarations of the installed @hashicorp/design-system-components package, so it is the authoritative answer to 'what arguments does this take' — prefer it over prose documentation, which can lag the code.",
|
|
56
|
+
`Pass the template invocation, the class name, the bare name or the module path. When the name does not resolve, ${SEARCH_COMPONENTS_TOOL_NAME} results come back as suggestions rather than an error.`,
|
|
57
|
+
`An argument's \`values\` list is capped at ${MAX_ARG_VALUES}; when \`valuesTruncated\` is true, \`valuesCount\` holds the real total. In practice only icon-name and HTML-tag arguments hit the cap — use ${SEARCH_ICONS_TOOL_NAME} to pick an icon name.`,
|
|
58
|
+
"Yielded contextual components are listed by the name they take inside the block. Their own arguments are separate catalog entries, so look up 'Hds::Alert::Title' to see what [A].Title accepts.",
|
|
59
|
+
"The catalog is read from disk and never fetched.",
|
|
60
|
+
].join(" ");
|
|
61
|
+
export const getComponent = (store, input) => {
|
|
62
|
+
const meta = store.getMeta();
|
|
63
|
+
// bounded before it is echoed back, rather than only at the declared schema
|
|
64
|
+
const requestedName = clampFilterValue(input.name);
|
|
65
|
+
const component = store.getComponentByName(requestedName);
|
|
66
|
+
if (component === null) {
|
|
67
|
+
const suggestions = store.suggestComponentNames(requestedName, NOT_FOUND_SUGGESTION_LIMIT);
|
|
68
|
+
return {
|
|
69
|
+
found: false,
|
|
70
|
+
requestedName,
|
|
71
|
+
message: `No Helios component resolves to "${requestedName}". Use ${SEARCH_COMPONENTS_TOOL_NAME} to find the exact invocation name.`,
|
|
72
|
+
...(suggestions.length === 0 ? {} : { suggestions }),
|
|
73
|
+
source: meta.source,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
found: true,
|
|
78
|
+
requestedName,
|
|
79
|
+
component: toSerializableComponentApi(component),
|
|
80
|
+
source: meta.source,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
export const createGetComponentTool = (getStore) => {
|
|
84
|
+
const executeCallback = withSafeToolHandler(GET_COMPONENT_TOOL_NAME, (input) => toJsonToolResponse(getComponent(getStore(), input)));
|
|
85
|
+
return defineTool({
|
|
86
|
+
name: GET_COMPONENT_TOOL_NAME,
|
|
87
|
+
config: {
|
|
88
|
+
title: "Get an HDS component API",
|
|
89
|
+
description: DESCRIPTION,
|
|
90
|
+
inputSchema: getComponentInputShape,
|
|
91
|
+
outputSchema: getComponentOutputShape,
|
|
92
|
+
annotations: CATALOG_TOOL_ANNOTATIONS,
|
|
93
|
+
},
|
|
94
|
+
executeCallback,
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
const getComponentTool = createGetComponentTool(getOrLoadComponentStore);
|
|
98
|
+
export default getComponentTool;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright IBM Corp. 2021, 2026
|
|
3
|
+
* SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
*/
|
|
5
|
+
import getComponentTool from "./get-component.js";
|
|
6
|
+
import searchComponentsTool from "./search-components.js";
|
|
7
|
+
const COMPONENTS_TOOLS = [
|
|
8
|
+
searchComponentsTool,
|
|
9
|
+
getComponentTool,
|
|
10
|
+
];
|
|
11
|
+
export default COMPONENTS_TOOLS;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright IBM Corp. 2021, 2026
|
|
3
|
+
* SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { CATALOG_TOOL_ANNOTATIONS, defineTool } from "../define-tool.js";
|
|
7
|
+
import { toJsonToolResponse, withSafeToolHandler } from "../responses.js";
|
|
8
|
+
import { clampSearchLimit, searchCountsOutputShape, searchLimitSchema, } from "../search.js";
|
|
9
|
+
import catalogSourceOutputSchema from "../output-schema.js";
|
|
10
|
+
import { DEFAULT_SEARCH_LIMIT, MAX_QUERY_LENGTH, MAX_SEARCH_LIMIT, } from "../constants.js";
|
|
11
|
+
import { GET_COMPONENT_TOOL_NAME, SEARCH_COMPONENTS_TOOL_NAME, } from "./constants.js";
|
|
12
|
+
import { SEARCH_DOCS_TOOL_NAME } from "../docs/constants.js";
|
|
13
|
+
import { getOrLoadComponentStore } from "../../stores/components/index.js";
|
|
14
|
+
export const searchComponentsInputShape = {
|
|
15
|
+
query: z
|
|
16
|
+
.string()
|
|
17
|
+
.min(1)
|
|
18
|
+
.describe("Component name fragment, e.g. 'button', 'advanced table', 'Hds::Flyout' or 'HdsCopyButton'. Matched as a case-insensitive substring against the invocation name, the class name, the module path and the docs route."),
|
|
19
|
+
limit: searchLimitSchema(MAX_SEARCH_LIMIT, DEFAULT_SEARCH_LIMIT),
|
|
20
|
+
};
|
|
21
|
+
const searchComponentsResultShape = z.object({
|
|
22
|
+
// the invocation name, as it is written in a template: Hds::AdvancedTable::Th
|
|
23
|
+
name: z.string(),
|
|
24
|
+
modulePath: z.string(),
|
|
25
|
+
docsPath: z.string().optional(),
|
|
26
|
+
});
|
|
27
|
+
export const searchComponentsOutputShape = {
|
|
28
|
+
query: z.string(),
|
|
29
|
+
...searchCountsOutputShape,
|
|
30
|
+
totalComponentCount: z.number().int(),
|
|
31
|
+
results: z.array(searchComponentsResultShape),
|
|
32
|
+
source: catalogSourceOutputSchema,
|
|
33
|
+
};
|
|
34
|
+
export const searchComponentsOutputSchema = z.object(searchComponentsOutputShape);
|
|
35
|
+
const DESCRIPTION = [
|
|
36
|
+
"Find Helios Design System components by name, from the component catalog shipped inside the installed @hashicorp/design-system-components package.",
|
|
37
|
+
"This is a name lookup, not a question answerer: it matches a substring against each component's invocation name, class name, module path and docs route. Pass 'advanced table' or 'Hds::Flyout', not 'how do I make a table sortable'.",
|
|
38
|
+
"Use it to confirm a component exists and to recover its exact invocation name and spelling before writing a template.",
|
|
39
|
+
`Results are intentionally concise. Follow up with ${GET_COMPONENT_TOOL_NAME} for a component's arguments, accepted values, blocks and yielded contextual components. Use ${SEARCH_DOCS_TOOL_NAME} with the result's docsPath for usage guidance, examples, accessibility guidance and design recommendations. Every documented component carries a docsPath; Hds::Yield is the sole exception.`,
|
|
40
|
+
"The catalog is read from disk and never fetched. Exact and prefix matches rank first, so if `truncated` is true, narrow the query or use the first results as the most relevant matches.",
|
|
41
|
+
].join(" ");
|
|
42
|
+
export const searchComponents = (store, input) => {
|
|
43
|
+
const meta = store.getMeta();
|
|
44
|
+
const limit = clampSearchLimit(input.limit, MAX_SEARCH_LIMIT);
|
|
45
|
+
// the query is echoed back, so it is bounded before it is repeated rather than only at the
|
|
46
|
+
// declared schema; truncating beats refusing a caller that pasted something long
|
|
47
|
+
const query = input.query.slice(0, MAX_QUERY_LENGTH);
|
|
48
|
+
const { totalMatches, hits } = store.searchComponents({ query, limit });
|
|
49
|
+
return {
|
|
50
|
+
query,
|
|
51
|
+
totalMatches,
|
|
52
|
+
returnedMatches: hits.length,
|
|
53
|
+
truncated: totalMatches > hits.length,
|
|
54
|
+
// so `totalMatches` can be read against the size of the catalog it came from
|
|
55
|
+
totalComponentCount: meta.totalComponentCount,
|
|
56
|
+
results: hits,
|
|
57
|
+
source: meta.source,
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
export const createSearchComponentsTool = (getStore) => {
|
|
61
|
+
const executeCallback = withSafeToolHandler(SEARCH_COMPONENTS_TOOL_NAME, (input) => toJsonToolResponse(searchComponents(getStore(), input)));
|
|
62
|
+
return defineTool({
|
|
63
|
+
name: SEARCH_COMPONENTS_TOOL_NAME,
|
|
64
|
+
config: {
|
|
65
|
+
title: "Search HDS components",
|
|
66
|
+
description: DESCRIPTION,
|
|
67
|
+
inputSchema: searchComponentsInputShape,
|
|
68
|
+
outputSchema: searchComponentsOutputShape,
|
|
69
|
+
annotations: CATALOG_TOOL_ANNOTATIONS,
|
|
70
|
+
},
|
|
71
|
+
executeCallback,
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
const searchComponentsTool = createSearchComponentsTool(getOrLoadComponentStore);
|
|
75
|
+
export default searchComponentsTool;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright IBM Corp. 2021, 2026
|
|
3
|
+
* SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { MAX_ARG_VALUES } from "./constants.js";
|
|
6
|
+
// `valuesCount` and `valuesTruncated` are only present when the cap actually bit, so the
|
|
7
|
+
// common case stays as small as it reads
|
|
8
|
+
export const toSerializableArg = (arg) => {
|
|
9
|
+
const isTruncated = arg.values !== undefined && arg.values.length > MAX_ARG_VALUES;
|
|
10
|
+
return {
|
|
11
|
+
name: arg.name,
|
|
12
|
+
type: arg.type,
|
|
13
|
+
required: arg.required,
|
|
14
|
+
...(arg.values === undefined
|
|
15
|
+
? {}
|
|
16
|
+
: { values: arg.values.slice(0, MAX_ARG_VALUES) }),
|
|
17
|
+
...(isTruncated
|
|
18
|
+
? { valuesCount: arg.values?.length, valuesTruncated: true }
|
|
19
|
+
: {}),
|
|
20
|
+
...(arg.inheritedFrom === undefined
|
|
21
|
+
? {}
|
|
22
|
+
: { inheritedFrom: arg.inheritedFrom }),
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
export const toSerializableComponentApi = (component) => {
|
|
26
|
+
return {
|
|
27
|
+
name: component.name,
|
|
28
|
+
modulePath: component.modulePath,
|
|
29
|
+
...(component.docsPath === undefined
|
|
30
|
+
? {}
|
|
31
|
+
: { docsPath: component.docsPath }),
|
|
32
|
+
...(component.element === undefined ? {} : { element: component.element }),
|
|
33
|
+
args: component.args.map((arg) => toSerializableArg(arg)),
|
|
34
|
+
blocks: component.blocks.map((block) => ({
|
|
35
|
+
name: block.name,
|
|
36
|
+
yields: block.yields.map((blockYield) => ({
|
|
37
|
+
name: blockYield.name,
|
|
38
|
+
type: blockYield.type,
|
|
39
|
+
})),
|
|
40
|
+
})),
|
|
41
|
+
};
|
|
42
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright IBM Corp. 2021, 2026
|
|
3
|
+
* SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
*/
|
|
5
|
+
export const defineTool = (tool) => {
|
|
6
|
+
return {
|
|
7
|
+
name: tool.name,
|
|
8
|
+
register: (server) => {
|
|
9
|
+
server.registerTool(tool.name, tool.config, tool.executeCallback);
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export const CATALOG_TOOL_ANNOTATIONS = {
|
|
14
|
+
readOnlyHint: true,
|
|
15
|
+
idempotentHint: true,
|
|
16
|
+
openWorldHint: false,
|
|
17
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright IBM Corp. 2021, 2026
|
|
3
|
+
* SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
*/
|
|
5
|
+
export const SEARCH_DOCS_TOOL_NAME = "search_hds_docs";
|
|
6
|
+
export const READ_DOCS_TOOL_NAME = "read_hds_docs";
|
|
7
|
+
export const DEFAULT_SEARCH_LIMIT = 8;
|
|
8
|
+
// cap keeps a search from ever approaching limits
|
|
9
|
+
export const MAX_SEARCH_LIMIT = 25;
|
|
10
|
+
export const MAX_QUERY_LENGTH = 2_000;
|
|
11
|
+
export const DEFAULT_READ_MAX_BYTES = 16_000;
|
|
12
|
+
export const MIN_READ_MAX_BYTES = 500;
|
|
13
|
+
export const MAX_READ_MAX_BYTES = 60_000;
|
|
14
|
+
export const VERSION_HISTORY_TAB = "Version history";
|
|
15
|
+
export const DOCS_TABS = [
|
|
16
|
+
"Accessibility",
|
|
17
|
+
"Code",
|
|
18
|
+
"Components",
|
|
19
|
+
"Content",
|
|
20
|
+
"Core concepts",
|
|
21
|
+
"Design tokens",
|
|
22
|
+
"Figma Libraries",
|
|
23
|
+
"Flight Icons",
|
|
24
|
+
"Guidelines",
|
|
25
|
+
"Interaction concepts",
|
|
26
|
+
"Library",
|
|
27
|
+
"Palette",
|
|
28
|
+
"Research",
|
|
29
|
+
"Resources",
|
|
30
|
+
"Specifications",
|
|
31
|
+
"Validation",
|
|
32
|
+
VERSION_HISTORY_TAB,
|
|
33
|
+
];
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright IBM Corp. 2021, 2026
|
|
3
|
+
* SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { defineTool } from "../define-tool.js";
|
|
7
|
+
import { toDocumentToolResponse, toJsonToolResponse, withSafeToolHandler, } from "../responses.js";
|
|
8
|
+
import { clampFilterValue, MAX_FILTER_LENGTH } from "../search.js";
|
|
9
|
+
import { DEFAULT_READ_MAX_BYTES, MAX_READ_MAX_BYTES, MIN_READ_MAX_BYTES, READ_DOCS_TOOL_NAME, SEARCH_DOCS_TOOL_NAME, } from "./constants.js";
|
|
10
|
+
import { getOrLoadDocsStore } from "../../stores/docs/index.js";
|
|
11
|
+
import { getHeadingPath } from "../../stores/docs/lookup.js";
|
|
12
|
+
import { selectChunkContent, toSerializablePageLinks } from "./utils.js";
|
|
13
|
+
export const readDocInputShape = {
|
|
14
|
+
id: z
|
|
15
|
+
.string()
|
|
16
|
+
.min(1)
|
|
17
|
+
.max(MAX_FILTER_LENGTH)
|
|
18
|
+
.describe(`Passage id from a ${SEARCH_DOCS_TOOL_NAME} result, e.g. 'components/button#code/how-to-use-this-component/full-width'.`),
|
|
19
|
+
includeChildren: z
|
|
20
|
+
.boolean()
|
|
21
|
+
.default(false)
|
|
22
|
+
.describe("Also return the passages nested under this heading. Use when the passage is a section header whose detail lives in its subsections."),
|
|
23
|
+
maxBytes: z
|
|
24
|
+
.number()
|
|
25
|
+
.int()
|
|
26
|
+
.min(MIN_READ_MAX_BYTES)
|
|
27
|
+
.max(MAX_READ_MAX_BYTES)
|
|
28
|
+
.default(DEFAULT_READ_MAX_BYTES)
|
|
29
|
+
.describe(`Byte cap on the returned markdown (${MIN_READ_MAX_BYTES}-${MAX_READ_MAX_BYTES}). Defaults to ${DEFAULT_READ_MAX_BYTES}.`),
|
|
30
|
+
};
|
|
31
|
+
const DESCRIPTION = [
|
|
32
|
+
"Read one passage of the bundled Helios Design System documentation by id, as markdown.",
|
|
33
|
+
`Ids come from ${SEARCH_DOCS_TOOL_NAME}; run that first rather than guessing an id.`,
|
|
34
|
+
"The returned markdown keeps the <Doc::*> and <Hds::*> usage examples and the inlined code snippets, which is what search snippets strip out.",
|
|
35
|
+
"The corpus is a snapshot committed into this server: nothing is fetched. The first content block carries the canonical URL and the bundledAt timestamp so a client can fetch the live page itself.",
|
|
36
|
+
].join(" ");
|
|
37
|
+
export const readDoc = (store, input) => {
|
|
38
|
+
const meta = store.getMeta();
|
|
39
|
+
const requestedId = clampFilterValue(input.id);
|
|
40
|
+
const chunk = store.getChunkById(requestedId);
|
|
41
|
+
if (chunk === null) {
|
|
42
|
+
return toJsonToolResponse({
|
|
43
|
+
found: false,
|
|
44
|
+
requestedId,
|
|
45
|
+
message: `No documentation passage found for id "${requestedId}". Ids come from ${SEARCH_DOCS_TOOL_NAME} results.`,
|
|
46
|
+
suggestions: store.suggestChunkIds(requestedId),
|
|
47
|
+
bundledAt: meta.bundledAt,
|
|
48
|
+
siteBaseUrl: meta.siteBaseUrl,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const children = store.getChildChunks(chunk.id);
|
|
52
|
+
const requested = input.includeChildren ? [chunk, ...children] : [chunk];
|
|
53
|
+
const { content, truncated, includedIds, omittedIds } = selectChunkContent(requested, input.maxBytes);
|
|
54
|
+
const page = store.getPageByRoute(chunk.route);
|
|
55
|
+
const links = page === null ? undefined : toSerializablePageLinks(page);
|
|
56
|
+
return toDocumentToolResponse({
|
|
57
|
+
found: true,
|
|
58
|
+
id: chunk.id,
|
|
59
|
+
title: chunk.pageTitle,
|
|
60
|
+
route: chunk.route,
|
|
61
|
+
docsPath: chunk.route,
|
|
62
|
+
section: chunk.section,
|
|
63
|
+
...(chunk.tab === undefined ? {} : { tab: chunk.tab }),
|
|
64
|
+
headingPath: getHeadingPath(chunk),
|
|
65
|
+
...(chunk.level === undefined ? {} : { level: chunk.level }),
|
|
66
|
+
url: chunk.url,
|
|
67
|
+
pageUrl: chunk.pageUrl,
|
|
68
|
+
...(page === null || page.related.length === 0
|
|
69
|
+
? {}
|
|
70
|
+
: { relatedDocsPaths: page.related }),
|
|
71
|
+
...(links === undefined ? {} : { links }),
|
|
72
|
+
...(page?.status === undefined ? {} : { status: page.status }),
|
|
73
|
+
includedChunkIds: includedIds,
|
|
74
|
+
...(omittedIds.length === 0 ? {} : { omittedChunkIds: omittedIds }),
|
|
75
|
+
childChunkCount: children.length,
|
|
76
|
+
contentBytes: Buffer.byteLength(content, "utf8"),
|
|
77
|
+
truncated,
|
|
78
|
+
bundledAt: meta.bundledAt,
|
|
79
|
+
siteBaseUrl: meta.siteBaseUrl,
|
|
80
|
+
provenance: `Bundled snapshot of ${meta.siteBaseUrl} taken at ${meta.bundledAt}. Fetch ${chunk.url} for the live page.`,
|
|
81
|
+
}, content);
|
|
82
|
+
};
|
|
83
|
+
export const createReadDocTool = (getStore) => {
|
|
84
|
+
const executeCallback = withSafeToolHandler(READ_DOCS_TOOL_NAME, (input) => readDoc(getStore(), input));
|
|
85
|
+
return defineTool({
|
|
86
|
+
name: READ_DOCS_TOOL_NAME,
|
|
87
|
+
config: {
|
|
88
|
+
title: "Read an HDS documentation passage",
|
|
89
|
+
description: DESCRIPTION,
|
|
90
|
+
inputSchema: readDocInputShape,
|
|
91
|
+
annotations: {
|
|
92
|
+
readOnlyHint: true,
|
|
93
|
+
idempotentHint: true,
|
|
94
|
+
openWorldHint: false,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
executeCallback,
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
const readDocTool = createReadDocTool(getOrLoadDocsStore);
|
|
101
|
+
export default readDocTool;
|