impact-ui-mcp-server 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/DEPLOYMENT.md +224 -0
- package/QUICKSTART.md +119 -0
- package/QUICK_SETUP.md +95 -0
- package/README.md +259 -0
- package/package.json +49 -0
- package/src/index.js +565 -0
- package/src/parsers/componentParser.js +68 -0
- package/src/parsers/storybookParser.js +140 -0
- package/src/tools/codeExample.js +81 -0
- package/src/tools/componentInfo.js +26 -0
- package/src/utils/fileReader.js +91 -0
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "impact-ui-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP Server for Impact UI Library - Provides AI access to component documentation and code examples",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"impact-ui-mcp": "src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"DEPLOYMENT.md",
|
|
14
|
+
"QUICK_SETUP.md",
|
|
15
|
+
"QUICKSTART.md"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"start": "node src/index.js",
|
|
22
|
+
"dev": "node --watch src/index.js",
|
|
23
|
+
"generate-config": "node generate-cursor-config.js"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"mcp",
|
|
27
|
+
"model-context-protocol",
|
|
28
|
+
"ui-library",
|
|
29
|
+
"component-documentation",
|
|
30
|
+
"impact-ui",
|
|
31
|
+
"cursor",
|
|
32
|
+
"claude"
|
|
33
|
+
],
|
|
34
|
+
"author": "Impact Analytics",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/impact-analytics/impact-ui.git",
|
|
39
|
+
"directory": "mcp-server"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18.0.0"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@modelcontextprotocol/sdk": "^0.5.0",
|
|
46
|
+
"glob": "^10.3.10",
|
|
47
|
+
"fs-extra": "^11.1.1"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import {
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
ListToolsRequestSchema,
|
|
8
|
+
ListResourcesRequestSchema,
|
|
9
|
+
ReadResourceRequestSchema,
|
|
10
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
+
import { glob } from "glob";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { parseStorybookStory } from "./parsers/storybookParser.js";
|
|
14
|
+
import { parseComponentFile } from "./parsers/componentParser.js";
|
|
15
|
+
import { getProjectRoot } from "./utils/fileReader.js";
|
|
16
|
+
import { getComponentInfo } from "./tools/componentInfo.js";
|
|
17
|
+
import { generateCodeExample } from "./tools/codeExample.js";
|
|
18
|
+
|
|
19
|
+
// Component registry - populated on startup
|
|
20
|
+
let componentRegistry = {};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Discover and parse all components from Storybook stories
|
|
24
|
+
*/
|
|
25
|
+
async function buildComponentRegistry() {
|
|
26
|
+
const projectRoot = getProjectRoot();
|
|
27
|
+
const storiesPath = join(projectRoot, "src/stories");
|
|
28
|
+
const componentsPath = join(projectRoot, "src/components");
|
|
29
|
+
|
|
30
|
+
console.error("Building component registry...");
|
|
31
|
+
|
|
32
|
+
// Find all story files
|
|
33
|
+
const storyFiles = await glob("*.stories.js", { cwd: storiesPath });
|
|
34
|
+
|
|
35
|
+
for (const file of storyFiles) {
|
|
36
|
+
const storyFilePath = join(storiesPath, file);
|
|
37
|
+
const componentName = file.replace(".stories.js", "");
|
|
38
|
+
|
|
39
|
+
// Parse storybook story
|
|
40
|
+
const storyMetadata = parseStorybookStory(storyFilePath, componentName);
|
|
41
|
+
|
|
42
|
+
if (!storyMetadata) {
|
|
43
|
+
console.error(`Failed to parse story for ${componentName}`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Try to find component file
|
|
48
|
+
const componentFile = await findComponentFile(componentsPath, componentName);
|
|
49
|
+
let componentInfo = null;
|
|
50
|
+
|
|
51
|
+
if (componentFile) {
|
|
52
|
+
componentInfo = parseComponentFile(componentFile, componentName);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Merge story metadata with component info
|
|
56
|
+
componentRegistry[componentName] = {
|
|
57
|
+
...storyMetadata,
|
|
58
|
+
componentFile: componentFile || null,
|
|
59
|
+
componentInfo: componentInfo,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.error(`Loaded ${Object.keys(componentRegistry).length} components`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Find component file in components directory
|
|
68
|
+
*/
|
|
69
|
+
async function findComponentFile(componentsPath, componentName) {
|
|
70
|
+
const { existsSync } = await import("fs");
|
|
71
|
+
const possiblePaths = [
|
|
72
|
+
join(componentsPath, componentName, "index.js"),
|
|
73
|
+
join(componentsPath, componentName, `${componentName}.js`),
|
|
74
|
+
join(componentsPath, componentName, `${componentName}.jsx`),
|
|
75
|
+
join(componentsPath, componentName, "index.jsx"),
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
for (const path of possiblePaths) {
|
|
79
|
+
try {
|
|
80
|
+
if (existsSync(path)) {
|
|
81
|
+
return path;
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// Continue searching
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Initialize MCP Server
|
|
92
|
+
const server = new Server(
|
|
93
|
+
{
|
|
94
|
+
name: "impact-ui-mcp-server",
|
|
95
|
+
version: "1.0.0",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
capabilities: {
|
|
99
|
+
tools: {},
|
|
100
|
+
resources: {},
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// List available tools
|
|
106
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
107
|
+
return {
|
|
108
|
+
tools: [
|
|
109
|
+
{
|
|
110
|
+
name: "get_component_info",
|
|
111
|
+
description: "Get detailed information about a specific UI component including props, description, usage examples, and file locations",
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {
|
|
115
|
+
componentName: {
|
|
116
|
+
type: "string",
|
|
117
|
+
description: "Name of the component (e.g., 'Button', 'Modal', 'Table'). Use list_components to see all available components.",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
required: ["componentName"],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "list_components",
|
|
125
|
+
description: "List all available UI components in the library, optionally filtered by category",
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: {
|
|
129
|
+
category: {
|
|
130
|
+
type: "string",
|
|
131
|
+
description: "Optional: Filter by category - 'Components' or 'Patterns'",
|
|
132
|
+
enum: ["Components", "Patterns"],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "get_component_props",
|
|
139
|
+
description: "Get all props for a component with their types, defaults, descriptions, and whether they're required",
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: "object",
|
|
142
|
+
properties: {
|
|
143
|
+
componentName: {
|
|
144
|
+
type: "string",
|
|
145
|
+
description: "Name of the component",
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
required: ["componentName"],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: "generate_code_example",
|
|
153
|
+
description: "Generate a ready-to-use code example for a component with specified props. Automatically includes state management for components that need it (like Modal, Panel, etc.)",
|
|
154
|
+
inputSchema: {
|
|
155
|
+
type: "object",
|
|
156
|
+
properties: {
|
|
157
|
+
componentName: {
|
|
158
|
+
type: "string",
|
|
159
|
+
description: "Name of the component",
|
|
160
|
+
},
|
|
161
|
+
props: {
|
|
162
|
+
type: "object",
|
|
163
|
+
description: "Props to include in the example (optional). Key-value pairs where keys are prop names and values are prop values.",
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
required: ["componentName"],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: "search_components",
|
|
171
|
+
description: "Search for components by name or description. Useful for finding components when you don't know the exact name.",
|
|
172
|
+
inputSchema: {
|
|
173
|
+
type: "object",
|
|
174
|
+
properties: {
|
|
175
|
+
query: {
|
|
176
|
+
type: "string",
|
|
177
|
+
description: "Search query to match against component names or descriptions",
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
required: ["query"],
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: "get_component_usage_tips",
|
|
185
|
+
description: "Get best practices, usage tips, and common patterns for using a component",
|
|
186
|
+
inputSchema: {
|
|
187
|
+
type: "object",
|
|
188
|
+
properties: {
|
|
189
|
+
componentName: {
|
|
190
|
+
type: "string",
|
|
191
|
+
description: "Name of the component",
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
required: ["componentName"],
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// List available resources (components)
|
|
202
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
203
|
+
const resources = Object.values(componentRegistry)
|
|
204
|
+
.sort((a, b) => {
|
|
205
|
+
// Sort by category first, then by name
|
|
206
|
+
if (a.category !== b.category) {
|
|
207
|
+
return a.category.localeCompare(b.category);
|
|
208
|
+
}
|
|
209
|
+
return a.name.localeCompare(b.name);
|
|
210
|
+
})
|
|
211
|
+
.map((component) => ({
|
|
212
|
+
uri: `impact-ui://component/${component.name}`,
|
|
213
|
+
name: component.name,
|
|
214
|
+
description: component.description || `Component: ${component.name}`,
|
|
215
|
+
mimeType: "application/json",
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
resources,
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Fetch a specific component resource
|
|
224
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
225
|
+
const { uri } = request.params;
|
|
226
|
+
|
|
227
|
+
// Parse the URI: impact-ui://component/ComponentName
|
|
228
|
+
const match = uri.match(/^impact-ui:\/\/component\/(.+)$/);
|
|
229
|
+
if (!match) {
|
|
230
|
+
throw new Error(`Invalid resource URI: ${uri}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const componentName = match[1];
|
|
234
|
+
const component = componentRegistry[componentName];
|
|
235
|
+
|
|
236
|
+
if (!component) {
|
|
237
|
+
throw new Error(`Component '${componentName}' not found`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Format component data as JSON
|
|
241
|
+
const componentData = {
|
|
242
|
+
name: component.name,
|
|
243
|
+
category: component.category,
|
|
244
|
+
description: component.description,
|
|
245
|
+
props: component.props,
|
|
246
|
+
examples: component.examples || [],
|
|
247
|
+
storyFile: component.storyFile,
|
|
248
|
+
componentFile: component.componentFile,
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
contents: [
|
|
253
|
+
{
|
|
254
|
+
uri,
|
|
255
|
+
mimeType: "application/json",
|
|
256
|
+
text: JSON.stringify(componentData, null, 2),
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Handle tool calls
|
|
263
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
264
|
+
const { name, arguments: args } = request.params;
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
switch (name) {
|
|
268
|
+
case "get_component_info": {
|
|
269
|
+
const result = getComponentInfo(componentRegistry, args.componentName);
|
|
270
|
+
|
|
271
|
+
if (!result.found) {
|
|
272
|
+
return {
|
|
273
|
+
content: [
|
|
274
|
+
{
|
|
275
|
+
type: "text",
|
|
276
|
+
text: result.message,
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Format the response nicely
|
|
283
|
+
const component = result.component;
|
|
284
|
+
let response = `# ${component.name}\n\n`;
|
|
285
|
+
response += `**Category:** ${component.category}\n\n`;
|
|
286
|
+
response += `**Description:**\n${component.description}\n\n`;
|
|
287
|
+
|
|
288
|
+
if (Object.keys(component.props).length > 0) {
|
|
289
|
+
response += `**Props:**\n`;
|
|
290
|
+
for (const [propName, propInfo] of Object.entries(component.props)) {
|
|
291
|
+
response += `- \`${propName}\` (${propInfo.type})`;
|
|
292
|
+
if (propInfo.defaultValue !== undefined) {
|
|
293
|
+
response += ` - Default: ${JSON.stringify(propInfo.defaultValue)}`;
|
|
294
|
+
}
|
|
295
|
+
if (propInfo.required) {
|
|
296
|
+
response += ` - **Required**`;
|
|
297
|
+
}
|
|
298
|
+
response += `\n ${propInfo.description}\n`;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (component.examples && component.examples.length > 0) {
|
|
303
|
+
response += `\n**Available Examples:** ${component.examples.join(", ")}\n`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
content: [
|
|
308
|
+
{
|
|
309
|
+
type: "text",
|
|
310
|
+
text: response,
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
case "list_components": {
|
|
317
|
+
let components = Object.values(componentRegistry);
|
|
318
|
+
|
|
319
|
+
if (args.category) {
|
|
320
|
+
components = components.filter((c) => c.category === args.category);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const componentNames = components.map((c) => c.name).sort();
|
|
324
|
+
|
|
325
|
+
const categorized = {};
|
|
326
|
+
for (const comp of components) {
|
|
327
|
+
if (!categorized[comp.category]) {
|
|
328
|
+
categorized[comp.category] = [];
|
|
329
|
+
}
|
|
330
|
+
categorized[comp.category].push(comp.name);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let response = "## Available Components\n\n";
|
|
334
|
+
for (const [category, names] of Object.entries(categorized)) {
|
|
335
|
+
response += `### ${category}\n`;
|
|
336
|
+
response += names.sort().join(", ") + "\n\n";
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
content: [
|
|
341
|
+
{
|
|
342
|
+
type: "text",
|
|
343
|
+
text: response,
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
case "get_component_props": {
|
|
350
|
+
const component = componentRegistry[args.componentName];
|
|
351
|
+
|
|
352
|
+
if (!component) {
|
|
353
|
+
return {
|
|
354
|
+
content: [
|
|
355
|
+
{
|
|
356
|
+
type: "text",
|
|
357
|
+
text: `Component '${args.componentName}' not found. Use list_components to see available components.`,
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (Object.keys(component.props).length === 0) {
|
|
364
|
+
return {
|
|
365
|
+
content: [
|
|
366
|
+
{
|
|
367
|
+
type: "text",
|
|
368
|
+
text: `Component '${args.componentName}' has no documented props.`,
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let response = `# ${args.componentName} Props\n\n`;
|
|
375
|
+
|
|
376
|
+
for (const [propName, propInfo] of Object.entries(component.props)) {
|
|
377
|
+
response += `## ${propName}\n`;
|
|
378
|
+
response += `- **Type:** ${propInfo.type}\n`;
|
|
379
|
+
if (propInfo.defaultValue !== undefined) {
|
|
380
|
+
response += `- **Default:** ${JSON.stringify(propInfo.defaultValue)}\n`;
|
|
381
|
+
}
|
|
382
|
+
if (propInfo.required) {
|
|
383
|
+
response += `- **Required:** Yes\n`;
|
|
384
|
+
}
|
|
385
|
+
if (propInfo.options) {
|
|
386
|
+
response += `- **Options:** ${propInfo.options.join(", ")}\n`;
|
|
387
|
+
}
|
|
388
|
+
response += `- **Description:** ${propInfo.description}\n\n`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
content: [
|
|
393
|
+
{
|
|
394
|
+
type: "text",
|
|
395
|
+
text: response,
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
case "generate_code_example": {
|
|
402
|
+
const result = generateCodeExample(
|
|
403
|
+
componentRegistry,
|
|
404
|
+
args.componentName,
|
|
405
|
+
args.props || {}
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
if (result.error) {
|
|
409
|
+
return {
|
|
410
|
+
content: [
|
|
411
|
+
{
|
|
412
|
+
type: "text",
|
|
413
|
+
text: result.error,
|
|
414
|
+
},
|
|
415
|
+
],
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
let response = `## Code Example for ${args.componentName}\n\n`;
|
|
420
|
+
response += "```jsx\n";
|
|
421
|
+
response += result.example;
|
|
422
|
+
response += "\n```\n";
|
|
423
|
+
|
|
424
|
+
if (result.basicExample) {
|
|
425
|
+
response += "\n## Basic Example (without state)\n\n";
|
|
426
|
+
response += "```jsx\n";
|
|
427
|
+
response += result.basicExample;
|
|
428
|
+
response += "\n```\n";
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
content: [
|
|
433
|
+
{
|
|
434
|
+
type: "text",
|
|
435
|
+
text: response,
|
|
436
|
+
},
|
|
437
|
+
],
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
case "search_components": {
|
|
442
|
+
const query = args.query.toLowerCase();
|
|
443
|
+
const matches = Object.values(componentRegistry).filter(
|
|
444
|
+
(comp) =>
|
|
445
|
+
comp.name.toLowerCase().includes(query) ||
|
|
446
|
+
comp.description.toLowerCase().includes(query) ||
|
|
447
|
+
Object.keys(comp.props).some((prop) =>
|
|
448
|
+
prop.toLowerCase().includes(query)
|
|
449
|
+
)
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
if (matches.length === 0) {
|
|
453
|
+
return {
|
|
454
|
+
content: [
|
|
455
|
+
{
|
|
456
|
+
type: "text",
|
|
457
|
+
text: `No components found matching '${args.query}'`,
|
|
458
|
+
},
|
|
459
|
+
],
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
let response = `## Search Results for "${args.query}"\n\n`;
|
|
464
|
+
for (const comp of matches) {
|
|
465
|
+
response += `### ${comp.name}\n`;
|
|
466
|
+
response += `${comp.description}\n`;
|
|
467
|
+
response += `**Category:** ${comp.category}\n\n`;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
content: [
|
|
472
|
+
{
|
|
473
|
+
type: "text",
|
|
474
|
+
text: response,
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
case "get_component_usage_tips": {
|
|
481
|
+
const component = componentRegistry[args.componentName];
|
|
482
|
+
|
|
483
|
+
if (!component) {
|
|
484
|
+
return {
|
|
485
|
+
content: [
|
|
486
|
+
{
|
|
487
|
+
type: "text",
|
|
488
|
+
text: `Component '${args.componentName}' not found.`,
|
|
489
|
+
},
|
|
490
|
+
],
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
let response = `# Usage Tips for ${args.componentName}\n\n`;
|
|
495
|
+
response += `## Description\n${component.description}\n\n`;
|
|
496
|
+
|
|
497
|
+
// Extract tips from description
|
|
498
|
+
const tips = [];
|
|
499
|
+
if (component.description.includes("Note:")) {
|
|
500
|
+
const noteMatch = component.description.match(/Note:([^\n]+)/i);
|
|
501
|
+
if (noteMatch) {
|
|
502
|
+
tips.push(noteMatch[1].trim());
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Add prop-specific tips
|
|
507
|
+
response += `## Important Props\n\n`;
|
|
508
|
+
const importantProps = Object.entries(component.props)
|
|
509
|
+
.filter(([_, info]) => info.required || info.description.length > 50)
|
|
510
|
+
.slice(0, 5);
|
|
511
|
+
|
|
512
|
+
for (const [propName, propInfo] of importantProps) {
|
|
513
|
+
response += `- **${propName}**: ${propInfo.description}\n`;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (component.examples && component.examples.length > 0) {
|
|
517
|
+
response += `\n## Available Examples\n`;
|
|
518
|
+
response += `Check Storybook for these examples: ${component.examples.join(", ")}\n`;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
content: [
|
|
523
|
+
{
|
|
524
|
+
type: "text",
|
|
525
|
+
text: response,
|
|
526
|
+
},
|
|
527
|
+
],
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
default:
|
|
532
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
533
|
+
}
|
|
534
|
+
} catch (error) {
|
|
535
|
+
return {
|
|
536
|
+
content: [
|
|
537
|
+
{
|
|
538
|
+
type: "text",
|
|
539
|
+
text: `Error: ${error.message}\n\nStack: ${error.stack}`,
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
isError: true,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// Start server
|
|
548
|
+
async function main() {
|
|
549
|
+
try {
|
|
550
|
+
await buildComponentRegistry();
|
|
551
|
+
|
|
552
|
+
const transport = new StdioServerTransport();
|
|
553
|
+
await server.connect(transport);
|
|
554
|
+
|
|
555
|
+
console.error("Impact UI MCP Server running on stdio");
|
|
556
|
+
} catch (error) {
|
|
557
|
+
console.error("Failed to start server:", error);
|
|
558
|
+
process.exit(1);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
main().catch((error) => {
|
|
563
|
+
console.error("Fatal error:", error);
|
|
564
|
+
process.exit(1);
|
|
565
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { readFile } from "../utils/fileReader.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse component source file to extract PropTypes or additional info
|
|
5
|
+
*/
|
|
6
|
+
export function parseComponentFile(filePath, componentName) {
|
|
7
|
+
const content = readFile(filePath);
|
|
8
|
+
if (!content) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const info = {
|
|
13
|
+
hasPropTypes: false,
|
|
14
|
+
propTypes: {},
|
|
15
|
+
imports: [],
|
|
16
|
+
exports: [],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Extract PropTypes
|
|
20
|
+
const propTypesMatch = content.match(
|
|
21
|
+
/(?:\.propTypes\s*=|PropTypes\.shape\(|PropTypes\.exact\()\s*{([\s\S]*?)}/m
|
|
22
|
+
);
|
|
23
|
+
if (propTypesMatch) {
|
|
24
|
+
info.hasPropTypes = true;
|
|
25
|
+
info.propTypes = parsePropTypes(propTypesMatch[1]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Extract imports
|
|
29
|
+
const importMatches = content.matchAll(
|
|
30
|
+
/import\s+(?:{([^}]+)}|(\w+)|(\*\s+as\s+\w+))\s+from\s+["']([^"']+)["']/g
|
|
31
|
+
);
|
|
32
|
+
for (const match of importMatches) {
|
|
33
|
+
info.imports.push({
|
|
34
|
+
source: match[4],
|
|
35
|
+
named: match[1] || match[2] || match[3],
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Extract exports
|
|
40
|
+
const exportMatches = content.matchAll(
|
|
41
|
+
/export\s+(?:const|function|class)\s+(\w+)/g
|
|
42
|
+
);
|
|
43
|
+
for (const match of exportMatches) {
|
|
44
|
+
info.exports.push(match[1]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return info;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse PropTypes definition
|
|
52
|
+
*/
|
|
53
|
+
function parsePropTypes(propTypesContent) {
|
|
54
|
+
const propTypes = {};
|
|
55
|
+
|
|
56
|
+
// Match PropTypes definitions like: propName: PropTypes.string
|
|
57
|
+
const propRegex = /(\w+):\s*PropTypes\.(\w+)(?:\.isRequired)?/g;
|
|
58
|
+
let match;
|
|
59
|
+
|
|
60
|
+
while ((match = propRegex.exec(propTypesContent)) !== null) {
|
|
61
|
+
propTypes[match[1]] = {
|
|
62
|
+
type: match[2],
|
|
63
|
+
required: propTypesContent.includes(`${match[1]}: PropTypes.${match[2]}.isRequired`),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return propTypes;
|
|
68
|
+
}
|