@movable/ui-mcp 1.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 +158 -0
- package/data/categories.json +49 -0
- package/data/components.json +218 -0
- package/data/eslint-rules.json +7 -0
- package/data/stories.json +800 -0
- package/data/theme.json +68 -0
- package/data/tokens.json +892 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +667 -0
- package/package.json +58 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { readFileSync, existsSync } from 'fs';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Mode Detection: Bundled (npx) vs Local Development
|
|
12
|
+
// ============================================================================
|
|
13
|
+
const UI_REPO_PATH = process.env.UI_REPO_PATH || join(__dirname, '..', '..');
|
|
14
|
+
const DATA_DIR = join(__dirname, '..', 'data');
|
|
15
|
+
const isNpxMode = existsSync(join(DATA_DIR, 'components.json'));
|
|
16
|
+
const isLocalMode = existsSync(join(UI_REPO_PATH, 'src', 'components', 'index.ts'));
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Data Loading
|
|
19
|
+
// ============================================================================
|
|
20
|
+
function loadJsonData(filename) {
|
|
21
|
+
const filePath = join(DATA_DIR, filename);
|
|
22
|
+
if (existsSync(filePath)) {
|
|
23
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
// Cached data (loaded on first use)
|
|
28
|
+
let cachedComponents = null;
|
|
29
|
+
let cachedTokens = null;
|
|
30
|
+
let cachedTheme = null;
|
|
31
|
+
let cachedStories = null;
|
|
32
|
+
let cachedCategories = null;
|
|
33
|
+
let cachedEslintRules = null;
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Component Functions
|
|
36
|
+
// ============================================================================
|
|
37
|
+
function getComponentsFromSource() {
|
|
38
|
+
const indexPath = join(UI_REPO_PATH, 'src', 'components', 'index.ts');
|
|
39
|
+
if (!existsSync(indexPath)) {
|
|
40
|
+
throw new Error(`Components index not found at ${indexPath}`);
|
|
41
|
+
}
|
|
42
|
+
const content = readFileSync(indexPath, 'utf-8');
|
|
43
|
+
const components = [];
|
|
44
|
+
const exportRegex = /export\s+(?:\{([^}]+)\}|\*)\s+from\s+['"]\.\/([^'"]+)['"]/g;
|
|
45
|
+
let match;
|
|
46
|
+
while ((match = exportRegex.exec(content)) !== null) {
|
|
47
|
+
const exports = match[1];
|
|
48
|
+
const path = match[2];
|
|
49
|
+
if (exports) {
|
|
50
|
+
const exportNames = exports
|
|
51
|
+
.split(',')
|
|
52
|
+
.map((e) => e.trim())
|
|
53
|
+
.filter((e) => !e.startsWith('type '))
|
|
54
|
+
.map((e) => e.replace(/\s+as\s+\w+/, '').trim())
|
|
55
|
+
.filter((e) => e.length > 0);
|
|
56
|
+
for (const exportName of exportNames) {
|
|
57
|
+
components.push({
|
|
58
|
+
name: exportName,
|
|
59
|
+
exportName,
|
|
60
|
+
path: `src/components/${path}`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
components.push({
|
|
66
|
+
name: path,
|
|
67
|
+
exportName: `* (barrel export)`,
|
|
68
|
+
path: `src/components/${path}`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return components;
|
|
73
|
+
}
|
|
74
|
+
function getComponents() {
|
|
75
|
+
if (cachedComponents)
|
|
76
|
+
return cachedComponents;
|
|
77
|
+
if (isNpxMode) {
|
|
78
|
+
cachedComponents = loadJsonData('components.json') || [];
|
|
79
|
+
}
|
|
80
|
+
else if (isLocalMode) {
|
|
81
|
+
cachedComponents = getComponentsFromSource();
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
throw new Error('MCP server requires either bundled data or access to UI repo source files. ' +
|
|
85
|
+
'Set UI_REPO_PATH environment variable if running in development mode.');
|
|
86
|
+
}
|
|
87
|
+
return cachedComponents;
|
|
88
|
+
}
|
|
89
|
+
function getCategories() {
|
|
90
|
+
if (cachedCategories)
|
|
91
|
+
return cachedCategories;
|
|
92
|
+
if (isNpxMode) {
|
|
93
|
+
cachedCategories =
|
|
94
|
+
loadJsonData('categories.json') || {};
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// Fallback categories for local mode
|
|
98
|
+
cachedCategories = {
|
|
99
|
+
inputs: [
|
|
100
|
+
'InkTextField',
|
|
101
|
+
'InkSelect',
|
|
102
|
+
'InkCheckboxGroup',
|
|
103
|
+
'InkRadioGroup',
|
|
104
|
+
'InkSwitch',
|
|
105
|
+
'InkRadioTiles',
|
|
106
|
+
],
|
|
107
|
+
toggles: [
|
|
108
|
+
'InkToggleText',
|
|
109
|
+
'InkToggleTextGroup',
|
|
110
|
+
'InkToggleIcon',
|
|
111
|
+
'InkToggleIconGroup',
|
|
112
|
+
],
|
|
113
|
+
feedback: ['InkSnackbar', 'InkHighlightAlert', 'InkEmptyState'],
|
|
114
|
+
layout: ['InkPaper', 'InkCard', 'InkDrawer', 'InkPersistentFilterDrawer'],
|
|
115
|
+
dialogs: ['InkDialog'],
|
|
116
|
+
headers: [
|
|
117
|
+
'InkPageHeader',
|
|
118
|
+
'InkWorkflowHeader',
|
|
119
|
+
'HeaderMetadata',
|
|
120
|
+
'HeaderSubtitle',
|
|
121
|
+
'LinkBreadcrumbs',
|
|
122
|
+
'PageHeaderActionButtons',
|
|
123
|
+
],
|
|
124
|
+
'data-display': [
|
|
125
|
+
'InkDataGrid',
|
|
126
|
+
'InkDataGridEmpty',
|
|
127
|
+
'InkAttributeList',
|
|
128
|
+
'InkImage',
|
|
129
|
+
'InkChart',
|
|
130
|
+
],
|
|
131
|
+
chips: ['InkChip', 'InkChipGroup'],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return cachedCategories;
|
|
135
|
+
}
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// Token Functions
|
|
138
|
+
// ============================================================================
|
|
139
|
+
const TOKEN_CATEGORIES = [
|
|
140
|
+
'blue',
|
|
141
|
+
'green',
|
|
142
|
+
'lightBlue',
|
|
143
|
+
'lightGreen',
|
|
144
|
+
'neutral',
|
|
145
|
+
'orange',
|
|
146
|
+
'purple',
|
|
147
|
+
'yellow',
|
|
148
|
+
'red',
|
|
149
|
+
'cyan',
|
|
150
|
+
'deepPurple',
|
|
151
|
+
];
|
|
152
|
+
function getTokens() {
|
|
153
|
+
if (cachedTokens)
|
|
154
|
+
return cachedTokens;
|
|
155
|
+
if (isNpxMode) {
|
|
156
|
+
cachedTokens = loadJsonData('tokens.json') || [];
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// In local mode, return empty - users should run extract-data first
|
|
160
|
+
console.error('Warning: Token data not available in local mode. Run npm run extract-data first.');
|
|
161
|
+
cachedTokens = [];
|
|
162
|
+
}
|
|
163
|
+
return cachedTokens;
|
|
164
|
+
}
|
|
165
|
+
// ============================================================================
|
|
166
|
+
// Theme Functions
|
|
167
|
+
// ============================================================================
|
|
168
|
+
function getTheme() {
|
|
169
|
+
if (cachedTheme)
|
|
170
|
+
return cachedTheme;
|
|
171
|
+
if (isNpxMode) {
|
|
172
|
+
cachedTheme = loadJsonData('theme.json');
|
|
173
|
+
}
|
|
174
|
+
else if (isLocalMode) {
|
|
175
|
+
// Read from source in local mode
|
|
176
|
+
const themePath = join(UI_REPO_PATH, 'src', 'theme.ts');
|
|
177
|
+
let componentOverrides = [];
|
|
178
|
+
if (existsSync(themePath)) {
|
|
179
|
+
const themeSource = readFileSync(themePath, 'utf-8');
|
|
180
|
+
const componentMatches = themeSource.match(/Mui\w+:/g);
|
|
181
|
+
if (componentMatches) {
|
|
182
|
+
componentOverrides = [...new Set(componentMatches)].map((c) => c.replace(':', ''));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
cachedTheme = {
|
|
186
|
+
semanticColors: {},
|
|
187
|
+
textColors: {},
|
|
188
|
+
typography: { fontFamily: ['Inter', 'sans-serif'] },
|
|
189
|
+
componentOverrides,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return cachedTheme;
|
|
193
|
+
}
|
|
194
|
+
// ============================================================================
|
|
195
|
+
// Story Functions
|
|
196
|
+
// ============================================================================
|
|
197
|
+
function getStories() {
|
|
198
|
+
if (cachedStories)
|
|
199
|
+
return cachedStories;
|
|
200
|
+
if (isNpxMode) {
|
|
201
|
+
cachedStories = loadJsonData('stories.json') || [];
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
// In local mode, return empty - parsing stories requires glob
|
|
205
|
+
cachedStories = [];
|
|
206
|
+
}
|
|
207
|
+
return cachedStories;
|
|
208
|
+
}
|
|
209
|
+
// ============================================================================
|
|
210
|
+
// ESLint Rules Functions
|
|
211
|
+
// ============================================================================
|
|
212
|
+
function getEslintRules() {
|
|
213
|
+
if (cachedEslintRules)
|
|
214
|
+
return cachedEslintRules;
|
|
215
|
+
if (isNpxMode) {
|
|
216
|
+
cachedEslintRules =
|
|
217
|
+
loadJsonData('eslint-rules.json') || [];
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
cachedEslintRules = [];
|
|
221
|
+
}
|
|
222
|
+
return cachedEslintRules;
|
|
223
|
+
}
|
|
224
|
+
// ============================================================================
|
|
225
|
+
// MCP Server Setup
|
|
226
|
+
// ============================================================================
|
|
227
|
+
const server = new McpServer({
|
|
228
|
+
name: 'movable-ui',
|
|
229
|
+
version: '1.0.0',
|
|
230
|
+
});
|
|
231
|
+
// Register the list_components tool
|
|
232
|
+
server.tool('list_components', 'List all available components from @movable/ui', {}, async () => {
|
|
233
|
+
try {
|
|
234
|
+
const components = getComponents();
|
|
235
|
+
const componentList = components
|
|
236
|
+
.map((c) => `- ${c.name} (from ${c.path})${c.category ? ` [${c.category}]` : ''}`)
|
|
237
|
+
.join('\n');
|
|
238
|
+
return {
|
|
239
|
+
content: [
|
|
240
|
+
{
|
|
241
|
+
type: 'text',
|
|
242
|
+
text: `# @movable/ui Components\n\nFound ${components.length} exported components:\n\n${componentList}`,
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
return {
|
|
249
|
+
content: [
|
|
250
|
+
{
|
|
251
|
+
type: 'text',
|
|
252
|
+
text: `Error listing components: ${error instanceof Error ? error.message : String(error)}`,
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
isError: true,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
// Register the get_component tool
|
|
260
|
+
server.tool('get_component', 'Get detailed information about a specific component', {
|
|
261
|
+
name: z.string().describe('The name of the component to look up'),
|
|
262
|
+
}, async ({ name }) => {
|
|
263
|
+
try {
|
|
264
|
+
const components = getComponents();
|
|
265
|
+
const component = components.find((c) => c.name.toLowerCase() === name.toLowerCase());
|
|
266
|
+
if (!component) {
|
|
267
|
+
return {
|
|
268
|
+
content: [
|
|
269
|
+
{
|
|
270
|
+
type: 'text',
|
|
271
|
+
text: `Component "${name}" not found. Use list_components to see available components.`,
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
isError: true,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
let componentDetails = `# ${component.name}\n\n`;
|
|
278
|
+
componentDetails += `**Export name:** ${component.exportName}\n`;
|
|
279
|
+
componentDetails += `**Path:** ${component.path}\n`;
|
|
280
|
+
if (component.category) {
|
|
281
|
+
componentDetails += `**Category:** ${component.category}\n`;
|
|
282
|
+
}
|
|
283
|
+
componentDetails += '\n';
|
|
284
|
+
if (component.description) {
|
|
285
|
+
componentDetails += `## Description\n\n${component.description}\n\n`;
|
|
286
|
+
}
|
|
287
|
+
if (component.props) {
|
|
288
|
+
componentDetails += `## Props\n\n\`\`\`typescript\n${component.props}\n\`\`\`\n`;
|
|
289
|
+
}
|
|
290
|
+
// In local mode, try to read additional details from source
|
|
291
|
+
if (isLocalMode && !component.props) {
|
|
292
|
+
const possiblePaths = [
|
|
293
|
+
join(UI_REPO_PATH, component.path, 'index.ts'),
|
|
294
|
+
join(UI_REPO_PATH, component.path, 'index.tsx'),
|
|
295
|
+
join(UI_REPO_PATH, component.path + '.tsx'),
|
|
296
|
+
join(UI_REPO_PATH, component.path + '.ts'),
|
|
297
|
+
];
|
|
298
|
+
for (const sourcePath of possiblePaths) {
|
|
299
|
+
if (existsSync(sourcePath)) {
|
|
300
|
+
const source = readFileSync(sourcePath, 'utf-8');
|
|
301
|
+
const jsdocMatch = source.match(/\/\*\*[\s\S]*?\*\/\s*(?=export)/);
|
|
302
|
+
if (jsdocMatch && !component.description) {
|
|
303
|
+
componentDetails += `## Description\n\n${jsdocMatch[0]}\n\n`;
|
|
304
|
+
}
|
|
305
|
+
const propsMatch = source.match(/(?:interface|type)\s+(\w+Props)\s*(?:=\s*)?{[\s\S]*?}/);
|
|
306
|
+
if (propsMatch) {
|
|
307
|
+
componentDetails += `## Props\n\n\`\`\`typescript\n${propsMatch[0]}\n\`\`\`\n`;
|
|
308
|
+
}
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
content: [
|
|
315
|
+
{
|
|
316
|
+
type: 'text',
|
|
317
|
+
text: componentDetails,
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
return {
|
|
324
|
+
content: [
|
|
325
|
+
{
|
|
326
|
+
type: 'text',
|
|
327
|
+
text: `Error getting component: ${error instanceof Error ? error.message : String(error)}`,
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
isError: true,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
// Register the get_theme tool
|
|
335
|
+
server.tool('get_theme', 'Get theme configuration including palette, typography, and component overrides', {}, async () => {
|
|
336
|
+
try {
|
|
337
|
+
const theme = getTheme();
|
|
338
|
+
if (!theme) {
|
|
339
|
+
return {
|
|
340
|
+
content: [
|
|
341
|
+
{
|
|
342
|
+
type: 'text',
|
|
343
|
+
text: 'Theme data not available. In local mode, run npm run extract-data first.',
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
isError: true,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
let themeInfo = '# @movable/ui Theme\n\n';
|
|
350
|
+
// Semantic colors
|
|
351
|
+
if (Object.keys(theme.semanticColors).length > 0) {
|
|
352
|
+
themeInfo += '## Semantic Colors\n\n';
|
|
353
|
+
for (const [colorName, values] of Object.entries(theme.semanticColors)) {
|
|
354
|
+
themeInfo += `### ${colorName}\n`;
|
|
355
|
+
themeInfo += '| Variant | Value |\n|---------|-------|\n';
|
|
356
|
+
for (const [variant, value] of Object.entries(values)) {
|
|
357
|
+
themeInfo += `| ${variant} | \`${value}\` |\n`;
|
|
358
|
+
}
|
|
359
|
+
themeInfo += '\n';
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// Text colors
|
|
363
|
+
if (Object.keys(theme.textColors).length > 0) {
|
|
364
|
+
themeInfo += '## Text Colors\n\n';
|
|
365
|
+
themeInfo += '| Type | Value |\n|------|-------|\n';
|
|
366
|
+
for (const [type, value] of Object.entries(theme.textColors)) {
|
|
367
|
+
themeInfo += `| ${type} | \`${value}\` |\n`;
|
|
368
|
+
}
|
|
369
|
+
themeInfo += '\n';
|
|
370
|
+
}
|
|
371
|
+
// Typography
|
|
372
|
+
themeInfo += '## Typography\n\n';
|
|
373
|
+
themeInfo += `- Font family: \`${theme.typography.fontFamily.join(', ')}\`\n\n`;
|
|
374
|
+
// Component overrides
|
|
375
|
+
if (theme.componentOverrides.length > 0) {
|
|
376
|
+
themeInfo += '## Component Overrides\n\n';
|
|
377
|
+
themeInfo += theme.componentOverrides.map((c) => `- ${c}`).join('\n');
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
content: [{ type: 'text', text: themeInfo }],
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
return {
|
|
385
|
+
content: [
|
|
386
|
+
{
|
|
387
|
+
type: 'text',
|
|
388
|
+
text: `Error getting theme: ${error instanceof Error ? error.message : String(error)}`,
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
isError: true,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
// Register the get_design_tokens tool
|
|
396
|
+
server.tool('get_design_tokens', 'Get design tokens by category (colors, spacing). Categories: blue, green, lightBlue, lightGreen, neutral, orange, purple, yellow, red, cyan, deepPurple', {
|
|
397
|
+
category: z
|
|
398
|
+
.string()
|
|
399
|
+
.optional()
|
|
400
|
+
.describe('Token category to retrieve (e.g., "blue", "neutral"). If omitted, lists all categories.'),
|
|
401
|
+
}, async ({ category }) => {
|
|
402
|
+
try {
|
|
403
|
+
const tokens = getTokens();
|
|
404
|
+
if (tokens.length === 0) {
|
|
405
|
+
return {
|
|
406
|
+
content: [
|
|
407
|
+
{
|
|
408
|
+
type: 'text',
|
|
409
|
+
text: 'Token data not available. Run npm run extract-data to generate token data.',
|
|
410
|
+
},
|
|
411
|
+
],
|
|
412
|
+
isError: true,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
// If no category specified, list available categories
|
|
416
|
+
if (!category) {
|
|
417
|
+
return {
|
|
418
|
+
content: [
|
|
419
|
+
{
|
|
420
|
+
type: 'text',
|
|
421
|
+
text: `# Available Design Token Categories\n\n${TOKEN_CATEGORIES.map((c) => `- ${c}`).join('\n')}\n\nUse \`get_design_tokens\` with a category parameter to get specific tokens.`,
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
const normalizedCategory = category.toLowerCase();
|
|
427
|
+
const matchingCategory = tokens.find((t) => t.name.toLowerCase() === normalizedCategory);
|
|
428
|
+
if (!matchingCategory) {
|
|
429
|
+
return {
|
|
430
|
+
content: [
|
|
431
|
+
{
|
|
432
|
+
type: 'text',
|
|
433
|
+
text: `Category "${category}" not found. Available categories: ${TOKEN_CATEGORIES.join(', ')}`,
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
isError: true,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
let result = `# ${matchingCategory.name} Design Tokens\n\n`;
|
|
440
|
+
result += '| Token | Value |\n|-------|-------|\n';
|
|
441
|
+
for (const token of matchingCategory.tokens) {
|
|
442
|
+
result += `| ${token.name} | \`${token.value}\` |\n`;
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
content: [{ type: 'text', text: result }],
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
return {
|
|
450
|
+
content: [
|
|
451
|
+
{
|
|
452
|
+
type: 'text',
|
|
453
|
+
text: `Error getting design tokens: ${error instanceof Error ? error.message : String(error)}`,
|
|
454
|
+
},
|
|
455
|
+
],
|
|
456
|
+
isError: true,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
// Register the get_component_example tool
|
|
461
|
+
server.tool('get_component_example', 'Get code examples from Storybook stories for a component', {
|
|
462
|
+
name: z.string().describe('The name of the component to get examples for'),
|
|
463
|
+
story: z
|
|
464
|
+
.string()
|
|
465
|
+
.optional()
|
|
466
|
+
.describe('Specific story name (e.g., "Default", "Error"). If omitted, lists available stories.'),
|
|
467
|
+
}, async ({ name, story }) => {
|
|
468
|
+
try {
|
|
469
|
+
const stories = getStories();
|
|
470
|
+
if (stories.length === 0) {
|
|
471
|
+
return {
|
|
472
|
+
content: [
|
|
473
|
+
{
|
|
474
|
+
type: 'text',
|
|
475
|
+
text: 'Story data not available. Run npm run extract-data to generate story metadata.',
|
|
476
|
+
},
|
|
477
|
+
],
|
|
478
|
+
isError: true,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
// Find stories matching the component name
|
|
482
|
+
const matchingStories = stories.filter((s) => s.component.toLowerCase().includes(name.toLowerCase()) ||
|
|
483
|
+
s.title.toLowerCase().includes(name.toLowerCase()));
|
|
484
|
+
if (matchingStories.length === 0) {
|
|
485
|
+
return {
|
|
486
|
+
content: [
|
|
487
|
+
{
|
|
488
|
+
type: 'text',
|
|
489
|
+
text: `No Storybook stories found for "${name}". Try using the exact component name.`,
|
|
490
|
+
},
|
|
491
|
+
],
|
|
492
|
+
isError: true,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
let result = `# Storybook Examples for ${name}\n\n`;
|
|
496
|
+
for (const storyFile of matchingStories) {
|
|
497
|
+
result += `## ${storyFile.title}\n`;
|
|
498
|
+
result += `**File:** \`${storyFile.file}\`\n`;
|
|
499
|
+
if (storyFile.figmaUrl) {
|
|
500
|
+
result += `**Figma:** [Design](${storyFile.figmaUrl})\n`;
|
|
501
|
+
}
|
|
502
|
+
result += '\n';
|
|
503
|
+
if (!story) {
|
|
504
|
+
result += `**Available stories:** ${storyFile.stories.join(', ')}\n\n`;
|
|
505
|
+
result += `Use \`get_component_example\` with a story parameter to get specific code.\n\n`;
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
const matchingStory = storyFile.stories.find((s) => s.toLowerCase() === story.toLowerCase());
|
|
509
|
+
if (matchingStory) {
|
|
510
|
+
result += `### ${matchingStory}\n\n`;
|
|
511
|
+
result += `Story found in \`${storyFile.file}\`. `;
|
|
512
|
+
result += `View the source file for the full implementation.\n\n`;
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
result += `Story "${story}" not found. Available: ${storyFile.stories.join(', ')}\n\n`;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
content: [{ type: 'text', text: result }],
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
return {
|
|
525
|
+
content: [
|
|
526
|
+
{
|
|
527
|
+
type: 'text',
|
|
528
|
+
text: `Error getting examples: ${error instanceof Error ? error.message : String(error)}`,
|
|
529
|
+
},
|
|
530
|
+
],
|
|
531
|
+
isError: true,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
// Register the search_components tool
|
|
536
|
+
server.tool('search_components', 'Search components by keyword or category. Categories: inputs, toggles, feedback, layout, dialogs, headers, data-display, chips', {
|
|
537
|
+
query: z
|
|
538
|
+
.string()
|
|
539
|
+
.optional()
|
|
540
|
+
.describe('Search keyword to match against component names'),
|
|
541
|
+
category: z
|
|
542
|
+
.string()
|
|
543
|
+
.optional()
|
|
544
|
+
.describe('Filter by category (e.g., "inputs", "feedback")'),
|
|
545
|
+
}, async ({ query, category }) => {
|
|
546
|
+
try {
|
|
547
|
+
const components = getComponents();
|
|
548
|
+
const categories = getCategories();
|
|
549
|
+
let filtered = components;
|
|
550
|
+
// Filter by category if provided
|
|
551
|
+
if (category) {
|
|
552
|
+
const normalizedCategory = category.toLowerCase();
|
|
553
|
+
const categoryComponents = categories[normalizedCategory];
|
|
554
|
+
if (!categoryComponents) {
|
|
555
|
+
return {
|
|
556
|
+
content: [
|
|
557
|
+
{
|
|
558
|
+
type: 'text',
|
|
559
|
+
text: `Category "${category}" not found. Available categories: ${Object.keys(categories).join(', ')}`,
|
|
560
|
+
},
|
|
561
|
+
],
|
|
562
|
+
isError: true,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
filtered = components.filter((c) => categoryComponents.some((cc) => cc.toLowerCase() === c.name.toLowerCase()));
|
|
566
|
+
}
|
|
567
|
+
// Filter by query if provided
|
|
568
|
+
if (query) {
|
|
569
|
+
const normalizedQuery = query.toLowerCase();
|
|
570
|
+
filtered = filtered.filter((c) => c.name.toLowerCase().includes(normalizedQuery) ||
|
|
571
|
+
c.path.toLowerCase().includes(normalizedQuery));
|
|
572
|
+
}
|
|
573
|
+
if (filtered.length === 0) {
|
|
574
|
+
let message = 'No components found';
|
|
575
|
+
if (query)
|
|
576
|
+
message += ` matching "${query}"`;
|
|
577
|
+
if (category)
|
|
578
|
+
message += ` in category "${category}"`;
|
|
579
|
+
return {
|
|
580
|
+
content: [{ type: 'text', text: message }],
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
let result = '# Search Results\n\n';
|
|
584
|
+
if (query)
|
|
585
|
+
result += `**Query:** ${query}\n`;
|
|
586
|
+
if (category)
|
|
587
|
+
result += `**Category:** ${category}\n`;
|
|
588
|
+
result += `**Found:** ${filtered.length} components\n\n`;
|
|
589
|
+
result += filtered.map((c) => `- ${c.name} (${c.path})`).join('\n');
|
|
590
|
+
if (!category && !query) {
|
|
591
|
+
result += '\n\n## Available Categories\n\n';
|
|
592
|
+
for (const [cat, comps] of Object.entries(categories)) {
|
|
593
|
+
result += `- **${cat}**: ${comps.join(', ')}\n`;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
content: [{ type: 'text', text: result }],
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
return {
|
|
602
|
+
content: [
|
|
603
|
+
{
|
|
604
|
+
type: 'text',
|
|
605
|
+
text: `Error searching components: ${error instanceof Error ? error.message : String(error)}`,
|
|
606
|
+
},
|
|
607
|
+
],
|
|
608
|
+
isError: true,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
// Register the get_eslint_rules tool
|
|
613
|
+
server.tool('get_eslint_rules', 'Get available ESLint rules from @movable/ui/eslint-plugin', {}, async () => {
|
|
614
|
+
try {
|
|
615
|
+
const rules = getEslintRules();
|
|
616
|
+
let result = '# @movable/ui ESLint Rules\n\n';
|
|
617
|
+
result += '## Installation\n\n';
|
|
618
|
+
result +=
|
|
619
|
+
'```js\n// eslint.config.js\nimport movableUiPlugin from "@movable/eslint-plugin-ui";\n\nexport default [\n movableUiPlugin.configs.recommended,\n];\n```\n\n';
|
|
620
|
+
result += '## Available Rules\n\n';
|
|
621
|
+
if (rules.length === 0) {
|
|
622
|
+
result += 'No custom rules found or rule data not available.\n';
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
for (const rule of rules) {
|
|
626
|
+
result += `### ${rule.name}\n\n`;
|
|
627
|
+
if (rule.description) {
|
|
628
|
+
result += `**Description:** ${rule.description}\n\n`;
|
|
629
|
+
}
|
|
630
|
+
if (rule.message) {
|
|
631
|
+
result += `**Message:** ${rule.message}\n\n`;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return {
|
|
636
|
+
content: [{ type: 'text', text: result }],
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
return {
|
|
641
|
+
content: [
|
|
642
|
+
{
|
|
643
|
+
type: 'text',
|
|
644
|
+
text: `Error getting ESLint rules: ${error instanceof Error ? error.message : String(error)}`,
|
|
645
|
+
},
|
|
646
|
+
],
|
|
647
|
+
isError: true,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
// ============================================================================
|
|
652
|
+
// Start Server
|
|
653
|
+
// ============================================================================
|
|
654
|
+
async function main() {
|
|
655
|
+
const transport = new StdioServerTransport();
|
|
656
|
+
await server.connect(transport);
|
|
657
|
+
const mode = isNpxMode
|
|
658
|
+
? 'npx (bundled data)'
|
|
659
|
+
: isLocalMode
|
|
660
|
+
? 'local development'
|
|
661
|
+
: 'unknown';
|
|
662
|
+
console.error(`Movable UI MCP server running on stdio [${mode}]`);
|
|
663
|
+
}
|
|
664
|
+
main().catch((error) => {
|
|
665
|
+
console.error('Failed to start server:', error);
|
|
666
|
+
process.exit(1);
|
|
667
|
+
});
|