@cooperco/component-library-mcp-server 0.1.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.
Files changed (34) hide show
  1. package/README.md +106 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +55 -0
  4. package/dist/parsers/passthrough-parser.d.ts +13 -0
  5. package/dist/parsers/passthrough-parser.js +115 -0
  6. package/dist/parsers/typescript-parser.d.ts +11 -0
  7. package/dist/parsers/typescript-parser.js +77 -0
  8. package/dist/parsers/vue-sfc-parser.d.ts +10 -0
  9. package/dist/parsers/vue-sfc-parser.js +61 -0
  10. package/dist/registry/component-descriptions.d.ts +1 -0
  11. package/dist/registry/component-descriptions.js +43 -0
  12. package/dist/registry/component-registry.d.ts +23 -0
  13. package/dist/registry/component-registry.js +256 -0
  14. package/dist/scaffolding/templates.d.ts +21 -0
  15. package/dist/scaffolding/templates.js +122 -0
  16. package/dist/server.d.ts +3 -0
  17. package/dist/server.js +61 -0
  18. package/dist/tools/get-color-palettes.d.ts +14 -0
  19. package/dist/tools/get-color-palettes.js +68 -0
  20. package/dist/tools/get-component-props.d.ts +14 -0
  21. package/dist/tools/get-component-props.js +32 -0
  22. package/dist/tools/get-component.d.ts +14 -0
  23. package/dist/tools/get-component.js +109 -0
  24. package/dist/tools/get-passthrough-config.d.ts +14 -0
  25. package/dist/tools/get-passthrough-config.js +77 -0
  26. package/dist/tools/list-components.d.ts +7 -0
  27. package/dist/tools/list-components.js +21 -0
  28. package/dist/tools/scaffold-component.d.ts +29 -0
  29. package/dist/tools/scaffold-component.js +128 -0
  30. package/dist/tools/search-components.d.ts +7 -0
  31. package/dist/tools/search-components.js +21 -0
  32. package/dist/types.d.ts +43 -0
  33. package/dist/types.js +1 -0
  34. package/package.json +41 -0
