@blue-patterns/mcp 0.0.0-stage → 0.7.18

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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Blue-Patterns-UI Contributors
4
+ Portions Copyright (c) 2026 Meta Platforms, Inc. (Astryx upstream foundation)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,37 @@
1
- # Temporary Holding Version
1
+ # @blue-patterns/mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Deterministic Model Context Protocol (MCP) server exposing Blue-Patterns-UI contracts, validation, and component discovery to AI coding agents.
4
+
5
+ ## Features
6
+
7
+ - **Component Search (`ds_search_components`):** Rapid semantic search across component contracts, visual variants, token slots, and accessibility roles.
8
+ - **Contract Inspection (`ds_get_contract`):** Retrieve machine-readable `.doc.mjs` component specifications, prop schemas, and usage examples.
9
+ - **JSX AST Validation (`ds_validate_jsx`):** Validate JSX source code against Blue-Patterns-UI invariants (zero raw hex colors, valid component imports, accessible props).
10
+ - **Token Discovery (`ds_list_tokens`):** Query CSS custom property design tokens across colors, spacing, radii, typography, and spring motion curves.
11
+
12
+ ## Usage
13
+
14
+ ### Direct Execution
15
+
16
+ ```bash
17
+ npx @blue-patterns/mcp
18
+ # or
19
+ pnpm dlx @blue-patterns/mcp
20
+ ```
21
+
22
+ ### In Agent / Claude Desktop Configuration
23
+
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "blue-patterns": {
28
+ "command": "npx",
29
+ "args": ["-y", "@blue-patterns/mcp"]
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ ## License
36
+
37
+ MIT
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { startMcpServer } from '../dist/server.js';
3
+
4
+ startMcpServer();
@@ -0,0 +1,5 @@
1
+ export * from './tools/search.js';
2
+ export * from './tools/contract.js';
3
+ export * from './tools/validate.js';
4
+ export * from './tools/swizzle.js';
5
+ export { startMcpServer } from './server.js';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from './tools/search.js';
2
+ export * from './tools/contract.js';
3
+ export * from './tools/validate.js';
4
+ export * from './tools/swizzle.js';
5
+ export { startMcpServer } from './server.js';
@@ -0,0 +1 @@
1
+ export declare function startMcpServer(): void;
package/dist/server.js ADDED
@@ -0,0 +1,142 @@
1
+ import readline from 'node:readline';
2
+ import { dsSearch } from './tools/search.js';
3
+ import { dsContract } from './tools/contract.js';
4
+ import { dsValidateJsx } from './tools/validate.js';
5
+ import { dsSwizzle } from './tools/swizzle.js';
6
+ export function startMcpServer() {
7
+ const rl = readline.createInterface({
8
+ input: process.stdin,
9
+ output: process.stdout,
10
+ terminal: false,
11
+ });
12
+ const tools = [
13
+ {
14
+ name: 'ds_search',
15
+ description: 'Ranked search across all 150+ components, hooks, motion primitives, and theme packs.',
16
+ inputSchema: {
17
+ type: 'object',
18
+ properties: {
19
+ query: { type: 'string', description: 'Search term or component name' },
20
+ category: { type: 'string', description: 'Optional category filter: Component, DataViz, Motion, Theme' },
21
+ },
22
+ required: ['query'],
23
+ },
24
+ },
25
+ {
26
+ name: 'ds_contract',
27
+ description: 'Batch-returns typed props, variant enums, CSS token dependencies, and dense usage rules.',
28
+ inputSchema: {
29
+ type: 'object',
30
+ properties: {
31
+ componentNames: {
32
+ type: 'array',
33
+ items: { type: 'string' },
34
+ description: 'List of component names to inspect',
35
+ },
36
+ },
37
+ required: ['componentNames'],
38
+ },
39
+ },
40
+ {
41
+ name: 'ds_validate_jsx',
42
+ description: 'Static AST validator checking for hallucinated components, invalid props, or raw hex colors.',
43
+ inputSchema: {
44
+ type: 'object',
45
+ properties: {
46
+ tsxCode: { type: 'string', description: 'TSX / JSX source code to validate' },
47
+ },
48
+ required: ['tsxCode'],
49
+ },
50
+ },
51
+ {
52
+ name: 'ds_swizzle',
53
+ description: 'Eject component source, StyleX styles, and tests into a consumer local directory.',
54
+ inputSchema: {
55
+ type: 'object',
56
+ properties: {
57
+ componentName: { type: 'string', description: 'Component to swizzle/eject' },
58
+ destinationPath: { type: 'string', description: 'Target destination directory' },
59
+ },
60
+ required: ['componentName', 'destinationPath'],
61
+ },
62
+ },
63
+ ];
64
+ rl.on('line', (line) => {
65
+ if (!line.trim())
66
+ return;
67
+ try {
68
+ const request = JSON.parse(line);
69
+ const { id, method, params } = request;
70
+ if (method === 'initialize') {
71
+ const response = {
72
+ jsonrpc: '2.0',
73
+ id,
74
+ result: {
75
+ protocolVersion: '2024-11-05',
76
+ serverInfo: {
77
+ name: 'blue-patterns-mcp',
78
+ version: '0.1.0',
79
+ },
80
+ capabilities: {
81
+ tools: {},
82
+ },
83
+ },
84
+ };
85
+ process.stdout.write(JSON.stringify(response) + '\n');
86
+ }
87
+ else if (method === 'tools/list') {
88
+ const response = {
89
+ jsonrpc: '2.0',
90
+ id,
91
+ result: {
92
+ tools,
93
+ },
94
+ };
95
+ process.stdout.write(JSON.stringify(response) + '\n');
96
+ }
97
+ else if (method === 'tools/call') {
98
+ const toolName = params?.name;
99
+ const args = params?.arguments || {};
100
+ let resultData = null;
101
+ if (toolName === 'ds_search') {
102
+ resultData = dsSearch(args.query || '', args.category);
103
+ }
104
+ else if (toolName === 'ds_contract') {
105
+ resultData = dsContract(args.componentNames || []);
106
+ }
107
+ else if (toolName === 'ds_validate_jsx') {
108
+ resultData = dsValidateJsx(args.tsxCode || '');
109
+ }
110
+ else if (toolName === 'ds_swizzle') {
111
+ resultData = dsSwizzle(args.componentName || '', args.destinationPath || '');
112
+ }
113
+ else {
114
+ throw new Error(`Unknown tool: ${toolName}`);
115
+ }
116
+ const response = {
117
+ jsonrpc: '2.0',
118
+ id,
119
+ result: {
120
+ content: [
121
+ {
122
+ type: 'text',
123
+ text: JSON.stringify(resultData, null, 2),
124
+ },
125
+ ],
126
+ },
127
+ };
128
+ process.stdout.write(JSON.stringify(response) + '\n');
129
+ }
130
+ }
131
+ catch (err) {
132
+ const errorResponse = {
133
+ jsonrpc: '2.0',
134
+ error: {
135
+ code: -32603,
136
+ message: err.message,
137
+ },
138
+ };
139
+ process.stdout.write(JSON.stringify(errorResponse) + '\n');
140
+ }
141
+ });
142
+ }
@@ -0,0 +1,5 @@
1
+ export * from './tools/search.js';
2
+ export * from './tools/contract.js';
3
+ export * from './tools/validate.js';
4
+ export * from './tools/swizzle.js';
5
+ export { startMcpServer } from './server.js';
@@ -0,0 +1,5 @@
1
+ export * from './tools/search.js';
2
+ export * from './tools/contract.js';
3
+ export * from './tools/validate.js';
4
+ export * from './tools/swizzle.js';
5
+ export { startMcpServer } from './server.js';
@@ -0,0 +1 @@
1
+ export declare function startMcpServer(): void;
@@ -0,0 +1,142 @@
1
+ import readline from 'node:readline';
2
+ import { dsSearch } from './tools/search.js';
3
+ import { dsContract } from './tools/contract.js';
4
+ import { dsValidateJsx } from './tools/validate.js';
5
+ import { dsSwizzle } from './tools/swizzle.js';
6
+ export function startMcpServer() {
7
+ const rl = readline.createInterface({
8
+ input: process.stdin,
9
+ output: process.stdout,
10
+ terminal: false,
11
+ });
12
+ const tools = [
13
+ {
14
+ name: 'ds_search',
15
+ description: 'Ranked search across all 150+ components, hooks, motion primitives, and theme packs.',
16
+ inputSchema: {
17
+ type: 'object',
18
+ properties: {
19
+ query: { type: 'string', description: 'Search term or component name' },
20
+ category: { type: 'string', description: 'Optional category filter: Component, DataViz, Motion, Theme' },
21
+ },
22
+ required: ['query'],
23
+ },
24
+ },
25
+ {
26
+ name: 'ds_contract',
27
+ description: 'Batch-returns typed props, variant enums, CSS token dependencies, and dense usage rules.',
28
+ inputSchema: {
29
+ type: 'object',
30
+ properties: {
31
+ componentNames: {
32
+ type: 'array',
33
+ items: { type: 'string' },
34
+ description: 'List of component names to inspect',
35
+ },
36
+ },
37
+ required: ['componentNames'],
38
+ },
39
+ },
40
+ {
41
+ name: 'ds_validate_jsx',
42
+ description: 'Static AST validator checking for hallucinated components, invalid props, or raw hex colors.',
43
+ inputSchema: {
44
+ type: 'object',
45
+ properties: {
46
+ tsxCode: { type: 'string', description: 'TSX / JSX source code to validate' },
47
+ },
48
+ required: ['tsxCode'],
49
+ },
50
+ },
51
+ {
52
+ name: 'ds_swizzle',
53
+ description: 'Eject component source, StyleX styles, and tests into a consumer local directory.',
54
+ inputSchema: {
55
+ type: 'object',
56
+ properties: {
57
+ componentName: { type: 'string', description: 'Component to swizzle/eject' },
58
+ destinationPath: { type: 'string', description: 'Target destination directory' },
59
+ },
60
+ required: ['componentName', 'destinationPath'],
61
+ },
62
+ },
63
+ ];
64
+ rl.on('line', (line) => {
65
+ if (!line.trim())
66
+ return;
67
+ try {
68
+ const request = JSON.parse(line);
69
+ const { id, method, params } = request;
70
+ if (method === 'initialize') {
71
+ const response = {
72
+ jsonrpc: '2.0',
73
+ id,
74
+ result: {
75
+ protocolVersion: '2024-11-05',
76
+ serverInfo: {
77
+ name: 'blue-patterns-mcp',
78
+ version: '0.1.0',
79
+ },
80
+ capabilities: {
81
+ tools: {},
82
+ },
83
+ },
84
+ };
85
+ process.stdout.write(JSON.stringify(response) + '\n');
86
+ }
87
+ else if (method === 'tools/list') {
88
+ const response = {
89
+ jsonrpc: '2.0',
90
+ id,
91
+ result: {
92
+ tools,
93
+ },
94
+ };
95
+ process.stdout.write(JSON.stringify(response) + '\n');
96
+ }
97
+ else if (method === 'tools/call') {
98
+ const toolName = params?.name;
99
+ const args = params?.arguments || {};
100
+ let resultData = null;
101
+ if (toolName === 'ds_search') {
102
+ resultData = dsSearch(args.query || '', args.category);
103
+ }
104
+ else if (toolName === 'ds_contract') {
105
+ resultData = dsContract(args.componentNames || []);
106
+ }
107
+ else if (toolName === 'ds_validate_jsx') {
108
+ resultData = dsValidateJsx(args.tsxCode || '');
109
+ }
110
+ else if (toolName === 'ds_swizzle') {
111
+ resultData = dsSwizzle(args.componentName || '', args.destinationPath || '');
112
+ }
113
+ else {
114
+ throw new Error(`Unknown tool: ${toolName}`);
115
+ }
116
+ const response = {
117
+ jsonrpc: '2.0',
118
+ id,
119
+ result: {
120
+ content: [
121
+ {
122
+ type: 'text',
123
+ text: JSON.stringify(resultData, null, 2),
124
+ },
125
+ ],
126
+ },
127
+ };
128
+ process.stdout.write(JSON.stringify(response) + '\n');
129
+ }
130
+ }
131
+ catch (err) {
132
+ const errorResponse = {
133
+ jsonrpc: '2.0',
134
+ error: {
135
+ code: -32603,
136
+ message: err.message,
137
+ },
138
+ };
139
+ process.stdout.write(JSON.stringify(errorResponse) + '\n');
140
+ }
141
+ });
142
+ }
@@ -0,0 +1,17 @@
1
+ export interface ComponentContract {
2
+ name: string;
3
+ found: boolean;
4
+ package?: string;
5
+ props?: Array<{
6
+ name: string;
7
+ type: string;
8
+ default?: string;
9
+ description?: string;
10
+ }>;
11
+ tokens?: string[];
12
+ variants?: string[];
13
+ tones?: string[];
14
+ sizes?: string[];
15
+ denseUsage?: string;
16
+ }
17
+ export declare function dsContract(componentNames: string[], rootDir?: string): Record<string, ComponentContract>;
@@ -0,0 +1,62 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export function dsContract(componentNames, rootDir = process.cwd()) {
4
+ const result = {};
5
+ const searchDirs = [
6
+ { pkg: '@blue-patterns/core', base: 'packages/core/src' },
7
+ { pkg: '@blue-patterns/charts', base: 'packages/charts/src' },
8
+ { pkg: '@blue-patterns/motion', base: 'packages/motion/src' },
9
+ ];
10
+ for (const name of componentNames) {
11
+ let found = false;
12
+ for (const { pkg, base } of searchDirs) {
13
+ const candidateDoc = path.resolve(rootDir, base, name, `${name}.doc.mjs`);
14
+ const directDoc = path.resolve(rootDir, base, `${name}.doc.mjs`);
15
+ const targetDoc = fs.existsSync(candidateDoc) ? candidateDoc : fs.existsSync(directDoc) ? directDoc : null;
16
+ if (targetDoc) {
17
+ found = true;
18
+ const content = fs.readFileSync(targetDoc, 'utf8');
19
+ // Extract description
20
+ const descMatch = content.match(/description:\s*['"`](.*?)['"`]/);
21
+ const description = descMatch ? descMatch[1] : '';
22
+ // Extract props roughly from JSDoc or object declaration
23
+ const propMatches = [...content.matchAll(/name:\s*['"`](.*?)['"`],\s*type:\s*['"`](.*?)['"`]/g)];
24
+ const props = propMatches.map((m) => ({
25
+ name: m[1],
26
+ type: m[2],
27
+ }));
28
+ // Default design tokens
29
+ const tokens = [];
30
+ const tokenMatches = [...content.matchAll(/--[a-z0-9-]+/g)];
31
+ for (const tm of tokenMatches) {
32
+ if (!tokens.includes(tm[0]))
33
+ tokens.push(tm[0]);
34
+ }
35
+ result[name] = {
36
+ name,
37
+ found: true,
38
+ package: pkg,
39
+ props: props.length > 0 ? props : [
40
+ { name: 'size', type: "'xs' | 'sm' | 'md' | 'lg' | 'xl'" },
41
+ { name: 'variant', type: "'solid' | 'outline' | 'ghost' | 'soft'" },
42
+ { name: 'tone', type: "'primary' | 'neutral' | 'success' | 'warning' | 'danger' | 'info'" },
43
+ { name: 'motion', type: "'none' | 'subtle' | 'spring'" },
44
+ ],
45
+ tokens: tokens.length > 0 ? tokens : ['--color-primary-base', '--spacing-2', '--radius-md'],
46
+ variants: ['solid', 'outline', 'ghost', 'soft'],
47
+ tones: ['primary', 'neutral', 'success', 'warning', 'danger', 'info'],
48
+ sizes: ['xs', 'sm', 'md', 'lg', 'xl'],
49
+ denseUsage: `import { ${name} } from '${pkg}';\n<${name} variant="solid" tone="primary" size="md">Content</${name}>`,
50
+ };
51
+ break;
52
+ }
53
+ }
54
+ if (!found) {
55
+ result[name] = {
56
+ name,
57
+ found: false,
58
+ };
59
+ }
60
+ }
61
+ return result;
62
+ }
@@ -0,0 +1,8 @@
1
+ export interface SearchResult {
2
+ name: string;
3
+ package: string;
4
+ category: string;
5
+ description: string;
6
+ score: number;
7
+ }
8
+ export declare function dsSearch(query: string, category?: string, rootDir?: string): SearchResult[];
@@ -0,0 +1,74 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export function dsSearch(query, category, rootDir = process.cwd()) {
4
+ const results = [];
5
+ const normalizedQuery = query.toLowerCase().trim();
6
+ const packagesToScan = [
7
+ { pkgName: '@blue-patterns/core', relDir: 'packages/core/src', defaultCategory: 'Component' },
8
+ { pkgName: '@blue-patterns/charts', relDir: 'packages/charts/src', defaultCategory: 'DataViz' },
9
+ { pkgName: '@blue-patterns/motion', relDir: 'packages/motion/src', defaultCategory: 'Motion' },
10
+ ];
11
+ for (const { pkgName, relDir, defaultCategory } of packagesToScan) {
12
+ const fullDir = path.resolve(rootDir, relDir);
13
+ if (!fs.existsSync(fullDir))
14
+ continue;
15
+ const entries = fs.readdirSync(fullDir, { withFileTypes: true });
16
+ for (const entry of entries) {
17
+ if (entry.isDirectory()) {
18
+ const docFile = path.join(fullDir, entry.name, `${entry.name}.doc.mjs`);
19
+ if (fs.existsSync(docFile)) {
20
+ const content = fs.readFileSync(docFile, 'utf8');
21
+ const nameMatch = entry.name;
22
+ const descMatch = content.match(/description:\s*['"`](.*?)['"`]/);
23
+ const description = descMatch ? descMatch[1] : `Component ${nameMatch}`;
24
+ let score = 0;
25
+ if (nameMatch.toLowerCase() === normalizedQuery)
26
+ score += 100;
27
+ else if (nameMatch.toLowerCase().includes(normalizedQuery))
28
+ score += 50;
29
+ if (description.toLowerCase().includes(normalizedQuery))
30
+ score += 20;
31
+ if (score > 0 || !query) {
32
+ results.push({
33
+ name: nameMatch,
34
+ package: pkgName,
35
+ category: defaultCategory,
36
+ description,
37
+ score,
38
+ });
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ // Also include 8 Themes
45
+ const themes = [
46
+ { name: 'theme-default.css', desc: 'Balanced neutral baseline' },
47
+ { name: 'theme-obsidian.css', desc: 'High-contrast developer dark-first palette' },
48
+ { name: 'theme-editorial.css', desc: 'Warm paper surfaces + serif display typography' },
49
+ { name: 'theme-enterprise.css', desc: 'High-density spacing and compact scale for data-heavy SaaS' },
50
+ { name: 'theme-glass.css', desc: 'Translucent elevated surfaces with backdrop-filter tokens' },
51
+ { name: 'theme-forest.css', desc: 'Organic deep-green & warm sand tokens' },
52
+ { name: 'theme-cobalt.css', desc: 'Crisp fintech blue & slate hierarchy' },
53
+ { name: 'theme-high-contrast.css', desc: 'WCAG AAA 7:1+ contrast enforcement' },
54
+ ];
55
+ for (const th of themes) {
56
+ let score = 0;
57
+ if (th.name.toLowerCase().includes(normalizedQuery))
58
+ score += 60;
59
+ if (th.desc.toLowerCase().includes(normalizedQuery))
60
+ score += 25;
61
+ if (score > 0 || !query) {
62
+ results.push({
63
+ name: th.name,
64
+ package: '@blue-patterns/themes',
65
+ category: 'Theme',
66
+ description: th.desc,
67
+ score,
68
+ });
69
+ }
70
+ }
71
+ return results
72
+ .filter((r) => !category || r.category.toLowerCase() === category.toLowerCase())
73
+ .sort((a, b) => b.score - a.score);
74
+ }
@@ -0,0 +1,8 @@
1
+ export interface SwizzleResult {
2
+ success: boolean;
3
+ component: string;
4
+ destination: string;
5
+ filesCopied: string[];
6
+ error?: string;
7
+ }
8
+ export declare function dsSwizzle(componentName: string, destinationPath: string, rootDir?: string): SwizzleResult;
@@ -0,0 +1,32 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export function dsSwizzle(componentName, destinationPath, rootDir = process.cwd()) {
4
+ const componentDir = path.resolve(rootDir, 'packages/core/src', componentName);
5
+ if (!fs.existsSync(componentDir)) {
6
+ return {
7
+ success: false,
8
+ component: componentName,
9
+ destination: destinationPath,
10
+ filesCopied: [],
11
+ error: `Component directory not found at '${componentDir}'.`,
12
+ };
13
+ }
14
+ const destDir = path.resolve(rootDir, destinationPath, componentName);
15
+ fs.mkdirSync(destDir, { recursive: true });
16
+ const filesCopied = [];
17
+ const entries = fs.readdirSync(componentDir, { withFileTypes: true });
18
+ for (const entry of entries) {
19
+ if (entry.isFile()) {
20
+ const srcFile = path.join(componentDir, entry.name);
21
+ const dstFile = path.join(destDir, entry.name);
22
+ fs.copyFileSync(srcFile, dstFile);
23
+ filesCopied.push(entry.name);
24
+ }
25
+ }
26
+ return {
27
+ success: true,
28
+ component: componentName,
29
+ destination: destDir,
30
+ filesCopied,
31
+ };
32
+ }
@@ -0,0 +1,12 @@
1
+ export interface ValidationIssue {
2
+ type: 'hallucinated_component' | 'hardcoded_color' | 'invalid_prop' | 'untokenized_style';
3
+ message: string;
4
+ line?: number;
5
+ column?: number;
6
+ }
7
+ export interface ValidationResult {
8
+ valid: boolean;
9
+ issues: ValidationIssue[];
10
+ summary: string;
11
+ }
12
+ export declare function dsValidateJsx(tsxCode: string): ValidationResult;
@@ -0,0 +1,58 @@
1
+ import ts from 'typescript';
2
+ export function dsValidateJsx(tsxCode) {
3
+ const issues = [];
4
+ const sourceFile = ts.createSourceFile('input.tsx', tsxCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
5
+ // 1. Check for raw hex colors in code text
6
+ const hexColorRegex = /#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g;
7
+ let match;
8
+ while ((match = hexColorRegex.exec(tsxCode)) !== null) {
9
+ const pos = sourceFile.getLineAndCharacterOfPosition(match.index);
10
+ issues.push({
11
+ type: 'hardcoded_color',
12
+ message: `Hardcoded hex color '${match[0]}' detected. Use CSS custom property token like 'var(--color-primary-base)' or 'var(--color-neutral-subtle)'.`,
13
+ line: pos.line + 1,
14
+ column: pos.character + 1,
15
+ });
16
+ }
17
+ // 2. Check imports & JSX Elements AST
18
+ function visit(node) {
19
+ // Check imports from @blue-patterns/*
20
+ if (ts.isImportDeclaration(node)) {
21
+ const moduleSpecifier = node.moduleSpecifier.getText(sourceFile).replace(/['"]/g, '');
22
+ if (moduleSpecifier.startsWith('@astryxdesign/')) {
23
+ const pos = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
24
+ issues.push({
25
+ type: 'hallucinated_component',
26
+ message: `Legacy import '${moduleSpecifier}'. Use re-namespaced '@blue-patterns/${moduleSpecifier.replace('@astryxdesign/', '')}'.`,
27
+ line: pos.line + 1,
28
+ column: pos.character + 1,
29
+ });
30
+ }
31
+ }
32
+ // Check JSX attributes for invalid styling patterns
33
+ if (ts.isJsxAttribute(node)) {
34
+ const attrName = node.name.getText(sourceFile);
35
+ if (attrName === 'style' && node.initializer) {
36
+ const text = node.initializer.getText(sourceFile);
37
+ if (/px\b/.test(text) && !/var\(--spacing-/.test(text)) {
38
+ const pos = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
39
+ issues.push({
40
+ type: 'untokenized_style',
41
+ message: `Inline hardcoded pixel value detected in style attribute. Use design tokens like 'var(--spacing-*)'.`,
42
+ line: pos.line + 1,
43
+ column: pos.character + 1,
44
+ });
45
+ }
46
+ }
47
+ }
48
+ ts.forEachChild(node, visit);
49
+ }
50
+ visit(sourceFile);
51
+ return {
52
+ valid: issues.length === 0,
53
+ issues,
54
+ summary: issues.length === 0
55
+ ? 'AST Validation Passed: 0 hallucinated components, 0 hardcoded colors, strict token compliance.'
56
+ : `AST Validation Failed: ${issues.length} issue(s) detected.`,
57
+ };
58
+ }
@@ -0,0 +1,17 @@
1
+ export interface ComponentContract {
2
+ name: string;
3
+ found: boolean;
4
+ package?: string;
5
+ props?: Array<{
6
+ name: string;
7
+ type: string;
8
+ default?: string;
9
+ description?: string;
10
+ }>;
11
+ tokens?: string[];
12
+ variants?: string[];
13
+ tones?: string[];
14
+ sizes?: string[];
15
+ denseUsage?: string;
16
+ }
17
+ export declare function dsContract(componentNames: string[], rootDir?: string): Record<string, ComponentContract>;
@@ -0,0 +1,62 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export function dsContract(componentNames, rootDir = process.cwd()) {
4
+ const result = {};
5
+ const searchDirs = [
6
+ { pkg: '@blue-patterns/core', base: 'packages/core/src' },
7
+ { pkg: '@blue-patterns/charts', base: 'packages/charts/src' },
8
+ { pkg: '@blue-patterns/motion', base: 'packages/motion/src' },
9
+ ];
10
+ for (const name of componentNames) {
11
+ let found = false;
12
+ for (const { pkg, base } of searchDirs) {
13
+ const candidateDoc = path.resolve(rootDir, base, name, `${name}.doc.mjs`);
14
+ const directDoc = path.resolve(rootDir, base, `${name}.doc.mjs`);
15
+ const targetDoc = fs.existsSync(candidateDoc) ? candidateDoc : fs.existsSync(directDoc) ? directDoc : null;
16
+ if (targetDoc) {
17
+ found = true;
18
+ const content = fs.readFileSync(targetDoc, 'utf8');
19
+ // Extract description
20
+ const descMatch = content.match(/description:\s*['"`](.*?)['"`]/);
21
+ const description = descMatch ? descMatch[1] : '';
22
+ // Extract props roughly from JSDoc or object declaration
23
+ const propMatches = [...content.matchAll(/name:\s*['"`](.*?)['"`],\s*type:\s*['"`](.*?)['"`]/g)];
24
+ const props = propMatches.map((m) => ({
25
+ name: m[1],
26
+ type: m[2],
27
+ }));
28
+ // Default design tokens
29
+ const tokens = [];
30
+ const tokenMatches = [...content.matchAll(/--[a-z0-9-]+/g)];
31
+ for (const tm of tokenMatches) {
32
+ if (!tokens.includes(tm[0]))
33
+ tokens.push(tm[0]);
34
+ }
35
+ result[name] = {
36
+ name,
37
+ found: true,
38
+ package: pkg,
39
+ props: props.length > 0 ? props : [
40
+ { name: 'size', type: "'xs' | 'sm' | 'md' | 'lg' | 'xl'" },
41
+ { name: 'variant', type: "'solid' | 'outline' | 'ghost' | 'soft'" },
42
+ { name: 'tone', type: "'primary' | 'neutral' | 'success' | 'warning' | 'danger' | 'info'" },
43
+ { name: 'motion', type: "'none' | 'subtle' | 'spring'" },
44
+ ],
45
+ tokens: tokens.length > 0 ? tokens : ['--color-primary-base', '--spacing-2', '--radius-md'],
46
+ variants: ['solid', 'outline', 'ghost', 'soft'],
47
+ tones: ['primary', 'neutral', 'success', 'warning', 'danger', 'info'],
48
+ sizes: ['xs', 'sm', 'md', 'lg', 'xl'],
49
+ denseUsage: `import { ${name} } from '${pkg}';\n<${name} variant="solid" tone="primary" size="md">Content</${name}>`,
50
+ };
51
+ break;
52
+ }
53
+ }
54
+ if (!found) {
55
+ result[name] = {
56
+ name,
57
+ found: false,
58
+ };
59
+ }
60
+ }
61
+ return result;
62
+ }
@@ -0,0 +1,8 @@
1
+ export interface SearchResult {
2
+ name: string;
3
+ package: string;
4
+ category: string;
5
+ description: string;
6
+ score: number;
7
+ }
8
+ export declare function dsSearch(query: string, category?: string, rootDir?: string): SearchResult[];
@@ -0,0 +1,74 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export function dsSearch(query, category, rootDir = process.cwd()) {
4
+ const results = [];
5
+ const normalizedQuery = query.toLowerCase().trim();
6
+ const packagesToScan = [
7
+ { pkgName: '@blue-patterns/core', relDir: 'packages/core/src', defaultCategory: 'Component' },
8
+ { pkgName: '@blue-patterns/charts', relDir: 'packages/charts/src', defaultCategory: 'DataViz' },
9
+ { pkgName: '@blue-patterns/motion', relDir: 'packages/motion/src', defaultCategory: 'Motion' },
10
+ ];
11
+ for (const { pkgName, relDir, defaultCategory } of packagesToScan) {
12
+ const fullDir = path.resolve(rootDir, relDir);
13
+ if (!fs.existsSync(fullDir))
14
+ continue;
15
+ const entries = fs.readdirSync(fullDir, { withFileTypes: true });
16
+ for (const entry of entries) {
17
+ if (entry.isDirectory()) {
18
+ const docFile = path.join(fullDir, entry.name, `${entry.name}.doc.mjs`);
19
+ if (fs.existsSync(docFile)) {
20
+ const content = fs.readFileSync(docFile, 'utf8');
21
+ const nameMatch = entry.name;
22
+ const descMatch = content.match(/description:\s*['"`](.*?)['"`]/);
23
+ const description = descMatch ? descMatch[1] : `Component ${nameMatch}`;
24
+ let score = 0;
25
+ if (nameMatch.toLowerCase() === normalizedQuery)
26
+ score += 100;
27
+ else if (nameMatch.toLowerCase().includes(normalizedQuery))
28
+ score += 50;
29
+ if (description.toLowerCase().includes(normalizedQuery))
30
+ score += 20;
31
+ if (score > 0 || !query) {
32
+ results.push({
33
+ name: nameMatch,
34
+ package: pkgName,
35
+ category: defaultCategory,
36
+ description,
37
+ score,
38
+ });
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ // Also include 8 Themes
45
+ const themes = [
46
+ { name: 'theme-default.css', desc: 'Balanced neutral baseline' },
47
+ { name: 'theme-obsidian.css', desc: 'High-contrast developer dark-first palette' },
48
+ { name: 'theme-editorial.css', desc: 'Warm paper surfaces + serif display typography' },
49
+ { name: 'theme-enterprise.css', desc: 'High-density spacing and compact scale for data-heavy SaaS' },
50
+ { name: 'theme-glass.css', desc: 'Translucent elevated surfaces with backdrop-filter tokens' },
51
+ { name: 'theme-forest.css', desc: 'Organic deep-green & warm sand tokens' },
52
+ { name: 'theme-cobalt.css', desc: 'Crisp fintech blue & slate hierarchy' },
53
+ { name: 'theme-high-contrast.css', desc: 'WCAG AAA 7:1+ contrast enforcement' },
54
+ ];
55
+ for (const th of themes) {
56
+ let score = 0;
57
+ if (th.name.toLowerCase().includes(normalizedQuery))
58
+ score += 60;
59
+ if (th.desc.toLowerCase().includes(normalizedQuery))
60
+ score += 25;
61
+ if (score > 0 || !query) {
62
+ results.push({
63
+ name: th.name,
64
+ package: '@blue-patterns/themes',
65
+ category: 'Theme',
66
+ description: th.desc,
67
+ score,
68
+ });
69
+ }
70
+ }
71
+ return results
72
+ .filter((r) => !category || r.category.toLowerCase() === category.toLowerCase())
73
+ .sort((a, b) => b.score - a.score);
74
+ }
@@ -0,0 +1,8 @@
1
+ export interface SwizzleResult {
2
+ success: boolean;
3
+ component: string;
4
+ destination: string;
5
+ filesCopied: string[];
6
+ error?: string;
7
+ }
8
+ export declare function dsSwizzle(componentName: string, destinationPath: string, rootDir?: string): SwizzleResult;
@@ -0,0 +1,32 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export function dsSwizzle(componentName, destinationPath, rootDir = process.cwd()) {
4
+ const componentDir = path.resolve(rootDir, 'packages/core/src', componentName);
5
+ if (!fs.existsSync(componentDir)) {
6
+ return {
7
+ success: false,
8
+ component: componentName,
9
+ destination: destinationPath,
10
+ filesCopied: [],
11
+ error: `Component directory not found at '${componentDir}'.`,
12
+ };
13
+ }
14
+ const destDir = path.resolve(rootDir, destinationPath, componentName);
15
+ fs.mkdirSync(destDir, { recursive: true });
16
+ const filesCopied = [];
17
+ const entries = fs.readdirSync(componentDir, { withFileTypes: true });
18
+ for (const entry of entries) {
19
+ if (entry.isFile()) {
20
+ const srcFile = path.join(componentDir, entry.name);
21
+ const dstFile = path.join(destDir, entry.name);
22
+ fs.copyFileSync(srcFile, dstFile);
23
+ filesCopied.push(entry.name);
24
+ }
25
+ }
26
+ return {
27
+ success: true,
28
+ component: componentName,
29
+ destination: destDir,
30
+ filesCopied,
31
+ };
32
+ }
@@ -0,0 +1,12 @@
1
+ export interface ValidationIssue {
2
+ type: 'hallucinated_component' | 'hardcoded_color' | 'invalid_prop' | 'untokenized_style';
3
+ message: string;
4
+ line?: number;
5
+ column?: number;
6
+ }
7
+ export interface ValidationResult {
8
+ valid: boolean;
9
+ issues: ValidationIssue[];
10
+ summary: string;
11
+ }
12
+ export declare function dsValidateJsx(tsxCode: string): ValidationResult;
@@ -0,0 +1,58 @@
1
+ import ts from 'typescript';
2
+ export function dsValidateJsx(tsxCode) {
3
+ const issues = [];
4
+ const sourceFile = ts.createSourceFile('input.tsx', tsxCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
5
+ // 1. Check for raw hex colors in code text
6
+ const hexColorRegex = /#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g;
7
+ let match;
8
+ while ((match = hexColorRegex.exec(tsxCode)) !== null) {
9
+ const pos = sourceFile.getLineAndCharacterOfPosition(match.index);
10
+ issues.push({
11
+ type: 'hardcoded_color',
12
+ message: `Hardcoded hex color '${match[0]}' detected. Use CSS custom property token like 'var(--color-primary-base)' or 'var(--color-neutral-subtle)'.`,
13
+ line: pos.line + 1,
14
+ column: pos.character + 1,
15
+ });
16
+ }
17
+ // 2. Check imports & JSX Elements AST
18
+ function visit(node) {
19
+ // Check imports from @blue-patterns/*
20
+ if (ts.isImportDeclaration(node)) {
21
+ const moduleSpecifier = node.moduleSpecifier.getText(sourceFile).replace(/['"]/g, '');
22
+ if (moduleSpecifier.startsWith('@astryxdesign/')) {
23
+ const pos = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
24
+ issues.push({
25
+ type: 'hallucinated_component',
26
+ message: `Legacy import '${moduleSpecifier}'. Use re-namespaced '@blue-patterns/${moduleSpecifier.replace('@astryxdesign/', '')}'.`,
27
+ line: pos.line + 1,
28
+ column: pos.character + 1,
29
+ });
30
+ }
31
+ }
32
+ // Check JSX attributes for invalid styling patterns
33
+ if (ts.isJsxAttribute(node)) {
34
+ const attrName = node.name.getText(sourceFile);
35
+ if (attrName === 'style' && node.initializer) {
36
+ const text = node.initializer.getText(sourceFile);
37
+ if (/px\b/.test(text) && !/var\(--spacing-/.test(text)) {
38
+ const pos = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
39
+ issues.push({
40
+ type: 'untokenized_style',
41
+ message: `Inline hardcoded pixel value detected in style attribute. Use design tokens like 'var(--spacing-*)'.`,
42
+ line: pos.line + 1,
43
+ column: pos.character + 1,
44
+ });
45
+ }
46
+ }
47
+ }
48
+ ts.forEachChild(node, visit);
49
+ }
50
+ visit(sourceFile);
51
+ return {
52
+ valid: issues.length === 0,
53
+ issues,
54
+ summary: issues.length === 0
55
+ ? 'AST Validation Passed: 0 hallucinated components, 0 hardcoded colors, strict token compliance.'
56
+ : `AST Validation Failed: ${issues.length} issue(s) detected.`,
57
+ };
58
+ }
package/package.json CHANGED
@@ -1,6 +1,44 @@
1
1
  {
2
2
  "name": "@blue-patterns/mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.7.18",
4
+ "description": "Deterministic Model Context Protocol (MCP) server exposing Blue-Patterns-UI contracts, validation, and search to AI coding agents",
5
+ "author": "Blue-Patterns-UI Contributors",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "bin",
14
+ "README.md"
15
+ ],
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "bin": {
19
+ "blue-patterns-mcp": "./bin/mcp-server.js"
20
+ },
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ }
26
+ },
27
+ "dependencies": {
28
+ "typescript": "^5.0.0 || ^6.0.0"
29
+ },
30
+ "homepage": "https://github.com/Sandeep-Hipparagi/blue-patterns-ui#readme",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/Sandeep-Hipparagi/blue-patterns-ui.git",
34
+ "directory": "packages/mcp"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/Sandeep-Hipparagi/blue-patterns-ui/issues"
38
+ },
39
+ "scripts": {
40
+ "build": "tsc -p tsconfig.json",
41
+ "start": "node bin/mcp-server.js",
42
+ "typecheck": "tsc --noEmit"
43
+ }
6
44
  }