@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.
Files changed (6) hide show
  1. package/index.js +13 -335
  2. package/lib.d.ts +40 -0
  3. package/lib.js +499 -0
  4. package/package.json +12 -2
  5. package/registry.json +5100 -4413
  6. package/tools.js +226 -0
package/index.js CHANGED
@@ -11,16 +11,20 @@ import {
11
11
  ReadResourceRequestSchema,
12
12
  } from "@modelcontextprotocol/sdk/types.js"
13
13
 
14
+ import { createQueries, toolResult } from "./lib.js"
15
+ import { TOOL_SPECS, toJsonSchema, callTool, SERVER_VERSION } from "./tools.js"
16
+
14
17
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
15
18
  const registryPath = path.join(__dirname, "registry.json")
16
19
 
17
- const registry = loadRegistry(registryPath)
20
+ // Fail loud on a missing/empty registry never serve "0 components" silently.
21
+ const registry = JSON.parse(fs.readFileSync(registryPath, "utf8"))
22
+ const queries = createQueries(registry)
23
+
18
24
  const server = new Server(
19
25
  {
20
26
  name: "volt-ui-mcp",
21
- version: registry.schemaVersion
22
- ? `schema-${registry.schemaVersion}`
23
- : "0.1.0",
27
+ version: SERVER_VERSION,
24
28
  },
25
29
  {
26
30
  capabilities: {
@@ -31,358 +35,32 @@ const server = new Server(
31
35
  )
32
36
 
33
37
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
34
- const baseResources = [
35
- {
36
- uri: "volt-ui://components",
37
- name: "Volt UI Components",
38
- description: "List of all available Volt UI components.",
39
- mimeType: "application/json",
40
- },
41
- {
42
- uri: "volt-ui://tokens",
43
- name: "Volt UI Tokens",
44
- description: "List of Volt UI design tokens.",
45
- mimeType: "application/json",
46
- },
47
- ]
48
-
49
- const componentResources = registry.components.map((component) => ({
50
- uri: `volt-ui://components/${encodeURIComponent(component.name)}`,
51
- name: component.name,
52
- description: component.description ?? "",
53
- mimeType: "application/json",
54
- }))
55
-
56
- const tokenNames = Array.from(
57
- new Set((registry.tokens || []).map((token) => token.name))
58
- ).sort((a, b) => a.localeCompare(b))
59
- const tokenResources = tokenNames.map((name) => ({
60
- uri: `volt-ui://tokens/${encodeURIComponent(name)}`,
61
- name,
62
- description: "Design token",
63
- mimeType: "application/json",
64
- }))
65
-
66
- return {
67
- resources: [...baseResources, ...componentResources, ...tokenResources],
68
- }
38
+ return { resources: queries.listResources() }
69
39
  })
70
40
 
71
41
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
72
42
  const uri = request.params.uri
73
- const response = resolveResource(uri)
43
+ const response = queries.resolveResource(uri)
74
44
 
75
45
  return {
76
46
  contents: [
77
47
  {
78
48
  uri,
79
49
  mimeType: "application/json",
80
- text: JSON.stringify(response, null, 2),
50
+ text: JSON.stringify(response),
81
51
  },
82
52
  ],
83
53
  }
84
54
  })
85
55
 
