codeep 1.3.42 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +208 -0
  2. package/dist/acp/commands.js +770 -7
  3. package/dist/acp/protocol.d.ts +11 -2
  4. package/dist/acp/server.js +179 -11
  5. package/dist/acp/session.d.ts +3 -0
  6. package/dist/acp/session.js +5 -0
  7. package/dist/api/index.js +39 -6
  8. package/dist/config/index.d.ts +13 -0
  9. package/dist/config/index.js +45 -0
  10. package/dist/config/providers.js +76 -1
  11. package/dist/renderer/App.d.ts +12 -0
  12. package/dist/renderer/App.js +109 -4
  13. package/dist/renderer/agentExecution.js +5 -0
  14. package/dist/renderer/commands.js +638 -2
  15. package/dist/renderer/components/Help.js +28 -0
  16. package/dist/renderer/components/Login.d.ts +1 -0
  17. package/dist/renderer/components/Login.js +24 -9
  18. package/dist/renderer/handlers.d.ts +11 -1
  19. package/dist/renderer/handlers.js +30 -0
  20. package/dist/renderer/main.js +73 -0
  21. package/dist/utils/agent.d.ts +17 -0
  22. package/dist/utils/agent.js +91 -7
  23. package/dist/utils/agentChat.d.ts +10 -2
  24. package/dist/utils/agentChat.js +48 -9
  25. package/dist/utils/agentStream.js +6 -2
  26. package/dist/utils/checkpoints.d.ts +93 -0
  27. package/dist/utils/checkpoints.js +205 -0
  28. package/dist/utils/context.d.ts +24 -0
  29. package/dist/utils/context.js +57 -0
  30. package/dist/utils/customCommands.d.ts +62 -0
  31. package/dist/utils/customCommands.js +201 -0
  32. package/dist/utils/hooks.d.ts +97 -0
  33. package/dist/utils/hooks.js +223 -0
  34. package/dist/utils/mcpClient.d.ts +229 -0
  35. package/dist/utils/mcpClient.js +497 -0
  36. package/dist/utils/mcpConfig.d.ts +55 -0
  37. package/dist/utils/mcpConfig.js +177 -0
  38. package/dist/utils/mcpMarketplace.d.ts +49 -0
  39. package/dist/utils/mcpMarketplace.js +175 -0
  40. package/dist/utils/mcpRegistry.d.ts +129 -0
  41. package/dist/utils/mcpRegistry.js +427 -0
  42. package/dist/utils/mcpSamplingBridge.d.ts +32 -0
  43. package/dist/utils/mcpSamplingBridge.js +88 -0
  44. package/dist/utils/mcpStreamableHttp.d.ts +65 -0
  45. package/dist/utils/mcpStreamableHttp.js +207 -0
  46. package/dist/utils/openrouterPrefs.d.ts +36 -0
  47. package/dist/utils/openrouterPrefs.js +83 -0
  48. package/dist/utils/skillBundles.d.ts +84 -0
  49. package/dist/utils/skillBundles.js +257 -0
  50. package/dist/utils/skillBundlesCloud.d.ts +69 -0
  51. package/dist/utils/skillBundlesCloud.js +202 -0
  52. package/dist/utils/tokenTracker.d.ts +14 -2
  53. package/dist/utils/tokenTracker.js +59 -41
  54. package/dist/utils/toolExecution.d.ts +17 -1
  55. package/dist/utils/toolExecution.js +184 -6
  56. package/dist/utils/tools.d.ts +22 -6
  57. package/dist/utils/tools.js +83 -8
  58. package/package.json +3 -2
  59. package/bin/codeep-macos-arm64 +0 -0
  60. package/bin/codeep-macos-x64 +0 -0
