bestax-mcp 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -0
- package/data/catalog.json +1869 -0
- package/data/components/Autocomplete.json +604 -0
- package/data/components/Avatar.json +220 -0
- package/data/components/Avatars.json +294 -0
- package/data/components/Badge.json +261 -0
- package/data/components/Block.json +105 -0
- package/data/components/Box.json +157 -0
- package/data/components/Breadcrumb.json +155 -0
- package/data/components/Button.json +599 -0
- package/data/components/Buttons.json +533 -0
- package/data/components/Card.json +506 -0
- package/data/components/Carousel.json +512 -0
- package/data/components/Cell.json +154 -0
- package/data/components/Checkbox.json +176 -0
- package/data/components/Checkboxes.json +240 -0
- package/data/components/Code.json +128 -0
- package/data/components/Collapse.json +218 -0
- package/data/components/Column.json +284 -0
- package/data/components/Columns.json +542 -0
- package/data/components/ConfigProvider.json +16 -0
- package/data/components/Container.json +149 -0
- package/data/components/Content.json +228 -0
- package/data/components/Control.json +484 -0
- package/data/components/DateInput.json +872 -0
- package/data/components/DateTimeInput.json +952 -0
- package/data/components/Delete.json +153 -0
- package/data/components/Dialog.json +285 -0
- package/data/components/Divider.json +68 -0
- package/data/components/Dropdown.json +353 -0
- package/data/components/Emphasis.json +99 -0
- package/data/components/Field.json +539 -0
- package/data/components/Figure.json +143 -0
- package/data/components/File.json +403 -0
- package/data/components/Footer.json +109 -0
- package/data/components/Grid.json +357 -0
- package/data/components/Hero.json +340 -0
- package/data/components/Icon.json +232 -0
- package/data/components/IconText.json +307 -0
- package/data/components/Image.json +166 -0
- package/data/components/Input.json +583 -0
- package/data/components/Level.json +332 -0
- package/data/components/Link.json +154 -0
- package/data/components/LinkButton.json +247 -0
- package/data/components/ListItem.json +101 -0
- package/data/components/Loading.json +323 -0
- package/data/components/Media.json +345 -0
- package/data/components/Menu.json +317 -0
- package/data/components/Message.json +303 -0
- package/data/components/Modal.json +444 -0
- package/data/components/Navbar.json +996 -0
- package/data/components/Notification.json +165 -0
- package/data/components/Numberinput.json +364 -0
- package/data/components/OrderedList.json +189 -0
- package/data/components/Pagination.json +619 -0
- package/data/components/Panel.json +582 -0
- package/data/components/Paragraph.json +107 -0
- package/data/components/Pre.json +114 -0
- package/data/components/Progress.json +137 -0
- package/data/components/Radio.json +164 -0
- package/data/components/Radios.json +240 -0
- package/data/components/Rate.json +466 -0
- package/data/components/Reveal.json +177 -0
- package/data/components/Section.json +123 -0
- package/data/components/Select.json +526 -0
- package/data/components/Sidebar.json +488 -0
- package/data/components/Skeleton.json +162 -0
- package/data/components/Slider.json +534 -0
- package/data/components/Span.json +100 -0
- package/data/components/Steps.json +452 -0
- package/data/components/Strong.json +105 -0
- package/data/components/SubTitle.json +233 -0
- package/data/components/Switch.json +291 -0
- package/data/components/Table.json +490 -0
- package/data/components/Tabs.json +592 -0
- package/data/components/Tag.json +197 -0
- package/data/components/Taginput.json +587 -0
- package/data/components/Tags.json +181 -0
- package/data/components/TextArea.json +483 -0
- package/data/components/Theme.json +65 -0
- package/data/components/TimeInput.json +906 -0
- package/data/components/Title.json +240 -0
- package/data/components/Toast.json +308 -0
- package/data/components/Tooltip.json +212 -0
- package/data/components/UnorderedList.json +149 -0
- package/data/components/Valid value constants.json +16 -0
- package/data/components/classNames.json +65 -0
- package/data/components/useBulmaClasses.json +169 -0
- package/data/components/usePrefixedClassNames.json +25 -0
- package/data/skills/bestax-custom-component/SKILL.md +196 -0
- package/data/skills/bestax-custom-component/examples/stat-card.tsx +105 -0
- package/data/skills/bestax-custom-component/references/api.md +90 -0
- package/data/skills/bestax-custom-component/references/component-catalog.md +149 -0
- package/data/skills/bestax-custom-component/references/library-contributor.md +395 -0
- package/data/skills/bestax-custom-component/references/patterns.md +136 -0
- package/data/skills/bestax-form/SKILL.md +254 -0
- package/data/skills/bestax-form/references/api.md +103 -0
- package/data/skills/bestax-form/references/patterns.md +210 -0
- package/data/skills/bestax-icons/SKILL.md +80 -0
- package/data/skills/bestax-icons/examples/icon-usage.tsx +80 -0
- package/data/skills/bestax-icons/references/icon-libraries.md +131 -0
- package/data/skills/bestax-layout-scaffold/SKILL.md +196 -0
- package/data/skills/bestax-layout-scaffold/examples/app-shell.tsx +86 -0
- package/data/skills/bestax-layout-scaffold/examples/card-grid.tsx +106 -0
- package/data/skills/bestax-layout-scaffold/examples/centered.tsx +56 -0
- package/data/skills/bestax-layout-scaffold/examples/content-page.tsx +90 -0
- package/data/skills/bestax-layout-scaffold/examples/landing.tsx +122 -0
- package/data/skills/bestax-layout-scaffold/references/archetypes.md +246 -0
- package/data/skills/bestax-layout-scaffold/references/layout-components.md +269 -0
- package/data/skills/bestax-migrate/SKILL.md +78 -0
- package/data/skills/bestax-migrate/references/component-map.md +91 -0
- package/data/skills/bestax-migrate/references/css-migration.md +100 -0
- package/data/skills/bestax-migrate/references/prop-map.md +48 -0
- package/data/skills/bestax-migrate/references/unmappables.md +124 -0
- package/data/skills/bestax-optimize/SKILL.md +131 -0
- package/data/skills/bestax-optimize/references/modular-build.md +173 -0
- package/data/skills/bestax-theming/SKILL.md +117 -0
- package/data/skills/bestax-theming/examples/dark-mode.tsx +38 -0
- package/data/skills/bestax-theming/examples/theme-config.tsx +60 -0
- package/data/skills/bestax-theming/references/css-variables.md +466 -0
- package/data/skills/bestax-theming/references/themeable-components.md +119 -0
- package/data/skills.json +148 -0
- package/dist/data.d.ts +131 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +111 -0
- package/dist/format.d.ts +37 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +178 -0
- package/dist/helper-props.d.ts +57 -0
- package/dist/helper-props.d.ts.map +1 -0
- package/dist/helper-props.js +249 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/search.d.ts +32 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +194 -0
- package/dist/server.d.ts +28 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +616 -0
- package/dist/version.d.ts +29 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +147 -0
- package/package.json +70 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bestax MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Nine tools, four resource templates, and one prompt per Agent Skill — all
|
|
5
|
+
* built from the generated index in `data/`, so nothing here carries a list
|
|
6
|
+
* that can drift from the library.
|
|
7
|
+
*
|
|
8
|
+
* Two things shape the surface:
|
|
9
|
+
*
|
|
10
|
+
* 1. PROGRESSIVE DISCLOSURE. `search_bestax` and `list_components` are cheap
|
|
11
|
+
* and every result names the tool to call next; the expensive things (full
|
|
12
|
+
* prop tables, 24 usage examples, a 23 KB skill reference) are separate
|
|
13
|
+
* calls. A server that returns everything on the first call spends the
|
|
14
|
+
* context the client needs for the actual task.
|
|
15
|
+
*
|
|
16
|
+
* 2. MORE THAN A PROP LOOKUP. bestax is a component library with opinions —
|
|
17
|
+
* helper props instead of inline styles, `--bulma-*` overrides instead of
|
|
18
|
+
* hand-written CSS, Field/Control composition instead of bare inputs. Those
|
|
19
|
+
* live in the skills, so the skills ship as prompts and resources rather
|
|
20
|
+
* than being left to a separate install step.
|
|
21
|
+
*/
|
|
22
|
+
import { McpServer, ResourceTemplate, } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
import { loadCatalog, loadComponent, loadSkills, loadSkillFile, resolveName, } from './data.js';
|
|
25
|
+
import { errorResult, renderCatalog, renderComponent, renderCssVars, renderExamples, renderPart, renderSkills, table, textResult, } from './format.js';
|
|
26
|
+
import { GROUP_NAMES, renderHelperDefault, renderHelperGroup, resolveGroup, } from './helper-props.js';
|
|
27
|
+
import { searchAll, suggest } from './search.js';
|
|
28
|
+
import { resolveVersions, versionNote } from './version.js';
|
|
29
|
+
const ALL_KINDS = [
|
|
30
|
+
'component',
|
|
31
|
+
'prop',
|
|
32
|
+
'example',
|
|
33
|
+
'css-var',
|
|
34
|
+
'skill',
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* Bounds on the free-text arguments, because this is a single-threaded stdio process and
|
|
38
|
+
* every one of them feeds a linear scan.
|
|
39
|
+
*
|
|
40
|
+
* Unbounded, `query` reached `new RegExp` per term per haystack over ~1,500 props and ~900
|
|
41
|
+
* examples: 3,000 terms blocked the event loop for 5.8 s, and a single ~40 KB token threw
|
|
42
|
+
* "Regular expression too large" and reflected the whole query back as the error text. An
|
|
43
|
+
* unbounded `name` fed the not-found suggester's edit-distance scan — 1 MB blocked for 9.3 s.
|
|
44
|
+
* Rejecting at the schema costs the model one retry with a readable message; the alternative
|
|
45
|
+
* costs every other request on the connection.
|
|
46
|
+
*
|
|
47
|
+
* Both limits sit far above any real value: the longest component name is 20 characters and
|
|
48
|
+
* a useful search query is a handful of words.
|
|
49
|
+
*/
|
|
50
|
+
const MAX_NAME = 100;
|
|
51
|
+
const MAX_QUERY = 200;
|
|
52
|
+
/**
|
|
53
|
+
* Every tool here answers from a read-only index that ships inside this package: nothing
|
|
54
|
+
* writes, nothing calls out to the network, and the same arguments always reach the same
|
|
55
|
+
* committed files. Saying so is what lets a client stop asking the user to approve a
|
|
56
|
+
* documentation lookup — without these hints, a well-behaved client must treat every call as
|
|
57
|
+
* potentially destructive and prompt.
|
|
58
|
+
*/
|
|
59
|
+
const READ_ONLY = {
|
|
60
|
+
readOnlyHint: true,
|
|
61
|
+
destructiveHint: false,
|
|
62
|
+
idempotentHint: true,
|
|
63
|
+
openWorldHint: false,
|
|
64
|
+
};
|
|
65
|
+
const INCLUDES = [
|
|
66
|
+
'props',
|
|
67
|
+
'examples',
|
|
68
|
+
'cssVars',
|
|
69
|
+
'accessibility',
|
|
70
|
+
'related',
|
|
71
|
+
// Helpers only: the full reference prose. Opt-in because useBulmaClasses' is 51,054
|
|
72
|
+
// characters and it used to arrive on the default call whether asked for or not.
|
|
73
|
+
'reference',
|
|
74
|
+
];
|
|
75
|
+
/**
|
|
76
|
+
* What the `bestax://components/{name}` resource renders. Everything except `reference` —
|
|
77
|
+
* a resource takes no arguments, so a caller has no way to decline it, and for
|
|
78
|
+
* useBulmaClasses that one member is the difference between ~2.5 KB and 53 KB.
|
|
79
|
+
*/
|
|
80
|
+
const RESOURCE_INCLUDES = INCLUDES.filter(i => i !== 'reference');
|
|
81
|
+
/** Load every component record. Only `search_bestax` needs this much. */
|
|
82
|
+
async function allComponents() {
|
|
83
|
+
const catalog = await loadCatalog();
|
|
84
|
+
return Promise.all(catalog.components.map(c => loadComponent(c.name)));
|
|
85
|
+
}
|
|
86
|
+
export async function createServer(options = {}) {
|
|
87
|
+
const catalog = await loadCatalog();
|
|
88
|
+
const skills = await loadSkills();
|
|
89
|
+
const versions = await resolveVersions(catalog.generatedFrom.version, options.cwd);
|
|
90
|
+
const note = () => versionNote(versions);
|
|
91
|
+
const server = new McpServer({ name: 'bestax', version: catalog.generatedFrom.version }, {
|
|
92
|
+
// "Start with list_components" is measured, not assumed. Across twenty eval runs
|
|
93
|
+
// (eval/agent-loop/runs-v2/aggregate.md) list_components was called by 10/10 builders
|
|
94
|
+
// and search_bestax by 0/10 — despite this string previously naming search as the
|
|
95
|
+
// entry point. A cold-start builder's first question is "what does this library
|
|
96
|
+
// have?", which is enumeration; search answers "what is the thing called X?", which
|
|
97
|
+
// only arises once you know what you are missing. The docs were wrong about the first
|
|
98
|
+
// move, not the builders.
|
|
99
|
+
instructions: `Documentation for @allxsmith/bestax-bulma ${catalog.generatedFrom.version} — ` +
|
|
100
|
+
`React components for Bulma v1. Start with list_components to see what exists; ` +
|
|
101
|
+
`its output names the tool to call next. Use search_bestax when you need a ` +
|
|
102
|
+
`component whose name you do not know. Before writing any styling by hand, call ` +
|
|
103
|
+
`get_helper_props: this library expects spacing, colour and typography ` +
|
|
104
|
+
`to go through helper props rather than inline styles, and theming to go ` +
|
|
105
|
+
`through --bulma-* variables rather than custom CSS.`,
|
|
106
|
+
});
|
|
107
|
+
// Appended to every list_components answer, for the same reason the inline-style rule
|
|
108
|
+
// moved into get_helper_props: put guidance where the traffic already is.
|
|
109
|
+
//
|
|
110
|
+
// The eval (eval/agent-loop/runs-v2/aggregate.md) measured which tools ten MCP-only
|
|
111
|
+
// builders actually reached for. list_components: 10/10. search_bestax, which the server's
|
|
112
|
+
// own instructions name as the entry point: 0/10 — every builder enumerated instead of
|
|
113
|
+
// searching, and its results are what would otherwise have named the next tool.
|
|
114
|
+
// get_css_variables: 1/10, in an arm where every single run themed the site.
|
|
115
|
+
//
|
|
116
|
+
// The skills line is the important one. Builders pulled bestax-theming 10/10 and
|
|
117
|
+
// bestax-layout-scaffold 2/10, and layout-scaffold is the only place the no-inline-style
|
|
118
|
+
// rule lives — the two runs that pulled it are the only two that wrote zero inline styles.
|
|
119
|
+
// Nothing was telling them it existed at the moment they were choosing components.
|
|
120
|
+
const NEXT_STEPS = `\n\n---\n\n` +
|
|
121
|
+
`**Next:** \`get_component({ name })\` for props, examples and CSS variables · ` +
|
|
122
|
+
`\`get_props\` for one table, including compound parts like \`Navbar.Brand\` · ` +
|
|
123
|
+
`\`get_examples\` for working \`tsx\` · \`get_css_variables\` **before** theming a ` +
|
|
124
|
+
`component by hand.\n\n` +
|
|
125
|
+
`**Before writing layout or styling**, read the how-to guides — they carry rules the ` +
|
|
126
|
+
`prop tables do not. \`get_skill({ name: "bestax-layout-scaffold" })\` has the ` +
|
|
127
|
+
`inline-style → helper-prop mapping (this library expects props, not \`style={{}}\` ` +
|
|
128
|
+
`and not hand-written Bulma classes); ` +
|
|
129
|
+
`\`get_skill({ name: "bestax-custom-component" })\` has the spine every reusable ` +
|
|
130
|
+
`component needs. \`list_skills\` has all seven.`;
|
|
131
|
+
// The three components that lose to a core-Bulma near-miss, for the unfiltered
|
|
132
|
+
// list_components answer only.
|
|
133
|
+
//
|
|
134
|
+
// Why it exists: eval/agent-loop/runs-v4 measured bestax-layout-scaffold reaching 4
|
|
135
|
+
// builders in 10, and that arm came out a flat null against its control. The four it did
|
|
136
|
+
// reach used LinkButton 4/4 where the other six went 1/6, and every miss shipped one of
|
|
137
|
+
// the substitutions the table names. Guidance only counts where it is delivered, and
|
|
138
|
+
// list_components is called by 10/10.
|
|
139
|
+
//
|
|
140
|
+
// Why unfiltered only: the footer is a fixed cost on a variable-size answer.
|
|
141
|
+
// list_components({category:"grid"}) returns 241 characters of listing, and an
|
|
142
|
+
// unconditional near-miss block made that response 86% boilerplate. On the unfiltered call
|
|
143
|
+
// it is ~11%, and that is also the moment a builder is actually choosing components — a
|
|
144
|
+
// category filter means they have already chosen.
|
|
145
|
+
//
|
|
146
|
+
// Sourced from the skill rather than paraphrased, for the same reason as inlineStyleRule
|
|
147
|
+
// below: the hardcoded copy this replaces had already drifted from it, naming LinkButton
|
|
148
|
+
// with no variant prop and dropping the controlled-component alternative. The provenance
|
|
149
|
+
// paragraph is skipped — how our eval went is not something a builder needs in context.
|
|
150
|
+
const nearMissRule = async () => {
|
|
151
|
+
const heading = '## Three components core Bulma will talk you out of';
|
|
152
|
+
try {
|
|
153
|
+
const md = await loadSkillFile('bestax-layout-scaffold');
|
|
154
|
+
const start = md.indexOf(heading);
|
|
155
|
+
if (start < 0)
|
|
156
|
+
return '';
|
|
157
|
+
const end = md.indexOf('\n## ', start + 1);
|
|
158
|
+
const section = md.slice(start, end < 0 ? undefined : end).trimEnd();
|
|
159
|
+
// Keep the heading and everything from the table onward; drop the prose between them.
|
|
160
|
+
const table = section.indexOf('\n| ');
|
|
161
|
+
if (table < 0)
|
|
162
|
+
return '';
|
|
163
|
+
return `\n\n${heading}\n${section.slice(table)}`;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return '';
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
// The single highest-leverage thing this server says, prepended to every
|
|
170
|
+
// get_helper_props answer.
|
|
171
|
+
//
|
|
172
|
+
// Why here: a 20-run eval (eval/agent-loop/runs-v2/aggregate.md) found that all ten
|
|
173
|
+
// MCP-only runs called get_helper_props, and eight of them still wrote 46-162 inline
|
|
174
|
+
// styles — while all ten skills-channel runs wrote zero. The two MCP runs that wrote zero
|
|
175
|
+
// were exactly the two that pulled bestax-layout-scaffold, which is the only place the
|
|
176
|
+
// prohibition and its mapping table live. Skills auto-trigger on description match; an MCP
|
|
177
|
+
// tool has to be asked for, and nobody asks for layout guidance when the task does not
|
|
178
|
+
// sound like layout. So the rule is moved to the tool everyone already calls.
|
|
179
|
+
//
|
|
180
|
+
// Sourced from the skill rather than copied, so the two cannot drift. Failure is
|
|
181
|
+
// non-fatal: a missing or restructured skill costs the preamble, never the reference.
|
|
182
|
+
const inlineStyleRule = async () => {
|
|
183
|
+
const heading = '## Inline style → helper prop mapping';
|
|
184
|
+
try {
|
|
185
|
+
const md = await loadSkillFile('bestax-layout-scaffold');
|
|
186
|
+
const start = md.indexOf(heading);
|
|
187
|
+
if (start < 0)
|
|
188
|
+
return '';
|
|
189
|
+
const end = md.indexOf('\n## ', start + 1);
|
|
190
|
+
const section = md.slice(start, end < 0 ? undefined : end).trimEnd();
|
|
191
|
+
return (`**Do not write inline \`style={{ … }}\`, and do not hand-write Bulma ` +
|
|
192
|
+
`\`className\`s.** Spacing, colour, typography, flex and visibility are ` +
|
|
193
|
+
`props on every component. Translate the declaration you were about to ` +
|
|
194
|
+
`inline using the table below; if nothing matches (\`maxWidth\`, a ` +
|
|
195
|
+
`one-off gradient), add a named class to the project stylesheet and pass ` +
|
|
196
|
+
`it via \`className\` — still never inline \`style\`.\n\n${section}\n\n---\n\n`);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return '';
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const notFound = async (name) => {
|
|
203
|
+
const names = catalog.components.map(c => c.name);
|
|
204
|
+
const near = suggest(name, names);
|
|
205
|
+
return errorResult(`No component named "${name}". ` +
|
|
206
|
+
(near.length
|
|
207
|
+
? `Did you mean ${near.map(n => `"${n}"`).join(', ')}? `
|
|
208
|
+
: '') +
|
|
209
|
+
`Call list_components to see all ${names.length}.`);
|
|
210
|
+
};
|
|
211
|
+
// -- Discovery ------------------------------------------------------------
|
|
212
|
+
server.registerTool('search_bestax', {
|
|
213
|
+
title: 'Search bestax',
|
|
214
|
+
annotations: READ_ONLY,
|
|
215
|
+
description: 'Search components, props, usage examples, CSS variables and agent ' +
|
|
216
|
+
'skills at once. The entry point — each hit names the tool to call for ' +
|
|
217
|
+
'the full thing. Use this before guessing a component name.',
|
|
218
|
+
inputSchema: {
|
|
219
|
+
query: z
|
|
220
|
+
.string()
|
|
221
|
+
.max(MAX_QUERY)
|
|
222
|
+
.describe('What you are looking for, e.g. "date picker" or "spacing"'),
|
|
223
|
+
kind: z
|
|
224
|
+
.enum(['component', 'prop', 'example', 'css-var', 'skill', 'all'])
|
|
225
|
+
.optional()
|
|
226
|
+
.describe('Restrict to one kind of result. Defaults to all.'),
|
|
227
|
+
limit: z.number().int().min(1).max(50).optional(),
|
|
228
|
+
},
|
|
229
|
+
}, async ({ query, kind, limit }) => {
|
|
230
|
+
const kinds = !kind || kind === 'all' ? ALL_KINDS : [kind];
|
|
231
|
+
// Prop and example hits need every component record; the other kinds are
|
|
232
|
+
// answerable from the catalog alone, so don't pay for them.
|
|
233
|
+
const needsAll = kinds.includes('prop') || kinds.includes('example');
|
|
234
|
+
const hits = searchAll(query, catalog, needsAll ? await allComponents() : [], skills, kinds).slice(0, limit ?? 15);
|
|
235
|
+
if (!hits.length) {
|
|
236
|
+
return textResult(`No matches for "${query}". Call list_components for the full catalog, ` +
|
|
237
|
+
`or list_skills for the how-to guides.`, note());
|
|
238
|
+
}
|
|
239
|
+
return textResult(table(['Kind', 'Name', 'Detail', 'Next'], hits.map(h => [h.kind, h.name, h.detail, `\`${h.next}\``])), note());
|
|
240
|
+
});
|
|
241
|
+
server.registerTool('list_components', {
|
|
242
|
+
title: 'List components',
|
|
243
|
+
annotations: READ_ONLY,
|
|
244
|
+
description: 'Every documented component with a one-line purpose, grouped by ' +
|
|
245
|
+
'category. Cheap — use it to find the right name before calling ' +
|
|
246
|
+
'get_component.',
|
|
247
|
+
inputSchema: {
|
|
248
|
+
category: z
|
|
249
|
+
.string()
|
|
250
|
+
.optional()
|
|
251
|
+
.describe('elements, components, form, columns, grid, layout, helpers'),
|
|
252
|
+
},
|
|
253
|
+
}, async ({ category }) => {
|
|
254
|
+
const entries = category
|
|
255
|
+
? catalog.components.filter(c => c.category.toLowerCase() === category.toLowerCase())
|
|
256
|
+
: catalog.components;
|
|
257
|
+
if (!entries.length) {
|
|
258
|
+
return errorResult(`No category "${category}". Available: ${catalog.categories
|
|
259
|
+
.map(c => c.id)
|
|
260
|
+
.join(', ')}.`);
|
|
261
|
+
}
|
|
262
|
+
const nearMiss = category ? '' : await nearMissRule();
|
|
263
|
+
return textResult(renderCatalog(entries) + NEXT_STEPS + nearMiss, note());
|
|
264
|
+
});
|
|
265
|
+
// -- Component detail -----------------------------------------------------
|
|
266
|
+
server.registerTool('get_component', {
|
|
267
|
+
title: 'Get component documentation',
|
|
268
|
+
annotations: READ_ONLY,
|
|
269
|
+
description: 'Import statement, summary and prop table for one component. Add ' +
|
|
270
|
+
'`include` for examples, CSS variables, accessibility notes or related ' +
|
|
271
|
+
'components.',
|
|
272
|
+
inputSchema: {
|
|
273
|
+
name: z
|
|
274
|
+
.string()
|
|
275
|
+
.max(MAX_NAME)
|
|
276
|
+
.describe('Component name, e.g. "Button" or "Navbar"'),
|
|
277
|
+
include: z
|
|
278
|
+
.array(z.enum(INCLUDES))
|
|
279
|
+
.optional()
|
|
280
|
+
.describe('Extra sections. Defaults to ["props"].'),
|
|
281
|
+
},
|
|
282
|
+
}, async ({ name, include }) => {
|
|
283
|
+
const resolved = await resolveName(name);
|
|
284
|
+
if (!resolved)
|
|
285
|
+
return notFound(name);
|
|
286
|
+
const record = await loadComponent(resolved);
|
|
287
|
+
return textResult(renderComponent(record, include ?? ['props']), note());
|
|
288
|
+
});
|
|
289
|
+
server.registerTool('get_props', {
|
|
290
|
+
title: 'Get props',
|
|
291
|
+
annotations: READ_ONLY,
|
|
292
|
+
description: 'The prop table for a component, or for one part of a compound family ' +
|
|
293
|
+
'(pass a dot-path like "Navbar.Brand"). Cheaper than get_component when ' +
|
|
294
|
+
'the props are all you need.',
|
|
295
|
+
inputSchema: {
|
|
296
|
+
component: z
|
|
297
|
+
.string()
|
|
298
|
+
.max(MAX_NAME)
|
|
299
|
+
.describe('Component name, e.g. "Navbar"'),
|
|
300
|
+
path: z
|
|
301
|
+
.string()
|
|
302
|
+
.max(MAX_NAME)
|
|
303
|
+
.optional()
|
|
304
|
+
.describe('Dot-path of a subcomponent, e.g. "Navbar.Brand"'),
|
|
305
|
+
},
|
|
306
|
+
}, async ({ component, path }) => {
|
|
307
|
+
const resolved = await resolveName(component);
|
|
308
|
+
if (!resolved)
|
|
309
|
+
return notFound(component);
|
|
310
|
+
const record = await loadComponent(resolved);
|
|
311
|
+
if (record.kind === 'helper') {
|
|
312
|
+
return textResult(
|
|
313
|
+
// Points at get_helper_props, not get_component. get_component on a helper is
|
|
314
|
+
// itself now a pointer here, so sending a builder there made this a three-hop
|
|
315
|
+
// chain for one question.
|
|
316
|
+
`\`${record.name}\` is a hook/utility, not a component — it has no ` +
|
|
317
|
+
`prop table. Call get_helper_props() for every helper prop with its ` +
|
|
318
|
+
`accepted values, or get_helper_props({ group }) for one area.`, note());
|
|
319
|
+
}
|
|
320
|
+
// A dot-path given as `path`, or folded into `component` ("Navbar.Brand").
|
|
321
|
+
const wanted = path ?? (component.includes('.') ? component : null);
|
|
322
|
+
if (!wanted) {
|
|
323
|
+
const [root, ...subs] = record.parts;
|
|
324
|
+
const body = [renderPart(root, { heading: false })];
|
|
325
|
+
if (subs.length) {
|
|
326
|
+
body.push(`**Subcomponents:** ${subs.map(s => `\`${s.path}\``).join(', ')}.`);
|
|
327
|
+
}
|
|
328
|
+
return textResult(`# ${record.name}\n\n${body.join('\n\n')}`, note());
|
|
329
|
+
}
|
|
330
|
+
const part = record.parts.find(p => p.path === wanted) ??
|
|
331
|
+
record.parts.find(p => p.path.toLowerCase() === wanted.toLowerCase());
|
|
332
|
+
if (!part) {
|
|
333
|
+
return errorResult(`"${wanted}" is not a part of ${record.name}. Available: ${record.parts
|
|
334
|
+
.map(p => p.path)
|
|
335
|
+
.join(', ')}.`);
|
|
336
|
+
}
|
|
337
|
+
return textResult(renderPart(part), note());
|
|
338
|
+
});
|
|
339
|
+
server.registerTool('get_examples', {
|
|
340
|
+
title: 'Get usage examples',
|
|
341
|
+
annotations: READ_ONLY,
|
|
342
|
+
description: "Working examples from the component's documentation page. Every one " +
|
|
343
|
+
'is executed on the docs site, so they compile against this version.',
|
|
344
|
+
inputSchema: {
|
|
345
|
+
component: z.string().max(MAX_NAME),
|
|
346
|
+
query: z
|
|
347
|
+
.string()
|
|
348
|
+
.max(MAX_QUERY)
|
|
349
|
+
.optional()
|
|
350
|
+
.describe('Filter by example heading or code, e.g. "loading"'),
|
|
351
|
+
limit: z.number().int().min(1).max(30).optional(),
|
|
352
|
+
},
|
|
353
|
+
}, async ({ component, query, limit }) => {
|
|
354
|
+
const resolved = await resolveName(component);
|
|
355
|
+
if (!resolved)
|
|
356
|
+
return notFound(component);
|
|
357
|
+
const record = await loadComponent(resolved);
|
|
358
|
+
const q = query?.toLowerCase();
|
|
359
|
+
const matches = q
|
|
360
|
+
? record.examples.filter(e => e.title.toLowerCase().includes(q) ||
|
|
361
|
+
e.code.toLowerCase().includes(q))
|
|
362
|
+
: record.examples;
|
|
363
|
+
if (!matches.length) {
|
|
364
|
+
return textResult(query
|
|
365
|
+
? `No ${record.name} examples match "${query}". It has ` +
|
|
366
|
+
`${record.examples.length}: ${record.examples
|
|
367
|
+
.map(e => e.title)
|
|
368
|
+
.join(', ')}.`
|
|
369
|
+
: `${record.name} has no examples on its documentation page.`, note());
|
|
370
|
+
}
|
|
371
|
+
return textResult(`# ${record.name} examples\n\n${renderExamples(matches.slice(0, limit ?? 8))}`, note());
|
|
372
|
+
});
|
|
373
|
+
// -- Styling --------------------------------------------------------------
|
|
374
|
+
server.registerTool('get_css_variables', {
|
|
375
|
+
title: 'Get CSS and Sass variables',
|
|
376
|
+
annotations: READ_ONLY,
|
|
377
|
+
description: 'The --bulma-* custom properties a component reads, with their Sass ' +
|
|
378
|
+
'names and defaults. This is how you restyle bestax — reach for these ' +
|
|
379
|
+
'before writing custom CSS. Omit `component` to search across all of them.',
|
|
380
|
+
inputSchema: {
|
|
381
|
+
component: z.string().max(MAX_NAME).optional(),
|
|
382
|
+
query: z
|
|
383
|
+
.string()
|
|
384
|
+
.max(MAX_QUERY)
|
|
385
|
+
.optional()
|
|
386
|
+
.describe('Substring of a variable name, e.g. "radius"'),
|
|
387
|
+
},
|
|
388
|
+
}, async ({ component, query }) => {
|
|
389
|
+
if (component) {
|
|
390
|
+
const resolved = await resolveName(component);
|
|
391
|
+
if (!resolved)
|
|
392
|
+
return notFound(component);
|
|
393
|
+
const record = await loadComponent(resolved);
|
|
394
|
+
const vars = query
|
|
395
|
+
? record.cssVars.filter(v => v.css.toLowerCase().includes(query.toLowerCase()))
|
|
396
|
+
: record.cssVars;
|
|
397
|
+
if (!vars.length) {
|
|
398
|
+
return textResult(`${record.name} registers no CSS variables${query ? ` matching "${query}"` : ''}.`, note());
|
|
399
|
+
}
|
|
400
|
+
return textResult(`# ${record.name} CSS variables\n\n${renderCssVars(vars)}`, note());
|
|
401
|
+
}
|
|
402
|
+
if (!query) {
|
|
403
|
+
return errorResult("Pass `component` for one component's variables, or `query` to " +
|
|
404
|
+
'search all of them by name.');
|
|
405
|
+
}
|
|
406
|
+
const q = query.toLowerCase();
|
|
407
|
+
const rows = Object.entries(catalog.cssVarIndex)
|
|
408
|
+
.filter(([css]) => css.toLowerCase().includes(q))
|
|
409
|
+
.slice(0, 60)
|
|
410
|
+
.map(([css, owner]) => [`\`${css}\``, owner]);
|
|
411
|
+
if (!rows.length) {
|
|
412
|
+
return textResult(`No CSS variable matches "${query}".`, note());
|
|
413
|
+
}
|
|
414
|
+
return textResult(table(['CSS Variable', 'Declared by'], rows), note());
|
|
415
|
+
});
|
|
416
|
+
server.registerTool('get_helper_props', {
|
|
417
|
+
title: 'Get helper props',
|
|
418
|
+
annotations: READ_ONLY,
|
|
419
|
+
description: 'The helper props every bestax component accepts — spacing, colour, ' +
|
|
420
|
+
'typography, flexbox, visibility — and their valid values. Call this ' +
|
|
421
|
+
'BEFORE writing an inline style or a utility class by hand; this ' +
|
|
422
|
+
'library expects those to be props.',
|
|
423
|
+
inputSchema: {
|
|
424
|
+
group: z
|
|
425
|
+
.string()
|
|
426
|
+
.optional()
|
|
427
|
+
.describe('Filter to one area, e.g. "spacing", "flex", "color"'),
|
|
428
|
+
},
|
|
429
|
+
}, async ({ group }) => {
|
|
430
|
+
// The helper reference lives on the hook's own docs page, which is prose
|
|
431
|
+
// with signature blocks rather than a props table.
|
|
432
|
+
const record = await loadComponent('useBulmaClasses');
|
|
433
|
+
const doc = record.doc ?? '';
|
|
434
|
+
const rule = await inlineStyleRule();
|
|
435
|
+
if (!group)
|
|
436
|
+
return textResult(rule + renderHelperDefault(doc), note());
|
|
437
|
+
const resolved = resolveGroup(group);
|
|
438
|
+
if (!resolved) {
|
|
439
|
+
// No rule preamble here: this answered nothing, and 3,832 characters of
|
|
440
|
+
// prohibition on top of a 124-character "try again" was 97% overhead.
|
|
441
|
+
return textResult(`No helper-prop group matches "${group}". Groups: ` +
|
|
442
|
+
`${GROUP_NAMES.join(', ')}. Call get_helper_props with no argument ` +
|
|
443
|
+
`for every prop at once.`, note());
|
|
444
|
+
}
|
|
445
|
+
return textResult(rule + renderHelperGroup(doc, resolved), note());
|
|
446
|
+
});
|
|
447
|
+
// -- Skills ---------------------------------------------------------------
|
|
448
|
+
server.registerTool('list_skills', {
|
|
449
|
+
title: 'List agent skills',
|
|
450
|
+
annotations: READ_ONLY,
|
|
451
|
+
description: 'The bestax Agent Skills — task-level guides for forms, theming, ' +
|
|
452
|
+
'layout scaffolding, icons, custom components, migration and CSS size. ' +
|
|
453
|
+
'Read one with get_skill when a task matches.',
|
|
454
|
+
}, async () => textResult(renderSkills(skills), note()));
|
|
455
|
+
server.registerTool('get_skill', {
|
|
456
|
+
title: 'Get an agent skill',
|
|
457
|
+
annotations: READ_ONLY,
|
|
458
|
+
description: "A skill's instructions, or one of its reference documents. Load the " +
|
|
459
|
+
'skill first; pull a reference only when you need that depth.',
|
|
460
|
+
inputSchema: {
|
|
461
|
+
name: z
|
|
462
|
+
.string()
|
|
463
|
+
.describe('Skill name, e.g. "bestax-theming" or "theming"'),
|
|
464
|
+
reference: z
|
|
465
|
+
.string()
|
|
466
|
+
.optional()
|
|
467
|
+
.describe('Reference id from list_skills, e.g. "css-variables"'),
|
|
468
|
+
},
|
|
469
|
+
}, async ({ name, reference }) => {
|
|
470
|
+
const key = name.toLowerCase().replace(/^bestax-/, '');
|
|
471
|
+
const skill = skills.find(s => s.promptName.toLowerCase() === key ||
|
|
472
|
+
s.name.toLowerCase() === name.toLowerCase());
|
|
473
|
+
if (!skill) {
|
|
474
|
+
return errorResult(`No skill named "${name}". Available: ${skills
|
|
475
|
+
.map(s => s.name)
|
|
476
|
+
.join(', ')}.`);
|
|
477
|
+
}
|
|
478
|
+
if (!reference) {
|
|
479
|
+
return textResult(await loadSkillFile(skill.dir), note());
|
|
480
|
+
}
|
|
481
|
+
const ref = skill.references.find(r => r.id === reference) ??
|
|
482
|
+
skill.examples.find(r => r.id === reference);
|
|
483
|
+
if (!ref) {
|
|
484
|
+
return errorResult(`${skill.name} has no reference "${reference}". Available: ${[
|
|
485
|
+
...skill.references,
|
|
486
|
+
...skill.examples,
|
|
487
|
+
]
|
|
488
|
+
.map(r => r.id)
|
|
489
|
+
.join(', ')}.`);
|
|
490
|
+
}
|
|
491
|
+
return textResult(await loadSkillFile(skill.dir, ref.file), note());
|
|
492
|
+
});
|
|
493
|
+
// -- Resources ------------------------------------------------------------
|
|
494
|
+
server.registerResource('catalog', 'bestax://catalog', {
|
|
495
|
+
title: 'Component catalog',
|
|
496
|
+
description: 'Every bestax component with a one-line purpose.',
|
|
497
|
+
mimeType: 'text/markdown',
|
|
498
|
+
}, async (uri) => ({
|
|
499
|
+
contents: [
|
|
500
|
+
{
|
|
501
|
+
uri: uri.href,
|
|
502
|
+
mimeType: 'text/markdown',
|
|
503
|
+
text: renderCatalog(catalog.components),
|
|
504
|
+
},
|
|
505
|
+
],
|
|
506
|
+
}));
|
|
507
|
+
server.registerResource('component', new ResourceTemplate('bestax://components/{name}', {
|
|
508
|
+
list: undefined,
|
|
509
|
+
complete: {
|
|
510
|
+
name: value => catalog.components
|
|
511
|
+
.filter(c => c.name.toLowerCase().startsWith(value.toLowerCase()))
|
|
512
|
+
.slice(0, 20)
|
|
513
|
+
.map(c => c.name),
|
|
514
|
+
},
|
|
515
|
+
}), {
|
|
516
|
+
title: 'Component documentation',
|
|
517
|
+
description: 'Full documentation for one component.',
|
|
518
|
+
mimeType: 'text/markdown',
|
|
519
|
+
}, async (uri, { name }) => {
|
|
520
|
+
const resolved = await resolveName(String(name));
|
|
521
|
+
if (!resolved)
|
|
522
|
+
throw new Error(`No component named "${String(name)}"`);
|
|
523
|
+
const record = await loadComponent(resolved);
|
|
524
|
+
return {
|
|
525
|
+
contents: [
|
|
526
|
+
{
|
|
527
|
+
uri: uri.href,
|
|
528
|
+
mimeType: 'text/markdown',
|
|
529
|
+
text: renderComponent(record, RESOURCE_INCLUDES),
|
|
530
|
+
},
|
|
531
|
+
],
|
|
532
|
+
};
|
|
533
|
+
});
|
|
534
|
+
server.registerResource('skill', new ResourceTemplate('bestax://skills/{name}', {
|
|
535
|
+
list: undefined,
|
|
536
|
+
complete: { name: () => skills.map(s => s.name) },
|
|
537
|
+
}), {
|
|
538
|
+
title: 'Agent skill',
|
|
539
|
+
description: 'A bestax Agent Skill.',
|
|
540
|
+
mimeType: 'text/markdown',
|
|
541
|
+
}, async (uri, { name }) => {
|
|
542
|
+
const skill = skills.find(s => s.name === String(name));
|
|
543
|
+
if (!skill)
|
|
544
|
+
throw new Error(`No skill named "${String(name)}"`);
|
|
545
|
+
return {
|
|
546
|
+
contents: [
|
|
547
|
+
{
|
|
548
|
+
uri: uri.href,
|
|
549
|
+
mimeType: 'text/markdown',
|
|
550
|
+
text: await loadSkillFile(skill.dir),
|
|
551
|
+
},
|
|
552
|
+
],
|
|
553
|
+
};
|
|
554
|
+
});
|
|
555
|
+
server.registerResource('skill-reference', new ResourceTemplate('bestax://skills/{name}/references/{ref}', {
|
|
556
|
+
list: undefined,
|
|
557
|
+
complete: { name: () => skills.map(s => s.name) },
|
|
558
|
+
}), {
|
|
559
|
+
title: 'Agent skill reference',
|
|
560
|
+
description: 'A reference document belonging to a bestax Agent Skill.',
|
|
561
|
+
mimeType: 'text/markdown',
|
|
562
|
+
}, async (uri, { name, ref }) => {
|
|
563
|
+
const skill = skills.find(s => s.name === String(name));
|
|
564
|
+
if (!skill)
|
|
565
|
+
throw new Error(`No skill named "${String(name)}"`);
|
|
566
|
+
const doc = skill.references.find(r => r.id === String(ref));
|
|
567
|
+
if (!doc) {
|
|
568
|
+
throw new Error(`${skill.name} has no reference "${String(ref)}"`);
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
contents: [
|
|
572
|
+
{
|
|
573
|
+
uri: uri.href,
|
|
574
|
+
mimeType: 'text/markdown',
|
|
575
|
+
text: await loadSkillFile(skill.dir, doc.file),
|
|
576
|
+
},
|
|
577
|
+
],
|
|
578
|
+
};
|
|
579
|
+
});
|
|
580
|
+
// -- Prompts, one per skill ----------------------------------------------
|
|
581
|
+
//
|
|
582
|
+
// Generated from the manifest, never listed here. skills/CLAUDE.md counts the
|
|
583
|
+
// roster as already duplicated in five places; this would have been a sixth.
|
|
584
|
+
for (const skill of skills) {
|
|
585
|
+
server.registerPrompt(skill.promptName, {
|
|
586
|
+
title: skill.name,
|
|
587
|
+
description: skill.description,
|
|
588
|
+
argsSchema: {
|
|
589
|
+
task: z
|
|
590
|
+
.string()
|
|
591
|
+
.optional()
|
|
592
|
+
.describe('What you want to build, in your own words'),
|
|
593
|
+
},
|
|
594
|
+
}, async ({ task }) => ({
|
|
595
|
+
messages: [
|
|
596
|
+
{
|
|
597
|
+
role: 'user',
|
|
598
|
+
content: {
|
|
599
|
+
type: 'text',
|
|
600
|
+
text: [
|
|
601
|
+
await loadSkillFile(skill.dir),
|
|
602
|
+
'---',
|
|
603
|
+
task
|
|
604
|
+
? `Apply the guidance above to this task: ${task}`
|
|
605
|
+
: 'Apply the guidance above to the task at hand.',
|
|
606
|
+
`Reference documents are available via ` +
|
|
607
|
+
`get_skill({ name: "${skill.name}", reference: "…" }); ` +
|
|
608
|
+
`component props via get_props.`,
|
|
609
|
+
].join('\n\n'),
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
],
|
|
613
|
+
}));
|
|
614
|
+
}
|
|
615
|
+
return server;
|
|
616
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface VersionInfo {
|
|
2
|
+
/** The version this server's index was generated from. */
|
|
3
|
+
indexed: string;
|
|
4
|
+
/** The version resolved in the user's project, if one was found. */
|
|
5
|
+
installed: string | null;
|
|
6
|
+
/** How far apart they are. */
|
|
7
|
+
drift: 'none' | 'patch' | 'minor' | 'major' | 'unknown';
|
|
8
|
+
}
|
|
9
|
+
export declare function sanitizeVersion(value: unknown): string | null;
|
|
10
|
+
/**
|
|
11
|
+
* Walk up from `cwd` looking for the package in a node_modules directory.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately a filesystem walk rather than `createRequire(cwd).resolve`:
|
|
14
|
+
* pnpm's isolated linker puts the real package under `.pnpm` and exposes it
|
|
15
|
+
* through a symlink, and `resolve` from a directory that is not itself a
|
|
16
|
+
* package can throw for reasons that have nothing to do with the answer.
|
|
17
|
+
*/
|
|
18
|
+
export declare function findInstalledVersion(cwd?: string): Promise<string | null>;
|
|
19
|
+
export declare function resolveVersions(indexed: string, cwd?: string): Promise<VersionInfo>;
|
|
20
|
+
/**
|
|
21
|
+
* A one-line warning to append to a response, or null when there is nothing
|
|
22
|
+
* worth saying.
|
|
23
|
+
*
|
|
24
|
+
* Patch drift is deliberately silent: a patch release of this library does not
|
|
25
|
+
* move props, and a line on every single response is a line a model learns to
|
|
26
|
+
* skip — which is exactly what must not happen when the drift is real.
|
|
27
|
+
*/
|
|
28
|
+
export declare function versionNote(info: VersionInfo): string | null;
|
|
29
|
+
//# sourceMappingURL=version.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,WAAW;IAC1B,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,8BAA8B;IAC9B,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;CACzD;AAkBD,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAI7D;AAcD;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,GAAE,MAAsB,GAC1B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAwBxB;AAyCD,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,WAAW,CAAC,CAetB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAS5D"}
|