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/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
+ }