@ebowwa/claude-code-config-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.
package/dist/index.js ADDED
@@ -0,0 +1,1744 @@
1
+ #!/usr/bin/env bun
2
+ // ============================================================================
3
+ // Claude Code Config MCP Server
4
+ // ============================================================================
5
+ // Manage Claude Code configuration files with atomic writes, validation,
6
+ // and comprehensive error handling.
7
+ // ============================================================================
8
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
9
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
11
+ import { z } from 'zod';
12
+ import { join, resolve } from 'node:path';
13
+ import * as path from './utils/path.js';
14
+ import { getMCPConfigPaths } from './utils/path.js';
15
+ import * as file from './utils/file.js';
16
+ import * as errors from './utils/errors.js';
17
+ // ============================================================================
18
+ // MCP Server Setup
19
+ // ============================================================================
20
+ const server = new Server({
21
+ name: '@mcp/claude-code-config',
22
+ version: '1.0.0',
23
+ }, {
24
+ capabilities: {
25
+ tools: {},
26
+ },
27
+ });
28
+ // ============================================================================
29
+ // Tool Definitions
30
+ // ============================================================================
31
+ /**
32
+ * List all available tools
33
+ */
34
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
35
+ tools: [
36
+ // CLAUDE.md tools
37
+ {
38
+ name: 'read_global_claude_md',
39
+ description: 'Read the global CLAUDE.md file (~/.claude/CLAUDE.md)',
40
+ inputSchema: {
41
+ type: 'object',
42
+ properties: {},
43
+ },
44
+ },
45
+ {
46
+ name: 'write_global_claude_md',
47
+ description: 'Write to the global CLAUDE.md file (~/.claude/CLAUDE.md). Creates backup before writing.',
48
+ inputSchema: {
49
+ type: 'object',
50
+ properties: {
51
+ content: {
52
+ type: 'string',
53
+ description: 'Content to write to CLAUDE.md',
54
+ },
55
+ createBackup: {
56
+ type: 'boolean',
57
+ description: 'Create backup before writing (default: true)',
58
+ default: true,
59
+ },
60
+ },
61
+ required: ['content'],
62
+ },
63
+ },
64
+ {
65
+ name: 'read_project_claude_md',
66
+ description: 'Read the project-local CLAUDE.md file (.claude/CLAUDE.md)',
67
+ inputSchema: {
68
+ type: 'object',
69
+ properties: {
70
+ projectPath: {
71
+ type: 'string',
72
+ description: 'Project directory path (default: current working directory)',
73
+ },
74
+ },
75
+ },
76
+ },
77
+ {
78
+ name: 'write_project_claude_md',
79
+ description: 'Write to the project-local CLAUDE.md file (.claude/CLAUDE.md). Creates backup before writing.',
80
+ inputSchema: {
81
+ type: 'object',
82
+ properties: {
83
+ content: {
84
+ type: 'string',
85
+ description: 'Content to write to CLAUDE.md',
86
+ },
87
+ projectPath: {
88
+ type: 'string',
89
+ description: 'Project directory path (default: current working directory)',
90
+ },
91
+ createBackup: {
92
+ type: 'boolean',
93
+ description: 'Create backup before writing (default: true)',
94
+ default: true,
95
+ },
96
+ },
97
+ required: ['content'],
98
+ },
99
+ },
100
+ // Keybindings tools
101
+ {
102
+ name: 'read_keybindings',
103
+ description: 'Read the keybindings.json file',
104
+ inputSchema: {
105
+ type: 'object',
106
+ properties: {},
107
+ },
108
+ },
109
+ {
110
+ name: 'write_keybindings',
111
+ description: 'Write to keybindings.json. Validates the structure before writing.',
112
+ inputSchema: {
113
+ type: 'object',
114
+ properties: {
115
+ keybindings: {
116
+ type: 'object',
117
+ description: 'Keybindings configuration object',
118
+ },
119
+ createBackup: {
120
+ type: 'boolean',
121
+ description: 'Create backup before writing (default: true)',
122
+ default: true,
123
+ },
124
+ },
125
+ required: ['keybindings'],
126
+ },
127
+ },
128
+ {
129
+ name: 'add_keybinding',
130
+ description: 'Add or update a single keybinding',
131
+ inputSchema: {
132
+ type: 'object',
133
+ properties: {
134
+ key: {
135
+ type: 'string',
136
+ description: 'Key combination (e.g., "ctrl+s", "cmd+k")',
137
+ },
138
+ command: {
139
+ type: 'string',
140
+ description: 'Command to execute',
141
+ },
142
+ when: {
143
+ type: 'string',
144
+ description: 'Optional context condition',
145
+ },
146
+ },
147
+ required: ['key', 'command'],
148
+ },
149
+ },
150
+ {
151
+ name: 'remove_keybinding',
152
+ description: 'Remove a keybinding by key combination',
153
+ inputSchema: {
154
+ type: 'object',
155
+ properties: {
156
+ key: {
157
+ type: 'string',
158
+ description: 'Key combination to remove',
159
+ },
160
+ },
161
+ required: ['key'],
162
+ },
163
+ },
164
+ // Settings tools
165
+ {
166
+ name: 'read_settings',
167
+ description: 'Read the settings.json file',
168
+ inputSchema: {
169
+ type: 'object',
170
+ properties: {},
171
+ },
172
+ },
173
+ {
174
+ name: 'write_settings',
175
+ description: 'Write to settings.json',
176
+ inputSchema: {
177
+ type: 'object',
178
+ properties: {
179
+ settings: {
180
+ type: 'object',
181
+ description: 'Settings object',
182
+ },
183
+ createBackup: {
184
+ type: 'boolean',
185
+ description: 'Create backup before writing (default: true)',
186
+ default: true,
187
+ },
188
+ },
189
+ required: ['settings'],
190
+ },
191
+ },
192
+ // Hooks tools
193
+ {
194
+ name: 'list_hooks',
195
+ description: 'List all configured hooks',
196
+ inputSchema: {
197
+ type: 'object',
198
+ properties: {},
199
+ },
200
+ },
201
+ {
202
+ name: 'read_hook',
203
+ description: 'Read a specific hook file',
204
+ inputSchema: {
205
+ type: 'object',
206
+ properties: {
207
+ name: {
208
+ type: 'string',
209
+ description: 'Hook file name (without extension)',
210
+ },
211
+ },
212
+ required: ['name'],
213
+ },
214
+ },
215
+ {
216
+ name: 'write_hook',
217
+ description: 'Write or update a hook file',
218
+ inputSchema: {
219
+ type: 'object',
220
+ properties: {
221
+ name: {
222
+ type: 'string',
223
+ description: 'Hook file name',
224
+ },
225
+ event: {
226
+ type: 'string',
227
+ description: 'Hook event (SessionStart, SessionEnd, PreToolUse, PostToolUse, etc.)',
228
+ },
229
+ content: {
230
+ type: 'string',
231
+ description: 'Hook script content',
232
+ },
233
+ enabled: {
234
+ type: 'boolean',
235
+ description: 'Whether the hook is enabled',
236
+ default: true,
237
+ },
238
+ },
239
+ required: ['name', 'event', 'content'],
240
+ },
241
+ },
242
+ // Utility tools
243
+ {
244
+ name: 'list_config_files',
245
+ description: 'List all Claude Code config files with metadata',
246
+ inputSchema: {
247
+ type: 'object',
248
+ properties: {
249
+ includeProject: {
250
+ type: 'boolean',
251
+ description: 'Include project-local config files',
252
+ default: true,
253
+ },
254
+ },
255
+ },
256
+ },
257
+ {
258
+ name: 'get_config_path',
259
+ description: 'Get the path to a specific config file',
260
+ inputSchema: {
261
+ type: 'object',
262
+ properties: {
263
+ fileType: {
264
+ type: 'string',
265
+ enum: [
266
+ 'CLAUDE_MD_GLOBAL',
267
+ 'CLAUDE_MD_PROJECT',
268
+ 'KEYBINDINGS',
269
+ 'SETTINGS',
270
+ 'HOOKS_DIR',
271
+ ],
272
+ description: 'Type of config file',
273
+ },
274
+ },
275
+ required: ['fileType'],
276
+ },
277
+ },
278
+ {
279
+ name: 'backup_config',
280
+ description: 'Create a backup of a config file',
281
+ inputSchema: {
282
+ type: 'object',
283
+ properties: {
284
+ fileType: {
285
+ type: 'string',
286
+ enum: ['CLAUDE_MD', 'KEYBINDINGS', 'SETTINGS'],
287
+ description: 'Type of config file to backup',
288
+ },
289
+ },
290
+ required: ['fileType'],
291
+ },
292
+ },
293
+ // MCP Server management tools
294
+ {
295
+ name: 'mcp_list',
296
+ description: 'List all configured MCP servers from config files. Supports CLI (~/.claude.json) and app (~/.config/claude-code/config.json) configs.',
297
+ inputSchema: {
298
+ type: 'object',
299
+ properties: {
300
+ target: {
301
+ type: 'string',
302
+ enum: ['cli', 'app', 'both'],
303
+ description: 'Which config to read (default: both)',
304
+ default: 'both',
305
+ },
306
+ includeHealth: {
307
+ type: 'boolean',
308
+ description: 'Check server health status by running claude mcp list',
309
+ default: false,
310
+ },
311
+ },
312
+ },
313
+ },
314
+ {
315
+ name: 'mcp_get',
316
+ description: 'Get details about a specific MCP server from config files',
317
+ inputSchema: {
318
+ type: 'object',
319
+ properties: {
320
+ name: {
321
+ type: 'string',
322
+ description: 'MCP server name',
323
+ },
324
+ target: {
325
+ type: 'string',
326
+ enum: ['cli', 'app', 'both'],
327
+ description: 'Which config to read (default: both)',
328
+ default: 'both',
329
+ },
330
+ },
331
+ required: ['name'],
332
+ },
333
+ },
334
+ {
335
+ name: 'mcp_add',
336
+ description: 'Add a new MCP server configuration. Supports stdio, HTTP, SSE, and WebSocket transports. Can add to CLI config, app config, or both.',
337
+ inputSchema: {
338
+ type: 'object',
339
+ properties: {
340
+ name: {
341
+ type: 'string',
342
+ description: 'Unique name for the MCP server',
343
+ },
344
+ command: {
345
+ type: 'string',
346
+ description: 'Command to run (for stdio transport, e.g., "node", "bun", "npx")',
347
+ },
348
+ args: {
349
+ type: 'array',
350
+ items: { type: 'string' },
351
+ description: 'Arguments for the command (e.g., ["/path/to/server.js"])',
352
+ },
353
+ url: {
354
+ type: 'string',
355
+ description: 'URL for HTTP/SSE/WebSocket transport',
356
+ },
357
+ transport: {
358
+ type: 'string',
359
+ enum: ['stdio', 'sse', 'http', 'ws', 'websocket'],
360
+ description: 'Transport type (default: stdio)',
361
+ },
362
+ env: {
363
+ type: 'object',
364
+ additionalProperties: { type: 'string' },
365
+ description: 'Environment variables for the server',
366
+ },
367
+ headers: {
368
+ type: 'object',
369
+ additionalProperties: { type: 'string' },
370
+ description: 'HTTP headers for HTTP/SSE/WebSocket transport',
371
+ },
372
+ description: {
373
+ type: 'string',
374
+ description: 'Human-readable description of the server',
375
+ },
376
+ alwaysAllow: {
377
+ type: 'boolean',
378
+ description: 'Always allow this server (bypass project-scoped approval)',
379
+ },
380
+ timeout: {
381
+ type: 'number',
382
+ description: 'Timeout in milliseconds',
383
+ },
384
+ target: {
385
+ type: 'string',
386
+ enum: ['cli', 'app', 'both'],
387
+ description: 'Which config(s) to add to (default: both - adds to both CLI and app configs)',
388
+ default: 'both',
389
+ },
390
+ },
391
+ required: ['name'],
392
+ },
393
+ },
394
+ {
395
+ name: 'mcp_install',
396
+ description: 'Download and configure an MCP server from npm, GitHub, or a local path. Automatically detects configuration from package.json. Can add to CLI config, app config, or both.',
397
+ inputSchema: {
398
+ type: 'object',
399
+ properties: {
400
+ source: {
401
+ type: 'string',
402
+ enum: ['npm', 'github', 'local', 'url'],
403
+ description: 'Source type (auto-detected if not specified)',
404
+ },
405
+ package: {
406
+ type: 'string',
407
+ description: 'Package name (npm), repo URL (github), local path, or direct URL',
408
+ },
409
+ name: {
410
+ type: 'string',
411
+ description: 'Name for the MCP server (defaults to package name)',
412
+ },
413
+ version: {
414
+ type: 'string',
415
+ description: 'Version to install (for npm packages, e.g., "latest", "1.2.3")',
416
+ },
417
+ env: {
418
+ type: 'object',
419
+ additionalProperties: { type: 'string' },
420
+ description: 'Environment variables for the server',
421
+ },
422
+ description: {
423
+ type: 'string',
424
+ description: 'Description for the server',
425
+ },
426
+ args: {
427
+ type: 'array',
428
+ items: { type: 'string' },
429
+ description: 'Additional arguments to pass to the server',
430
+ },
431
+ installDir: {
432
+ type: 'string',
433
+ description: 'Global MCP packages directory (defaults to ~/.claude/mcp-servers)',
434
+ },
435
+ force: {
436
+ type: 'boolean',
437
+ description: 'Force reinstallation even if already installed',
438
+ },
439
+ target: {
440
+ type: 'string',
441
+ enum: ['cli', 'app', 'both'],
442
+ description: 'Which config(s) to add to (default: both - adds to both CLI and app configs)',
443
+ default: 'both',
444
+ },
445
+ },
446
+ required: ['package'],
447
+ },
448
+ },
449
+ {
450
+ name: 'mcp_remove',
451
+ description: 'Remove an MCP server configuration from config file(s)',
452
+ inputSchema: {
453
+ type: 'object',
454
+ properties: {
455
+ name: {
456
+ type: 'string',
457
+ description: 'MCP server name to remove',
458
+ },
459
+ target: {
460
+ type: 'string',
461
+ enum: ['cli', 'app', 'both'],
462
+ description: 'Which config(s) to remove from (default: both)',
463
+ default: 'both',
464
+ },
465
+ },
466
+ required: ['name'],
467
+ },
468
+ },
469
+ {
470
+ name: 'mcp_update',
471
+ description: 'Update an existing MCP server configuration',
472
+ inputSchema: {
473
+ type: 'object',
474
+ properties: {
475
+ name: {
476
+ type: 'string',
477
+ description: 'MCP server name to update',
478
+ },
479
+ command: {
480
+ type: 'string',
481
+ description: 'New command to run',
482
+ },
483
+ args: {
484
+ type: 'array',
485
+ items: { type: 'string' },
486
+ description: 'New arguments for the command',
487
+ },
488
+ url: {
489
+ type: 'string',
490
+ description: 'New URL for HTTP/SSE/WebSocket transport',
491
+ },
492
+ env: {
493
+ type: 'object',
494
+ additionalProperties: { type: 'string' },
495
+ description: 'New environment variables',
496
+ },
497
+ headers: {
498
+ type: 'object',
499
+ additionalProperties: { type: 'string' },
500
+ description: 'New HTTP headers',
501
+ },
502
+ description: {
503
+ type: 'string',
504
+ description: 'New description',
505
+ },
506
+ alwaysAllow: {
507
+ type: 'boolean',
508
+ description: 'Update always allow flag',
509
+ },
510
+ timeout: {
511
+ type: 'number',
512
+ description: 'New timeout in milliseconds',
513
+ },
514
+ target: {
515
+ type: 'string',
516
+ enum: ['cli', 'app', 'both'],
517
+ description: 'Which config(s) to update (default: both)',
518
+ default: 'both',
519
+ },
520
+ },
521
+ required: ['name'],
522
+ },
523
+ },
524
+ {
525
+ name: 'mcp_sync',
526
+ description: 'Synchronize MCP servers between CLI and app configs. Ensures both configs have the same servers.',
527
+ inputSchema: {
528
+ type: 'object',
529
+ properties: {
530
+ source: {
531
+ type: 'string',
532
+ enum: ['cli', 'app'],
533
+ description: 'Source config to sync from (default: app - syncs from app to CLI)',
534
+ default: 'app',
535
+ },
536
+ dryRun: {
537
+ type: 'boolean',
538
+ description: 'Show what would be synced without making changes',
539
+ default: false,
540
+ },
541
+ },
542
+ },
543
+ },
544
+ ],
545
+ }));
546
+ // ============================================================================
547
+ // Tool Handlers
548
+ // ============================================================================
549
+ /**
550
+ * Handle tool execution
551
+ */
552
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
553
+ const { name, arguments: args } = request.params;
554
+ try {
555
+ switch (name) {
556
+ // CLAUDE.md tools
557
+ case 'read_global_claude_md':
558
+ return await handleReadGlobalClaudeMd();
559
+ case 'write_global_claude_md':
560
+ return await handleWriteGlobalClaudeMd(args);
561
+ case 'read_project_claude_md':
562
+ return await handleReadProjectClaudeMd(args);
563
+ case 'write_project_claude_md':
564
+ return await handleWriteProjectClaudeMd(args);
565
+ // Keybindings tools
566
+ case 'read_keybindings':
567
+ return await handleReadKeybindings();
568
+ case 'write_keybindings':
569
+ return await handleWriteKeybindings(args);
570
+ case 'add_keybinding':
571
+ return await handleAddKeybinding(args);
572
+ case 'remove_keybinding':
573
+ return await handleRemoveKeybinding(args);
574
+ // Settings tools
575
+ case 'read_settings':
576
+ return await handleReadSettings();
577
+ case 'write_settings':
578
+ return await handleWriteSettings(args);
579
+ // Hooks tools
580
+ case 'list_hooks':
581
+ return await handleListHooks();
582
+ case 'read_hook':
583
+ return await handleReadHook(args);
584
+ case 'write_hook':
585
+ return await handleWriteHook(args);
586
+ // Utility tools
587
+ case 'list_config_files':
588
+ return await handleListConfigFiles(args);
589
+ case 'get_config_path':
590
+ return await handleGetConfigPath(args);
591
+ case 'backup_config':
592
+ return await handleBackupConfig(args);
593
+ // MCP management tools
594
+ case 'mcp_list':
595
+ return await handleMCPList(args);
596
+ case 'mcp_get':
597
+ return await handleMCPGet(args);
598
+ case 'mcp_add':
599
+ return await handleMCPAdd(args);
600
+ case 'mcp_install':
601
+ return await handleMCPInstall(args);
602
+ case 'mcp_remove':
603
+ return await handleMCPRemove(args);
604
+ case 'mcp_update':
605
+ return await handleMCPUpdate(args);
606
+ case 'mcp_sync':
607
+ return await handleMCPSync(args);
608
+ default:
609
+ throw new Error(`Unknown tool: ${name}`);
610
+ }
611
+ }
612
+ catch (error) {
613
+ const mcpError = errors.createMCPError(error);
614
+ return {
615
+ content: [
616
+ {
617
+ type: 'text',
618
+ text: JSON.stringify(mcpError, null, 2),
619
+ },
620
+ ],
621
+ isError: true,
622
+ };
623
+ }
624
+ });
625
+ // ============================================================================
626
+ // CLAUDE.md Handlers
627
+ // ============================================================================
628
+ async function handleReadGlobalClaudeMd() {
629
+ const filePath = path.getConfigPaths().globalClaudeMd;
630
+ const validatedPath = path.validatePath(filePath);
631
+ const content = await file.safeReadFile(validatedPath);
632
+ const stats = await getFileInfo(validatedPath);
633
+ return {
634
+ content: [
635
+ {
636
+ type: 'text',
637
+ text: content,
638
+ },
639
+ ],
640
+ };
641
+ }
642
+ async function handleWriteGlobalClaudeMd(args) {
643
+ const schema = z.object({
644
+ content: z.string(),
645
+ createBackup: z.boolean().optional(),
646
+ });
647
+ const { content, createBackup = true } = schema.parse(args);
648
+ const filePath = path.getConfigPaths().globalClaudeMd;
649
+ const validatedPath = path.validatePath(filePath);
650
+ // Normalize content
651
+ const normalized = file.normalizeLineEndings(content);
652
+ const stripped = file.stripBOM(normalized);
653
+ await file.atomicWrite(validatedPath, stripped, { createBackup });
654
+ return {
655
+ content: [
656
+ {
657
+ type: 'text',
658
+ text: `Successfully wrote global CLAUDE.md to ${path.getDisplayPath(validatedPath)}`,
659
+ },
660
+ ],
661
+ };
662
+ }
663
+ async function handleReadProjectClaudeMd(args) {
664
+ const schema = z.object({
665
+ projectPath: z.string().optional(),
666
+ });
667
+ const { projectPath } = schema.parse(args);
668
+ const cwd = projectPath || process.cwd();
669
+ const filePath = path.getConfigPaths(cwd).projectClaudeMd;
670
+ const validatedPath = path.validatePath(filePath, cwd);
671
+ const content = await file.safeReadFile(validatedPath);
672
+ return {
673
+ content: [
674
+ {
675
+ type: 'text',
676
+ text: content,
677
+ },
678
+ ],
679
+ };
680
+ }
681
+ async function handleWriteProjectClaudeMd(args) {
682
+ const schema = z.object({
683
+ content: z.string(),
684
+ projectPath: z.string().optional(),
685
+ createBackup: z.boolean().optional(),
686
+ });
687
+ const { content, projectPath, createBackup = true } = schema.parse(args);
688
+ const cwd = projectPath || process.cwd();
689
+ const filePath = path.getConfigPaths(cwd).projectClaudeMd;
690
+ const validatedPath = path.validatePath(filePath, cwd);
691
+ // Normalize content
692
+ const normalized = file.normalizeLineEndings(content);
693
+ const stripped = file.stripBOM(normalized);
694
+ await file.atomicWrite(validatedPath, stripped, { createBackup });
695
+ return {
696
+ content: [
697
+ {
698
+ type: 'text',
699
+ text: `Successfully wrote project CLAUDE.md to ${path.getDisplayPath(validatedPath)}`,
700
+ },
701
+ ],
702
+ };
703
+ }
704
+ // ============================================================================
705
+ // Keybindings Handlers
706
+ // ============================================================================
707
+ async function handleReadKeybindings() {
708
+ const filePath = path.getConfigPaths().keybindings;
709
+ const validatedPath = path.validatePath(filePath);
710
+ const content = await file.safeReadFile(validatedPath);
711
+ const config = file.safeJSONParse(content, validatedPath);
712
+ return {
713
+ content: [
714
+ {
715
+ type: 'text',
716
+ text: JSON.stringify(config, null, 2),
717
+ },
718
+ ],
719
+ };
720
+ }
721
+ async function handleWriteKeybindings(args) {
722
+ const schema = z.object({
723
+ keybindings: z.object({
724
+ $schema: z.string().optional(),
725
+ keybindings: z.record(z.string(), z.object({
726
+ key: z.string(),
727
+ command: z.string(),
728
+ when: z.string().optional(),
729
+ })),
730
+ }),
731
+ createBackup: z.boolean().optional(),
732
+ });
733
+ const { keybindings, createBackup = true } = schema.parse(args);
734
+ const filePath = path.getConfigPaths().keybindings;
735
+ const validatedPath = path.validatePath(filePath);
736
+ const content = file.formatJSON(keybindings);
737
+ await file.atomicWrite(validatedPath, content, { createBackup });
738
+ return {
739
+ content: [
740
+ {
741
+ type: 'text',
742
+ text: `Successfully wrote keybindings to ${path.getDisplayPath(validatedPath)}`,
743
+ },
744
+ ],
745
+ };
746
+ }
747
+ async function handleAddKeybinding(args) {
748
+ const schema = z.object({
749
+ key: z.string(),
750
+ command: z.string(),
751
+ when: z.string().optional(),
752
+ });
753
+ const { key, command, when } = schema.parse(args);
754
+ const filePath = path.getConfigPaths().keybindings;
755
+ const validatedPath = path.validatePath(filePath);
756
+ // Read existing config
757
+ let config;
758
+ try {
759
+ const content = await file.safeReadFile(validatedPath);
760
+ config = file.safeJSONParse(content, validatedPath);
761
+ }
762
+ catch {
763
+ // Create new config if file doesn't exist
764
+ config = { keybindings: {} };
765
+ }
766
+ // Add/update keybinding
767
+ config.keybindings[key] = { key, command, ...(when && { when }) };
768
+ const content = file.formatJSON(config);
769
+ await file.atomicWrite(validatedPath, content, { createBackup: true });
770
+ return {
771
+ content: [
772
+ {
773
+ type: 'text',
774
+ text: `Successfully added keybinding: ${key} -> ${command}`,
775
+ },
776
+ ],
777
+ };
778
+ }
779
+ async function handleRemoveKeybinding(args) {
780
+ const schema = z.object({
781
+ key: z.string(),
782
+ });
783
+ const { key } = schema.parse(args);
784
+ const filePath = path.getConfigPaths().keybindings;
785
+ const validatedPath = path.validatePath(filePath);
786
+ // Read existing config
787
+ const content = await file.safeReadFile(validatedPath);
788
+ const config = file.safeJSONParse(content, validatedPath);
789
+ // Remove keybinding
790
+ delete config.keybindings[key];
791
+ const newContent = file.formatJSON(config);
792
+ await file.atomicWrite(validatedPath, newContent, { createBackup: true });
793
+ return {
794
+ content: [
795
+ {
796
+ type: 'text',
797
+ text: `Successfully removed keybinding: ${key}`,
798
+ },
799
+ ],
800
+ };
801
+ }
802
+ // ============================================================================
803
+ // Settings Handlers
804
+ // ============================================================================
805
+ async function handleReadSettings() {
806
+ const filePath = path.getConfigPaths().settings;
807
+ const validatedPath = path.validatePath(filePath);
808
+ const content = await file.safeReadFile(validatedPath);
809
+ return {
810
+ content: [
811
+ {
812
+ type: 'text',
813
+ text: content,
814
+ },
815
+ ],
816
+ };
817
+ }
818
+ async function handleWriteSettings(args) {
819
+ const schema = z.object({
820
+ settings: z.object({
821
+ $schema: z.string().optional(),
822
+ }).passthrough(),
823
+ createBackup: z.boolean().optional(),
824
+ });
825
+ const { settings, createBackup = true } = schema.parse(args);
826
+ const filePath = path.getConfigPaths().settings;
827
+ const validatedPath = path.validatePath(filePath);
828
+ const content = file.formatJSON(settings);
829
+ await file.atomicWrite(validatedPath, content, { createBackup });
830
+ return {
831
+ content: [
832
+ {
833
+ type: 'text',
834
+ text: `Successfully wrote settings to ${path.getDisplayPath(validatedPath)}`,
835
+ },
836
+ ],
837
+ };
838
+ }
839
+ // ============================================================================
840
+ // Hooks Handlers
841
+ // ============================================================================
842
+ async function handleListHooks() {
843
+ const hooksDir = path.getConfigPaths().hooksDir;
844
+ const validatedPath = path.validatePath(hooksDir);
845
+ const hooks = await listHookFiles(validatedPath);
846
+ return {
847
+ content: [
848
+ {
849
+ type: 'text',
850
+ text: JSON.stringify(hooks, null, 2),
851
+ },
852
+ ],
853
+ };
854
+ }
855
+ async function handleReadHook(args) {
856
+ const schema = z.object({
857
+ name: z.string(),
858
+ });
859
+ const { name } = schema.parse(args);
860
+ const hooksDir = path.getConfigPaths().hooksDir;
861
+ const validatedPath = path.validatePath(hooksDir);
862
+ const hookPath = `${validatedPath}/${name}`;
863
+ const validatedHookPath = path.validatePath(hookPath);
864
+ const content = await file.safeReadFile(validatedHookPath);
865
+ return {
866
+ content: [
867
+ {
868
+ type: 'text',
869
+ text: content,
870
+ },
871
+ ],
872
+ };
873
+ }
874
+ async function handleWriteHook(args) {
875
+ const schema = z.object({
876
+ name: z.string(),
877
+ event: z.string(),
878
+ content: z.string(),
879
+ enabled: z.boolean().optional(),
880
+ });
881
+ const { name, event, content, enabled = true } = schema.parse(args);
882
+ const hooksDir = path.getConfigPaths().hooksDir;
883
+ const validatedPath = path.validatePath(hooksDir);
884
+ const hookPath = `${validatedPath}/${name}`;
885
+ const validatedHookPath = path.validatePath(hookPath);
886
+ await file.atomicWrite(validatedHookPath, content, { createBackup: true });
887
+ return {
888
+ content: [
889
+ {
890
+ type: 'text',
891
+ text: `Successfully wrote hook: ${name}`,
892
+ },
893
+ ],
894
+ };
895
+ }
896
+ // ============================================================================
897
+ // Utility Handlers
898
+ // ============================================================================
899
+ async function handleListConfigFiles(args) {
900
+ const schema = z.object({
901
+ includeProject: z.boolean().optional(),
902
+ });
903
+ const { includeProject = true } = schema.parse(args);
904
+ const paths = path.getConfigPaths();
905
+ const cwd = process.cwd();
906
+ const files = [
907
+ {
908
+ type: 'CLAUDE_MD_GLOBAL',
909
+ path: path.getDisplayPath(paths.globalClaudeMd),
910
+ exists: path.pathExists(paths.globalClaudeMd),
911
+ },
912
+ {
913
+ type: 'KEYBINDINGS',
914
+ path: path.getDisplayPath(paths.keybindings),
915
+ exists: path.pathExists(paths.keybindings),
916
+ },
917
+ {
918
+ type: 'SETTINGS',
919
+ path: path.getDisplayPath(paths.settings),
920
+ exists: path.pathExists(paths.settings),
921
+ },
922
+ {
923
+ type: 'HOOKS_DIR',
924
+ path: path.getDisplayPath(paths.hooksDir),
925
+ exists: path.pathExists(paths.hooksDir),
926
+ },
927
+ ];
928
+ if (includeProject) {
929
+ files.push({
930
+ type: 'CLAUDE_MD_PROJECT',
931
+ path: path.getDisplayPath(paths.projectClaudeMd),
932
+ exists: path.pathExists(paths.projectClaudeMd),
933
+ });
934
+ }
935
+ return {
936
+ content: [
937
+ {
938
+ type: 'text',
939
+ text: JSON.stringify(files, null, 2),
940
+ },
941
+ ],
942
+ };
943
+ }
944
+ async function handleGetConfigPath(args) {
945
+ const schema = z.object({
946
+ fileType: z.enum([
947
+ 'CLAUDE_MD_GLOBAL',
948
+ 'CLAUDE_MD_PROJECT',
949
+ 'KEYBINDINGS',
950
+ 'SETTINGS',
951
+ 'HOOKS_DIR',
952
+ ]),
953
+ });
954
+ const { fileType } = schema.parse(args);
955
+ const paths = path.getConfigPaths();
956
+ const filePathMap = {
957
+ CLAUDE_MD_GLOBAL: paths.globalClaudeMd,
958
+ CLAUDE_MD_PROJECT: paths.projectClaudeMd,
959
+ KEYBINDINGS: paths.keybindings,
960
+ SETTINGS: paths.settings,
961
+ HOOKS_DIR: paths.hooksDir,
962
+ };
963
+ const resolvedPath = filePathMap[fileType];
964
+ const displayPath = path.getDisplayPath(resolvedPath);
965
+ return {
966
+ content: [
967
+ {
968
+ type: 'text',
969
+ text: JSON.stringify({
970
+ type: fileType,
971
+ path: displayPath,
972
+ absolutePath: resolvedPath,
973
+ exists: path.pathExists(resolvedPath),
974
+ }, null, 2),
975
+ },
976
+ ],
977
+ };
978
+ }
979
+ async function handleBackupConfig(args) {
980
+ const schema = z.object({
981
+ fileType: z.enum(['CLAUDE_MD', 'KEYBINDINGS', 'SETTINGS']),
982
+ });
983
+ const { fileType } = schema.parse(args);
984
+ const paths = path.getConfigPaths();
985
+ const filePathMap = {
986
+ CLAUDE_MD: paths.globalClaudeMd,
987
+ KEYBINDINGS: paths.keybindings,
988
+ SETTINGS: paths.settings,
989
+ };
990
+ const filePath = filePathMap[fileType];
991
+ const validatedPath = path.validatePath(filePath);
992
+ if (!path.pathExists(validatedPath)) {
993
+ throw new Error(`File does not exist: ${fileType}`);
994
+ }
995
+ const backupPath = await createBackup(validatedPath);
996
+ return {
997
+ content: [
998
+ {
999
+ type: 'text',
1000
+ text: `Backup created: ${path.getDisplayPath(backupPath)}`,
1001
+ },
1002
+ ],
1003
+ };
1004
+ }
1005
+ // ============================================================================
1006
+ // Helper Functions
1007
+ // ============================================================================
1008
+ async function getFileInfo(filePath) {
1009
+ const { stat } = await import('node:fs/promises');
1010
+ const stats = await stat(filePath);
1011
+ return {
1012
+ size: stats.size,
1013
+ modified: stats.mtime,
1014
+ };
1015
+ }
1016
+ async function listHookFiles(hooksDir) {
1017
+ const { readdir } = await import('node:fs/promises');
1018
+ const { existsSync } = await import('node:fs');
1019
+ if (!existsSync(hooksDir)) {
1020
+ return [];
1021
+ }
1022
+ const files = await readdir(hooksDir, { withFileTypes: true });
1023
+ const hooks = [];
1024
+ for (const file of files) {
1025
+ if (file.isFile()) {
1026
+ // Parse event from filename (e.g., "PreToolUse.ts" -> "PreToolUse")
1027
+ const name = file.name;
1028
+ const event = name.replace(/\.(ts|js|sh)$/, '');
1029
+ hooks.push({ name, event, enabled: true });
1030
+ }
1031
+ }
1032
+ return hooks;
1033
+ }
1034
+ async function createBackup(filePath) {
1035
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1036
+ const backupPath = `${filePath}.backup.${timestamp}`;
1037
+ const { readFile, writeFile } = await import('node:fs/promises');
1038
+ const content = await readFile(filePath);
1039
+ await writeFile(backupPath, content);
1040
+ return backupPath;
1041
+ }
1042
+ // ============================================================================
1043
+ // MCP Management Handlers
1044
+ // ============================================================================
1045
+ async function handleMCPList(args) {
1046
+ const schema = z.object({
1047
+ target: z.enum(['cli', 'app', 'both']).optional(),
1048
+ includeHealth: z.boolean().optional(),
1049
+ });
1050
+ const { target = 'both', includeHealth = false } = schema.parse(args);
1051
+ const configPaths = getMCPConfigPaths(target);
1052
+ // Read configs from requested targets
1053
+ const results = {};
1054
+ if (configPaths.cli) {
1055
+ try {
1056
+ const validatedPath = path.validatePath(configPaths.cli);
1057
+ const content = await file.safeReadFile(validatedPath);
1058
+ results.cli = {
1059
+ config: file.safeJSONParse(content, validatedPath),
1060
+ source: 'cli',
1061
+ };
1062
+ }
1063
+ catch {
1064
+ results.cli = { config: { mcpServers: {} }, source: 'cli' };
1065
+ }
1066
+ }
1067
+ if (configPaths.app) {
1068
+ try {
1069
+ const validatedPath = path.validatePath(configPaths.app);
1070
+ const content = await file.safeReadFile(validatedPath);
1071
+ results.app = {
1072
+ config: file.safeJSONParse(content, validatedPath),
1073
+ source: 'app',
1074
+ };
1075
+ }
1076
+ catch {
1077
+ results.app = { config: { mcpServers: {} }, source: 'app' };
1078
+ }
1079
+ }
1080
+ // If health check is requested, run claude mcp list
1081
+ let healthInfo = {};
1082
+ if (includeHealth) {
1083
+ try {
1084
+ const { exec } = await import('node:child_process');
1085
+ const result = await new Promise((resolve, reject) => {
1086
+ exec('claude mcp list 2>&1', (error, stdout, stderr) => {
1087
+ if (error) {
1088
+ resolve(stderr || stdout);
1089
+ }
1090
+ else {
1091
+ resolve(stdout);
1092
+ }
1093
+ });
1094
+ });
1095
+ // Parse the output to extract health info
1096
+ const lines = result.split('\n');
1097
+ for (const line of lines) {
1098
+ const match = line.match(/^([^:]+):\s+.*?-\s+([✓✗])/);
1099
+ if (match) {
1100
+ const name = match[1].trim();
1101
+ const status = match[2] === '✓' ? 'connected' : 'error';
1102
+ healthInfo[name] = status;
1103
+ }
1104
+ }
1105
+ }
1106
+ catch {
1107
+ // Health check failed, continue without it
1108
+ }
1109
+ }
1110
+ // Build result with servers from each config
1111
+ const output = {};
1112
+ if (results.cli) {
1113
+ const cliServers = Object.entries(results.cli.config.mcpServers || {}).map(([name, serverConfig]) => ({
1114
+ ...serverConfig,
1115
+ name,
1116
+ source: 'cli',
1117
+ health: healthInfo[name],
1118
+ }));
1119
+ output.cli = { mcpServers: cliServers };
1120
+ }
1121
+ if (results.app) {
1122
+ const appServers = Object.entries(results.app.config.mcpServers || {}).map(([name, serverConfig]) => ({
1123
+ ...serverConfig,
1124
+ name,
1125
+ source: 'app',
1126
+ health: healthInfo[name],
1127
+ }));
1128
+ output.app = { mcpServers: appServers };
1129
+ }
1130
+ return {
1131
+ content: [
1132
+ {
1133
+ type: 'text',
1134
+ text: JSON.stringify(output, null, 2),
1135
+ },
1136
+ ],
1137
+ };
1138
+ }
1139
+ async function handleMCPGet(args) {
1140
+ const schema = z.object({
1141
+ name: z.string(),
1142
+ target: z.enum(['cli', 'app', 'both']).optional(),
1143
+ });
1144
+ const { name, target = 'both' } = schema.parse(args);
1145
+ const configPaths = getMCPConfigPaths(target);
1146
+ const results = {};
1147
+ let found = false;
1148
+ if (configPaths.cli) {
1149
+ try {
1150
+ const validatedPath = path.validatePath(configPaths.cli);
1151
+ const content = await file.safeReadFile(validatedPath);
1152
+ const config = file.safeJSONParse(content, validatedPath);
1153
+ const serverConfig = config.mcpServers?.[name];
1154
+ if (serverConfig) {
1155
+ results.cli = { name, ...serverConfig, source: 'cli' };
1156
+ found = true;
1157
+ }
1158
+ }
1159
+ catch {
1160
+ // Continue to app config
1161
+ }
1162
+ }
1163
+ if (configPaths.app) {
1164
+ try {
1165
+ const validatedPath = path.validatePath(configPaths.app);
1166
+ const content = await file.safeReadFile(validatedPath);
1167
+ const config = file.safeJSONParse(content, validatedPath);
1168
+ const serverConfig = config.mcpServers?.[name];
1169
+ if (serverConfig) {
1170
+ results.app = { name, ...serverConfig, source: 'app' };
1171
+ found = true;
1172
+ }
1173
+ }
1174
+ catch {
1175
+ // Server not found in app config
1176
+ }
1177
+ }
1178
+ if (!found) {
1179
+ throw new Error(`MCP server not found: ${name}`);
1180
+ }
1181
+ return {
1182
+ content: [
1183
+ {
1184
+ type: 'text',
1185
+ text: JSON.stringify(results, null, 2),
1186
+ },
1187
+ ],
1188
+ };
1189
+ }
1190
+ async function handleMCPAdd(args) {
1191
+ const schema = z.object({
1192
+ name: z.string().min(1),
1193
+ command: z.string().optional(),
1194
+ args: z.array(z.string()).optional(),
1195
+ url: z.string().optional(),
1196
+ transport: z.enum(['stdio', 'sse', 'http', 'ws', 'websocket']).optional(),
1197
+ env: z.record(z.string(), z.string()).optional(),
1198
+ headers: z.record(z.string(), z.string()).optional(),
1199
+ description: z.string().optional(),
1200
+ alwaysAllow: z.boolean().optional(),
1201
+ timeout: z.number().optional(),
1202
+ target: z.enum(['cli', 'app', 'both']).optional(),
1203
+ });
1204
+ const options = schema.parse(args);
1205
+ const configPaths = getMCPConfigPaths(options.target || 'both');
1206
+ const results = [];
1207
+ // Build server config
1208
+ const serverConfig = {};
1209
+ if (options.command)
1210
+ serverConfig.command = options.command;
1211
+ if (options.args)
1212
+ serverConfig.args = options.args;
1213
+ if (options.url)
1214
+ serverConfig.url = options.url;
1215
+ if (options.transport)
1216
+ serverConfig.transport = options.transport;
1217
+ if (options.env)
1218
+ serverConfig.env = options.env;
1219
+ if (options.headers)
1220
+ serverConfig.headers = options.headers;
1221
+ if (options.description)
1222
+ serverConfig.description = options.description;
1223
+ if (options.alwaysAllow !== undefined)
1224
+ serverConfig.alwaysAllow = options.alwaysAllow;
1225
+ if (options.timeout)
1226
+ serverConfig.timeout = options.timeout;
1227
+ // Add to CLI config
1228
+ if (configPaths.cli) {
1229
+ const validatedPath = path.validatePath(configPaths.cli);
1230
+ let config;
1231
+ try {
1232
+ const content = await file.safeReadFile(validatedPath);
1233
+ config = file.safeJSONParse(content, validatedPath);
1234
+ }
1235
+ catch {
1236
+ config = { mcpServers: {} };
1237
+ }
1238
+ if (!config.mcpServers) {
1239
+ config.mcpServers = {};
1240
+ }
1241
+ if (config.mcpServers[options.name]) {
1242
+ throw new Error(`MCP server already exists in CLI config: ${options.name}`);
1243
+ }
1244
+ config.mcpServers[options.name] = serverConfig;
1245
+ const content = file.formatJSON(config);
1246
+ await file.atomicWrite(validatedPath, content, { createBackup: true });
1247
+ results.push(`CLI config (${path.getDisplayPath(configPaths.cli)})`);
1248
+ }
1249
+ // Add to app config
1250
+ if (configPaths.app) {
1251
+ const validatedPath = path.validatePath(configPaths.app);
1252
+ let config;
1253
+ try {
1254
+ const content = await file.safeReadFile(validatedPath);
1255
+ config = file.safeJSONParse(content, validatedPath);
1256
+ }
1257
+ catch {
1258
+ config = { mcpServers: {} };
1259
+ }
1260
+ if (!config.mcpServers) {
1261
+ config.mcpServers = {};
1262
+ }
1263
+ if (config.mcpServers[options.name]) {
1264
+ throw new Error(`MCP server already exists in app config: ${options.name}`);
1265
+ }
1266
+ config.mcpServers[options.name] = serverConfig;
1267
+ const content = file.formatJSON(config);
1268
+ await file.atomicWrite(validatedPath, content, { createBackup: true });
1269
+ results.push(`App config (${path.getDisplayPath(configPaths.app)})`);
1270
+ }
1271
+ return {
1272
+ content: [
1273
+ {
1274
+ type: 'text',
1275
+ text: `Successfully added MCP server: ${options.name}\nAdded to: ${results.join(', ')}`,
1276
+ },
1277
+ ],
1278
+ };
1279
+ }
1280
+ async function handleMCPRemove(args) {
1281
+ const schema = z.object({
1282
+ name: z.string(),
1283
+ target: z.enum(['cli', 'app', 'both']).optional(),
1284
+ });
1285
+ const { name, target = 'both' } = schema.parse(args);
1286
+ const configPaths = getMCPConfigPaths(target);
1287
+ const results = [];
1288
+ let found = false;
1289
+ // Remove from CLI config
1290
+ if (configPaths.cli) {
1291
+ try {
1292
+ const validatedPath = path.validatePath(configPaths.cli);
1293
+ const content = await file.safeReadFile(validatedPath);
1294
+ const config = file.safeJSONParse(content, validatedPath);
1295
+ if (config.mcpServers && config.mcpServers[name]) {
1296
+ delete config.mcpServers[name];
1297
+ const newContent = file.formatJSON(config);
1298
+ await file.atomicWrite(validatedPath, newContent, { createBackup: true });
1299
+ results.push(`CLI config (${path.getDisplayPath(configPaths.cli)})`);
1300
+ found = true;
1301
+ }
1302
+ }
1303
+ catch {
1304
+ // Continue to app config
1305
+ }
1306
+ }
1307
+ // Remove from app config
1308
+ if (configPaths.app) {
1309
+ try {
1310
+ const validatedPath = path.validatePath(configPaths.app);
1311
+ const content = await file.safeReadFile(validatedPath);
1312
+ const config = file.safeJSONParse(content, validatedPath);
1313
+ if (config.mcpServers && config.mcpServers[name]) {
1314
+ delete config.mcpServers[name];
1315
+ const newContent = file.formatJSON(config);
1316
+ await file.atomicWrite(validatedPath, newContent, { createBackup: true });
1317
+ results.push(`App config (${path.getDisplayPath(configPaths.app)})`);
1318
+ found = true;
1319
+ }
1320
+ }
1321
+ catch {
1322
+ // Server not found
1323
+ }
1324
+ }
1325
+ if (!found) {
1326
+ throw new Error(`MCP server not found: ${name}`);
1327
+ }
1328
+ return {
1329
+ content: [
1330
+ {
1331
+ type: 'text',
1332
+ text: `Successfully removed MCP server: ${name}\nRemoved from: ${results.join(', ')}`,
1333
+ },
1334
+ ],
1335
+ };
1336
+ }
1337
+ async function handleMCPUpdate(args) {
1338
+ const schema = z.object({
1339
+ name: z.string(),
1340
+ command: z.string().optional(),
1341
+ args: z.array(z.string()).optional(),
1342
+ url: z.string().optional(),
1343
+ env: z.record(z.string(), z.string()).optional(),
1344
+ headers: z.record(z.string(), z.string()).optional(),
1345
+ description: z.string().optional(),
1346
+ alwaysAllow: z.boolean().optional(),
1347
+ timeout: z.number().optional(),
1348
+ target: z.enum(['cli', 'app', 'both']).optional(),
1349
+ });
1350
+ const options = schema.parse(args);
1351
+ const configPaths = getMCPConfigPaths(options.target || 'both');
1352
+ const results = [];
1353
+ let found = false;
1354
+ // Update CLI config
1355
+ if (configPaths.cli) {
1356
+ try {
1357
+ const validatedPath = path.validatePath(configPaths.cli);
1358
+ const content = await file.safeReadFile(validatedPath);
1359
+ const config = file.safeJSONParse(content, validatedPath);
1360
+ if (config.mcpServers && config.mcpServers[options.name]) {
1361
+ const serverConfig = config.mcpServers[options.name];
1362
+ if (options.command !== undefined)
1363
+ serverConfig.command = options.command;
1364
+ if (options.args !== undefined)
1365
+ serverConfig.args = options.args;
1366
+ if (options.url !== undefined)
1367
+ serverConfig.url = options.url;
1368
+ if (options.env !== undefined)
1369
+ serverConfig.env = options.env;
1370
+ if (options.headers !== undefined)
1371
+ serverConfig.headers = options.headers;
1372
+ if (options.description !== undefined)
1373
+ serverConfig.description = options.description;
1374
+ if (options.alwaysAllow !== undefined)
1375
+ serverConfig.alwaysAllow = options.alwaysAllow;
1376
+ if (options.timeout !== undefined)
1377
+ serverConfig.timeout = options.timeout;
1378
+ const newContent = file.formatJSON(config);
1379
+ await file.atomicWrite(validatedPath, newContent, { createBackup: true });
1380
+ results.push(`CLI config (${path.getDisplayPath(configPaths.cli)})`);
1381
+ found = true;
1382
+ }
1383
+ }
1384
+ catch {
1385
+ // Continue to app config
1386
+ }
1387
+ }
1388
+ // Update app config
1389
+ if (configPaths.app) {
1390
+ try {
1391
+ const validatedPath = path.validatePath(configPaths.app);
1392
+ const content = await file.safeReadFile(validatedPath);
1393
+ const config = file.safeJSONParse(content, validatedPath);
1394
+ if (config.mcpServers && config.mcpServers[options.name]) {
1395
+ const serverConfig = config.mcpServers[options.name];
1396
+ if (options.command !== undefined)
1397
+ serverConfig.command = options.command;
1398
+ if (options.args !== undefined)
1399
+ serverConfig.args = options.args;
1400
+ if (options.url !== undefined)
1401
+ serverConfig.url = options.url;
1402
+ if (options.env !== undefined)
1403
+ serverConfig.env = options.env;
1404
+ if (options.headers !== undefined)
1405
+ serverConfig.headers = options.headers;
1406
+ if (options.description !== undefined)
1407
+ serverConfig.description = options.description;
1408
+ if (options.alwaysAllow !== undefined)
1409
+ serverConfig.alwaysAllow = options.alwaysAllow;
1410
+ if (options.timeout !== undefined)
1411
+ serverConfig.timeout = options.timeout;
1412
+ const newContent = file.formatJSON(config);
1413
+ await file.atomicWrite(validatedPath, newContent, { createBackup: true });
1414
+ results.push(`App config (${path.getDisplayPath(configPaths.app)})`);
1415
+ found = true;
1416
+ }
1417
+ }
1418
+ catch {
1419
+ // Server not found
1420
+ }
1421
+ }
1422
+ if (!found) {
1423
+ throw new Error(`MCP server not found: ${options.name}`);
1424
+ }
1425
+ return {
1426
+ content: [
1427
+ {
1428
+ type: 'text',
1429
+ text: `Successfully updated MCP server: ${options.name}\nUpdated in: ${results.join(', ')}`,
1430
+ },
1431
+ ],
1432
+ };
1433
+ }
1434
+ async function handleMCPInstall(args) {
1435
+ const schema = z.object({
1436
+ source: z.enum(['npm', 'github', 'local', 'url']).optional(),
1437
+ package: z.string(),
1438
+ name: z.string().optional(),
1439
+ version: z.string().optional(),
1440
+ env: z.record(z.string(), z.string()).optional(),
1441
+ description: z.string().optional(),
1442
+ args: z.array(z.string()).optional(),
1443
+ installDir: z.string().optional(),
1444
+ force: z.boolean().optional(),
1445
+ target: z.enum(['cli', 'app', 'both']).optional(),
1446
+ });
1447
+ const options = schema.parse(args);
1448
+ const target = options.target || 'both';
1449
+ // Auto-detect source if not specified
1450
+ let source = options.source;
1451
+ if (!source) {
1452
+ if (options.package.startsWith('http') || options.package.startsWith('git+')) {
1453
+ source = 'github';
1454
+ }
1455
+ else if (options.package.startsWith('/') || options.package.startsWith('./') || options.package.startsWith('../')) {
1456
+ source = 'local';
1457
+ }
1458
+ else if (options.package.includes('/')) {
1459
+ // Could be npm scoped package or GitHub - default to npm for @scope/name pattern
1460
+ source = options.package.startsWith('@') ? 'npm' : 'github';
1461
+ }
1462
+ else {
1463
+ source = 'npm';
1464
+ }
1465
+ }
1466
+ // Determine install directory
1467
+ const installDir = options.installDir || join(path.getHomeDir(), '.claude', 'mcp-servers');
1468
+ // Ensure install directory exists
1469
+ const { mkdir } = await import('node:fs/promises');
1470
+ await mkdir(installDir, { recursive: true });
1471
+ let serverName = options.name;
1472
+ let serverConfig = {};
1473
+ let installPath;
1474
+ switch (source) {
1475
+ case 'npm': {
1476
+ // Install from npm
1477
+ const packageName = options.package;
1478
+ const version = options.version || 'latest';
1479
+ serverName = serverName || packageName.replace(/^@[^/]+\//, '').replace(/^mcp-server-/, '').replace(/^server-/, '');
1480
+ installPath = join(installDir, 'node_modules', packageName);
1481
+ // Check if already installed
1482
+ if (!options.force && path.pathExists(join(installPath, 'package.json'))) {
1483
+ // Already installed, just configure it
1484
+ }
1485
+ else {
1486
+ // Install using bun
1487
+ const { execSync } = await import('node:child_process');
1488
+ execSync(`bun install ${packageName}@${version}`, {
1489
+ cwd: installDir,
1490
+ stdio: 'inherit',
1491
+ });
1492
+ }
1493
+ // Read package.json to find entry point
1494
+ const packageJsonPath = join(installPath, 'package.json');
1495
+ const packageJsonContent = await file.safeReadFile(packageJsonPath);
1496
+ const packageJson = JSON.parse(packageJsonContent);
1497
+ // Determine command and args
1498
+ const entryPoint = packageJson.bin || packageJson.main || 'index.js';
1499
+ if (typeof entryPoint === 'string') {
1500
+ serverConfig.command = 'node';
1501
+ serverConfig.args = [join(installPath, entryPoint), ...(options.args || [])];
1502
+ }
1503
+ else if (typeof entryPoint === 'object') {
1504
+ const binName = Object.keys(entryPoint)[0];
1505
+ serverConfig.command = 'node';
1506
+ serverConfig.args = [join(installPath, entryPoint[binName]), ...(options.args || [])];
1507
+ }
1508
+ serverConfig.description = options.description || packageJson.description;
1509
+ break;
1510
+ }
1511
+ case 'github': {
1512
+ // Clone from GitHub
1513
+ const repo = options.package.replace(/\.git$/, '');
1514
+ const repoName = repo.split('/').pop() || 'mcp-server';
1515
+ serverName = serverName || repoName.replace(/^mcp-/, '').replace(/^server-/, '');
1516
+ installPath = join(installDir, repoName);
1517
+ if (options.force || !path.pathExists(join(installPath, '.git'))) {
1518
+ const { execSync } = await import('node:child_process');
1519
+ execSync(`git clone ${options.package} "${installPath}"`, {
1520
+ stdio: 'inherit',
1521
+ });
1522
+ }
1523
+ // Read package.json to find entry point
1524
+ const packageJsonPath = join(installPath, 'package.json');
1525
+ const packageJsonContent = await file.safeReadFile(packageJsonPath);
1526
+ const packageJson = JSON.parse(packageJsonContent);
1527
+ // Determine command and args
1528
+ const entryPoint = packageJson.main || 'dist/index.js';
1529
+ const distPath = join(installPath, entryPoint);
1530
+ serverConfig.command = 'node';
1531
+ serverConfig.args = [distPath, ...(options.args || [])];
1532
+ serverConfig.description = options.description || packageJson.description;
1533
+ break;
1534
+ }
1535
+ case 'local': {
1536
+ // Link local package
1537
+ installPath = resolve(options.package);
1538
+ if (!path.pathExists(installPath)) {
1539
+ throw new Error(`Local path does not exist: ${installPath}`);
1540
+ }
1541
+ const packageJsonPath = join(installPath, 'package.json');
1542
+ const packageJsonContent = await file.safeReadFile(packageJsonPath);
1543
+ const packageJson = JSON.parse(packageJsonContent);
1544
+ serverName = serverName || packageJson.name?.replace(/^@[^/]+\//, '').replace(/^mcp-server-/, '').replace(/^server-/, '') || 'local-mcp';
1545
+ const entryPoint = packageJson.bin || packageJson.main || 'dist/index.js';
1546
+ const entryPath = typeof entryPoint === 'string' ? join(installPath, entryPoint) : installPath;
1547
+ serverConfig.command = 'node';
1548
+ serverConfig.args = [entryPath, ...(options.args || [])];
1549
+ serverConfig.description = options.description || packageJson.description;
1550
+ break;
1551
+ }
1552
+ case 'url': {
1553
+ // Download from URL and install
1554
+ throw new Error('URL source not yet implemented');
1555
+ }
1556
+ default: {
1557
+ throw new Error(`Unknown source type: ${source}`);
1558
+ }
1559
+ }
1560
+ // Add environment variables
1561
+ if (options.env) {
1562
+ serverConfig.env = options.env;
1563
+ }
1564
+ // Add to config files based on target
1565
+ const configPaths = getMCPConfigPaths(target);
1566
+ const results = [];
1567
+ // Add to CLI config
1568
+ if (configPaths.cli) {
1569
+ const validatedConfigPath = path.validatePath(configPaths.cli);
1570
+ let config;
1571
+ try {
1572
+ const content = await file.safeReadFile(validatedConfigPath);
1573
+ config = file.safeJSONParse(content, validatedConfigPath);
1574
+ }
1575
+ catch {
1576
+ config = { mcpServers: {} };
1577
+ }
1578
+ if (!config.mcpServers) {
1579
+ config.mcpServers = {};
1580
+ }
1581
+ config.mcpServers[serverName] = serverConfig;
1582
+ const content = file.formatJSON(config);
1583
+ await file.atomicWrite(validatedConfigPath, content, { createBackup: true });
1584
+ results.push(`CLI config (${path.getDisplayPath(configPaths.cli)})`);
1585
+ }
1586
+ // Add to app config
1587
+ if (configPaths.app) {
1588
+ const validatedConfigPath = path.validatePath(configPaths.app);
1589
+ let config;
1590
+ try {
1591
+ const content = await file.safeReadFile(validatedConfigPath);
1592
+ config = file.safeJSONParse(content, validatedConfigPath);
1593
+ }
1594
+ catch {
1595
+ config = { mcpServers: {} };
1596
+ }
1597
+ if (!config.mcpServers) {
1598
+ config.mcpServers = {};
1599
+ }
1600
+ config.mcpServers[serverName] = serverConfig;
1601
+ const content = file.formatJSON(config);
1602
+ await file.atomicWrite(validatedConfigPath, content, { createBackup: true });
1603
+ results.push(`App config (${path.getDisplayPath(configPaths.app)})`);
1604
+ }
1605
+ return {
1606
+ content: [
1607
+ {
1608
+ type: 'text',
1609
+ text: JSON.stringify({
1610
+ success: true,
1611
+ message: `Successfully installed MCP server: ${serverName}`,
1612
+ addedTo: results,
1613
+ server: {
1614
+ name: serverName,
1615
+ source,
1616
+ package: options.package,
1617
+ installPath,
1618
+ config: serverConfig,
1619
+ },
1620
+ }, null, 2),
1621
+ },
1622
+ ],
1623
+ };
1624
+ }
1625
+ async function handleMCPSync(args) {
1626
+ const schema = z.object({
1627
+ source: z.enum(['cli', 'app']).optional(),
1628
+ dryRun: z.boolean().optional(),
1629
+ });
1630
+ const { source = 'app', dryRun = false } = schema.parse(args);
1631
+ const sourcePath = source === 'cli' ? path.getClaudeCliConfigPath() : path.getConfigPaths().appConfig;
1632
+ const targetPath = source === 'cli' ? path.getConfigPaths().appConfig : path.getClaudeCliConfigPath();
1633
+ const sourceLabel = source === 'cli' ? 'CLI' : 'App';
1634
+ const targetLabel = source === 'cli' ? 'App' : 'CLI';
1635
+ // Read source config
1636
+ let sourceConfig;
1637
+ try {
1638
+ const validatedSourcePath = path.validatePath(sourcePath);
1639
+ const content = await file.safeReadFile(validatedSourcePath);
1640
+ sourceConfig = file.safeJSONParse(content, validatedSourcePath);
1641
+ }
1642
+ catch {
1643
+ return {
1644
+ content: [
1645
+ {
1646
+ type: 'text',
1647
+ text: `Source ${sourceLabel} config not found or empty`,
1648
+ },
1649
+ ],
1650
+ };
1651
+ }
1652
+ const sourceServers = sourceConfig.mcpServers || {};
1653
+ // Read target config
1654
+ let targetConfig;
1655
+ try {
1656
+ const validatedTargetPath = path.validatePath(targetPath);
1657
+ const content = await file.safeReadFile(validatedTargetPath);
1658
+ targetConfig = file.safeJSONParse(content, validatedTargetPath);
1659
+ }
1660
+ catch {
1661
+ targetConfig = { mcpServers: {} };
1662
+ }
1663
+ if (!targetConfig.mcpServers) {
1664
+ targetConfig.mcpServers = {};
1665
+ }
1666
+ // Calculate what needs to be synced
1667
+ const sourceServerNames = new Set(Object.keys(sourceServers));
1668
+ const targetServerNames = new Set(Object.keys(targetConfig.mcpServers));
1669
+ const toAdd = [...sourceServerNames].filter(name => !targetServerNames.has(name));
1670
+ const toUpdate = [...sourceServerNames].filter(name => targetServerNames.has(name) &&
1671
+ JSON.stringify(sourceServers[name]) !== JSON.stringify(targetConfig.mcpServers[name]));
1672
+ const toRemove = [...targetServerNames].filter(name => !sourceServerNames.has(name));
1673
+ const summary = {
1674
+ source: sourceLabel,
1675
+ target: targetLabel,
1676
+ toAdd,
1677
+ toUpdate,
1678
+ toRemove,
1679
+ totalChanges: toAdd.length + toUpdate.length + toRemove.length,
1680
+ };
1681
+ if (dryRun) {
1682
+ return {
1683
+ content: [
1684
+ {
1685
+ type: 'text',
1686
+ text: JSON.stringify({
1687
+ mode: 'dry-run',
1688
+ ...summary,
1689
+ message: `Would sync ${summary.totalChanges} changes from ${sourceLabel} to ${targetLabel}`,
1690
+ }, null, 2),
1691
+ },
1692
+ ],
1693
+ };
1694
+ }
1695
+ // Apply changes
1696
+ const results = [];
1697
+ // Add new servers
1698
+ for (const name of toAdd) {
1699
+ targetConfig.mcpServers[name] = sourceServers[name];
1700
+ results.push(`Added: ${name}`);
1701
+ }
1702
+ // Update existing servers
1703
+ for (const name of toUpdate) {
1704
+ targetConfig.mcpServers[name] = sourceServers[name];
1705
+ results.push(`Updated: ${name}`);
1706
+ }
1707
+ // Remove servers not in source
1708
+ for (const name of toRemove) {
1709
+ delete targetConfig.mcpServers[name];
1710
+ results.push(`Removed: ${name}`);
1711
+ }
1712
+ // Write to target config
1713
+ const validatedTargetPath = path.validatePath(targetPath);
1714
+ const content = file.formatJSON(targetConfig);
1715
+ await file.atomicWrite(validatedTargetPath, content, { createBackup: true });
1716
+ return {
1717
+ content: [
1718
+ {
1719
+ type: 'text',
1720
+ text: JSON.stringify({
1721
+ mode: 'sync',
1722
+ ...summary,
1723
+ changes: results,
1724
+ message: `Synced ${summary.totalChanges} changes from ${sourceLabel} to ${targetLabel}`,
1725
+ }, null, 2),
1726
+ },
1727
+ ],
1728
+ };
1729
+ }
1730
+ // ============================================================================
1731
+ // Server Startup
1732
+ // ============================================================================
1733
+ async function main() {
1734
+ const transport = new StdioServerTransport();
1735
+ await server.connect(transport);
1736
+ console.error('Claude Code Config MCP Server running');
1737
+ console.error('Version: 1.0.0');
1738
+ console.error('Runtime: Bun');
1739
+ }
1740
+ main().catch((error) => {
1741
+ console.error('Fatal error:', error);
1742
+ process.exit(1);
1743
+ });
1744
+ //# sourceMappingURL=index.js.map