@movable/ui-mcp 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,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,667 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+ import { readFileSync, existsSync } from 'fs';
6
+ import { join, dirname } from 'path';
7
+ import { fileURLToPath } from 'url';
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ // ============================================================================
11
+ // Mode Detection: Bundled (npx) vs Local Development
12
+ // ============================================================================
13
+ const UI_REPO_PATH = process.env.UI_REPO_PATH || join(__dirname, '..', '..');
14
+ const DATA_DIR = join(__dirname, '..', 'data');
15
+ const isNpxMode = existsSync(join(DATA_DIR, 'components.json'));
16
+ const isLocalMode = existsSync(join(UI_REPO_PATH, 'src', 'components', 'index.ts'));
17
+ // ============================================================================
18
+ // Data Loading
19
+ // ============================================================================
20
+ function loadJsonData(filename) {
21
+ const filePath = join(DATA_DIR, filename);
22
+ if (existsSync(filePath)) {
23
+ return JSON.parse(readFileSync(filePath, 'utf-8'));
24
+ }
25
+ return null;
26
+ }
27
+ // Cached data (loaded on first use)
28
+ let cachedComponents = null;
29
+ let cachedTokens = null;
30
+ let cachedTheme = null;
31
+ let cachedStories = null;
32
+ let cachedCategories = null;
33
+ let cachedEslintRules = null;
34
+ // ============================================================================
35
+ // Component Functions
36
+ // ============================================================================
37
+ function getComponentsFromSource() {
38
+ const indexPath = join(UI_REPO_PATH, 'src', 'components', 'index.ts');
39
+ if (!existsSync(indexPath)) {
40
+ throw new Error(`Components index not found at ${indexPath}`);
41
+ }
42
+ const content = readFileSync(indexPath, 'utf-8');
43
+ const components = [];
44
+ const exportRegex = /export\s+(?:\{([^}]+)\}|\*)\s+from\s+['"]\.\/([^'"]+)['"]/g;
45
+ let match;
46
+ while ((match = exportRegex.exec(content)) !== null) {
47
+ const exports = match[1];
48
+ const path = match[2];
49
+ if (exports) {
50
+ const exportNames = exports
51
+ .split(',')
52
+ .map((e) => e.trim())
53
+ .filter((e) => !e.startsWith('type '))
54
+ .map((e) => e.replace(/\s+as\s+\w+/, '').trim())
55
+ .filter((e) => e.length > 0);
56
+ for (const exportName of exportNames) {
57
+ components.push({
58
+ name: exportName,
59
+ exportName,
60
+ path: `src/components/${path}`,
61
+ });
62
+ }
63
+ }
64
+ else {
65
+ components.push({
66
+ name: path,
67
+ exportName: `* (barrel export)`,
68
+ path: `src/components/${path}`,
69
+ });
70
+ }
71
+ }
72
+ return components;
73
+ }
74
+ function getComponents() {
75
+ if (cachedComponents)
76
+ return cachedComponents;
77
+ if (isNpxMode) {
78
+ cachedComponents = loadJsonData('components.json') || [];
79
+ }
80
+ else if (isLocalMode) {
81
+ cachedComponents = getComponentsFromSource();
82
+ }
83
+ else {
84
+ throw new Error('MCP server requires either bundled data or access to UI repo source files. ' +
85
+ 'Set UI_REPO_PATH environment variable if running in development mode.');
86
+ }
87
+ return cachedComponents;
88
+ }
89
+ function getCategories() {
90
+ if (cachedCategories)
91
+ return cachedCategories;
92
+ if (isNpxMode) {
93
+ cachedCategories =
94
+ loadJsonData('categories.json') || {};
95
+ }
96
+ else {
97
+ // Fallback categories for local mode
98
+ cachedCategories = {
99
+ inputs: [
100
+ 'InkTextField',
101
+ 'InkSelect',
102
+ 'InkCheckboxGroup',
103
+ 'InkRadioGroup',
104
+ 'InkSwitch',
105
+ 'InkRadioTiles',
106
+ ],
107
+ toggles: [
108
+ 'InkToggleText',
109
+ 'InkToggleTextGroup',
110
+ 'InkToggleIcon',
111
+ 'InkToggleIconGroup',
112
+ ],
113
+ feedback: ['InkSnackbar', 'InkHighlightAlert', 'InkEmptyState'],
114
+ layout: ['InkPaper', 'InkCard', 'InkDrawer', 'InkPersistentFilterDrawer'],
115
+ dialogs: ['InkDialog'],
116
+ headers: [
117
+ 'InkPageHeader',
118
+ 'InkWorkflowHeader',
119
+ 'HeaderMetadata',
120
+ 'HeaderSubtitle',
121
+ 'LinkBreadcrumbs',
122
+ 'PageHeaderActionButtons',
123
+ ],
124
+ 'data-display': [
125
+ 'InkDataGrid',
126
+ 'InkDataGridEmpty',
127
+ 'InkAttributeList',
128
+ 'InkImage',
129
+ 'InkChart',
130
+ ],
131
+ chips: ['InkChip', 'InkChipGroup'],
132
+ };
133
+ }
134
+ return cachedCategories;
135
+ }
136
+ // ============================================================================
137
+ // Token Functions
138
+ // ============================================================================
139
+ const TOKEN_CATEGORIES = [
140
+ 'blue',
141
+ 'green',
142
+ 'lightBlue',
143
+ 'lightGreen',
144
+ 'neutral',
145
+ 'orange',
146
+ 'purple',
147
+ 'yellow',
148
+ 'red',
149
+ 'cyan',
150
+ 'deepPurple',
151
+ ];
152
+ function getTokens() {
153
+ if (cachedTokens)
154
+ return cachedTokens;
155
+ if (isNpxMode) {
156
+ cachedTokens = loadJsonData('tokens.json') || [];
157
+ }
158
+ else {
159
+ // In local mode, return empty - users should run extract-data first
160
+ console.error('Warning: Token data not available in local mode. Run npm run extract-data first.');
161
+ cachedTokens = [];
162
+ }
163
+ return cachedTokens;
164
+ }
165
+ // ============================================================================
166
+ // Theme Functions
167
+ // ============================================================================
168
+ function getTheme() {
169
+ if (cachedTheme)
170
+ return cachedTheme;
171
+ if (isNpxMode) {
172
+ cachedTheme = loadJsonData('theme.json');
173
+ }
174
+ else if (isLocalMode) {
175
+ // Read from source in local mode
176
+ const themePath = join(UI_REPO_PATH, 'src', 'theme.ts');
177
+ let componentOverrides = [];
178
+ if (existsSync(themePath)) {
179
+ const themeSource = readFileSync(themePath, 'utf-8');
180
+ const componentMatches = themeSource.match(/Mui\w+:/g);
181
+ if (componentMatches) {
182
+ componentOverrides = [...new Set(componentMatches)].map((c) => c.replace(':', ''));
183
+ }
184
+ }
185
+ cachedTheme = {
186
+ semanticColors: {},
187
+ textColors: {},
188
+ typography: { fontFamily: ['Inter', 'sans-serif'] },
189
+ componentOverrides,
190
+ };
191
+ }
192
+ return cachedTheme;
193
+ }
194
+ // ============================================================================
195
+ // Story Functions
196
+ // ============================================================================
197
+ function getStories() {
198
+ if (cachedStories)
199
+ return cachedStories;
200
+ if (isNpxMode) {
201
+ cachedStories = loadJsonData('stories.json') || [];
202
+ }
203
+ else {
204
+ // In local mode, return empty - parsing stories requires glob
205
+ cachedStories = [];
206
+ }
207
+ return cachedStories;
208
+ }
209
+ // ============================================================================
210
+ // ESLint Rules Functions
211
+ // ============================================================================
212
+ function getEslintRules() {
213
+ if (cachedEslintRules)
214
+ return cachedEslintRules;
215
+ if (isNpxMode) {
216
+ cachedEslintRules =
217
+ loadJsonData('eslint-rules.json') || [];
218
+ }
219
+ else {
220
+ cachedEslintRules = [];
221
+ }
222
+ return cachedEslintRules;
223
+ }
224
+ // ============================================================================
225
+ // MCP Server Setup
226
+ // ============================================================================
227
+ const server = new McpServer({
228
+ name: 'movable-ui',
229
+ version: '1.0.0',
230
+ });
231
+ // Register the list_components tool
232
+ server.tool('list_components', 'List all available components from @movable/ui', {}, async () => {
233
+ try {
234
+ const components = getComponents();
235
+ const componentList = components
236
+ .map((c) => `- ${c.name} (from ${c.path})${c.category ? ` [${c.category}]` : ''}`)
237
+ .join('\n');
238
+ return {
239
+ content: [
240
+ {
241
+ type: 'text',
242
+ text: `# @movable/ui Components\n\nFound ${components.length} exported components:\n\n${componentList}`,
243
+ },
244
+ ],
245
+ };
246
+ }
247
+ catch (error) {
248
+ return {
249
+ content: [
250
+ {
251
+ type: 'text',
252
+ text: `Error listing components: ${error instanceof Error ? error.message : String(error)}`,
253
+ },
254
+ ],
255
+ isError: true,
256
+ };
257
+ }
258
+ });
259
+ // Register the get_component tool
260
+ server.tool('get_component', 'Get detailed information about a specific component', {
261
+ name: z.string().describe('The name of the component to look up'),
262
+ }, async ({ name }) => {
263
+ try {
264
+ const components = getComponents();
265
+ const component = components.find((c) => c.name.toLowerCase() === name.toLowerCase());
266
+ if (!component) {
267
+ return {
268
+ content: [
269
+ {
270
+ type: 'text',
271
+ text: `Component "${name}" not found. Use list_components to see available components.`,
272
+ },
273
+ ],
274
+ isError: true,
275
+ };
276
+ }
277
+ let componentDetails = `# ${component.name}\n\n`;
278
+ componentDetails += `**Export name:** ${component.exportName}\n`;
279
+ componentDetails += `**Path:** ${component.path}\n`;
280
+ if (component.category) {
281
+ componentDetails += `**Category:** ${component.category}\n`;
282
+ }
283
+ componentDetails += '\n';
284
+ if (component.description) {
285
+ componentDetails += `## Description\n\n${component.description}\n\n`;
286
+ }
287
+ if (component.props) {
288
+ componentDetails += `## Props\n\n\`\`\`typescript\n${component.props}\n\`\`\`\n`;
289
+ }
290
+ // In local mode, try to read additional details from source
291
+ if (isLocalMode && !component.props) {
292
+ const possiblePaths = [
293
+ join(UI_REPO_PATH, component.path, 'index.ts'),
294
+ join(UI_REPO_PATH, component.path, 'index.tsx'),
295
+ join(UI_REPO_PATH, component.path + '.tsx'),
296
+ join(UI_REPO_PATH, component.path + '.ts'),
297
+ ];
298
+ for (const sourcePath of possiblePaths) {
299
+ if (existsSync(sourcePath)) {
300
+ const source = readFileSync(sourcePath, 'utf-8');
301
+ const jsdocMatch = source.match(/\/\*\*[\s\S]*?\*\/\s*(?=export)/);
302
+ if (jsdocMatch && !component.description) {
303
+ componentDetails += `## Description\n\n${jsdocMatch[0]}\n\n`;
304
+ }
305
+ const propsMatch = source.match(/(?:interface|type)\s+(\w+Props)\s*(?:=\s*)?{[\s\S]*?}/);
306
+ if (propsMatch) {
307
+ componentDetails += `## Props\n\n\`\`\`typescript\n${propsMatch[0]}\n\`\`\`\n`;
308
+ }
309
+ break;
310
+ }
311
+ }
312
+ }
313
+ return {
314
+ content: [
315
+ {
316
+ type: 'text',
317
+ text: componentDetails,
318
+ },
319
+ ],
320
+ };
321
+ }
322
+ catch (error) {
323
+ return {
324
+ content: [
325
+ {
326
+ type: 'text',
327
+ text: `Error getting component: ${error instanceof Error ? error.message : String(error)}`,
328
+ },
329
+ ],
330
+ isError: true,
331
+ };
332
+ }
333
+ });
334
+ // Register the get_theme tool
335
+ server.tool('get_theme', 'Get theme configuration including palette, typography, and component overrides', {}, async () => {
336
+ try {
337
+ const theme = getTheme();
338
+ if (!theme) {
339
+ return {
340
+ content: [
341
+ {
342
+ type: 'text',
343
+ text: 'Theme data not available. In local mode, run npm run extract-data first.',
344
+ },
345
+ ],
346
+ isError: true,
347
+ };
348
+ }
349
+ let themeInfo = '# @movable/ui Theme\n\n';
350
+ // Semantic colors
351
+ if (Object.keys(theme.semanticColors).length > 0) {
352
+ themeInfo += '## Semantic Colors\n\n';
353
+ for (const [colorName, values] of Object.entries(theme.semanticColors)) {
354
+ themeInfo += `### ${colorName}\n`;
355
+ themeInfo += '| Variant | Value |\n|---------|-------|\n';
356
+ for (const [variant, value] of Object.entries(values)) {
357
+ themeInfo += `| ${variant} | \`${value}\` |\n`;
358
+ }
359
+ themeInfo += '\n';
360
+ }
361
+ }
362
+ // Text colors
363
+ if (Object.keys(theme.textColors).length > 0) {
364
+ themeInfo += '## Text Colors\n\n';
365
+ themeInfo += '| Type | Value |\n|------|-------|\n';
366
+ for (const [type, value] of Object.entries(theme.textColors)) {
367
+ themeInfo += `| ${type} | \`${value}\` |\n`;
368
+ }
369
+ themeInfo += '\n';
370
+ }
371
+ // Typography
372
+ themeInfo += '## Typography\n\n';
373
+ themeInfo += `- Font family: \`${theme.typography.fontFamily.join(', ')}\`\n\n`;
374
+ // Component overrides
375
+ if (theme.componentOverrides.length > 0) {
376
+ themeInfo += '## Component Overrides\n\n';
377
+ themeInfo += theme.componentOverrides.map((c) => `- ${c}`).join('\n');
378
+ }
379
+ return {
380
+ content: [{ type: 'text', text: themeInfo }],
381
+ };
382
+ }
383
+ catch (error) {
384
+ return {
385
+ content: [
386
+ {
387
+ type: 'text',
388
+ text: `Error getting theme: ${error instanceof Error ? error.message : String(error)}`,
389
+ },
390
+ ],
391
+ isError: true,
392
+ };
393
+ }
394
+ });
395
+ // Register the get_design_tokens tool
396
+ server.tool('get_design_tokens', 'Get design tokens by category (colors, spacing). Categories: blue, green, lightBlue, lightGreen, neutral, orange, purple, yellow, red, cyan, deepPurple', {
397
+ category: z
398
+ .string()
399
+ .optional()
400
+ .describe('Token category to retrieve (e.g., "blue", "neutral"). If omitted, lists all categories.'),
401
+ }, async ({ category }) => {
402
+ try {
403
+ const tokens = getTokens();
404
+ if (tokens.length === 0) {
405
+ return {
406
+ content: [
407
+ {
408
+ type: 'text',
409
+ text: 'Token data not available. Run npm run extract-data to generate token data.',
410
+ },
411
+ ],
412
+ isError: true,
413
+ };
414
+ }
415
+ // If no category specified, list available categories
416
+ if (!category) {
417
+ return {
418
+ content: [
419
+ {
420
+ type: 'text',
421
+ text: `# Available Design Token Categories\n\n${TOKEN_CATEGORIES.map((c) => `- ${c}`).join('\n')}\n\nUse \`get_design_tokens\` with a category parameter to get specific tokens.`,
422
+ },
423
+ ],
424
+ };
425
+ }
426
+ const normalizedCategory = category.toLowerCase();
427
+ const matchingCategory = tokens.find((t) => t.name.toLowerCase() === normalizedCategory);
428
+ if (!matchingCategory) {
429
+ return {
430
+ content: [
431
+ {
432
+ type: 'text',
433
+ text: `Category "${category}" not found. Available categories: ${TOKEN_CATEGORIES.join(', ')}`,
434
+ },
435
+ ],
436
+ isError: true,
437
+ };
438
+ }
439
+ let result = `# ${matchingCategory.name} Design Tokens\n\n`;
440
+ result += '| Token | Value |\n|-------|-------|\n';
441
+ for (const token of matchingCategory.tokens) {
442
+ result += `| ${token.name} | \`${token.value}\` |\n`;
443
+ }
444
+ return {
445
+ content: [{ type: 'text', text: result }],
446
+ };
447
+ }
448
+ catch (error) {
449
+ return {
450
+ content: [
451
+ {
452
+ type: 'text',
453
+ text: `Error getting design tokens: ${error instanceof Error ? error.message : String(error)}`,
454
+ },
455
+ ],
456
+ isError: true,
457
+ };
458
+ }
459
+ });
460
+ // Register the get_component_example tool
461
+ server.tool('get_component_example', 'Get code examples from Storybook stories for a component', {
462
+ name: z.string().describe('The name of the component to get examples for'),
463
+ story: z
464
+ .string()
465
+ .optional()
466
+ .describe('Specific story name (e.g., "Default", "Error"). If omitted, lists available stories.'),
467
+ }, async ({ name, story }) => {
468
+ try {
469
+ const stories = getStories();
470
+ if (stories.length === 0) {
471
+ return {
472
+ content: [
473
+ {
474
+ type: 'text',
475
+ text: 'Story data not available. Run npm run extract-data to generate story metadata.',
476
+ },
477
+ ],
478
+ isError: true,
479
+ };
480
+ }
481
+ // Find stories matching the component name
482
+ const matchingStories = stories.filter((s) => s.component.toLowerCase().includes(name.toLowerCase()) ||
483
+ s.title.toLowerCase().includes(name.toLowerCase()));
484
+ if (matchingStories.length === 0) {
485
+ return {
486
+ content: [
487
+ {
488
+ type: 'text',
489
+ text: `No Storybook stories found for "${name}". Try using the exact component name.`,
490
+ },
491
+ ],
492
+ isError: true,
493
+ };
494
+ }
495
+ let result = `# Storybook Examples for ${name}\n\n`;
496
+ for (const storyFile of matchingStories) {
497
+ result += `## ${storyFile.title}\n`;
498
+ result += `**File:** \`${storyFile.file}\`\n`;
499
+ if (storyFile.figmaUrl) {
500
+ result += `**Figma:** [Design](${storyFile.figmaUrl})\n`;
501
+ }
502
+ result += '\n';
503
+ if (!story) {
504
+ result += `**Available stories:** ${storyFile.stories.join(', ')}\n\n`;
505
+ result += `Use \`get_component_example\` with a story parameter to get specific code.\n\n`;
506
+ }
507
+ else {
508
+ const matchingStory = storyFile.stories.find((s) => s.toLowerCase() === story.toLowerCase());
509
+ if (matchingStory) {
510
+ result += `### ${matchingStory}\n\n`;
511
+ result += `Story found in \`${storyFile.file}\`. `;
512
+ result += `View the source file for the full implementation.\n\n`;
513
+ }
514
+ else {
515
+ result += `Story "${story}" not found. Available: ${storyFile.stories.join(', ')}\n\n`;
516
+ }
517
+ }
518
+ }
519
+ return {
520
+ content: [{ type: 'text', text: result }],
521
+ };
522
+ }
523
+ catch (error) {
524
+ return {
525
+ content: [
526
+ {
527
+ type: 'text',
528
+ text: `Error getting examples: ${error instanceof Error ? error.message : String(error)}`,
529
+ },
530
+ ],
531
+ isError: true,
532
+ };
533
+ }
534
+ });
535
+ // Register the search_components tool
536
+ server.tool('search_components', 'Search components by keyword or category. Categories: inputs, toggles, feedback, layout, dialogs, headers, data-display, chips', {
537
+ query: z
538
+ .string()
539
+ .optional()
540
+ .describe('Search keyword to match against component names'),
541
+ category: z
542
+ .string()
543
+ .optional()
544
+ .describe('Filter by category (e.g., "inputs", "feedback")'),
545
+ }, async ({ query, category }) => {
546
+ try {
547
+ const components = getComponents();
548
+ const categories = getCategories();
549
+ let filtered = components;
550
+ // Filter by category if provided
551
+ if (category) {
552
+ const normalizedCategory = category.toLowerCase();
553
+ const categoryComponents = categories[normalizedCategory];
554
+ if (!categoryComponents) {
555
+ return {
556
+ content: [
557
+ {
558
+ type: 'text',
559
+ text: `Category "${category}" not found. Available categories: ${Object.keys(categories).join(', ')}`,
560
+ },
561
+ ],
562
+ isError: true,
563
+ };
564
+ }
565
+ filtered = components.filter((c) => categoryComponents.some((cc) => cc.toLowerCase() === c.name.toLowerCase()));
566
+ }
567
+ // Filter by query if provided
568
+ if (query) {
569
+ const normalizedQuery = query.toLowerCase();
570
+ filtered = filtered.filter((c) => c.name.toLowerCase().includes(normalizedQuery) ||
571
+ c.path.toLowerCase().includes(normalizedQuery));
572
+ }
573
+ if (filtered.length === 0) {
574
+ let message = 'No components found';
575
+ if (query)
576
+ message += ` matching "${query}"`;
577
+ if (category)
578
+ message += ` in category "${category}"`;
579
+ return {
580
+ content: [{ type: 'text', text: message }],
581
+ };
582
+ }
583
+ let result = '# Search Results\n\n';
584
+ if (query)
585
+ result += `**Query:** ${query}\n`;
586
+ if (category)
587
+ result += `**Category:** ${category}\n`;
588
+ result += `**Found:** ${filtered.length} components\n\n`;
589
+ result += filtered.map((c) => `- ${c.name} (${c.path})`).join('\n');
590
+ if (!category && !query) {
591
+ result += '\n\n## Available Categories\n\n';
592
+ for (const [cat, comps] of Object.entries(categories)) {
593
+ result += `- **${cat}**: ${comps.join(', ')}\n`;
594
+ }
595
+ }
596
+ return {
597
+ content: [{ type: 'text', text: result }],
598
+ };
599
+ }
600
+ catch (error) {
601
+ return {
602
+ content: [
603
+ {
604
+ type: 'text',
605
+ text: `Error searching components: ${error instanceof Error ? error.message : String(error)}`,
606
+ },
607
+ ],
608
+ isError: true,
609
+ };
610
+ }
611
+ });
612
+ // Register the get_eslint_rules tool
613
+ server.tool('get_eslint_rules', 'Get available ESLint rules from @movable/ui/eslint-plugin', {}, async () => {
614
+ try {
615
+ const rules = getEslintRules();
616
+ let result = '# @movable/ui ESLint Rules\n\n';
617
+ result += '## Installation\n\n';
618
+ result +=
619
+ '```js\n// eslint.config.js\nimport movableUiPlugin from "@movable/eslint-plugin-ui";\n\nexport default [\n movableUiPlugin.configs.recommended,\n];\n```\n\n';
620
+ result += '## Available Rules\n\n';
621
+ if (rules.length === 0) {
622
+ result += 'No custom rules found or rule data not available.\n';
623
+ }
624
+ else {
625
+ for (const rule of rules) {
626
+ result += `### ${rule.name}\n\n`;
627
+ if (rule.description) {
628
+ result += `**Description:** ${rule.description}\n\n`;
629
+ }
630
+ if (rule.message) {
631
+ result += `**Message:** ${rule.message}\n\n`;
632
+ }
633
+ }
634
+ }
635
+ return {
636
+ content: [{ type: 'text', text: result }],
637
+ };
638
+ }
639
+ catch (error) {
640
+ return {
641
+ content: [
642
+ {
643
+ type: 'text',
644
+ text: `Error getting ESLint rules: ${error instanceof Error ? error.message : String(error)}`,
645
+ },
646
+ ],
647
+ isError: true,
648
+ };
649
+ }
650
+ });
651
+ // ============================================================================
652
+ // Start Server
653
+ // ============================================================================
654
+ async function main() {
655
+ const transport = new StdioServerTransport();
656
+ await server.connect(transport);
657
+ const mode = isNpxMode
658
+ ? 'npx (bundled data)'
659
+ : isLocalMode
660
+ ? 'local development'
661
+ : 'unknown';
662
+ console.error(`Movable UI MCP server running on stdio [${mode}]`);
663
+ }
664
+ main().catch((error) => {
665
+ console.error('Failed to start server:', error);
666
+ process.exit(1);
667
+ });