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.
@@ -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
+ }