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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { readFile } from "../utils/fileReader.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse Storybook story file to extract component metadata
|
|
5
|
+
*/
|
|
6
|
+
export function parseStorybookStory(filePath, componentName) {
|
|
7
|
+
const content = readFile(filePath);
|
|
8
|
+
if (!content) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const metadata = {
|
|
13
|
+
name: componentName,
|
|
14
|
+
description: "",
|
|
15
|
+
props: {},
|
|
16
|
+
examples: [],
|
|
17
|
+
category: "Components",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Extract component description
|
|
21
|
+
const descriptionMatch = content.match(
|
|
22
|
+
/description:\s*{\s*component:\s*["']([^"']+)["']/s
|
|
23
|
+
);
|
|
24
|
+
if (descriptionMatch) {
|
|
25
|
+
metadata.description = descriptionMatch[1]
|
|
26
|
+
.replace(/\\n/g, "\n")
|
|
27
|
+
.trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Extract title to determine category
|
|
31
|
+
const titleMatch = content.match(/title:\s*["']([^"']+)["']/);
|
|
32
|
+
if (titleMatch) {
|
|
33
|
+
const title = titleMatch[1];
|
|
34
|
+
if (title.startsWith("Patterns/")) {
|
|
35
|
+
metadata.category = "Patterns";
|
|
36
|
+
} else if (title.startsWith("Components/")) {
|
|
37
|
+
metadata.category = "Components";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Extract argTypes
|
|
42
|
+
const argTypesMatch = content.match(/argTypes:\s*{([\s\S]*?)},?\s*(?:};|export)/);
|
|
43
|
+
if (argTypesMatch) {
|
|
44
|
+
metadata.props = parseArgTypes(argTypesMatch[1]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Extract story examples
|
|
48
|
+
const storyExports = content.match(/export const (\w+)\s*=\s*Template\.bind\(/g);
|
|
49
|
+
if (storyExports) {
|
|
50
|
+
metadata.examples = storyExports.map((match) => {
|
|
51
|
+
const nameMatch = match.match(/export const (\w+)/);
|
|
52
|
+
return nameMatch ? nameMatch[1] : null;
|
|
53
|
+
}).filter(Boolean);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return metadata;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Parse argTypes object from Storybook story
|
|
61
|
+
*/
|
|
62
|
+
function parseArgTypes(argTypesContent) {
|
|
63
|
+
const props = {};
|
|
64
|
+
|
|
65
|
+
// Match each property definition
|
|
66
|
+
const propRegex = /(\w+):\s*{([^}]+(?:{[^}]*}[^}]*)*)}/g;
|
|
67
|
+
let match;
|
|
68
|
+
|
|
69
|
+
while ((match = propRegex.exec(argTypesContent)) !== null) {
|
|
70
|
+
const propName = match[1];
|
|
71
|
+
const propContent = match[2];
|
|
72
|
+
|
|
73
|
+
const propInfo = {
|
|
74
|
+
description: "",
|
|
75
|
+
type: "unknown",
|
|
76
|
+
defaultValue: undefined,
|
|
77
|
+
options: undefined,
|
|
78
|
+
required: false,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Extract description
|
|
82
|
+
const descriptionMatch = propContent.match(
|
|
83
|
+
/description:\s*["']([^"']+)["']|description:\s*`([^`]+)`/
|
|
84
|
+
);
|
|
85
|
+
if (descriptionMatch) {
|
|
86
|
+
propInfo.description = (descriptionMatch[1] || descriptionMatch[2])
|
|
87
|
+
.replace(/\\n/g, "\n")
|
|
88
|
+
.trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Extract type
|
|
92
|
+
const typeMatch = propContent.match(
|
|
93
|
+
/type:\s*{\s*summary:\s*["']([^"']+)["']/
|
|
94
|
+
);
|
|
95
|
+
if (typeMatch) {
|
|
96
|
+
propInfo.type = typeMatch[1];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Extract default value
|
|
100
|
+
const defaultValueMatch = propContent.match(
|
|
101
|
+
/defaultValue:\s*{\s*summary:\s*["']([^"']+)["']|defaultValue:\s*{\s*summary:\s*([^}]+)}/
|
|
102
|
+
);
|
|
103
|
+
if (defaultValueMatch) {
|
|
104
|
+
const defaultValue = defaultValueMatch[1] || defaultValueMatch[2];
|
|
105
|
+
try {
|
|
106
|
+
// Try to parse as JSON if it looks like an object/array
|
|
107
|
+
if (defaultValue.startsWith("[") || defaultValue.startsWith("{")) {
|
|
108
|
+
propInfo.defaultValue = JSON.parse(defaultValue);
|
|
109
|
+
} else {
|
|
110
|
+
propInfo.defaultValue = defaultValue;
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
propInfo.defaultValue = defaultValue;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Extract options (for enum-like props)
|
|
118
|
+
const optionsMatch = propContent.match(/options:\s*\[([^\]]+)\]/);
|
|
119
|
+
if (optionsMatch) {
|
|
120
|
+
try {
|
|
121
|
+
propInfo.options = JSON.parse(`[${optionsMatch[1]}]`);
|
|
122
|
+
} catch {
|
|
123
|
+
// If parsing fails, try to extract string values
|
|
124
|
+
const stringValues = optionsMatch[1].match(/["']([^"']+)["']/g);
|
|
125
|
+
if (stringValues) {
|
|
126
|
+
propInfo.options = stringValues.map((s) => s.replace(/["']/g, ""));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check if required (no defaultValue usually means required)
|
|
132
|
+
if (!defaultValueMatch && !propContent.includes("optional")) {
|
|
133
|
+
propInfo.required = true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
props[propName] = propInfo;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return props;
|
|
140
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a code example for a component
|
|
3
|
+
*/
|
|
4
|
+
export function generateCodeExample(componentRegistry, componentName, props = {}) {
|
|
5
|
+
const component = componentRegistry[componentName];
|
|
6
|
+
|
|
7
|
+
if (!component) {
|
|
8
|
+
return {
|
|
9
|
+
error: `Component '${componentName}' not found.`,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Build props string
|
|
14
|
+
const propStrings = [];
|
|
15
|
+
|
|
16
|
+
// Add props from the request
|
|
17
|
+
for (const [key, value] of Object.entries(props)) {
|
|
18
|
+
if (value !== undefined && value !== null) {
|
|
19
|
+
if (typeof value === "string") {
|
|
20
|
+
propStrings.push(` ${key}="${value}"`);
|
|
21
|
+
} else if (typeof value === "boolean") {
|
|
22
|
+
propStrings.push(` ${key}={${value}}`);
|
|
23
|
+
} else if (typeof value === "number") {
|
|
24
|
+
propStrings.push(` ${key}={${value}}`);
|
|
25
|
+
} else if (Array.isArray(value)) {
|
|
26
|
+
propStrings.push(` ${key}={${JSON.stringify(value)}}`);
|
|
27
|
+
} else if (typeof value === "object") {
|
|
28
|
+
propStrings.push(` ${key}={${JSON.stringify(value)}}`);
|
|
29
|
+
} else {
|
|
30
|
+
propStrings.push(` ${key}={${value}}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const propsSection = propStrings.length > 0
|
|
36
|
+
? `\n${propStrings.join("\n")}\n`
|
|
37
|
+
: "";
|
|
38
|
+
|
|
39
|
+
// Generate the example
|
|
40
|
+
const example = `import { ${componentName} } from 'impact-ui';
|
|
41
|
+
|
|
42
|
+
function Example() {
|
|
43
|
+
return (
|
|
44
|
+
<${componentName}${propsSection} />
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default Example;`;
|
|
49
|
+
|
|
50
|
+
// Generate with state if needed (for components that typically need state)
|
|
51
|
+
const statefulComponents = ["Modal", "Panel", "BottomSheet", "Popover", "Select"];
|
|
52
|
+
if (statefulComponents.includes(componentName)) {
|
|
53
|
+
const statefulExample = `import { useState } from 'react';
|
|
54
|
+
import { ${componentName} } from 'impact-ui';
|
|
55
|
+
|
|
56
|
+
function Example() {
|
|
57
|
+
const [open, setOpen] = useState(false);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
<button onClick={() => setOpen(true)}>Open ${componentName}</button>
|
|
62
|
+
<${componentName}
|
|
63
|
+
open={open}
|
|
64
|
+
onClose={() => setOpen(false)}${propsSection}
|
|
65
|
+
/>
|
|
66
|
+
</>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default Example;`;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
example: statefulExample,
|
|
74
|
+
basicExample: example,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
example,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get detailed information about a component
|
|
3
|
+
*/
|
|
4
|
+
export function getComponentInfo(componentRegistry, componentName) {
|
|
5
|
+
const component = componentRegistry[componentName];
|
|
6
|
+
|
|
7
|
+
if (!component) {
|
|
8
|
+
return {
|
|
9
|
+
found: false,
|
|
10
|
+
message: `Component '${componentName}' not found. Use list_components to see available components.`,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
found: true,
|
|
16
|
+
component: {
|
|
17
|
+
name: component.name,
|
|
18
|
+
category: component.category,
|
|
19
|
+
description: component.description,
|
|
20
|
+
props: component.props,
|
|
21
|
+
examples: component.examples,
|
|
22
|
+
storyFile: component.storyFile,
|
|
23
|
+
componentFile: component.componentFile,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Read a file and return its contents as a string
|
|
7
|
+
*/
|
|
8
|
+
export function readFile(filePath) {
|
|
9
|
+
try {
|
|
10
|
+
if (!existsSync(filePath)) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return readFileSync(filePath, "utf-8");
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error(`Error reading file ${filePath}:`, error.message);
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get the project root directory (frontend folder or impact-ui package root)
|
|
22
|
+
* Supports multiple deployment scenarios:
|
|
23
|
+
*
|
|
24
|
+
* Priority order:
|
|
25
|
+
* 1. Auto-detect from node_modules (if impact-ui package includes source files) - BEST for Option 1
|
|
26
|
+
* 2. IMPACT_UI_FRONTEND_PATH env var (direct frontend path)
|
|
27
|
+
* 3. IMPACT_UI_PATH env var (Impact UI repository root, will append /frontend)
|
|
28
|
+
* 4. IMPACT_UI_NODE_MODULES env var (explicit path to node_modules/impact-ui)
|
|
29
|
+
* 5. Relative path from mcp-server (default, for local development)
|
|
30
|
+
*
|
|
31
|
+
* IMPACT_UI_PATH explanation:
|
|
32
|
+
* - This is the path to the CLONED Impact UI git repository (e.g., /path/to/impact-ui)
|
|
33
|
+
* - NOT the npm package path
|
|
34
|
+
* - NOT node_modules/impact-ui
|
|
35
|
+
* - Only needed if teams clone the repository instead of using npm package
|
|
36
|
+
*/
|
|
37
|
+
export function getProjectRoot() {
|
|
38
|
+
// Priority 1: Auto-detect from node_modules (works when source files are included in npm package)
|
|
39
|
+
// This is the BEST option when using Option 1 (source files in package)
|
|
40
|
+
const possibleNodeModulesPaths = [
|
|
41
|
+
join(process.cwd(), "node_modules", "impact-ui"),
|
|
42
|
+
join(process.cwd(), "..", "node_modules", "impact-ui"),
|
|
43
|
+
join(process.cwd(), "..", "..", "node_modules", "impact-ui"),
|
|
44
|
+
// Also check if mcp-server is installed in node_modules
|
|
45
|
+
join(process.cwd(), "node_modules", "@impact-analytics", "impact-ui-mcp-server", "..", "..", "impact-ui"),
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
for (const path of possibleNodeModulesPaths) {
|
|
49
|
+
const srcPath = join(path, "src");
|
|
50
|
+
const storiesPath = join(srcPath, "stories");
|
|
51
|
+
if (existsSync(storiesPath)) {
|
|
52
|
+
console.error(`Auto-detected Impact UI from node_modules: ${path}`);
|
|
53
|
+
return path;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Priority 2: Direct frontend path via environment variable
|
|
58
|
+
if (process.env.IMPACT_UI_FRONTEND_PATH) {
|
|
59
|
+
return process.env.IMPACT_UI_FRONTEND_PATH;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Priority 3: Impact UI repository root path (will append /frontend)
|
|
63
|
+
// IMPACT_UI_PATH = path to cloned git repository (e.g., /Users/team/projects/impact-ui)
|
|
64
|
+
if (process.env.IMPACT_UI_PATH) {
|
|
65
|
+
const frontendPath = join(process.env.IMPACT_UI_PATH, "frontend");
|
|
66
|
+
if (existsSync(frontendPath)) {
|
|
67
|
+
return frontendPath;
|
|
68
|
+
}
|
|
69
|
+
// If no frontend folder, assume IMPACT_UI_PATH is already the frontend path
|
|
70
|
+
if (existsSync(process.env.IMPACT_UI_PATH)) {
|
|
71
|
+
return process.env.IMPACT_UI_PATH;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Priority 4: Explicit node_modules path
|
|
76
|
+
if (process.env.IMPACT_UI_NODE_MODULES) {
|
|
77
|
+
const nodeModulesPath = process.env.IMPACT_UI_NODE_MODULES;
|
|
78
|
+
const srcPath = join(nodeModulesPath, "src");
|
|
79
|
+
if (existsSync(srcPath)) {
|
|
80
|
+
return nodeModulesPath;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Priority 5: Default - relative path from mcp-server (local development)
|
|
85
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
86
|
+
const __dirname = dirname(__filename);
|
|
87
|
+
|
|
88
|
+
// Assuming mcp-server is at impact-ui/mcp-server
|
|
89
|
+
// and frontend is at impact-ui/frontend
|
|
90
|
+
return join(__dirname, "../../../frontend");
|
|
91
|
+
}
|