86
56
  server.setRequestHandler(ListToolsRequestSchema, async () => {
87
- return {
88
- tools: [
89
- {
90
- name: "list_components",
91
- description: "List all available Volt UI components.",
92
- inputSchema: {
93
- type: "object",
94
- properties: {
95
- query: {
96
- type: "string",
97
- description:
98
- "Optional text filter for component name or description.",
99
- },
100
- },
101
- additionalProperties: false,
102
- },
103
- },
104
- {
105
- name: "get_component",
106
- description: "Get detailed information about a Volt UI component.",
107
- inputSchema: {
108
- type: "object",
109
- properties: {
110
- name: {
111
- type: "string",
112
- description: "Component name (e.g. Button, DialogContent).",
113
- },
114
- },
115
- required: ["name"],
116
- additionalProperties: false,
117
- },
118
- },
119
- {
120
- name: "search_components",
121
- description: "Search components by name, description, or prop names.",
122
- inputSchema: {
123
- type: "object",
124
- properties: {
125
- query: {
126
- type: "string",
127
- description: "Search term.",
128
- },
129
- },
130
- required: ["query"],
131
- additionalProperties: false,
132
- },
133
- },
134
- {
135
- name: "list_tokens",
136
- description: "List Volt UI design tokens.",
137
- inputSchema: {
138
- type: "object",
139
- properties: {
140
- query: {
141
- type: "string",
142
- description: "Optional text filter for token name or value.",
143
- },
144
- theme: {
145
- type: "string",
146
- description: "Optional theme filter (light, dark, global).",
147
- },
148
- group: {
149
- type: "string",
150
- description:
151
- "Optional group filter (palette, semantic, utility).",
152
- },
153
- },
154
- additionalProperties: false,
155
- },
156
- },
157
- {
158
- name: "get_token",
159
- description: "Get details for a specific Volt UI token.",
160
- inputSchema: {
161
- type: "object",
162
- properties: {
163
- name: {
164
- type: "string",
165
- description: "Token name (e.g. --volt-blue-9).",
166
- },
167
- },
168
- required: ["name"],
169
- additionalProperties: false,
170
- },
171
- },
172
- {
173
- name: "search_tokens",
174
- description: "Search tokens by name or value.",
175
- inputSchema: {
176
- type: "object",
177
- properties: {
178
- query: {
179
- type: "string",
180
- description: "Search term.",
181
- },
182
- },
183
- required: ["query"],
184
- additionalProperties: false,
185
- },
186
- },
187
- ],
188
- }
57
+ return { tools: TOOL_SPECS.map(toJsonSchema) }
189
58
  })
190
59
 
191
60
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
192
61
  const { name, arguments: args } = request.params
193
-
194
- switch (name) {
195
- case "list_components": {
196
- const query = typeof args?.query === "string" ? args.query : ""
197
- return toolResult(listComponents(query))
198
- }
199
- case "get_component": {
200
- const componentName = typeof args?.name === "string" ? args.name : ""
201
- return toolResult(getComponent(componentName))
202
- }
203
- case "search_components": {
204
- const query = typeof args?.query === "string" ? args.query : ""
205
- return toolResult(searchComponents(query))
206
- }
207
- case "list_tokens": {
208
- const query = typeof args?.query === "string" ? args.query : ""
209
- const theme = typeof args?.theme === "string" ? args.theme : ""
210
- const group = typeof args?.group === "string" ? args.group : ""
211
- return toolResult(listTokens({ query, theme, group }))
212
- }
213
- case "get_token": {
214
- const tokenName = typeof args?.name === "string" ? args.name : ""
215
- return toolResult(getToken(tokenName))
216
- }
217
- case "search_tokens": {
218
- const query = typeof args?.query === "string" ? args.query : ""
219
- return toolResult(searchTokens(query))
220
- }
221
- default:
222
- return toolResult({ error: `Unknown tool: ${name}` })
223
- }
62
+ return toolResult(callTool(queries, name, args))
224
63
  })
225
64
 
226
65
  const transport = new StdioServerTransport()
227
66
  await server.connect(transport)