@@ -0,0 +1,497 @@
1
+ /**
2
+ * Minimal MCP (Model Context Protocol) stdio client.
3
+ *
4
+ * Each `McpClient` instance owns one child process running an MCP server
5
+ * (e.g. `npx @modelcontextprotocol/server-filesystem /some/path`). It speaks
6
+ * JSON-RPC 2.0 over stdio per the MCP spec, performs the
7
+ * initialize → tools/list handshake, and exposes a `callTool` method that
8
+ * agent tool dispatch routes through.
9
+ *
10
+ * Scope of this MVP:
11
+ * - initialize + tools/list discovery
12
+ * - tools/call forwarding
13
+ * - stop() kills the process and rejects in-flight requests
14
+ *
15
+ * NOT covered yet (defer to a future iteration):
16
+ * - resources / prompts / sampling MCP primitives
17
+ * - capability negotiation beyond "we want tools"
18
+ * - server-initiated requests (we ignore them)
19
+ * - reconnect on crash (process exit is fatal for that client)
20
+ */
21
+ import { spawn } from 'child_process';
22
+ import { StreamableHttpClient } from './mcpStreamableHttp.js';
23
+ /** JSON-RPC request id sequence. Module-level so ids stay unique across clients
24
+ * (helps when scanning logs from multiple servers in the same session). */
25
+ let nextRequestId = 1;
26
+ export class McpClient {
27
+ server;
28
+ clientOpts;
29
+ /** Stdio transport state. Null when running over HTTP (or before start). */
30
+ child = null;
31
+ /** HTTP transport state. Null when running over stdio. */
32
+ http = null;
33
+ pending = new Map();
34
+ buffer = '';
35
+ stopped = false;
36
+ toolsCache = null;
37
+ /** True when this client is configured for the Streamable HTTP transport. */
38
+ get isHttp() {
39
+ return Boolean(this.server.url);
40
+ }
41
+ /**
42
+ * Rolling-window record of recent crash times (ms epoch). Used by the
43
+ * auto-reconnect logic: too many crashes in a short window → give up
44
+ * instead of spinning indefinitely on a broken server.
45
+ */
46
+ crashTimestamps = [];
47
+ /** Reconnect tuning — generous defaults, configurable via env if needed. */
48
+ MAX_RESTARTS = 3;
49
+ RESTART_WINDOW_MS = 60_000;
50
+ /** Has the agent loop been notified that this server is fully gone? */
51
+ gaveUp = false;
52
+ /**
53
+ * Optional callback fired after a successful auto-restart. The registry
54
+ * uses this to drop its tools cache so the next `listTools()` re-queries
55
+ * (the server may expose a different tool set after restart).
56
+ */
57
+ onRestart;
58
+ /**
59
+ * Optional callback fired when the client gives up after exceeding the
60
+ * restart budget. The registry uses this to surface a visible "MCP
61
+ * server died" error in /mcp.
62
+ */
63
+ onGaveUp;
64
+ /**
65
+ * Optional callback fired when the server sends a `notifications/*`
66
+ * indicating its catalog changed (tools, resources, prompts). The
67
+ * registry forwards this up so the agent loop can re-fetch on the next
68
+ * iteration.
69
+ */
70
+ onCatalogChanged;
71
+ /**
72
+ * @param server MCP server config (command, args, env, name).
73
+ * @param opts Optional client metadata.
74
+ * - `workspaceRoot` exposed to the server as a root via
75
+ * the `roots` capability so filesystem-style servers
76
+ * can scope their reads.
77
+ * - `onSamplingRequest` makes the client advertise the
78
+ * `sampling` capability and routes server-initiated
79
+ * `sampling/createMessage` to the host LLM.
80
+ */
81
+ constructor(server, clientOpts = {}) {
82
+ this.server = server;
83
+ this.clientOpts = clientOpts;
84
+ }
85
+ /** Open the transport and perform the MCP handshake. */
86
+ async start(opts = {}) {
87
+ if (this.child || this.http)
88
+ throw new Error(`MCP server "${this.server.name}" already started`);
89
+ const initTimeoutMs = opts.initTimeoutMs ?? 15_000;
90
+ if (this.isHttp) {
91
+ // HTTP transport: no child process. Frames arrive via onFrame
92
+ // callback wired into the StreamableHttpClient.
93
+ this.http = new StreamableHttpClient({
94
+ url: this.server.url,
95
+ headers: this.server.headers,
96
+ onFrame: (msg) => this.dispatchFrame(msg),
97
+ onError: (err) => {
98
+ // Surface transport-level failures the same way a stdio crash
99
+ // does — reject pending requests and let the registry decide
100
+ // whether to retry.
101
+ for (const [, req] of this.pending)
102
+ req.reject(err);
103
+ this.pending.clear();
104
+ },
105
+ });
106
+ }
107
+ else {
108
+ if (!this.server.command) {
109
+ throw new Error(`MCP server "${this.server.name}" has neither command nor url`);
110
+ }
111
+ this.child = spawn(this.server.command, this.server.args ?? [], {
112
+ env: { ...process.env, ...this.server.env },
113
+ stdio: ['pipe', 'pipe', 'pipe'],
114
+ });
115
+ this.attachChildHandlers();
116
+ }
117
+ // Initialize handshake per MCP spec. protocolVersion is required.
118
+ // We advertise `roots` so filesystem-shaped MCP servers can scope to
119
+ // the user's workspace. Sampling is advertised when the client passed
120
+ // a sampling callback into the constructor — otherwise we omit it so
121
+ // the server doesn't try (and fail) to use a capability we can't
122
+ // back. `listChanged` is true everywhere so the server knows we
123
+ // listen for catalog updates.
124
+ const capabilities = {
125
+ roots: { listChanged: true },
126
+ };
127
+ if (this.clientOpts.onSamplingRequest) {
128
+ capabilities.sampling = {};
129
+ }
130
+ await this.request('initialize', {
131
+ protocolVersion: '2024-11-05',
132
+ capabilities,
133
+ clientInfo: { name: 'codeep', version: '2.0.0' },
134
+ }, { timeoutMs: initTimeoutMs });
135
+ // Spec: after `initialize` reply, send `notifications/initialized` so the
136
+ // server knows we're done with the boot sequence.
137
+ this.notify('notifications/initialized', {});
138
+ }
139
+ /** Discover tools the server exposes. Cached on first call. */
140
+ async listTools() {
141
+ if (this.toolsCache)
142
+ return this.toolsCache;
143
+ if (!this.child)
144
+ throw new Error(`MCP server "${this.server.name}" not started`);
145
+ const result = await this.request('tools/list', {});
146
+ this.toolsCache = result.tools ?? [];
147
+ return this.toolsCache;
148
+ }
149
+ /**
150
+ * Discover resources the server exposes. Not all servers implement
151
+ * resources/list — those return a `-32601 Method not found`, which we
152
+ * surface as an empty array (callers can treat absence and emptiness
153
+ * the same way).
154
+ */
155
+ async listResources() {
156
+ if (!this.child)
157
+ throw new Error(`MCP server "${this.server.name}" not started`);
158
+ try {
159
+ const result = await this.request('resources/list', {});
160
+ return result.resources ?? [];
161
+ }
162
+ catch (err) {
163
+ // -32601 (method not found) on resources/list means the server
164
+ // doesn't expose any. Other errors propagate.
165
+ if (/Method not found/.test(err.message))
166
+ return [];
167
+ throw err;
168
+ }
169
+ }
170
+ /** Read one resource by URI. */
171
+ async readResource(uri) {
172
+ if (!this.child)
173
+ throw new Error(`MCP server "${this.server.name}" not started`);
174
+ const result = await this.request('resources/read', { uri });
175
+ return result.contents ?? [];
176
+ }
177
+ /** Discover prompt templates the server exposes (optional capability). */
178
+ async listPrompts() {
179
+ if (!this.child)
180
+ throw new Error(`MCP server "${this.server.name}" not started`);
181
+ try {
182
+ const result = await this.request('prompts/list', {});
183
+ return result.prompts ?? [];
184
+ }
185
+ catch (err) {
186
+ if (/Method not found/.test(err.message))
187
+ return [];
188
+ throw err;
189
+ }
190
+ }
191
+ /** Materialise a prompt template into its message sequence. */
192
+ async getPrompt(name, args = {}) {
193
+ if (!this.child)
194
+ throw new Error(`MCP server "${this.server.name}" not started`);
195
+ const result = await this.request('prompts/get', { name, arguments: args });
196
+ return { description: result.description, messages: result.messages ?? [] };
197
+ }
198
+ /** Invoke a tool on this server. */
199
+ async callTool(name, args, opts = {}) {
200
+ if (!this.child)
201
+ throw new Error(`MCP server "${this.server.name}" not started`);
202
+ const result = await this.request('tools/call', { name, arguments: args }, opts);
203
+ // Per spec the tool result is a content array. We flatten text parts —
204
+ // images and embedded resources would need more work, deferred.
205
+ const text = (result.content ?? [])
206
+ .filter(c => c.type === 'text' && typeof c.text === 'string')
207
+ .map(c => c.text)
208
+ .join('\n');
209
+ if (result.isError)
210
+ throw new Error(text || `MCP tool ${name} returned an error`);
211
+ return text;
212
+ }
213
+ /**
214
+ * Attempt to spawn a fresh child process after a crash. Tries up to
215
+ * MAX_RESTARTS times within RESTART_WINDOW_MS, then gives up. After a
216
+ * successful restart, `toolsCache` is cleared so the next listTools()
217
+ * re-queries — the server may legitimately expose different tools after
218
+ * a code reload.
219
+ */
220
+ async attemptRestart() {
221
+ const now = Date.now();
222
+ // Trim crash entries outside the window.
223
+ this.crashTimestamps = this.crashTimestamps.filter(t => now - t < this.RESTART_WINDOW_MS);
224
+ this.crashTimestamps.push(now);
225
+ if (this.crashTimestamps.length > this.MAX_RESTARTS) {
226
+ this.gaveUp = true;
227
+ const reason = `crashed ${this.crashTimestamps.length} times in ${Math.round(this.RESTART_WINDOW_MS / 1000)}s`;
228
+ try {
229
+ this.onGaveUp?.(reason);
230
+ }
231
+ catch { /* never let a callback throw kill us */ }
232
+ return;
233
+ }
234
+ // Small backoff so we don't hot-loop if the server crashes on startup.
235
+ const attempt = this.crashTimestamps.length;
236
+ const backoffMs = Math.min(500 * Math.pow(2, attempt - 1), 5000);
237
+ await new Promise(r => setTimeout(r, backoffMs));
238
+ if (this.stopped)
239
+ return;
240
+ try {
241
+ // Need to allow `start()` to proceed even though `child` was already
242
+ // set previously — clear the toolsCache to force a re-list and let
243
+ // start() reset state.
244
+ this.toolsCache = null;
245
+ // Direct private re-spawn: can't call start() because it throws when
246
+ // child was previously set. Inline the spawn + handshake here.
247
+ // Restart only handles stdio — HTTP transport doesn't crash in the
248
+ // same sense (no child to die); transient HTTP errors reject pending
249
+ // requests and the next user prompt will retry naturally.
250
+ if (this.isHttp)
251
+ return;
252
+ if (!this.server.command)
253
+ return;
254
+ this.child = spawn(this.server.command, this.server.args ?? [], {
255
+ env: { ...process.env, ...this.server.env },
256
+ stdio: ['pipe', 'pipe', 'pipe'],
257
+ });
258
+ this.attachChildHandlers();
259
+ await this.request('initialize', {
260
+ protocolVersion: '2024-11-05',
261
+ capabilities: {},
262
+ clientInfo: { name: 'codeep', version: '1.4.0' },
263
+ }, { timeoutMs: 15_000 });
264
+ this.notify('notifications/initialized', {});
265
+ try {
266
+ this.onRestart?.();
267
+ }
268
+ catch { /* swallow */ }
269
+ }
270
+ catch {
271
+ // Restart attempt itself failed — let the next 'exit' (if any) try
272
+ // again, or just sit idle if the spawn never reached 'exit'.
273
+ }
274
+ }
275
+ /** Wire up data/exit/error listeners on the current child. Used by start() and attemptRestart(). */
276
+ attachChildHandlers() {
277
+ if (!this.child)
278
+ return;
279
+ this.child.on('exit', (code) => {
280
+ const err = new Error(`MCP server "${this.server.name}" exited (code ${code})`);
281
+ for (const [, req] of this.pending)
282
+ req.reject(err);
283
+ this.pending.clear();
284
+ this.child = null;
285
+ if (!this.stopped && !this.gaveUp) {
286
+ void this.attemptRestart();
287
+ }
288
+ });
289
+ this.child.on('error', (err) => {
290
+ for (const [, req] of this.pending)
291
+ req.reject(err);
292
+ this.pending.clear();
293
+ });
294
+ this.child.stdout?.setEncoding('utf-8');
295
+ this.child.stdout?.on('data', (chunk) => this.handleStdout(chunk));
296
+ }
297
+ /** Tear down the transport (stdio child or HTTP stream) and reject pending requests. */
298
+ async stop() {
299
+ if (this.stopped)
300
+ return;
301
+ this.stopped = true;
302
+ const err = new Error(`MCP server "${this.server.name}" stopped`);
303
+ for (const [, req] of this.pending)
304
+ req.reject(err);
305
+ this.pending.clear();
306
+ if (this.child) {
307
+ try {
308
+ this.child.kill('SIGTERM');
309
+ }
310
+ catch { /* ignore */ }
311
+ // Give the server a moment to exit cleanly before forcing it. We
312
+ // don't await the exit — callers shouldn't block on cleanup.
313
+ setTimeout(() => {
314
+ if (this.child) {
315
+ try {
316
+ this.child.kill('SIGKILL');
317
+ }
318
+ catch { /* ignore */ }
319
+ }
320
+ }, 1000);
321
+ this.child = null;
322
+ }
323
+ if (this.http) {
324
+ await this.http.stop().catch(() => { });
325
+ this.http = null;
326
+ }
327
+ }
328
+ // ── JSON-RPC plumbing ───────────────────────────────────────────────────────
329
+ handleStdout(chunk) {
330
+ this.buffer += chunk;
331
+ // MCP uses newline-delimited JSON-RPC. Iterate every complete line.
332
+ for (;;) {
333
+ const newline = this.buffer.indexOf('\n');
334
+ if (newline < 0)
335
+ break;
336
+ const line = this.buffer.slice(0, newline).trim();
337
+ this.buffer = this.buffer.slice(newline + 1);
338
+ if (!line)
339
+ continue;
340
+ try {
341
+ const msg = JSON.parse(line);
342
+ this.dispatchFrame(msg);
343
+ }
344
+ catch {
345
+ // Malformed line — skip rather than crash the agent.
346
+ }
347
+ }
348
+ }
349
+ /**
350
+ * Handle a request from the MCP server (server-initiated JSON-RPC).
351
+ * Currently handled methods:
352
+ * - `roots/list` — return the workspace folder if provided
353
+ * - `sampling/createMessage` — delegate to the host LLM callback if
354
+ * one was wired into the constructor; otherwise -32601 (so a
355
+ * server that asks without us advertising the capability gets a
356
+ * clear "no" instead of a hang).
357
+ *
358
+ * Anything else replies with `-32601 Method not found` per JSON-RPC spec.
359
+ */
360
+ handleServerRequest(id, method, params) {
361
+ if (method === 'roots/list') {
362
+ const roots = this.clientOpts.workspaceRoot
363
+ ? [{
364
+ uri: `file://${this.clientOpts.workspaceRoot}`,
365
+ name: this.clientOpts.workspaceRoot.split('/').pop() || 'workspace',
366
+ }]
367
+ : [];
368
+ this.writeResponse({ id, result: { roots } });
369
+ return;
370
+ }
371
+ if (method === 'sampling/createMessage' && this.clientOpts.onSamplingRequest) {
372
+ // Async — handled out-of-band so server doesn't see a sync error.
373
+ void (async () => {
374
+ try {
375
+ const result = await this.clientOpts.onSamplingRequest(params);
376
+ this.writeResponse({ id, result });
377
+ }
378
+ catch (err) {
379
+ this.writeResponse({ id, error: { code: -32603, message: `sampling failed: ${err.message}` } });
380
+ }
381
+ })();
382
+ return;
383
+ }
384
+ this.writeResponse({ id, error: { code: -32601, message: `Method not found: ${method}` } });
385
+ }
386
+ /** Serialise and send a JSON-RPC response over whichever transport is active. */
387
+ writeResponse(payload) {
388
+ const frame = { jsonrpc: '2.0', ...payload };
389
+ this.writeFrame(frame);
390
+ }
391
+ /**
392
+ * Single send path used by request/notify/writeResponse. Stdio just
393
+ * pipes the serialised frame + newline. HTTP POSTs the JSON body; the
394
+ * response (or any later SSE event) re-enters via `dispatchFrame`.
395
+ * Errors on the HTTP path reject pending request promises so the
396
+ * agent doesn't hang waiting on a frame that'll never come.
397
+ */
398
+ writeFrame(frame) {
399
+ const json = JSON.stringify(frame);
400
+ if (this.http) {
401
+ void this.http.send(frame).catch((err) => {
402
+ // If the POST itself fails (network, 5xx) we need to fail
403
+ // anything we were waiting on so the caller sees the error
404
+ // instead of timing out.
405
+ const e = err;
406
+ // Best-effort: if this frame had an id and is still pending,
407
+ // reject just that one. Otherwise reject everything.
408
+ const f = frame;
409
+ if (typeof f.id === 'number' && this.pending.has(f.id)) {
410
+ this.pending.get(f.id).reject(e);
411
+ this.pending.delete(f.id);
412
+ }
413
+ else {
414
+ for (const [, req] of this.pending)
415
+ req.reject(e);
416
+ this.pending.clear();
417
+ }
418
+ });
419
+ return;
420
+ }
421
+ if (this.child?.stdin) {
422
+ this.child.stdin.write(json + '\n');
423
+ }
424
+ }
425
+ /**
426
+ * Common entry point for every incoming JSON-RPC frame, regardless of
427
+ * transport. Stdio's `handleStdout` parses lines and forwards each
428
+ * here; the HTTP transport calls this directly from its `onFrame`.
429
+ */
430
+ dispatchFrame(msg) {
431
+ // Server-initiated request — has method AND id.
432
+ if (typeof msg.method === 'string' && typeof msg.id === 'number') {
433
+ this.handleServerRequest(msg.id, msg.method, msg.params);
434
+ return;
435
+ }
436
+ // Server notification — method, no id. Track catalog-change ones.
437
+ if (typeof msg.method === 'string' && msg.id === undefined) {
438
+ const method = msg.method;
439
+ if (method === 'notifications/tools/list_changed') {
440
+ this.toolsCache = null;
441
+ try {
442
+ this.onCatalogChanged?.('tools');
443
+ }
444
+ catch { /* swallow */ }
445
+ }
446
+ else if (method === 'notifications/resources/list_changed') {
447
+ try {
448
+ this.onCatalogChanged?.('resources');
449
+ }
450
+ catch { /* swallow */ }
451
+ }
452
+ else if (method === 'notifications/prompts/list_changed') {
453
+ try {
454
+ this.onCatalogChanged?.('prompts');
455
+ }
456
+ catch { /* swallow */ }
457
+ }
458
+ return;
459
+ }
460
+ // Otherwise: response to one of our requests.
461
+ if (typeof msg.id !== 'number')
462
+ return;
463
+ const req = this.pending.get(msg.id);
464
+ if (!req)
465
+ return;
466
+ this.pending.delete(msg.id);
467
+ const err = msg.error;
468
+ if (err)
469
+ req.reject(new Error(`${req.method}: ${err.message} (code ${err.code})`));
470
+ else
471
+ req.resolve(msg.result);
472
+ }
473
+ request(method, params, opts = {}) {
474
+ if (!this.child && !this.http)
475
+ return Promise.reject(new Error(`MCP server "${this.server.name}" transport closed`));
476
+ const id = nextRequestId++;
477
+ const timeoutMs = opts.timeoutMs ?? 60_000;
478
+ return new Promise((resolve, reject) => {
479
+ const timer = setTimeout(() => {
480
+ if (this.pending.delete(id)) {
481
+ reject(new Error(`MCP ${method} timed out after ${timeoutMs}ms`));
482
+ }
483
+ }, timeoutMs);
484
+ this.pending.set(id, {
485
+ method,
486
+ resolve: (v) => { clearTimeout(timer); resolve(v); },
487
+ reject: (e) => { clearTimeout(timer); reject(e); },
488
+ });
489
+ this.writeFrame({ jsonrpc: '2.0', id, method, params });
490
+ });
491
+ }
492
+ notify(method, params) {
493
+ if (!this.child && !this.http)
494
+ return;
495
+ this.writeFrame({ jsonrpc: '2.0', method, params });
496
+ }
497
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * On-disk MCP server config — the `mcpServers` array that ACP clients
3
+ * usually pass over the wire, but loaded straight from JSON so direct CLI
4
+ * users (and our VS Code extension) don't have to roll their own config UI.
5
+ *
6
+ * Lookup precedence:
7
+ * 1. `<workspace>/.codeep/mcp_servers.json` (project — committed with repo)
8
+ * 2. `~/.codeep/mcp_servers.json` (global — user's machine)
9
+ * Project entries shadow global entries with the same server name.
10
+ *
11
+ * File format mirrors what Claude Code accepts so existing user configs can
12
+ * be reused verbatim:
13
+ *
14
+ * {
15
+ * "mcpServers": {
16
+ * "fs": {
17
+ * "command": "npx",
18
+ * "args": ["@modelcontextprotocol/server-filesystem", "/some/path"],
19
+ * "env": { "READ_ONLY": "1" }
20
+ * },
21
+ * "gh": { ... }
22
+ * }
23
+ * }
24
+ *
25
+ * A flat array form (`{"mcpServers": [{...}, ...]}`) is also accepted because
26
+ * that's the shape ACP passes over JSON-RPC.
27
+ */
28
+ import type { McpServer } from '../acp/protocol.js';
29
+ export interface McpConfigFile {
30
+ /** Either the named-map form (Claude Code style) or a flat array (ACP style). */
31
+ mcpServers?: Record<string, Omit<McpServer, 'name'>> | McpServer[];
32
+ }
33
+ /**
34
+ * Load MCP server definitions for a workspace. Project entries shadow
35
+ * global entries with the same server name. Workspace-less calls
36
+ * (TUI without project) return only the global config.
37
+ */
38
+ export declare function loadMcpServerConfig(workspaceRoot?: string): McpServer[];
39
+ /**
40
+ * Merge two server lists: ACP-provided + on-disk. ACP wins on collisions
41
+ * — the client knows its own config, so a Zed-passed server overrides a
42
+ * project file entry of the same name (and we never have to teach Zed to
43
+ * "skip" file-based ones).
44
+ */
45
+ export declare function mergeMcpServers(fromConfig: McpServer[], fromAcp: McpServer[] | undefined): McpServer[];
46
+ /**
47
+ * Add or replace a server entry in the project config file. Used by the
48
+ * interactive `/mcp add` command. Project file is created if missing.
49
+ */
50
+ export declare function addProjectMcpServer(workspaceRoot: string, server: McpServer): void;
51
+ /**
52
+ * Remove a server entry from the project config file. Returns true if a
53
+ * server with that name was actually removed.
54
+ */
55
+ export declare function removeProjectMcpServer(workspaceRoot: string, name: string): boolean;