@@ -0,0 +1,109 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ export const handleGetComponent = async (registry, name, projectRoot) => {
4
+ const metadata = await registry.getComponent(name);
5
+ if (!metadata) {
6
+ return {
7
+ content: [
8
+ {
9
+ type: 'text',
10
+ text: `Component "${name}" not found. Use list-components to see all available components.`,
11
+ },
12
+ ],
13
+ isError: true,
14
+ };
15
+ }
16
+ let output = `# ${metadata.name}\n\n`;
17
+ output += `**Category:** ${metadata.category}\n`;
18
+ output += `**GraphQL typename:** ${metadata.typename}\n`;
19
+ output += `**Extends Component:** ${metadata.extendsComponent ? 'Yes' : 'No'}\n`;
20
+ output += `**Uses color palette:** ${metadata.hasColorPalette ? 'Yes' : 'No'}\n`;
21
+ output += `**Directory:** ${metadata.directory}/\n`;
22
+ if (metadata.variants.length > 0) {
23
+ output += `**Variants:** ${metadata.variants.join(', ')}\n`;
24
+ }
25
+ // Props table
26
+ output += '\n## Props\n\n';
27
+ if (metadata.props.length > 0) {
28
+ output += '| Prop | Type | Required | Default | Description |\n';
29
+ output += '|------|------|----------|---------|-------------|\n';
30
+ for (const prop of metadata.props) {
31
+ const typeStr = prop.type.length > 60 ? prop.type.substring(0, 57) + '...' : prop.type;
32
+ output += `| ${prop.name} | \`${typeStr}\` | ${prop.required ? 'Yes' : 'No'} | ${prop.defaultValue ?? '-'} | ${prop.description ?? '-'} |\n`;
33
+ }
34
+ }
35
+ else {
36
+ output += 'No props defined.\n';
37
+ }
38
+ // Emits
39
+ output += '\n## Events\n\n';
40
+ if (metadata.emits.length > 0) {
41
+ for (const emit of metadata.emits) {
42
+ output += `- \`${emit.name}\`${emit.payload ? ` (payload: ${emit.payload})` : ''}\n`;
43
+ }
44
+ }
45
+ else {
46
+ output += 'No custom events.\n';
47
+ }
48
+ // Slots
49
+ output += '\n## Slots\n\n';
50
+ if (metadata.slots.length > 0) {
51
+ for (const slot of metadata.slots) {
52
+ output += `- \`${slot.name}\`\n`;
53
+ }
54
+ }
55
+ else {
56
+ output += 'No named slots.\n';
57
+ }
58
+ // Passthrough
59
+ if (metadata.passthroughInterface) {
60
+ output += `\n## Passthrough Interface: ${metadata.passthroughInterface}\n\n`;
61
+ }
62
+ const pt = await registry.getPassthroughForComponent(metadata.name);
63
+ if (pt) {
64
+ if (pt.variants) {
65
+ for (const [variant, keys] of Object.entries(pt.variants)) {
66
+ output += `\n### Passthrough: ${variant} variant\n\n`;
67
+ if (keys.length > 0) {
68
+ output += '| Key | Default Classes |\n';
69
+ output += '|-----|-----------------|\n';
70
+ for (const key of keys) {
71
+ output += `| ${key.key} | \`${key.defaultClasses}\` |\n`;
72
+ }
73
+ }
74
+ }
75
+ }
76
+ else if (pt.keys.length > 0) {
77
+ output += '\n### Passthrough Overrides\n\n';
78
+ output += '| Key | Default Classes |\n';
79
+ output += '|-----|-----------------|\n';
80
+ for (const key of pt.keys) {
81
+ output += `| ${key.key} | \`${key.defaultClasses}\` |\n`;
82
+ }
83
+ }
84
+ }
85
+ // Source files
86
+ output += '\n## Source Files\n\n';
87
+ output += `- **Vue SFC:** ${metadata.vueFile}\n`;
88
+ if (metadata.typeFile) {
89
+ output += `- **Types:** ${metadata.typeFile}\n`;
90
+ }
91
+ // Include raw Vue source for full context
92
+ try {
93
+ const vueSource = await fs.readFile(path.join(projectRoot, metadata.vueFile), 'utf-8');
94
+ output += `\n## Vue Source\n\n\`\`\`vue\n${vueSource}\`\`\`\n`;
95
+ }
96
+ catch {
97
+ // File read failed, skip raw source
98
+ }
99
+ if (metadata.typeFile) {
100
+ try {
101
+ const typeSource = await fs.readFile(path.join(projectRoot, metadata.typeFile), 'utf-8');
102
+ output += `\n## Type Definitions\n\n\`\`\`typescript\n${typeSource}\`\`\`\n`;
103
+ }
104
+ catch {
105
+ // File read failed, skip raw source
106
+ }
107
+ }
108
+ return { content: [{ type: 'text', text: output }] };
109
+ };
@@ -0,0 +1,14 @@
1
+ import { ComponentRegistry } from '../registry/component-registry.js';
2
+ export declare const handleGetPassthroughConfig: (registry: ComponentRegistry, name: string) => Promise<{
3
+ content: {
4
+ type: "text";
5
+ text: string;
6
+ }[];
7
+ isError: boolean;
8
+ } | {
9
+ content: {
10
+ type: "text";
11
+ text: string;
12
+ }[];
13
+ isError?: undefined;
14
+ }>;
@@ -0,0 +1,77 @@
1
+ export const handleGetPassthroughConfig = async (registry, name) => {
2
+ const metadata = await registry.getComponent(name);
3
+ if (!metadata) {
4
+ return {
5
+ content: [
6
+ {
7
+ type: 'text',
8
+ text: `Component "${name}" not found. Use list-components to see all available components.`,
9
+ },
10
+ ],
11
+ isError: true,
12
+ };
13
+ }
14
+ const pt = await registry.getPassthroughForComponent(metadata.name);
15
+ let output = `# ${metadata.name} Passthrough Configuration\n\n`;
16
+ if (metadata.passthroughInterface) {
17
+ output += `**Interface:** ${metadata.passthroughInterface}\n\n`;
18
+ }
19
+ if (!pt) {
20
+ output += 'No passthrough configuration found for this component.\n';
21
+ }
22
+ else if (pt.variants) {
23
+ for (const [variant, keys] of Object.entries(pt.variants)) {
24
+ output += `## ${variant} Variant\n\n`;
25
+ if (keys.length > 0) {
26
+ output += '| Key | Default Classes |\n';
27
+ output += '|-----|-----------------|\n';
28
+ for (const key of keys) {
29
+ output += `| ${key.key} | \`${key.defaultClasses}\` |\n`;
30
+ }
31
+ }
32
+ else {
33
+ output += 'No overrides.\n';
34
+ }
35
+ output += '\n';
36
+ }
37
+ }
38
+ else if (pt.keys.length > 0) {
39
+ output += '## Overrides\n\n';
40
+ output += '| Key | Default Classes |\n';
41
+ output += '|-----|-----------------|\n';
42
+ for (const key of pt.keys) {
43
+ output += `| ${key.key} | \`${key.defaultClasses}\` |\n`;
44
+ }
45
+ output += '\n';
46
+ }
47
+ output += '## Usage\n\n';
48
+ output += '```typescript\n';
49
+ output +=
50
+ "import { combinePassthroughs } from '@/config/defaultPassthrough'\n";
51
+ output += `import { ${metadata.name}Pt } from '@/config/defaultPassthrough'\n\n`;
52
+ output += 'const pt = computed(() =>\n';
53
+ if (metadata.variants.length > 0) {
54
+ const propName = metadata.variantPropName ?? 'variant';
55
+ output += ` combinePassthroughs(${metadata.name}Pt[props.${propName}], props.pt ?? {})\n`;
56
+ }
57
+ else {
58
+ output += ` combinePassthroughs(${metadata.name}Pt, props.pt ?? {})\n`;
59
+ }
60
+ output += ')\n';
61
+ output += '```\n\n';
62
+ const defaultPt = await registry.getDefaultPassthrough();
63
+ output += '## Base DEFAULT_PASSTHROUGH\n\n';
64
+ output += 'All passthroughs extend from:\n';
65
+ if (defaultPt.length > 0) {
66
+ output += '| Key | Default |\n';
67
+ output += '|-----|---------|\n';
68
+ for (const entry of defaultPt) {
69
+ const displayValue = entry.defaultClasses || "''";
70
+ output += `| ${entry.key} | \`${displayValue}\` |\n`;
71
+ }
72
+ }
73
+ else {
74
+ output += 'Could not parse DEFAULT_PASSTHROUGH from source.\n';
75
+ }
76
+ return { content: [{ type: 'text', text: output }] };
77
+ };
@@ -0,0 +1,7 @@
1
+ import { ComponentRegistry } from '../registry/component-registry.js';
2
+ export declare const handleListComponents: (registry: ComponentRegistry) => Promise<{
3
+ content: {
4
+ type: "text";
5
+ text: string;
6
+ }[];
7
+ }>;
@@ -0,0 +1,21 @@
1
+ export const handleListComponents = async (registry) => {
2
+ const components = await registry.listComponents();
3
+ const modules = components.filter((c) => c.category === 'module');
4
+ const comps = components.filter((c) => c.category === 'component');
5
+ let output = `# Cooper Component Library (${components.length} components)\n\n`;
6
+ output += '## Modules\n\n';
7
+ output += '| Name | Variants | Description |\n';
8
+ output += '|------|----------|-------------|\n';
9
+ for (const m of modules) {
10
+ const variants = m.variants.length > 0 ? m.variants.join(', ') : '-';
11
+ output += `| ${m.name} | ${variants} | ${m.description} |\n`;
12
+ }
13
+ output += '\n## Components\n\n';
14
+ output += '| Name | Variants | Description |\n';
15
+ output += '|------|----------|-------------|\n';
16
+ for (const c of comps) {
17
+ const variants = c.variants.length > 0 ? c.variants.join(', ') : '-';
18
+ output += `| ${c.name} | ${variants} | ${c.description} |\n`;
19
+ }
20
+ return { content: [{ type: 'text', text: output }] };
21
+ };
@@ -0,0 +1,29 @@
1
+ import { ComponentRegistry } from '../registry/component-registry.js';
2
+ interface ScaffoldArgs {
3
+ name: string;
4
+ category: 'component' | 'module';
5
+ props?: Array<{
6
+ name: string;
7
+ type: string;
8
+ required?: boolean;
9
+ default?: string;
10
+ }>;
11
+ hasColorPalette?: boolean;
12
+ hasPassthrough?: boolean;
13
+ variants?: string[];
14
+ variantPropName?: string;
15
+ }
16
+ export declare const handleScaffoldComponent: (registry: ComponentRegistry, args: ScaffoldArgs, projectRoot: string) => Promise<{
17
+ content: {
18
+ type: "text";
19
+ text: string;
20
+ }[];
21
+ isError: boolean;
22
+ } | {
23
+ content: {
24
+ type: "text";
25
+ text: string;
26
+ }[];
27
+ isError?: undefined;
28
+ }>;
29
+ export {};
@@ -0,0 +1,128 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { generateTypeFile, generateVueFile, generateStoryFile, generateComponentExport, generateTypeExport, } from '../scaffolding/templates.js';
4
+ export const handleScaffoldComponent = async (registry, args, projectRoot) => {
5
+ const { name, category, props, hasColorPalette, hasPassthrough, variants, variantPropName, } = args;
6
+ // Validate name is PascalCase
7
+ if (!/^[A-Z][a-zA-Z0-9]+$/.test(name)) {
8
+ return {
9
+ content: [
10
+ {
11
+ type: 'text',
12
+ text: `Error: Component name "${name}" must be PascalCase (e.g., "BannerModule").`,
13
+ },
14
+ ],
15
+ isError: true,
16
+ };
17
+ }
18
+ // Validate prop names are valid TypeScript identifiers
19
+ const TS_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
20
+ if (props) {
21
+ for (const prop of props) {
22
+ if (!TS_IDENTIFIER.test(prop.name)) {
23
+ return {
24
+ content: [
25
+ {
26
+ type: 'text',
27
+ text: `Error: Prop name "${prop.name}" is not a valid TypeScript identifier.`,
28
+ },
29
+ ],
30
+ isError: true,
31
+ };
32
+ }
33
+ }
34
+ }
35
+ // Validate variant values are safe string literals
36
+ if (variants) {
37
+ for (const variant of variants) {
38
+ if (!/^[a-zA-Z][a-zA-Z0-9 _-]*$/.test(variant)) {
39
+ return {
40
+ content: [
41
+ {
42
+ type: 'text',
43
+ text: `Error: Variant "${variant}" contains invalid characters. Use only letters, numbers, spaces, hyphens, and underscores.`,
44
+ },
45
+ ],
46
+ isError: true,
47
+ };
48
+ }
49
+ }
50
+ }
51
+ // Validate variantPropName is a valid TypeScript identifier
52
+ if (variantPropName && !TS_IDENTIFIER.test(variantPropName)) {
53
+ return {
54
+ content: [
55
+ {
56
+ type: 'text',
57
+ text: `Error: Variant prop name "${variantPropName}" is not a valid TypeScript identifier.`,
58
+ },
59
+ ],
60
+ isError: true,
61
+ };
62
+ }
63
+ // Check if component already exists
64
+ const exists = await registry.componentExists(name);
65
+ if (exists) {
66
+ return {
67
+ content: [
68
+ {
69
+ type: 'text',
70
+ text: `Error: Component "${name}" already exists.`,
71
+ },
72
+ ],
73
+ isError: true,
74
+ };
75
+ }
76
+ const options = {
77
+ name,
78
+ category,
79
+ props,
80
+ hasColorPalette: hasColorPalette ?? true,
81
+ hasPassthrough: hasPassthrough ?? true,
82
+ variants,
83
+ variantPropName,
84
+ };
85
+ const componentDir = path.join(projectRoot, 'src/components', name);
86
+ const storyDir = path.join(projectRoot, '.storybook', category === 'module' ? 'modules' : 'components');
87
+ // Generate file contents
88
+ const typeContent = generateTypeFile(options);
89
+ const vueContent = generateVueFile(options);
90
+ const storyContent = generateStoryFile(options);
91
+ const componentExport = generateComponentExport(name);
92
+ const typeExport = generateTypeExport(name);
93
+ // Create directories
94
+ await fs.mkdir(componentDir, { recursive: true });
95
+ await fs.mkdir(storyDir, { recursive: true });
96
+ // Write files
97
+ const filePaths = {
98
+ type: path.join(componentDir, `${name}.ts`),
99
+ vue: path.join(componentDir, `${name}.vue`),
100
+ story: path.join(storyDir, `${name}.stories.ts`),
101
+ };
102
+ await fs.writeFile(filePaths.type, typeContent, 'utf-8');
103
+ await fs.writeFile(filePaths.vue, vueContent, 'utf-8');
104
+ await fs.writeFile(filePaths.story, storyContent, 'utf-8');
105
+ // Update barrel exports
106
+ const componentsPath = path.join(projectRoot, 'src/components/components.ts');
107
+ const typesPath = path.join(projectRoot, 'src/components/types.ts');
108
+ const componentsSource = await fs.readFile(componentsPath, 'utf-8');
109
+ const typesSource = await fs.readFile(typesPath, 'utf-8');
110
+ await fs.writeFile(componentsPath, componentsSource.trimEnd() + '\n' + componentExport + '\n', 'utf-8');
111
+ await fs.writeFile(typesPath, typesSource.trimEnd() + '\n' + typeExport + '\n', 'utf-8');
112
+ // Build output summary
113
+ let output = `# Scaffolded: ${name}\n\n`;
114
+ output += '## Created Files\n\n';
115
+ output += `- \`src/components/${name}/${name}.ts\` — TypeScript interfaces\n`;
116
+ output += `- \`src/components/${name}/${name}.vue\` — Vue component\n`;
117
+ output += `- \`.storybook/${category === 'module' ? 'modules' : 'components'}/${name}.stories.ts\` — Storybook story\n\n`;
118
+ output += '## Updated Files\n\n';
119
+ output += '- `src/components/components.ts` — Added component export\n';
120
+ output += '- `src/components/types.ts` — Added type export\n\n';
121
+ output += '## Generated Type File\n\n';
122
+ output += '```typescript\n' + typeContent + '```\n\n';
123
+ output += '## Generated Vue File\n\n';
124
+ output += '```vue\n' + vueContent + '```\n\n';
125
+ output += '## Generated Story File\n\n';
126
+ output += '```typescript\n' + storyContent + '```\n';
127
+ return { content: [{ type: 'text', text: output }] };
128
+ };
@@ -0,0 +1,7 @@
1
+ import { ComponentRegistry } from '../registry/component-registry.js';
2
+ export declare const handleSearchComponents: (registry: ComponentRegistry, query: string, category?: 'component' | 'module' | 'all') => Promise<{
3
+ content: {
4
+ type: "text";
5
+ text: string;
6
+ }[];
7
+ }>;
@@ -0,0 +1,21 @@
1
+ export const handleSearchComponents = async (registry, query, category) => {
2
+ const results = await registry.searchComponents(query, category);
3
+ if (results.length === 0) {
4
+ return {
5
+ content: [
6
+ {
7
+ type: 'text',
8
+ text: `No components found matching "${query}"${category && category !== 'all' ? ` in category "${category}"` : ''}. Use list-components to see all available components.`,
9
+ },
10
+ ],
11
+ };
12
+ }
13
+ let output = `# Search Results for "${query}" (${results.length} match${results.length === 1 ? '' : 'es'})\n\n`;
14
+ output += '| Name | Category | Variants | Description |\n';
15
+ output += '|------|----------|----------|-------------|\n';
16
+ for (const c of results) {
17
+ const variants = c.variants.length > 0 ? c.variants.join(', ') : '-';
18
+ output += `| ${c.name} | ${c.category} | ${variants} | ${c.description} |\n`;
19
+ }
20
+ return { content: [{ type: 'text', text: output }] };
21
+ };
@@ -0,0 +1,43 @@
1
+ export interface PropInfo {
2
+ name: string;
3
+ type: string;
4
+ required: boolean;
5
+ description?: string;
6
+ defaultValue?: string;
7
+ }
8
+ export interface EmitInfo {
9
+ name: string;
10
+ payload?: string;
11
+ }
12
+ export interface SlotInfo {
13
+ name: string;
14
+ }
15
+ export interface PassthroughKeyInfo {
16
+ key: string;
17
+ defaultClasses: string;
18
+ }
19
+ export interface ComponentMetadata {
20
+ name: string;
21
+ category: 'component' | 'module';
22
+ typename: string;
23
+ directory: string;
24
+ vueFile: string;
25
+ typeFile: string | null;
26
+ interfaceName: string;
27
+ props: PropInfo[];
28
+ emits: EmitInfo[];
29
+ slots: SlotInfo[];
30
+ variants: string[];
31
+ variantPropName: string | null;
32
+ passthroughInterface: string | null;
33
+ passthroughKeys: PassthroughKeyInfo[];
34
+ extendsComponent: boolean;
35
+ hasColorPalette: boolean;
36
+ description: string;
37
+ }
38
+ export interface ComponentDiscovery {
39
+ exportName: string;
40
+ relativePath: string;
41
+ absoluteVuePath: string;
42
+ absoluteTypePath: string | null;
43
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@cooperco/component-library-mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for the Cooper Component Library. Provides component documentation, prop inspection, color palette info, and scaffolding tools for AI assistants.",
5
+ "private": false,
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "bin": {
9
+ "component-library-mcp-server": "dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "keywords": [
15
+ "mcp",
16
+ "model-context-protocol",
17
+ "vue",
18
+ "component-library",
19
+ "cooper",
20
+ "ai-tools"
21
+ ],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/Cryobank/Cordblood-Component-Library.git",
25
+ "directory": "mcp"
26
+ },
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.27.0",
29
+ "typescript": "~5.3.3",
30
+ "zod": "^3.23.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^20.0.0",
34
+ "tsx": "^4.7.0"
35
+ },
36
+ "scripts": {
37
+ "dev": "tsx src/index.ts",
38
+ "build": "tsc",
39
+ "watch": "tsc --watch"
40
+ }
41
+ }