@nexrall/code-core 1.4.66 → 1.4.68

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 (91) hide show
  1. package/dist/agent/agentRegistry.d.ts +27 -0
  2. package/dist/agent/agentRegistry.d.ts.map +1 -1
  3. package/dist/agent/agentRegistry.js +45 -0
  4. package/dist/agent/agentTypes.d.ts +6 -2
  5. package/dist/agent/agentTypes.d.ts.map +1 -1
  6. package/dist/agent/agentTypes.js +13 -4
  7. package/dist/agent/askOnce.d.ts +12 -0
  8. package/dist/agent/askOnce.d.ts.map +1 -0
  9. package/dist/agent/askOnce.js +41 -0
  10. package/dist/agent/compaction.d.ts +244 -0
  11. package/dist/agent/compaction.d.ts.map +1 -0
  12. package/dist/agent/compaction.js +976 -0
  13. package/dist/agent/fileLocks.d.ts +34 -0
  14. package/dist/agent/fileLocks.d.ts.map +1 -0
  15. package/dist/agent/fileLocks.js +114 -0
  16. package/dist/agent/hooks.d.ts +331 -0
  17. package/dist/agent/hooks.d.ts.map +1 -0
  18. package/dist/agent/hooks.js +1239 -0
  19. package/dist/agent/iterationPolicy.d.ts +121 -0
  20. package/dist/agent/iterationPolicy.d.ts.map +1 -0
  21. package/dist/agent/iterationPolicy.js +297 -0
  22. package/dist/agent/lifecycleHost.d.ts +55 -0
  23. package/dist/agent/lifecycleHost.d.ts.map +1 -0
  24. package/dist/agent/lifecycleHost.js +294 -0
  25. package/dist/agent/loop.d.ts +39 -491
  26. package/dist/agent/loop.d.ts.map +1 -1
  27. package/dist/agent/loop.js +538 -3069
  28. package/dist/agent/planMode.d.ts.map +1 -1
  29. package/dist/agent/planMode.js +1 -0
  30. package/dist/agent/sharedTasks.d.ts +7 -0
  31. package/dist/agent/sharedTasks.d.ts.map +1 -1
  32. package/dist/agent/sharedTasks.js +16 -0
  33. package/dist/agent/subAgentBudget.d.ts +65 -0
  34. package/dist/agent/subAgentBudget.d.ts.map +1 -0
  35. package/dist/agent/subAgentBudget.js +269 -0
  36. package/dist/agent/subTask.d.ts +15 -0
  37. package/dist/agent/subTask.d.ts.map +1 -0
  38. package/dist/agent/subTask.js +730 -0
  39. package/dist/agent/subTaskSupport.d.ts +167 -0
  40. package/dist/agent/subTaskSupport.d.ts.map +1 -0
  41. package/dist/agent/subTaskSupport.js +425 -0
  42. package/dist/agent/toolDescriptions.d.ts +3 -0
  43. package/dist/agent/toolDescriptions.d.ts.map +1 -0
  44. package/dist/agent/toolDescriptions.js +116 -0
  45. package/dist/api/client.d.ts +41 -0
  46. package/dist/api/client.d.ts.map +1 -1
  47. package/dist/api/client.js +173 -1
  48. package/dist/index.d.ts +5 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +5 -0
  51. package/dist/mcp/client.d.ts +104 -0
  52. package/dist/mcp/client.d.ts.map +1 -1
  53. package/dist/mcp/client.js +136 -2
  54. package/dist/mcp/httpClient.d.ts +17 -1
  55. package/dist/mcp/httpClient.d.ts.map +1 -1
  56. package/dist/mcp/httpClient.js +120 -19
  57. package/dist/mcp/manager.d.ts +77 -2
  58. package/dist/mcp/manager.d.ts.map +1 -1
  59. package/dist/mcp/manager.js +275 -9
  60. package/dist/mcp/server.d.ts +39 -0
  61. package/dist/mcp/server.d.ts.map +1 -0
  62. package/dist/mcp/server.js +189 -0
  63. package/dist/mcp/sseClient.d.ts +7 -1
  64. package/dist/mcp/sseClient.d.ts.map +1 -1
  65. package/dist/mcp/sseClient.js +45 -1
  66. package/dist/mcp/stats.d.ts +41 -0
  67. package/dist/mcp/stats.d.ts.map +1 -0
  68. package/dist/mcp/stats.js +108 -0
  69. package/dist/permissions/destructive.d.ts +2 -0
  70. package/dist/permissions/destructive.d.ts.map +1 -1
  71. package/dist/permissions/destructive.js +6 -2
  72. package/dist/permissions/destructiveTokens.d.ts +5 -0
  73. package/dist/permissions/destructiveTokens.d.ts.map +1 -1
  74. package/dist/permissions/destructiveTokens.js +9 -3
  75. package/dist/permissions/modePolicy.d.ts.map +1 -1
  76. package/dist/permissions/modePolicy.js +5 -2
  77. package/dist/permissions/rules.d.ts +4 -1
  78. package/dist/permissions/rules.d.ts.map +1 -1
  79. package/dist/permissions/rules.js +29 -0
  80. package/dist/plugins/data.d.ts +23 -0
  81. package/dist/plugins/data.d.ts.map +1 -0
  82. package/dist/plugins/data.js +141 -0
  83. package/dist/plugins/index.d.ts +14 -2
  84. package/dist/plugins/index.d.ts.map +1 -1
  85. package/dist/plugins/index.js +45 -4
  86. package/dist/plugins/installer.d.ts +83 -0
  87. package/dist/plugins/installer.d.ts.map +1 -1
  88. package/dist/plugins/installer.js +207 -1
  89. package/dist/types.d.ts +45 -1
  90. package/dist/types.d.ts.map +1 -1
  91. package/package.json +1 -1
