@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/.claude/CLAUDE.md +3 -0
- package/README.md +237 -0
- package/bun.lock +206 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1744 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +197 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +51 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/errors.d.ts +63 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +156 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/file.d.ts +32 -0
- package/dist/utils/file.d.ts.map +1 -0
- package/dist/utils/file.js +146 -0
- package/dist/utils/file.js.map +1 -0
- package/dist/utils/path.d.ts +59 -0
- package/dist/utils/path.d.ts.map +1 -0
- package/dist/utils/path.js +146 -0
- package/dist/utils/path.js.map +1 -0
- package/lmdb.db +0 -0
- package/lmdb.db-lock +0 -0
- package/package.json +43 -0
- package/src/index.js +2171 -0
- package/src/index.ts +1981 -0
- package/src/types.js +53 -0
- package/src/types.ts +237 -0
- package/src/utils/errors.js +231 -0
- package/src/utils/errors.ts +210 -0
- package/src/utils/file.js +251 -0
- package/src/utils/file.ts +174 -0
- package/src/utils/path.js +169 -0
- package/src/utils/path.ts +173 -0
- package/test/test.js +136 -0
- package/test/test.ts +79 -0
- package/test/write-test.js +153 -0
- package/test/write-test.ts +102 -0
- package/tsconfig.json +21 -0
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
|