codeep 1.3.41 → 2.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/README.md +208 -0
- package/dist/acp/commands.js +770 -7
- package/dist/acp/protocol.d.ts +11 -2
- package/dist/acp/server.js +179 -11
- package/dist/acp/session.d.ts +3 -0
- package/dist/acp/session.js +5 -0
- package/dist/api/index.js +39 -6
- package/dist/config/index.d.ts +13 -0
- package/dist/config/index.js +46 -1
- package/dist/config/providers.js +76 -1
- package/dist/renderer/App.d.ts +12 -0
- package/dist/renderer/App.js +96 -4
- package/dist/renderer/agentExecution.js +5 -0
- package/dist/renderer/commands.js +348 -2
- package/dist/renderer/components/Login.d.ts +1 -0
- package/dist/renderer/components/Login.js +24 -9
- package/dist/renderer/handlers.d.ts +11 -1
- package/dist/renderer/handlers.js +30 -0
- package/dist/renderer/main.js +73 -0
- package/dist/utils/agent.d.ts +17 -0
- package/dist/utils/agent.js +91 -7
- package/dist/utils/agentChat.d.ts +10 -2
- package/dist/utils/agentChat.js +48 -9
- package/dist/utils/agentStream.js +6 -2
- package/dist/utils/checkpoints.d.ts +93 -0
- package/dist/utils/checkpoints.js +205 -0
- package/dist/utils/context.d.ts +24 -0
- package/dist/utils/context.js +57 -0
- package/dist/utils/customCommands.d.ts +62 -0
- package/dist/utils/customCommands.js +201 -0
- package/dist/utils/hooks.d.ts +97 -0
- package/dist/utils/hooks.js +223 -0
- package/dist/utils/mcpClient.d.ts +229 -0
- package/dist/utils/mcpClient.js +497 -0
- package/dist/utils/mcpConfig.d.ts +55 -0
- package/dist/utils/mcpConfig.js +177 -0
- package/dist/utils/mcpMarketplace.d.ts +49 -0
- package/dist/utils/mcpMarketplace.js +175 -0
- package/dist/utils/mcpRegistry.d.ts +129 -0
- package/dist/utils/mcpRegistry.js +427 -0
- package/dist/utils/mcpSamplingBridge.d.ts +32 -0
- package/dist/utils/mcpSamplingBridge.js +88 -0
- package/dist/utils/mcpStreamableHttp.d.ts +65 -0
- package/dist/utils/mcpStreamableHttp.js +207 -0
- package/dist/utils/openrouterPrefs.d.ts +36 -0
- package/dist/utils/openrouterPrefs.js +83 -0
- package/dist/utils/skillBundles.d.ts +84 -0
- package/dist/utils/skillBundles.js +257 -0
- package/dist/utils/skillBundlesCloud.d.ts +66 -0
- package/dist/utils/skillBundlesCloud.js +196 -0
- package/dist/utils/tokenTracker.d.ts +14 -2
- package/dist/utils/tokenTracker.js +59 -45
- package/dist/utils/toolExecution.d.ts +17 -1
- package/dist/utils/toolExecution.js +184 -6
- package/dist/utils/tools.d.ts +22 -6
- package/dist/utils/tools.js +83 -8
- package/package.json +3 -2
- package/bin/codeep-macos-arm64 +0 -0
- 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;
|