@epilot/volt-ui-mcp 0.1.3 → 0.3.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/index.js +13 -335
- package/lib.d.ts +40 -0
- package/lib.js +499 -0
- package/package.json +12 -2
- package/registry.json +5100 -4413
- package/tools.js +226 -0
package/lib.js
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure query functions over the volt-ui registry.
|
|
3
|
+
*
|
|
4
|
+
* Shared by the stdio bin (index.js) and the remote MCP route on the docs app
|
|
5
|
+
* (docs/app/api/[transport]/route.ts). Transport wiring stays in the consumers —
|
|
6
|
+
* this module is plain data-in/data-out so both servers answer identically.
|
|
7
|
+
*
|
|
8
|
+
* Response-size discipline (remote clients hard-truncate large tool results):
|
|
9
|
+
* - list/search_components return lean summaries; search_components is capped to
|
|
10
|
+
* the top SEARCH_LIMIT matches (the true total stays in `count`)
|
|
11
|
+
* - list_tokens is topics-first: a bare call returns only the category index;
|
|
12
|
+
* token pages (DEFAULT_TOKEN_LIMIT) come back only when drilling into a group
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_TOKEN_LIMIT = 200
|
|
16
|
+
export const MAX_TOKEN_LIMIT = 500
|
|
17
|
+
|
|
18
|
+
// Coerce a tool argument to a string before string ops — tolerant of null,
|
|
19
|
+
// undefined, or a non-string value from a misbehaving client. The transports
|
|
20
|
+
// already coerce, but these query functions are exported and called directly.
|
|
21
|
+
const asText = (value) =>
|
|
22
|
+
typeof value === "string" ? value : value == null ? "" : String(value)
|
|
23
|
+
|
|
24
|
+
// decodeURIComponent throws on malformed escapes (e.g. "%ZZ"); fall back to the
|
|
25
|
+
// raw segment so a bad resource URI yields a clean "not found", not a crash.
|
|
26
|
+
const safeDecode = (value) => {
|
|
27
|
+
try {
|
|
28
|
+
return decodeURIComponent(value)
|
|
29
|
+
} catch {
|
|
30
|
+
return value
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// One-line descriptions of each token group, surfaced in the list_tokens
|
|
35
|
+
// `groups` index so consumers can see the full token taxonomy (not just colors)
|
|
36
|
+
// and learn how each group is used.
|
|
37
|
+
const GROUP_DESCRIPTIONS = {
|
|
38
|
+
spacing:
|
|
39
|
+
"Intent-based gap scale: element (inside a component), group (between items in a group), layout (between groups/sections). Shipped as gap-* and p-* utilities ONLY (not m-/space-); prefix volt- in consumer apps (e.g. volt-gap-group-2).",
|
|
40
|
+
palette: "Raw color scale steps per hue (1–12 plus alpha aN).",
|
|
41
|
+
semantic:
|
|
42
|
+
"Semantic color roles (accent/gray/success/error/warning/info × soft/solid/surface/contrast/…).",
|
|
43
|
+
utility: "Color utility aliases resolved from the palette.",
|
|
44
|
+
other:
|
|
45
|
+
"Uncategorized tokens — discovered from the CSS but not yet assigned a known group; surfaced (never dropped) so they can be classified.",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createQueries(registry) {
|
|
49
|
+
if (
|
|
50
|
+
!registry ||
|
|
51
|
+
!Array.isArray(registry.components) ||
|
|
52
|
+
registry.components.length === 0
|
|
53
|
+
) {
|
|
54
|
+
// Fail loud: an empty/missing registry must never masquerade as a working
|
|
55
|
+
// server with 0 components (e.g. a file-tracing miss on the host).
|
|
56
|
+
throw new Error(
|
|
57
|
+
"volt-ui-mcp: registry is missing or empty — refusing to serve. " +
|
|
58
|
+
"Regenerate with `bun run build:mcp` from the repo root."
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const tokens = registry.tokens || []
|
|
63
|
+
const guidelines = registry.guidelines || []
|
|
64
|
+
|
|
65
|
+
// Category index computed once — exposes which token groups exist (spacing,
|
|
66
|
+
// palette, semantic, utility, …) so consumers can tell the surface is
|
|
67
|
+
// complete and discover non-color tokens.
|
|
68
|
+
const tokenGroups = (() => {
|
|
69
|
+
const counts = {}
|
|
70
|
+
for (const token of tokens) {
|
|
71
|
+
counts[token.group] = (counts[token.group] || 0) + 1
|
|
72
|
+
}
|
|
73
|
+
return Object.keys(counts)
|
|
74
|
+
.sort()
|
|
75
|
+
.map((group) => ({
|
|
76
|
+
group,
|
|
77
|
+
count: counts[group],
|
|
78
|
+
description: GROUP_DESCRIPTIONS[group] || "",
|
|
79
|
+
}))
|
|
80
|
+
})()
|
|
81
|
+
|
|
82
|
+
// Lean summary for browse/search results — name/title/description only. The
|
|
83
|
+
// heavier fields (docs/source paths, urls, props, examples) come from
|
|
84
|
+
// get_component when the agent drills in.
|
|
85
|
+
function componentSummary(component) {
|
|
86
|
+
return {
|
|
87
|
+
name: component.name,
|
|
88
|
+
title: component.title,
|
|
89
|
+
description: component.description,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function findComponent(name) {
|
|
94
|
+
return registry.components.find(
|
|
95
|
+
(component) => component.name.toLowerCase() === name.toLowerCase()
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function listComponents(query = "") {
|
|
100
|
+
const q = asText(query).trim().toLowerCase()
|
|
101
|
+
const components = registry.components.filter((component) => {
|
|
102
|
+
if (!q) {
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
return (
|
|
106
|
+
component.name.toLowerCase().includes(q) ||
|
|
107
|
+
(component.description ?? "").toLowerCase().includes(q)
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
count: components.length,
|
|
113
|
+
components: components.map(componentSummary),
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Lean default projection. composition/constraints/canonicalExample are
|
|
118
|
+
// high-signal and default-on (they close the composition-error gap); the
|
|
119
|
+
// bulky examples[] and recommendedTokens are opt-in via include=. aliases/
|
|
120
|
+
// useCases are search-only and never returned here.
|
|
121
|
+
function projectComponent(component, includeSet) {
|
|
122
|
+
const projected = {
|
|
123
|
+
name: component.name,
|
|
124
|
+
title: component.title,
|
|
125
|
+
description: component.description,
|
|
126
|
+
docsPath: component.docsPath,
|
|
127
|
+
docSlug: component.docSlug,
|
|
128
|
+
documentationUrl: component.documentationUrl,
|
|
129
|
+
apiReferenceUrl: component.apiReferenceUrl,
|
|
130
|
+
sourcePaths: component.sourcePaths,
|
|
131
|
+
props: component.props,
|
|
132
|
+
}
|
|
133
|
+
if (component.composition) {
|
|
134
|
+
projected.composition = component.composition
|
|
135
|
+
}
|
|
136
|
+
if (component.constraints) {
|
|
137
|
+
projected.constraints = component.constraints
|
|
138
|
+
}
|
|
139
|
+
if (component.canonicalExample) {
|
|
140
|
+
projected.canonicalExample = component.canonicalExample
|
|
141
|
+
}
|
|
142
|
+
const all = includeSet.has("all")
|
|
143
|
+
if (all || includeSet.has("examples")) {
|
|
144
|
+
projected.examples = component.examples
|
|
145
|
+
}
|
|
146
|
+
if ((all || includeSet.has("tokens")) && component.recommendedTokens) {
|
|
147
|
+
projected.recommendedTokens = component.recommendedTokens
|
|
148
|
+
}
|
|
149
|
+
return projected
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getComponent(name, include = "") {
|
|
153
|
+
const nameText = asText(name)
|
|
154
|
+
if (!nameText.trim()) {
|
|
155
|
+
return { error: "Component name is required." }
|
|
156
|
+
}
|
|
157
|
+
const component = findComponent(nameText)
|
|
158
|
+
if (!component) {
|
|
159
|
+
return { error: `Component not found: ${nameText}` }
|
|
160
|
+
}
|
|
161
|
+
const includeSet = new Set(
|
|
162
|
+
String(include || "")
|
|
163
|
+
.split(",")
|
|
164
|
+
.map((part) => part.trim().toLowerCase())
|
|
165
|
+
.filter(Boolean)
|
|
166
|
+
)
|
|
167
|
+
return projectComponent(component, includeSet)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const SEARCH_LIMIT = 25
|
|
171
|
+
|
|
172
|
+
function searchComponents(query) {
|
|
173
|
+
const qFull = asText(query).trim().toLowerCase()
|
|
174
|
+
if (!qFull) {
|
|
175
|
+
return { count: 0, returned: 0, hasMore: false, components: [] }
|
|
176
|
+
}
|
|
177
|
+
const qWords = qFull.split(/\s+/).filter(Boolean)
|
|
178
|
+
|
|
179
|
+
// Tiered scoring so an EXACT name match always beats an incidental intent
|
|
180
|
+
// substring (the old bug: "button" ranked Tooltip #1 via its useCase text).
|
|
181
|
+
// Exact alias phrase beats alias substring (so "dropdown" → Select, not
|
|
182
|
+
// Popover's "dropdown panel"). A final all/any-words tier gives recall for
|
|
183
|
+
// non-verbatim multi-word intents.
|
|
184
|
+
const scored = []
|
|
185
|
+
for (const component of registry.components) {
|
|
186
|
+
const name = (component.name || "").toLowerCase()
|
|
187
|
+
const title = (component.title || "").toLowerCase()
|
|
188
|
+
const description = (component.description || "").toLowerCase()
|
|
189
|
+
const aliases = (component.aliases || []).map((a) => a.toLowerCase())
|
|
190
|
+
const useCases = (component.useCases || []).map((u) => u.toLowerCase())
|
|
191
|
+
const aliasText = aliases.concat(useCases).join(" ")
|
|
192
|
+
const propNames = (component.props || [])
|
|
193
|
+
.map((prop) => prop.name)
|
|
194
|
+
.join(" ")
|
|
195
|
+
.toLowerCase()
|
|
196
|
+
|
|
197
|
+
let score = 0
|
|
198
|
+
if (name === qFull) {
|
|
199
|
+
score = 100
|
|
200
|
+
} else if (name.startsWith(qFull)) {
|
|
201
|
+
score = 85
|
|
202
|
+
} else if (aliases.some((a) => a === qFull)) {
|
|
203
|
+
score = 80
|
|
204
|
+
} else if (name.includes(qFull)) {
|
|
205
|
+
score = 70
|
|
206
|
+
} else if (aliasText.includes(qFull)) {
|
|
207
|
+
score = 55
|
|
208
|
+
} else if (title.includes(qFull)) {
|
|
209
|
+
score = 45
|
|
210
|
+
} else {
|
|
211
|
+
const hay = [name, aliasText, title, description, propNames].join(" ")
|
|
212
|
+
const hits = qWords.filter((w) => hay.includes(w)).length
|
|
213
|
+
if (qWords.length > 0 && hits === qWords.length) {
|
|
214
|
+
score = 30 + hits
|
|
215
|
+
} else if (hits > 0) {
|
|
216
|
+
score = 10 + hits
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (score > 0) {
|
|
220
|
+
scored.push({ component, score })
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
scored.sort(
|
|
225
|
+
(a, b) =>
|
|
226
|
+
b.score - a.score || a.component.name.localeCompare(b.component.name)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
// Summaries only, capped to the most relevant — full objects blow up remote
|
|
230
|
+
// clients on broad queries; callers drill in via get_component. `count` is
|
|
231
|
+
// the true match total so the caller knows results were capped.
|
|
232
|
+
const page = scored.slice(0, SEARCH_LIMIT)
|
|
233
|
+
return {
|
|
234
|
+
count: scored.length,
|
|
235
|
+
returned: page.length,
|
|
236
|
+
hasMore: scored.length > page.length,
|
|
237
|
+
components: page.map((entry) => componentSummary(entry.component)),
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function listTokens({
|
|
242
|
+
query = "",
|
|
243
|
+
theme = "",
|
|
244
|
+
group = "",
|
|
245
|
+
limit = DEFAULT_TOKEN_LIMIT,
|
|
246
|
+
offset = 0,
|
|
247
|
+
} = {}) {
|
|
248
|
+
const q = asText(query).trim().toLowerCase()
|
|
249
|
+
const t = asText(theme).trim().toLowerCase()
|
|
250
|
+
const g = asText(group).trim().toLowerCase()
|
|
251
|
+
|
|
252
|
+
// Drill-down level 0: with no query and no group, return only the category
|
|
253
|
+
// index (cheap) so an agent can see the full taxonomy and pick a category
|
|
254
|
+
// to drill into — instead of receiving a ~200-token color dump.
|
|
255
|
+
if (!q && !g) {
|
|
256
|
+
return {
|
|
257
|
+
mode: "categories",
|
|
258
|
+
groups: tokenGroups,
|
|
259
|
+
hint: "Drill in with list_tokens({ group }) for a category's tokens, or search_tokens(<name|intent>) / get_token(<name>) to narrow large color groups.",
|
|
260
|
+
tokens: [],
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const matched = tokens.filter((token) => {
|
|
265
|
+
if (t && token.theme.toLowerCase() !== t) {
|
|
266
|
+
return false
|
|
267
|
+
}
|
|
268
|
+
if (g && token.group.toLowerCase() !== g) {
|
|
269
|
+
return false
|
|
270
|
+
}
|
|
271
|
+
if (!q) {
|
|
272
|
+
return true
|
|
273
|
+
}
|
|
274
|
+
// Match name/value AND intent metadata (utility/usage) so non-color
|
|
275
|
+
// tokens are discoverable by intent — e.g. "gap" or "spacing" → the
|
|
276
|
+
// gap-token scale, "between items" → the group tier.
|
|
277
|
+
return (
|
|
278
|
+
token.name.toLowerCase().includes(q) ||
|
|
279
|
+
token.value.toLowerCase().includes(q) ||
|
|
280
|
+
(token.utility || "").toLowerCase().includes(q) ||
|
|
281
|
+
(token.usage || "").toLowerCase().includes(q)
|
|
282
|
+
)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
const safeLimit = Math.min(
|
|
286
|
+
Math.max(1, Number(limit) || DEFAULT_TOKEN_LIMIT),
|
|
287
|
+
MAX_TOKEN_LIMIT
|
|
288
|
+
)
|
|
289
|
+
const safeOffset = Math.max(0, Number(offset) || 0)
|
|
290
|
+
const page = matched.slice(safeOffset, safeOffset + safeLimit)
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
count: matched.length,
|
|
294
|
+
returned: page.length,
|
|
295
|
+
offset: safeOffset,
|
|
296
|
+
limit: safeLimit,
|
|
297
|
+
hasMore: safeOffset + page.length < matched.length,
|
|
298
|
+
// Always present so consumers can see the full token taxonomy and tell
|
|
299
|
+
// the surface is complete (not color-only).
|
|
300
|
+
groups: tokenGroups,
|
|
301
|
+
tokens: page,
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function getToken(name) {
|
|
306
|
+
const target = asText(name).trim().toLowerCase()
|
|
307
|
+
if (!target) {
|
|
308
|
+
return { error: "Token name is required." }
|
|
309
|
+
}
|
|
310
|
+
const matches = tokens.filter(
|
|
311
|
+
(token) => token.name.toLowerCase() === target
|
|
312
|
+
)
|
|
313
|
+
if (matches.length === 0) {
|
|
314
|
+
return { error: `Token not found: ${name}` }
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
name,
|
|
318
|
+
tokens: matches,
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function searchTokens(query) {
|
|
323
|
+
const qFull = asText(query).trim().toLowerCase()
|
|
324
|
+
if (!qFull) {
|
|
325
|
+
return {
|
|
326
|
+
count: 0,
|
|
327
|
+
returned: 0,
|
|
328
|
+
hasMore: false,
|
|
329
|
+
groups: tokenGroups,
|
|
330
|
+
tokens: [],
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Word-tokenized (like search_components): a match is the whole phrase OR
|
|
334
|
+
// every query word appearing in the token's searchable text, so reworded
|
|
335
|
+
// multi-word intents ("between groups", "icon to label") still resolve.
|
|
336
|
+
const qWords = qFull.split(/\s+/).filter(Boolean)
|
|
337
|
+
const matched = tokens.filter((token) => {
|
|
338
|
+
const hay = [token.name, token.value, token.utility, token.usage]
|
|
339
|
+
.filter(Boolean)
|
|
340
|
+
.join(" ")
|
|
341
|
+
.toLowerCase()
|
|
342
|
+
return hay.includes(qFull) || qWords.every((w) => hay.includes(w))
|
|
343
|
+
})
|
|
344
|
+
const page = matched.slice(0, DEFAULT_TOKEN_LIMIT)
|
|
345
|
+
return {
|
|
346
|
+
count: matched.length,
|
|
347
|
+
returned: page.length,
|
|
348
|
+
hasMore: matched.length > page.length,
|
|
349
|
+
groups: tokenGroups,
|
|
350
|
+
tokens: page,
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function guidelineSummary(guideline) {
|
|
355
|
+
return {
|
|
356
|
+
slug: guideline.slug,
|
|
357
|
+
title: guideline.title,
|
|
358
|
+
description: guideline.description,
|
|
359
|
+
headings: guideline.headings,
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function listGuidelines(query = "") {
|
|
364
|
+
const q = asText(query).trim().toLowerCase()
|
|
365
|
+
const matched = guidelines.filter((guideline) => {
|
|
366
|
+
if (!q) {
|
|
367
|
+
return true
|
|
368
|
+
}
|
|
369
|
+
const haystack = [
|
|
370
|
+
guideline.slug,
|
|
371
|
+
guideline.title,
|
|
372
|
+
guideline.description,
|
|
373
|
+
(guideline.headings || []).join(" "),
|
|
374
|
+
]
|
|
375
|
+
.filter(Boolean)
|
|
376
|
+
.join(" ")
|
|
377
|
+
.toLowerCase()
|
|
378
|
+
return haystack.includes(q)
|
|
379
|
+
})
|
|
380
|
+
// Index only (no body) — get_guideline returns the full prose on demand.
|
|
381
|
+
return {
|
|
382
|
+
count: matched.length,
|
|
383
|
+
guidelines: matched.map(guidelineSummary),
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function getGuideline(slug) {
|
|
388
|
+
const target = asText(slug).trim().toLowerCase()
|
|
389
|
+
if (!target) {
|
|
390
|
+
return { error: "Guideline slug is required." }
|
|
391
|
+
}
|
|
392
|
+
const guideline = guidelines.find(
|
|
393
|
+
(entry) => entry.slug.toLowerCase() === target
|
|
394
|
+
)
|
|
395
|
+
if (!guideline) {
|
|
396
|
+
return { error: `Guideline not found: ${slug}` }
|
|
397
|
+
}
|
|
398
|
+
// Project out the internal sourcePath — consumers get the content, not the
|
|
399
|
+
// repo layout (consistent with the other tools' projections).
|
|
400
|
+
return {
|
|
401
|
+
slug: guideline.slug,
|
|
402
|
+
title: guideline.title,
|
|
403
|
+
description: guideline.description,
|
|
404
|
+
headings: guideline.headings,
|
|
405
|
+
body: guideline.body,
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function resolveResource(uri) {
|
|
410
|
+
const u = asText(uri)
|
|
411
|
+
if (u === "volt-ui://components") {
|
|
412
|
+
return listComponents("")
|
|
413
|
+
}
|
|
414
|
+
if (u === "volt-ui://tokens") {
|
|
415
|
+
return listTokens({})
|
|
416
|
+
}
|
|
417
|
+
if (u.startsWith("volt-ui://components/")) {
|
|
418
|
+
const name = safeDecode(u.replace("volt-ui://components/", ""))
|
|
419
|
+
// Resources have no include= param, so return full detail (examples +
|
|
420
|
+
// tokens) — a resource read shouldn't silently drop usage examples.
|
|
421
|
+
return getComponent(name, "all")
|
|
422
|
+
}
|
|
423
|
+
if (u.startsWith("volt-ui://tokens/")) {
|
|
424
|
+
const name = safeDecode(u.replace("volt-ui://tokens/", ""))
|
|
425
|
+
return getToken(name)
|
|
426
|
+
}
|
|
427
|
+
return { error: `Unknown resource: ${u}` }
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function listResources() {
|
|
431
|
+
const baseResources = [
|
|
432
|
+
{
|
|
433
|
+
uri: "volt-ui://components",
|
|
434
|
+
name: "Volt UI Components",
|
|
435
|
+
description: "List of all available Volt UI components.",
|
|
436
|
+
mimeType: "application/json",
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
uri: "volt-ui://tokens",
|
|
440
|
+
name: "Volt UI Tokens",
|
|
441
|
+
description: "List of Volt UI design tokens.",
|
|
442
|
+
mimeType: "application/json",
|
|
443
|
+
},
|
|
444
|
+
]
|
|
445
|
+
|
|
446
|
+
const componentResources = registry.components.map((component) => ({
|
|
447
|
+
uri: `volt-ui://components/${encodeURIComponent(component.name)}`,
|
|
448
|
+
name: component.name,
|
|
449
|
+
description: component.description ?? "",
|
|
450
|
+
mimeType: "application/json",
|
|
451
|
+
}))
|
|
452
|
+
|
|
453
|
+
const tokenNames = Array.from(
|
|
454
|
+
new Set(tokens.map((token) => token.name))
|
|
455
|
+
).sort((a, b) => a.localeCompare(b))
|
|
456
|
+
const tokenResources = tokenNames.map((name) => ({
|
|
457
|
+
uri: `volt-ui://tokens/${encodeURIComponent(name)}`,
|
|
458
|
+
name,
|
|
459
|
+
description: "Design token",
|
|
460
|
+
mimeType: "application/json",
|
|
461
|
+
}))
|
|
462
|
+
|
|
463
|
+
return [...baseResources, ...componentResources, ...tokenResources]
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
listComponents,
|
|
468
|
+
getComponent,
|
|
469
|
+
searchComponents,
|
|
470
|
+
listTokens,
|
|
471
|
+
getToken,
|
|
472
|
+
searchTokens,
|
|
473
|
+
listGuidelines,
|
|
474
|
+
getGuideline,
|
|
475
|
+
resolveResource,
|
|
476
|
+
listResources,
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Compact tool-result envelope (no pretty-printing — size discipline). Sets
|
|
482
|
+
* isError:true for error-shaped payloads ({error}) so MCP clients surface the
|
|
483
|
+
* failure instead of silently consuming it as a successful result.
|
|
484
|
+
*/
|
|
485
|
+
export function toolResult(payload) {
|
|
486
|
+
const isError =
|
|
487
|
+
payload != null &&
|
|
488
|
+
typeof payload === "object" &&
|
|
489
|
+
typeof payload.error === "string"
|
|
490
|
+
return {
|
|
491
|
+
content: [
|
|
492
|
+
{
|
|
493
|
+
type: "text",
|
|
494
|
+
text: JSON.stringify(payload),
|
|
495
|
+
},
|
|
496
|
+
],
|
|
497
|
+
...(isError ? { isError: true } : {}),
|
|
498
|
+
}
|
|
499
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@epilot/volt-ui-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"volt-ui-mcp": "./index.js"
|
|
8
8
|
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./index.js",
|
|
11
|
+
"./lib.js": "./lib.js",
|
|
12
|
+
"./tools.js": "./tools.js",
|
|
13
|
+
"./registry.json": "./registry.json"
|
|
14
|
+
},
|
|
9
15
|
"files": [
|
|
10
16
|
"index.js",
|
|
17
|
+
"lib.js",
|
|
18
|
+
"lib.d.ts",
|
|
19
|
+
"tools.js",
|
|
11
20
|
"registry.json"
|
|
12
21
|
],
|
|
13
22
|
"scripts": {
|
|
14
|
-
"prepack": "bun ../
|
|
23
|
+
"prepack": "bun ../react/scripts/build-mcp-registry.ts",
|
|
24
|
+
"test": "bun test"
|
|
15
25
|
},
|
|
16
26
|
"dependencies": {
|
|
17
27
|
"@modelcontextprotocol/sdk": "^1.0.0"
|