@@ -41,17 +41,40 @@ const client_1 = require("./client");
41
41
  const httpClient_1 = require("./httpClient");
42
42
  const sseClient_1 = require("./sseClient");
43
43
  const index_1 = require("../plugins/index");
44
+ const stats_1 = require("./stats");
44
45
  // ─── McpManager ───────────────────────────────────────────────────────────────
45
46
  class McpManager {
46
47
  constructor() {
47
48
  this._clients = new Map();
48
49
  // Map: prefixed tool name → server name
49
50
  this._toolMap = new Map();
51
+ // Prompt metadata per server, fetched on first use (the `/` menu, `/mcp prompts`
52
+ // and positional→named argument mapping all read it). Cleared whenever the
53
+ // client is dropped, so a reconnect never serves a dead session's list.
54
+ this._promptCache = new Map();
50
55
  // Config + last connection error per server (for status UI, incl. failures)
51
56
  this._configs = new Map();
52
57
  this._errors = new Map();
53
58
  // Servers that returned 401 → need OAuth. Value = WWW-Authenticate challenge header.
54
59
  this._needsAuth = new Map();
60
+ // Names the user has switched off. Kept per-manager (not just in the file) so a
61
+ // toggle takes effect in THIS session, not at the next start.
62
+ this._disabled = new Set();
63
+ /** Per-tool call counts and timings — the "is it slow or broken?" answer (spec §42). */
64
+ this.stats = new stats_1.ToolStats();
65
+ // The human-facing elicitation handler (set by the host that owns a UI). Null means
66
+ // "nobody can be asked" — transports then auto-decline, so a server never hangs.
67
+ this._elicitationHandler = null;
68
+ }
69
+ /**
70
+ * Route every server's elicitation (`elicitation/create`) through this handler —
71
+ * including servers that connect later, which is why it is also re-applied in
72
+ * connectAll/enable. Pass null to restore auto-decline.
73
+ */
74
+ setElicitationHandler(handler) {
75
+ this._elicitationHandler = handler;
76
+ for (const client of this._clients.values())
77
+ client.setElicitationHandler(handler);
55
78
  }
56
79
  /** Pick the transport for a server config: stdio (command), Streamable HTTP, or legacy SSE. */
57
80
  static makeClient(name, cfg) {
@@ -73,24 +96,38 @@ class McpManager {
73
96
  path.join(os.homedir(), '.nexrall', 'mcp.json'),
74
97
  path.join(workDir, '.nexrall', 'mcp.json'),
75
98
  ];
99
+ // A list, so the two scopes UNION rather than override: switching a server off in
100
+ // the project must add to whatever the user file already silences.
101
+ const disabled = new Set();
76
102
  for (const loc of locations) {
77
103
  try {
78
104
  const raw = fs.readFileSync(loc, 'utf-8');
79
105
  const cfg = JSON.parse(raw);
80
106
  Object.assign(merged.mcpServers, cfg.mcpServers ?? {});
107
+ for (const name of cfg.disabledServers ?? [])
108
+ disabled.add(name);
81
109
  }
82
110
  catch {
83
111
  // File doesn't exist or invalid JSON — skip
84
112
  }
85
113
  }
114
+ if (disabled.size)
115
+ merged.disabledServers = [...disabled].sort();
86
116
  return merged;
87
117
  }
88
118
  /** Connect to all configured servers. Errors per-server are logged, not thrown. */
89
119
  async connectAll(config) {
120
+ this._disabled = new Set(config.disabledServers ?? []);
90
121
  const entries = Object.entries(config.mcpServers);
91
122
  await Promise.all(entries.map(async ([name, serverCfg]) => {
92
123
  this._configs.set(name, serverCfg);
124
+ // Switched off by the user: remember it (so /mcp can list it) and do not spawn.
125
+ if (this._disabled.has(name)) {
126
+ this._errors.delete(name);
127
+ return;
128
+ }
93
129
  const client = McpManager.makeClient(name, serverCfg);
130
+ client.setElicitationHandler(this._elicitationHandler);
94
131
  try {
95
132
  await client.connect();
96
133
  this._clients.set(name, client);
@@ -161,7 +198,14 @@ class McpManager {
161
198
  // Enforce a timeout — a hung MCP server would otherwise block the agent loop forever.
162
199
  const MCP_TOOL_TIMEOUT_MS = 60000;
163
200
  const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`MCP tool "${prefixedName}" timed out after ${MCP_TOOL_TIMEOUT_MS / 1000}s`)), MCP_TOOL_TIMEOUT_MS));
164
- const racers = [client.callTool(toolName, input), timeoutPromise];
201
+ // Timing covers the whole path the user waits on (server round-trip AND the
202
+ // race against the 60s timeout / abort), which is what "how long did that take"
203
+ // means from the outside. The tool name recorded is the PREFIXED one, so the
204
+ // stats of two servers with a same-named tool never merge.
205
+ const startedAt = Date.now();
206
+ this.stats.begin();
207
+ const timed = (p) => p.then((v) => { this.stats.ok(prefixedName, Date.now() - startedAt); return v; }, (e) => { this.stats.fail(prefixedName, Date.now() - startedAt, e.message || String(e)); throw e; });
208
+ const racers = [timed(client.callTool(toolName, input)), timed(timeoutPromise)];
165
209
  if (abortSignal) {
166
210
  let pollId;
167
211
  const abortPromise = new Promise((_, reject) => {
@@ -172,7 +216,7 @@ class McpManager {
172
216
  }
173
217
  }, 100);
174
218
  });
175
- racers.push(abortPromise);
219
+ racers.push(timed(abortPromise));
176
220
  // Clearing the poll on whichever branch wins avoids leaving a live interval
177
221
  // (and the closure it holds) running for the rest of the process lifetime —
178
222
  // the same leak-prevention pattern client.ts uses for its own abort poll.
@@ -180,6 +224,97 @@ class McpManager {
180
224
  }
181
225
  return Promise.race(racers);
182
226
  }
227
+ /** Resources across connected, enabled servers. One server failing never hides the others. */
228
+ async listResources(server) {
229
+ const out = [];
230
+ for (const [name, client] of this._clients) {
231
+ if (server && name !== server)
232
+ continue;
233
+ for (const r of await client.listResources())
234
+ out.push({ ...r, server: name });
235
+ }
236
+ return out;
237
+ }
238
+ /** Read one resource. Throws a clear error for an unknown server; bounded by the tool timeout. */
239
+ async readResource(server, uri) {
240
+ const client = this._clients.get(server);
241
+ if (!client)
242
+ throw new Error(`No connected MCP server named "${server}". Connected: ${[...this._clients.keys()].join(', ') || '(none)'}`);
243
+ let timer;
244
+ const timeout = new Promise((_, rej) => { timer = setTimeout(() => rej(new Error(`Reading ${uri} timed out after 60s`)), 60000); });
245
+ try {
246
+ return await Promise.race([client.readResource(uri), timeout]);
247
+ }
248
+ finally {
249
+ if (timer)
250
+ clearTimeout(timer);
251
+ }
252
+ }
253
+ /**
254
+ * Prompt metadata for one connected server. Fetched once per connection and
255
+ * cached — including an empty list (server has no prompts capability), so the
256
+ * `/` menu does not re-ask a server on every keystroke; reconnecting clears
257
+ * the entry. Returns [] for a server that is not connected.
258
+ */
259
+ async promptsFor(server) {
260
+ const cached = this._promptCache.get(server);
261
+ if (cached)
262
+ return cached;
263
+ const client = this._clients.get(server);
264
+ if (!client)
265
+ return [];
266
+ const list = await client.listPrompts();
267
+ this._promptCache.set(server, list);
268
+ return list;
269
+ }
270
+ /** Prompts across connected, enabled servers, tagged with the server they came from. */
271
+ async listPrompts(server) {
272
+ const out = [];
273
+ for (const name of this._clients.keys()) {
274
+ if (server && name !== server)
275
+ continue;
276
+ for (const p of await this.promptsFor(name))
277
+ out.push({ ...p, server: name });
278
+ }
279
+ return out;
280
+ }
281
+ /**
282
+ * Expand one prompt to text. Positional values map, in order, onto the
283
+ * arguments the server declared in prompts/list — the same positional form
284
+ * Claude Code accepts (`/mcp__server__prompt first second`). Validation runs
285
+ * here, before the request, so mistakes read as sentences instead of raw
286
+ * JSON-RPC errors; the request itself is bounded by the same 60s timeout the
287
+ * resource reader uses.
288
+ */
289
+ async getPrompt(server, name, values = []) {
290
+ const client = this._clients.get(server);
291
+ if (!client)
292
+ throw new Error(`No connected MCP server named "${server}". Connected: ${[...this._clients.keys()].join(', ') || '(none)'}`);
293
+ const list = await this.promptsFor(server);
294
+ const meta = list.find((p) => p.name === name);
295
+ if (!meta) {
296
+ throw new Error(`No MCP prompt named "${name}" on server "${server}"${list.length ? ` (it offers: ${list.map((p) => p.name).join(', ')})` : ''}.`);
297
+ }
298
+ const declared = meta.arguments ?? [];
299
+ if (values.length > declared.length) {
300
+ throw new Error(`MCP prompt "${server}:${name}" takes ${declared.length} argument(s)${declared.length ? ` (${declared.map((a) => a.name).join(', ')})` : ''} — got ${values.length}.`);
301
+ }
302
+ const args = {};
303
+ declared.forEach((a, i) => { const v = values[i]; if (v !== undefined && v !== '')
304
+ args[a.name] = v; });
305
+ const missing = declared.filter((a) => a.required && args[a.name] === undefined);
306
+ if (missing.length)
307
+ throw new Error(`MCP prompt "${server}:${name}" is missing required argument(s): ${missing.map((a) => a.name).join(', ')}.`);
308
+ let timer;
309
+ const timeout = new Promise((_, rej) => { timer = setTimeout(() => rej(new Error(`MCP prompt ${server}:${name} timed out after 60s`)), 60000); });
310
+ try {
311
+ return await Promise.race([client.getPrompt(name, args), timeout]);
312
+ }
313
+ finally {
314
+ if (timer)
315
+ clearTimeout(timer);
316
+ }
317
+ }
183
318
  /** True if this tool name belongs to an MCP server. */
184
319
  isMcpTool(name) {
185
320
  return this._toolMap.has(name);
@@ -195,23 +330,103 @@ class McpManager {
195
330
  return cfg.type;
196
331
  return cfg?.url && !cfg?.command ? 'http' : 'stdio';
197
332
  }
198
- /** Returns per-server status for UI display, including failed / auth-required connections. */
333
+ /** Returns per-server status for UI display, including failed / auth-required / disabled. */
199
334
  getStatus() {
200
335
  const out = [];
201
336
  for (const [name, client] of this._clients) {
202
- out.push({ name, transport: McpManager.transportOf(this._configs.get(name)), tools: client.tools, connected: true });
337
+ out.push({ name, transport: McpManager.transportOf(this._configs.get(name)), tools: client.tools, connected: true, disabled: false });
203
338
  }
204
- for (const [name, error] of this._errors) {
339
+ for (const name of this._configs.keys()) {
205
340
  if (this._clients.has(name))
206
341
  continue;
207
- out.push({ name, transport: McpManager.transportOf(this._configs.get(name)), tools: [], connected: false, error, needsAuth: this._needsAuth.has(name) });
342
+ const disabled = this._disabled.has(name);
343
+ const error = this._errors.get(name);
344
+ // A disabled server that never connected has no error to report — say nothing
345
+ // rather than repeat "disconnected" as if it were a failure.
346
+ if (disabled && !error) {
347
+ out.push({ name, transport: McpManager.transportOf(this._configs.get(name)), tools: [], connected: false, disabled: true });
348
+ continue;
349
+ }
350
+ out.push({
351
+ name, transport: McpManager.transportOf(this._configs.get(name)), tools: [],
352
+ connected: false, disabled, error, needsAuth: this._needsAuth.has(name),
353
+ });
208
354
  }
209
- return out;
355
+ return out.sort((a, b) => a.name.localeCompare(b.name));
210
356
  }
211
357
  /** The WWW-Authenticate challenge a server returned on 401 (empty if none/unknown). */
212
358
  authChallenge(name) {
213
359
  return this._needsAuth.get(name) ?? '';
214
360
  }
361
+ /** True when the user has switched this server off for the session. */
362
+ isDisabled(name) {
363
+ return this._disabled.has(name);
364
+ }
365
+ /**
366
+ * Switch a server off in THIS session: drop its client and forget its tool map, so
367
+ * the next turn's tool list no longer offers it. The caller persists the choice
368
+ * (McpManager.setServerDisabled) — this method deliberately does not touch disk.
369
+ */
370
+ disable(name) {
371
+ const existed = this._configs.has(name);
372
+ this._disabled.add(name);
373
+ this._dropClient(name);
374
+ return existed;
375
+ }
376
+ /**
377
+ * Switch a server back on and connect it, without a restart. Resolves to an error
378
+ * message when the connection fails, so the caller can print it and leave the
379
+ * server listed as broken instead of silently reporting success.
380
+ */
381
+ async enable(name) {
382
+ this._disabled.delete(name);
383
+ const cfg = this._configs.get(name);
384
+ if (!cfg)
385
+ return `no server named "${name}" is configured`;
386
+ this._dropClient(name);
387
+ const client = McpManager.makeClient(name, cfg);
388
+ client.setElicitationHandler(this._elicitationHandler);
389
+ try {
390
+ await client.connect();
391
+ }
392
+ catch (err) {
393
+ const msg = err.message;
394
+ if (err instanceof httpClient_1.McpOAuthRequiredError)
395
+ this._needsAuth.set(name, err.wwwAuthenticate);
396
+ this._errors.set(name, msg);
397
+ return msg;
398
+ }
399
+ this._clients.set(name, client);
400
+ this._errors.delete(name);
401
+ this._needsAuth.delete(name);
402
+ for (const tool of client.tools)
403
+ this._toolMap.set(`${name}__${tool.name}`, name);
404
+ return null;
405
+ }
406
+ /** Disconnect one server for the session (its config stays; `enable` brings it back). */
407
+ disconnect(name) {
408
+ const had = this._clients.has(name) || this._errors.has(name);
409
+ this._dropClient(name);
410
+ this._errors.set(name, 'disconnected');
411
+ return had;
412
+ }
413
+ /** Forgets one server's client and tools, leaving its config and status alone. */
414
+ _dropClient(name) {
415
+ const client = this._clients.get(name);
416
+ if (client) {
417
+ try {
418
+ client.disconnect();
419
+ }
420
+ catch { /* best effort */ }
421
+ }
422
+ this._clients.delete(name);
423
+ for (const key of [...this._toolMap.keys()]) {
424
+ if (this._toolMap.get(key) === name)
425
+ this._toolMap.delete(key);
426
+ }
427
+ this._promptCache.delete(name);
428
+ this.stats.reset();
429
+ }
215
430
  // ─── Config file persistence (for the management UI) ──────────────────────────
216
431
  /** Path to an mcp.json for the given scope. */
217
432
  static configPath(workDir, scope) {
@@ -222,7 +437,11 @@ class McpManager {
222
437
  static readConfigFile(p) {
223
438
  try {
224
439
  const cfg = JSON.parse(fs.readFileSync(p, 'utf-8'));
225
- return { mcpServers: cfg.mcpServers ?? {} };
440
+ // `disabledServers` has to survive the read: this is the object that gets written
441
+ // BACK by addServer/setServerDisabled/removeServer, and dropping the list here
442
+ // silently un-disabled every server but the one being toggled (found by a test
443
+ // that switched two off and watched the first reconnect).
444
+ return { mcpServers: cfg.mcpServers ?? {}, disabledServers: cfg.disabledServers };
226
445
  }
227
446
  catch {
228
447
  return { mcpServers: {} };
@@ -239,17 +458,62 @@ class McpManager {
239
458
  cfg.mcpServers[name] = server;
240
459
  McpManager.writeConfigFile(p, cfg);
241
460
  }
461
+ /**
462
+ * Switch a server off/on in the config FILE, so the choice survives a restart.
463
+ * Written to the project scope even when the server came from elsewhere (a plugin
464
+ * or the user file), because the project is the only scope this session is sure it
465
+ * may edit — and `disabledServers` is a name list, so it works for all three.
466
+ * Returns the file that was written.
467
+ */
468
+ static setServerDisabled(workDir, name, disabled, scope = 'project') {
469
+ const p = McpManager.configPath(workDir, scope);
470
+ const cfg = McpManager.readConfigFile(p);
471
+ const current = new Set(cfg.disabledServers ?? []);
472
+ if (disabled)
473
+ current.add(name);
474
+ else
475
+ current.delete(name);
476
+ cfg.disabledServers = [...current].sort();
477
+ if (cfg.disabledServers.length === 0)
478
+ delete cfg.disabledServers;
479
+ McpManager.writeConfigFile(p, cfg);
480
+ return p;
481
+ }
482
+ /** Where a server's definition comes from, for messages that have to be honest about it. */
483
+ static serverScope(workDir, name) {
484
+ const project = McpManager.readConfigFile(McpManager.configPath(workDir, 'project'));
485
+ if (project.mcpServers[name])
486
+ return 'project';
487
+ const user = McpManager.readConfigFile(McpManager.configPath(workDir, 'user'));
488
+ if (user.mcpServers[name])
489
+ return 'user';
490
+ try {
491
+ if ((0, index_1.pluginMcpServers)(workDir)[name])
492
+ return 'plugin';
493
+ }
494
+ catch { /* plugin scan is best-effort */ }
495
+ return 'none';
496
+ }
242
497
  /** Remove a server from whichever scope(s) define it. Returns true if anything was removed. */
243
498
  static removeServer(workDir, name) {
244
499
  let removed = false;
245
500
  for (const scope of ['project', 'user']) {
246
501
  const p = McpManager.configPath(workDir, scope);
247
502
  const cfg = McpManager.readConfigFile(p);
503
+ let touched = false;
248
504
  if (cfg.mcpServers[name]) {
249
505
  delete cfg.mcpServers[name];
250
- McpManager.writeConfigFile(p, cfg);
506
+ touched = true;
251
507
  removed = true;
252
508
  }
509
+ if (cfg.disabledServers?.includes(name)) {
510
+ cfg.disabledServers = cfg.disabledServers.filter((n) => n !== name);
511
+ if (cfg.disabledServers.length === 0)
512
+ delete cfg.disabledServers;
513
+ touched = true;
514
+ }
515
+ if (touched)
516
+ McpManager.writeConfigFile(p, cfg);
253
517
  }
254
518
  return removed;
255
519
  }
@@ -261,6 +525,8 @@ class McpManager {
261
525
  this._configs.clear();
262
526
  this._errors.clear();
263
527
  this._needsAuth.clear();
528
+ this._disabled.clear();
529
+ this.stats.reset();
264
530
  }
265
531
  }
266
532
  exports.McpManager = McpManager;
@@ -0,0 +1,39 @@
1
+ import type { Readable, Writable } from 'stream';
2
+ import type { ToolResult } from '../types';
3
+ export interface McpServerTool {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: {
7
+ type: 'object';
8
+ properties?: Record<string, unknown>;
9
+ required?: string[];
10
+ };
11
+ run(input: Record<string, unknown>, ctx: {
12
+ workDir: string;
13
+ }): Promise<ToolResult>;
14
+ }
15
+ export interface ServeOptions {
16
+ /** Workspace root relative paths resolve against (the client's project). */
17
+ workDir: string;
18
+ /** Reported in `serverInfo` during initialize. */
19
+ version?: string;
20
+ /** Defaults to `readOnlyMcpTools()`. */
21
+ tools?: McpServerTool[];
22
+ /** Injectable for tests; default process.stdin/stdout. */
23
+ input?: Readable;
24
+ output?: Writable;
25
+ /** Diagnostics — stderr by convention. NEVER stdout. */
26
+ log?: (message: string) => void;
27
+ }
28
+ /**
29
+ * The read-only surface `nex mcp serve` offers. Kept small on purpose: every tool here is one
30
+ * an outside agent can call with no human in the loop, so nothing that writes, deletes or
31
+ * executes may join without a permission story first.
32
+ */
33
+ export declare function readOnlyMcpTools(): McpServerTool[];
34
+ /**
35
+ * Serve MCP on stdio until the client closes the pipe. Resolves (never rejects) at EOF — the
36
+ * standard way an MCP client says "stop" is to close stdin, and the exit code is then 0.
37
+ */
38
+ export declare function serveMcp(opts: ServeOptions): Promise<void>;
39
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAEjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAI3C,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACrC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;IACF,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CACpF;AAED,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,KAAK,CAAC,EAAE,aAAa,EAAE,CAAC;IACxB,0DAA0D;IAC1D,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,wDAAwD;IACxD,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AAaD;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,aAAa,EAAE,CA+BlD;AASD;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAuHhE"}
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ // MCP server — the other direction of `mcp/client.ts`.
3
+ //
4
+ // Point any MCP client at `nex mcp serve` and it can read this machine's files with the same
5
+ // tools the CLI uses. Deliberately READ-ONLY for now: the client is usually another agent with
6
+ // its own permission model, and a server that silently edits files has no one to ask — a write
7
+ // toolset only belongs here once there is a person (or a policy file) behind the approval.
8
+ //
9
+ // Framing is the same newline-delimited JSON-RPC 2.0 the client parses, so our own client is
10
+ // the natural test driver. Two hard rules, both enforced by tests:
11
+ // * stdout carries protocol frames and NOTHING else — one stray console.log corrupts the
12
+ // channel; every diagnostic goes to stderr;
13
+ // * a bad line, an unknown method or a throwing tool must never end the loop.
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.readOnlyMcpTools = readOnlyMcpTools;
16
+ exports.serveMcp = serveMcp;
17
+ const executor_1 = require("../tools/executor");
18
+ // ─── The toolset ──────────────────────────────────────────────────────────────
19
+ /** Wrap one executor tool. The executor is the single source of behaviour — same caps, same
20
+ * error messages, same path resolution as the CLI's own turns. */
21
+ const fromExecutor = (name, description, inputSchema) => ({
22
+ name,
23
+ description,
24
+ inputSchema,
25
+ run: (input, ctx) => (0, executor_1.executeTool)(name, input, undefined, undefined, ctx.workDir),
26
+ });
27
+ /**
28
+ * The read-only surface `nex mcp serve` offers. Kept small on purpose: every tool here is one
29
+ * an outside agent can call with no human in the loop, so nothing that writes, deletes or
30
+ * executes may join without a permission story first.
31
+ */
32
+ function readOnlyMcpTools() {
33
+ return [
34
+ fromExecutor('read_file', 'Read a text file. Returns numbered lines so later edits can cite them. Binary files are refused.', { type: 'object', properties: {
35
+ path: { type: 'string', description: 'File path, absolute or relative to the workspace root' },
36
+ offset: { type: 'number', description: '0-based line to start from (default 0)' },
37
+ limit: { type: 'number', description: 'How many lines to return' },
38
+ }, required: ['path'] }),
39
+ fromExecutor('list_directory', 'List the entries of a directory, each marked as file or directory.', { type: 'object', properties: {
40
+ path: { type: 'string', description: 'Directory path (default: the workspace root)' },
41
+ } }),
42
+ fromExecutor('glob', 'Find files by glob pattern, e.g. "**/*.ts" or "src/**/*.test.js".', { type: 'object', properties: {
43
+ pattern: { type: 'string', description: 'Glob pattern' },
44
+ path: { type: 'string', description: 'Directory to search from (default: the workspace root)' },
45
+ }, required: ['pattern'] }),
46
+ fromExecutor('search_files', 'Search file contents with a regular expression. Returns matching file paths by default; ask for context lines to get the matches themselves.', { type: 'object', properties: {
47
+ pattern: { type: 'string', description: 'Regular expression' },
48
+ path: { type: 'string', description: 'Directory to search (default: the workspace root)' },
49
+ type: { type: 'string', description: '"files" (paths only) or "content" (matching lines)' },
50
+ include: { type: 'string', description: 'Only files matching this glob, e.g. "*.ts"' },
51
+ case_insensitive: { type: 'boolean', description: 'Case-insensitive match' },
52
+ context_lines: { type: 'number', description: 'Lines of context around each match (implies type "content")' },
53
+ }, required: ['pattern'] }),
54
+ ];
55
+ }
56
+ // ─── Protocol ─────────────────────────────────────────────────────────────────
57
+ /** Versions whose framing we actually implement (they are all the same for our surface). */
58
+ const SUPPORTED_PROTOCOLS = ['2024-11-05', '2025-03-26', '2025-06-18'];
59
+ const DEFAULT_PROTOCOL = '2024-11-05';
60
+ const SERVER_NAME = 'nexrall';
61
+ /**
62
+ * Serve MCP on stdio until the client closes the pipe. Resolves (never rejects) at EOF — the
63
+ * standard way an MCP client says "stop" is to close stdin, and the exit code is then 0.
64
+ */
65
+ async function serveMcp(opts) {
66
+ const input = opts.input ?? process.stdin;
67
+ const output = opts.output ?? process.stdout;
68
+ const log = opts.log ?? ((m) => process.stderr.write(`[nexrall mcp] ${m}\n`));
69
+ const tools = opts.tools ?? readOnlyMcpTools();
70
+ const version = opts.version ?? '0.0.0';
71
+ const byName = new Map(tools.map((t) => [t.name, t]));
72
+ const send = (msg) => { output.write(JSON.stringify(msg) + '\n'); };
73
+ const reply = (id, result) => send({ jsonrpc: '2.0', id, result });
74
+ const replyError = (id, code, message) => send({ jsonrpc: '2.0', id, error: { code, message } });
75
+ async function handle(message) {
76
+ const id = message.id;
77
+ const isRequest = id !== undefined && id !== null;
78
+ const method = message.method;
79
+ const params = (message.params ?? {});
80
+ switch (method) {
81
+ case 'initialize': {
82
+ // Echo the client's version when we implement it, else our default — that is the
83
+ // spec's instruction and the number is inert for this surface either way.
84
+ const asked = typeof params.protocolVersion === 'string' ? params.protocolVersion : '';
85
+ reply(id, {
86
+ protocolVersion: SUPPORTED_PROTOCOLS.includes(asked) ? asked : DEFAULT_PROTOCOL,
87
+ capabilities: { tools: {} },
88
+ serverInfo: { name: SERVER_NAME, version },
89
+ instructions: `Read-only file access to ${opts.workDir}. ` +
90
+ `Tools: ${tools.map((t) => t.name).join(', ')}. Nothing here writes, deletes or runs commands.`,
91
+ });
92
+ log(`initialize from ${typeof params.clientInfo === 'object' && params.clientInfo ? JSON.stringify(params.clientInfo.name ?? 'client') : 'client'}`);
93
+ return;
94
+ }
95
+ case 'notifications/initialized':
96
+ case 'notifications/cancelled':
97
+ return; // notifications take no reply
98
+ case 'ping':
99
+ reply(id, {});
100
+ return;
101
+ case 'tools/list':
102
+ reply(id, { tools: tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })) });
103
+ return;
104
+ case 'tools/call': {
105
+ const name = typeof params.name === 'string' ? params.name : '';
106
+ const args = (params.arguments ?? {});
107
+ if (!name)
108
+ return replyError(id, -32602, 'Invalid params: name is required');
109
+ const tool = byName.get(name);
110
+ if (!tool) {
111
+ // A RESULT, not a protocol error: the calling model should read "not offered" as tool
112
+ // output and pick another route, not see the whole call fail.
113
+ reply(id, { content: [{ type: 'text', text: `Tool "${name}" is not offered by this server (read-only). Available: ${[...byName.keys()].join(', ')}.` }], isError: true });
114
+ return;
115
+ }
116
+ try {
117
+ const res = await tool.run(args, { workDir: opts.workDir });
118
+ if (res.error)
119
+ reply(id, { content: [{ type: 'text', text: res.error }], isError: true });
120
+ else
121
+ reply(id, { content: [{ type: 'text', text: res.output ?? '' }] });
122
+ }
123
+ catch (err) {
124
+ // A throwing tool is the tool's problem, not the session's — answer and keep serving.
125
+ reply(id, { content: [{ type: 'text', text: err instanceof Error ? err.message : String(err) }], isError: true });
126
+ }
127
+ return;
128
+ }
129
+ default:
130
+ if (isRequest)
131
+ replyError(id, -32601, `Method not found: ${method}`);
132
+ return;
133
+ }
134
+ }
135
+ input.setEncoding('utf8');
136
+ let buffer = '';
137
+ let queue = Promise.resolve();
138
+ const feed = (line) => {
139
+ // Chain instead of racing: responses stay in request order and writes never interleave.
140
+ queue = queue.then(async () => {
141
+ const trimmed = line.trim();
142
+ if (!trimmed)
143
+ return;
144
+ let message;
145
+ try {
146
+ message = JSON.parse(trimmed);
147
+ }
148
+ catch {
149
+ log(`ignoring a line that is not JSON (${trimmed.slice(0, 80)})`);
150
+ return;
151
+ }
152
+ if (!message || message.jsonrpc !== '2.0' || typeof message.method !== 'string') {
153
+ log('ignoring a message without jsonrpc 2.0 + method');
154
+ return;
155
+ }
156
+ try {
157
+ await handle(message);
158
+ }
159
+ catch (err) {
160
+ // handle() is written not to throw, so reaching here is a bug in the server — report
161
+ // it as a JSON-RPC error rather than dropping the connection.
162
+ if (message.id !== undefined)
163
+ replyError(message.id, -32603, err instanceof Error ? err.message : String(err));
164
+ }
165
+ });
166
+ };
167
+ await new Promise((resolve) => {
168
+ input.on('data', (chunk) => {
169
+ buffer += chunk.toString();
170
+ let nl;
171
+ while ((nl = buffer.indexOf('\n')) >= 0) {
172
+ feed(buffer.slice(0, nl));
173
+ buffer = buffer.slice(nl + 1);
174
+ }
175
+ });
176
+ const done = () => {
177
+ if (buffer.trim()) {
178
+ feed(buffer);
179
+ buffer = '';
180
+ }
181
+ resolve();
182
+ };
183
+ input.on('end', done);
184
+ input.on('close', done);
185
+ });
186
+ await queue; // let in-flight calls finish before the process exits
187
+ log('client closed stdin — server exiting');
188
+ }
189
+ //# sourceMappingURL=server.js.map
@@ -1,4 +1,4 @@
1
- import type { McpServerConfig, McpToolDefinition, McpTransport } from './client';
1
+ import type { McpServerConfig, McpToolDefinition, McpTransport, McpResource, McpPrompt, McpElicitationHandler } from './client';
2
2
  export declare class McpSseClient implements McpTransport {
3
3
  private readonly config;
4
4
  private _nextId;
@@ -9,10 +9,16 @@ export declare class McpSseClient implements McpTransport {
9
9
  private _closed;
10
10
  private readonly _pending;
11
11
  private _endpointWaiter;
12
+ private _elicitationHandler;
12
13
  readonly name: string;
13
14
  constructor(name: string, config: McpServerConfig);
14
15
  get tools(): McpToolDefinition[];
16
+ setElicitationHandler(handler: McpElicitationHandler | null): void;
15
17
  connect(): Promise<void>;
18
+ listResources(): Promise<McpResource[]>;
19
+ readResource(uri: string): Promise<string>;
20
+ listPrompts(): Promise<McpPrompt[]>;
21
+ getPrompt(name: string, args: Record<string, string>): Promise<string>;
16
22
  callTool(name: string, args: Record<string, unknown>): Promise<string>;
17
23
  disconnect(): void;
18
24
  /** Open the GET event stream and wait for the server's `endpoint` event. */
@@ -1 +1 @@
1
- {"version":3,"file":"sseClient.d.ts","sourceRoot":"","sources":["../../src/mcp/sseClient.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AA6BjF,qBAAa,YAAa,YAAW,YAAY;IAWrB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAVjD,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,OAAO,CAAM;IACrB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA0H;IACnJ,OAAO,CAAC,eAAe,CAAoE;IAC3F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM,EAAmB,MAAM,EAAE,eAAe;IAIlE,IAAI,KAAK,IAAI,iBAAiB,EAAE,CAE/B;IAEK,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAaxB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAc5E,UAAU,IAAI,IAAI;IAalB,4EAA4E;YAC9D,WAAW;IAoCzB,OAAO,CAAC,eAAe;IAYvB,oFAAoF;IACpF,OAAO,CAAC,KAAK;IAWb,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,cAAc;IAuCtB,OAAO,CAAC,IAAI;YAgBE,OAAO;IAIrB,6EAA6E;YAC/D,YAAY;CAwB3B"}
1
+ {"version":3,"file":"sseClient.d.ts","sourceRoot":"","sources":["../../src/mcp/sseClient.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AA6BhI,qBAAa,YAAa,YAAW,YAAY;IAYrB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAXjD,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,OAAO,CAAM;IACrB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA0H;IACnJ,OAAO,CAAC,eAAe,CAAoE;IAC3F,OAAO,CAAC,mBAAmB,CAAsC;IACjE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM,EAAmB,MAAM,EAAE,eAAe;IAIlE,IAAI,KAAK,IAAI,iBAAiB,EAAE,CAE/B;IAED,qBAAqB,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI,GAAG,IAAI;IAE5D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAaxB,aAAa,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAOvC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI1C,WAAW,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IAOnC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAMtE,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAc5E,UAAU,IAAI,IAAI;IAalB,4EAA4E;YAC9D,WAAW;IAoCzB,OAAO,CAAC,eAAe;IAYvB,oFAAoF;IACpF,OAAO,CAAC,KAAK;IAWb,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,cAAc;IAqDtB,OAAO,CAAC,IAAI;YAgBE,OAAO;IAIrB,6EAA6E;YAC/D,YAAY;CAwB3B"}