228
-
229
- function loadRegistry(filePath) {
230
- try {
231
- const raw = fs.readFileSync(filePath, "utf8")
232
- return JSON.parse(raw)
233
- } catch (error) {
234
- return {
235
- schemaVersion: 1,
236
- components: [],
237
- tokens: [],
238
- error: String(error),
239
- }
240
- }
241
- }
242
-
243
- function resolveResource(uri) {
244
- if (uri === "volt-ui://components") {
245
- return listComponents("")
246
- }
247
- if (uri === "volt-ui://tokens") {
248
- return listTokens({})
249
- }
250
- if (uri.startsWith("volt-ui://components/")) {
251
- const name = decodeURIComponent(uri.replace("volt-ui://components/", ""))
252
- return getComponent(name)
253
- }
254
- if (uri.startsWith("volt-ui://tokens/")) {
255
- const name = decodeURIComponent(uri.replace("volt-ui://tokens/", ""))
256
- return getToken(name)
257
- }
258
- return { error: `Unknown resource: ${uri}` }
259
- }
260
-
261
- function listComponents(query) {
262
- const q = query.trim().toLowerCase()
263
- const components = registry.components.filter((component) => {
264
- if (!q) {
265
- return true
266
- }
267
- return (
268
- component.name.toLowerCase().includes(q) ||
269
- (component.description ?? "").toLowerCase().includes(q)
270
- )
271
- })
272
-
273
- return {
274
- count: components.length,
275
- components: components.map((component) => ({
276
- name: component.name,
277
- title: component.title,
278
- description: component.description,
279
- docsPath: component.docsPath,
280
- docSlug: component.docSlug,
281
- documentationUrl: component.documentationUrl,
282
- apiReferenceUrl: component.apiReferenceUrl,
283
- sourcePaths: component.sourcePaths,
284
- })),
285
- }
286
- }
287
-
288
- function getComponent(name) {
289
- const component = findComponent(name)
290
- if (!component) {
291
- return { error: `Component not found: ${name}` }
292
- }
293
- return component
294
- }
295
-
296
- function searchComponents(query) {
297
- const q = query.trim().toLowerCase()
298
- if (!q) {
299
- return { count: 0, components: [] }
300
- }
301
-
302
- const components = registry.components.filter((component) => {
303
- const haystack = [
304
- component.name,
305
- component.title,
306
- component.description,
307
- (component.props || []).map((prop) => prop.name).join(" "),
308
- ]
309
- .filter(Boolean)
310
- .join(" ")
311
- .toLowerCase()
312
-
313
- return haystack.includes(q)
314
- })
315
-
316
- return {
317
- count: components.length,
318
- components,
319
- }
320
- }
321
-
322
- function listTokens({ query = "", theme = "", group = "" }) {
323
- const q = query.trim().toLowerCase()
324
- const t = theme.trim().toLowerCase()
325
- const g = group.trim().toLowerCase()
326
- const tokens = (registry.tokens || []).filter((token) => {
327
- if (t && token.theme.toLowerCase() !== t) {
328
- return false
329
- }
330
- if (g && token.group.toLowerCase() !== g) {
331
- return false
332
- }
333
- if (!q) {
334
- return true
335
- }
336
- return (
337
- token.name.toLowerCase().includes(q) ||
338
- token.value.toLowerCase().includes(q)
339
- )
340
- })
341
-
342
- return {
343
- count: tokens.length,
344
- tokens,
345
- }
346
- }
347
-
348
- function getToken(name) {
349
- const target = name.trim().toLowerCase()
350
- if (!target) {
351
- return { error: "Token name is required." }
352
- }
353
- const tokens = (registry.tokens || []).filter(
354
- (token) => token.name.toLowerCase() === target
355
- )
356
- if (tokens.length === 0) {
357
- return { error: `Token not found: ${name}` }
358
- }
359
- return {
360
- name,
361
- tokens,
362
- }
363
- }
364
-
365
- function searchTokens(query) {
366
- const q = query.trim().toLowerCase()
367
- if (!q) {
368
- return { count: 0, tokens: [] }
369
- }
370
- return listTokens({ query: q })
371
- }
372
-
373
- function findComponent(name) {
374
- return registry.components.find(
375
- (component) => component.name.toLowerCase() === name.toLowerCase()
376
- )
377
- }
378
-
379
- function toolResult(payload) {
380
- return {
381
- content: [
382
- {
383
- type: "text",
384
- text: JSON.stringify(payload, null, 2),
385
- },
386
- ],
387
- }
388
- }
package/lib.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ export const DEFAULT_TOKEN_LIMIT: number
2
+ export const MAX_TOKEN_LIMIT: number
3
+
4
+ export type ToolResult = {
5
+ content: Array<{ type: "text"; text: string }>
6
+ isError?: true
7
+ }
8
+
9
+ export function toolResult(payload: unknown): ToolResult
10
+
11
+ export type Queries = {
12
+ listComponents(query?: string): unknown
13
+ /** include: CSV of extra detail to add to the lean default — "examples", "tokens", or "all". */
14
+ getComponent(name: string, include?: string): unknown
15
+ searchComponents(query: string): unknown
16
+ listTokens(opts?: {
17
+ query?: string
18
+ theme?: string
19
+ group?: string
20
+ limit?: number
21
+ offset?: number
22
+ }): unknown
23
+ getToken(name: string): unknown
24
+ searchTokens(query: string): unknown
25
+ listGuidelines(query?: string): unknown
26
+ getGuideline(slug: string): unknown
27
+ resolveResource(uri: string): unknown
28
+ listResources(): Array<{
29
+ uri: string
30
+ name: string
31
+ description: string
32
+ mimeType: string
33
+ }>
34
+ }
35
+
36
+ /**
37
+ * Throws if the registry is missing or has no components (fail loud — an empty
38
+ * registry must never masquerade as a working server).
39
+ */
40
+ export function createQueries(registry: unknown): Queries