neuro-cli 4.1.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.
Files changed (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +510 -0
  3. package/dist/advisor/advisor.d.ts +50 -0
  4. package/dist/advisor/advisor.js +178 -0
  5. package/dist/agents/base.d.ts +62 -0
  6. package/dist/agents/base.js +215 -0
  7. package/dist/agents/orchestrator.d.ts +46 -0
  8. package/dist/agents/orchestrator.js +192 -0
  9. package/dist/agents/team.d.ts +51 -0
  10. package/dist/agents/team.js +210 -0
  11. package/dist/api/models.d.ts +23 -0
  12. package/dist/api/models.js +514 -0
  13. package/dist/api/ollama.d.ts +153 -0
  14. package/dist/api/ollama.js +751 -0
  15. package/dist/api/openrouter.d.ts +55 -0
  16. package/dist/api/openrouter.js +223 -0
  17. package/dist/commands/commands.d.ts +43 -0
  18. package/dist/commands/commands.js +308 -0
  19. package/dist/config/config.d.ts +8 -0
  20. package/dist/config/config.js +311 -0
  21. package/dist/context/compaction.d.ts +54 -0
  22. package/dist/context/compaction.js +251 -0
  23. package/dist/context/custom-agents.d.ts +107 -0
  24. package/dist/context/custom-agents.js +397 -0
  25. package/dist/context/custom-tools.d.ts +120 -0
  26. package/dist/context/custom-tools.js +564 -0
  27. package/dist/context/git-checkpoint.d.ts +52 -0
  28. package/dist/context/git-checkpoint.js +240 -0
  29. package/dist/context/neuro-md.d.ts +48 -0
  30. package/dist/context/neuro-md.js +202 -0
  31. package/dist/context/neuroignore.d.ts +102 -0
  32. package/dist/context/neuroignore.js +441 -0
  33. package/dist/context/repo-map.d.ts +38 -0
  34. package/dist/context/repo-map.js +220 -0
  35. package/dist/context/skill-standard.d.ts +262 -0
  36. package/dist/context/skill-standard.js +1156 -0
  37. package/dist/context/skill-system.d.ts +75 -0
  38. package/dist/context/skill-system.js +578 -0
  39. package/dist/context/tree-sitter.d.ts +99 -0
  40. package/dist/context/tree-sitter.js +1956 -0
  41. package/dist/core/acp.d.ts +325 -0
  42. package/dist/core/acp.js +1498 -0
  43. package/dist/core/api-server.d.ts +143 -0
  44. package/dist/core/api-server.js +550 -0
  45. package/dist/core/approval.d.ts +81 -0
  46. package/dist/core/approval.js +432 -0
  47. package/dist/core/auto-compact.d.ts +127 -0
  48. package/dist/core/auto-compact.js +436 -0
  49. package/dist/core/auto-mode.d.ts +232 -0
  50. package/dist/core/auto-mode.js +831 -0
  51. package/dist/core/background-session.d.ts +166 -0
  52. package/dist/core/background-session.js +696 -0
  53. package/dist/core/cicd.d.ts +257 -0
  54. package/dist/core/cicd.js +1443 -0
  55. package/dist/core/cloud-sync.d.ts +156 -0
  56. package/dist/core/cloud-sync.js +582 -0
  57. package/dist/core/code-review.d.ts +132 -0
  58. package/dist/core/code-review.js +1191 -0
  59. package/dist/core/completion.d.ts +49 -0
  60. package/dist/core/completion.js +384 -0
  61. package/dist/core/context.d.ts +38 -0
  62. package/dist/core/context.js +144 -0
  63. package/dist/core/diff-preview.d.ts +35 -0
  64. package/dist/core/diff-preview.js +173 -0
  65. package/dist/core/doom-loop.d.ts +51 -0
  66. package/dist/core/doom-loop.js +179 -0
  67. package/dist/core/engine.d.ts +183 -0
  68. package/dist/core/engine.js +942 -0
  69. package/dist/core/extended-thinking.d.ts +103 -0
  70. package/dist/core/extended-thinking.js +269 -0
  71. package/dist/core/fallback.d.ts +54 -0
  72. package/dist/core/fallback.js +104 -0
  73. package/dist/core/git-worktree.d.ts +139 -0
  74. package/dist/core/git-worktree.js +614 -0
  75. package/dist/core/headless.d.ts +30 -0
  76. package/dist/core/headless.js +101 -0
  77. package/dist/core/i18n.d.ts +83 -0
  78. package/dist/core/i18n.js +739 -0
  79. package/dist/core/linting.d.ts +129 -0
  80. package/dist/core/linting.js +699 -0
  81. package/dist/core/model-router.d.ts +109 -0
  82. package/dist/core/model-router.js +581 -0
  83. package/dist/core/multi-model.d.ts +243 -0
  84. package/dist/core/multi-model.js +1099 -0
  85. package/dist/core/multi-session.d.ts +144 -0
  86. package/dist/core/multi-session.js +442 -0
  87. package/dist/core/multimodal.d.ts +125 -0
  88. package/dist/core/multimodal.js +286 -0
  89. package/dist/core/observability.d.ts +93 -0
  90. package/dist/core/observability.js +737 -0
  91. package/dist/core/os-sandbox.d.ts +122 -0
  92. package/dist/core/os-sandbox.js +1193 -0
  93. package/dist/core/outcome-grading.d.ts +228 -0
  94. package/dist/core/outcome-grading.js +1123 -0
  95. package/dist/core/output-styles.d.ts +57 -0
  96. package/dist/core/output-styles.js +382 -0
  97. package/dist/core/parallel-agents.d.ts +183 -0
  98. package/dist/core/parallel-agents.js +563 -0
  99. package/dist/core/plugin-bundle.d.ts +236 -0
  100. package/dist/core/plugin-bundle.js +887 -0
  101. package/dist/core/plugin-sdk.d.ts +139 -0
  102. package/dist/core/plugin-sdk.js +273 -0
  103. package/dist/core/prompt-cache.d.ts +163 -0
  104. package/dist/core/prompt-cache.js +599 -0
  105. package/dist/core/sandbox.d.ts +127 -0
  106. package/dist/core/sandbox.js +369 -0
  107. package/dist/core/scheduled-tasks.d.ts +151 -0
  108. package/dist/core/scheduled-tasks.js +502 -0
  109. package/dist/core/security-scanner.d.ts +160 -0
  110. package/dist/core/security-scanner.js +1494 -0
  111. package/dist/core/session.d.ts +83 -0
  112. package/dist/core/session.js +269 -0
  113. package/dist/core/shell-completion.d.ts +51 -0
  114. package/dist/core/shell-completion.js +674 -0
  115. package/dist/core/smart-monitor.d.ts +146 -0
  116. package/dist/core/smart-monitor.js +1199 -0
  117. package/dist/core/spec-driven.d.ts +233 -0
  118. package/dist/core/spec-driven.js +1485 -0
  119. package/dist/core/spending-warnings.d.ts +123 -0
  120. package/dist/core/spending-warnings.js +456 -0
  121. package/dist/core/sub-agent.d.ts +298 -0
  122. package/dist/core/sub-agent.js +1023 -0
  123. package/dist/core/telemetry.d.ts +157 -0
  124. package/dist/core/telemetry.js +412 -0
  125. package/dist/core/terminal-ux.d.ts +134 -0
  126. package/dist/core/terminal-ux.js +649 -0
  127. package/dist/core/testing.d.ts +146 -0
  128. package/dist/core/testing.js +1200 -0
  129. package/dist/core/types.d.ts +439 -0
  130. package/dist/core/types.js +6 -0
  131. package/dist/core/undo-redo.d.ts +112 -0
  132. package/dist/core/undo-redo.js +290 -0
  133. package/dist/core/updater.d.ts +159 -0
  134. package/dist/core/updater.js +608 -0
  135. package/dist/core/vim-mode.d.ts +151 -0
  136. package/dist/core/vim-mode.js +771 -0
  137. package/dist/core/voice.d.ts +137 -0
  138. package/dist/core/voice.js +538 -0
  139. package/dist/core/web-dashboard.d.ts +109 -0
  140. package/dist/core/web-dashboard.js +484 -0
  141. package/dist/hooks/hooks.d.ts +74 -0
  142. package/dist/hooks/hooks.js +160 -0
  143. package/dist/hooks/llm-evaluator.d.ts +165 -0
  144. package/dist/hooks/llm-evaluator.js +560 -0
  145. package/dist/index.d.ts +3 -0
  146. package/dist/index.js +1186 -0
  147. package/dist/lsp/lsp-manager.d.ts +63 -0
  148. package/dist/lsp/lsp-manager.js +351 -0
  149. package/dist/mcp/client.d.ts +133 -0
  150. package/dist/mcp/client.js +684 -0
  151. package/dist/mcp/mcp-apps.d.ts +70 -0
  152. package/dist/mcp/mcp-apps.js +1007 -0
  153. package/dist/tools/bash.d.ts +5 -0
  154. package/dist/tools/bash.js +195 -0
  155. package/dist/tools/browser.d.ts +92 -0
  156. package/dist/tools/browser.js +1570 -0
  157. package/dist/tools/extended.d.ts +6 -0
  158. package/dist/tools/extended.js +191 -0
  159. package/dist/tools/file.d.ts +10 -0
  160. package/dist/tools/file.js +382 -0
  161. package/dist/tools/github.d.ts +389 -0
  162. package/dist/tools/github.js +759 -0
  163. package/dist/tools/index.d.ts +9 -0
  164. package/dist/tools/index.js +40 -0
  165. package/dist/tools/memory.d.ts +6 -0
  166. package/dist/tools/memory.js +197 -0
  167. package/dist/tools/registry.d.ts +29 -0
  168. package/dist/tools/registry.js +64 -0
  169. package/dist/tools/web.d.ts +6 -0
  170. package/dist/tools/web.js +150 -0
  171. package/dist/ui/renderer.d.ts +97 -0
  172. package/dist/ui/renderer.js +279 -0
  173. package/dist/ui/theme.d.ts +27 -0
  174. package/dist/ui/theme.js +106 -0
  175. package/package.json +83 -0
@@ -0,0 +1,684 @@
1
+ // ============================================================
2
+ // NeuroCLI - MCP (Model Context Protocol) Client
3
+ // Full MCP support: stdio, SSE, HTTP transports
4
+ // With reconnect, health-check, resource support, and streaming
5
+ // ============================================================
6
+ import { spawn } from 'child_process';
7
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { homedir } from 'os';
10
+ import chalk from 'chalk';
11
+ // --- MCP Client ---
12
+ export class MCPClient {
13
+ processes = new Map();
14
+ nextId = 1;
15
+ pendingRequests = new Map();
16
+ tools = new Map();
17
+ resources = new Map();
18
+ prompts = new Map();
19
+ servers = new Map();
20
+ inputBuffers = new Map();
21
+ configPath;
22
+ connectionStates = new Map();
23
+ healthCheckTimers = new Map();
24
+ reconnectTimers = new Map();
25
+ eventSource = new Map();
26
+ sseEventHandlers = new Map();
27
+ constructor(configPath) {
28
+ this.configPath = configPath || join(homedir(), '.neuro', 'mcp.json');
29
+ }
30
+ loadConfig() {
31
+ const projectConfig = join(process.cwd(), '.neuro', 'mcp.json');
32
+ const paths = [projectConfig, this.configPath];
33
+ let merged = { mcpServers: {} };
34
+ for (const p of paths) {
35
+ if (existsSync(p)) {
36
+ try {
37
+ const data = JSON.parse(readFileSync(p, 'utf-8'));
38
+ if (data.mcpServers)
39
+ Object.assign(merged.mcpServers, data.mcpServers);
40
+ }
41
+ catch { }
42
+ }
43
+ }
44
+ return merged;
45
+ }
46
+ saveConfig(config, global = true) {
47
+ const path = global ? this.configPath : join(process.cwd(), '.neuro', 'mcp.json');
48
+ const dir = join(path, '..');
49
+ if (!existsSync(dir))
50
+ mkdirSync(dir, { recursive: true });
51
+ writeFileSync(path, JSON.stringify(config, null, 2), 'utf-8');
52
+ }
53
+ addServer(name, config) {
54
+ const cfg = this.loadConfig();
55
+ cfg.mcpServers[name] = config;
56
+ this.saveConfig(cfg);
57
+ }
58
+ removeServer(name) {
59
+ const cfg = this.loadConfig();
60
+ if (!cfg.mcpServers[name])
61
+ return false;
62
+ delete cfg.mcpServers[name];
63
+ this.saveConfig(cfg);
64
+ return true;
65
+ }
66
+ listServers() {
67
+ const cfg = this.loadConfig();
68
+ const result = [];
69
+ for (const [name, config] of Object.entries(cfg.mcpServers)) {
70
+ result.push({
71
+ name,
72
+ config,
73
+ connected: this.processes.has(name) || this.eventSource.has(name),
74
+ toolCount: this.tools.get(name)?.length || 0,
75
+ resourceCount: this.resources.get(name)?.length || 0,
76
+ });
77
+ }
78
+ return result;
79
+ }
80
+ // --- Connection State ---
81
+ getConnectionStates() {
82
+ return Array.from(this.connectionStates.values());
83
+ }
84
+ getConnectionState(name) {
85
+ return this.connectionStates.get(name);
86
+ }
87
+ setConnectionState(name, status, error) {
88
+ const existing = this.connectionStates.get(name);
89
+ this.connectionStates.set(name, {
90
+ name,
91
+ status,
92
+ lastConnected: status === 'connected' ? Date.now() : existing?.lastConnected,
93
+ lastError: error || existing?.lastError,
94
+ reconnectAttempts: status === 'reconnecting' ? (existing?.reconnectAttempts || 0) + 1 : 0,
95
+ toolCount: this.tools.get(name)?.length || 0,
96
+ resourceCount: this.resources.get(name)?.length || 0,
97
+ promptCount: this.prompts.get(name)?.length || 0,
98
+ });
99
+ }
100
+ // --- Health Check ---
101
+ startHealthCheck(name) {
102
+ this.stopHealthCheck(name);
103
+ const config = this.servers.get(name);
104
+ if (!config)
105
+ return;
106
+ const interval = config.healthCheckIntervalMs || 60000;
107
+ const timer = setInterval(async () => {
108
+ try {
109
+ await this.sendRequest(name, 'ping', {});
110
+ this.setConnectionState(name, 'connected');
111
+ }
112
+ catch {
113
+ this.setConnectionState(name, 'error', 'Health check failed');
114
+ // Attempt reconnect if auto-reconnect is enabled
115
+ if (config.autoReconnect !== false) {
116
+ this.attemptReconnect(name, config);
117
+ }
118
+ }
119
+ }, interval);
120
+ this.healthCheckTimers.set(name, timer);
121
+ }
122
+ stopHealthCheck(name) {
123
+ const timer = this.healthCheckTimers.get(name);
124
+ if (timer) {
125
+ clearInterval(timer);
126
+ this.healthCheckTimers.delete(name);
127
+ }
128
+ }
129
+ // --- Reconnect ---
130
+ attemptReconnect(name, config) {
131
+ const state = this.connectionStates.get(name);
132
+ const maxAttempts = config.maxReconnectAttempts || 3;
133
+ if (state && state.reconnectAttempts >= maxAttempts) {
134
+ this.setConnectionState(name, 'error', `Max reconnect attempts (${maxAttempts}) reached`);
135
+ return;
136
+ }
137
+ this.setConnectionState(name, 'reconnecting');
138
+ const delay = Math.min(1000 * Math.pow(2, state?.reconnectAttempts || 0), 30000);
139
+ // Clear existing reconnect timer
140
+ const existingTimer = this.reconnectTimers.get(name);
141
+ if (existingTimer)
142
+ clearTimeout(existingTimer);
143
+ const timer = setTimeout(async () => {
144
+ try {
145
+ console.log(chalk.gray(` MCP: Reconnecting to ${name}...`));
146
+ await this.connect(name, config);
147
+ console.log(chalk.green(` MCP: Reconnected to ${name}`));
148
+ }
149
+ catch (error) {
150
+ const errMsg = error instanceof Error ? error.message : String(error);
151
+ this.setConnectionState(name, 'error', `Reconnect failed: ${errMsg}`);
152
+ // Try again
153
+ this.attemptReconnect(name, config);
154
+ }
155
+ }, delay);
156
+ this.reconnectTimers.set(name, timer);
157
+ }
158
+ // --- Connect All ---
159
+ async connectAll() {
160
+ const cfg = this.loadConfig();
161
+ let connected = 0;
162
+ for (const [name, config] of Object.entries(cfg.mcpServers)) {
163
+ if (config.disabled)
164
+ continue;
165
+ try {
166
+ await this.connect(name, config);
167
+ connected++;
168
+ }
169
+ catch (error) {
170
+ console.error(`MCP: Failed to connect to ${name}: ${error instanceof Error ? error.message : String(error)}`);
171
+ }
172
+ }
173
+ return connected;
174
+ }
175
+ // --- Connect ---
176
+ async connect(name, config) {
177
+ if (this.processes.has(name) || this.eventSource.has(name)) {
178
+ await this.disconnect(name);
179
+ }
180
+ this.servers.set(name, config);
181
+ this.setConnectionState(name, 'connecting');
182
+ try {
183
+ if (config.transport === 'stdio') {
184
+ await this.connectStdio(name, config);
185
+ }
186
+ else if (config.transport === 'sse') {
187
+ await this.connectSSE(name, config);
188
+ }
189
+ else if (config.transport === 'http') {
190
+ // HTTP doesn't maintain a persistent connection
191
+ this.setConnectionState(name, 'connected');
192
+ }
193
+ await this.initialize(name);
194
+ this.startHealthCheck(name);
195
+ }
196
+ catch (error) {
197
+ const errMsg = error instanceof Error ? error.message : String(error);
198
+ this.setConnectionState(name, 'error', errMsg);
199
+ throw error;
200
+ }
201
+ }
202
+ // --- stdio Transport ---
203
+ async connectStdio(name, config) {
204
+ return new Promise((resolve, reject) => {
205
+ const env = { ...process.env, ...config.env };
206
+ const proc = spawn(config.command, config.args || [], { env, stdio: ['pipe', 'pipe', 'pipe'], shell: true });
207
+ let initialized = false;
208
+ const timeout = setTimeout(() => {
209
+ if (!initialized) {
210
+ reject(new Error(`MCP server ${name} initialization timed out`));
211
+ proc.kill();
212
+ }
213
+ }, config.timeout || 30000);
214
+ proc.stdout.on('data', (data) => {
215
+ const chunk = data.toString();
216
+ const existing = this.inputBuffers.get(name) || '';
217
+ this.inputBuffers.set(name, existing + chunk);
218
+ let buffer = this.inputBuffers.get(name);
219
+ const lines = buffer.split('\n');
220
+ for (let i = 0; i < lines.length - 1; i++) {
221
+ const line = lines[i].trim();
222
+ if (line) {
223
+ try {
224
+ const response = JSON.parse(line);
225
+ this.handleResponse(name, response);
226
+ }
227
+ catch { }
228
+ }
229
+ }
230
+ this.inputBuffers.set(name, lines[lines.length - 1]);
231
+ });
232
+ proc.stderr.on('data', (data) => {
233
+ const msg = data.toString().trim();
234
+ if (msg)
235
+ console.error(chalk.gray(` MCP [${name}]: ${msg}`));
236
+ });
237
+ proc.on('error', (err) => {
238
+ clearTimeout(timeout);
239
+ this.processes.delete(name);
240
+ this.setConnectionState(name, 'error', err.message);
241
+ if (!initialized)
242
+ reject(err);
243
+ else if (this.servers.get(name)?.autoReconnect !== false) {
244
+ this.attemptReconnect(name, this.servers.get(name));
245
+ }
246
+ });
247
+ proc.on('exit', (code) => {
248
+ clearTimeout(timeout);
249
+ this.processes.delete(name);
250
+ if (code !== 0 && code !== null) {
251
+ this.setConnectionState(name, 'error', `Process exited with code ${code}`);
252
+ if (this.servers.get(name)?.autoReconnect !== false) {
253
+ this.attemptReconnect(name, this.servers.get(name));
254
+ }
255
+ }
256
+ else {
257
+ this.setConnectionState(name, 'disconnected');
258
+ }
259
+ });
260
+ this.processes.set(name, proc);
261
+ initialized = true;
262
+ clearTimeout(timeout);
263
+ resolve();
264
+ });
265
+ }
266
+ // --- SSE Transport ---
267
+ async connectSSE(name, config) {
268
+ if (!config.url)
269
+ throw new Error(`No URL for SSE MCP server ${name}`);
270
+ return new Promise((resolve, reject) => {
271
+ try {
272
+ const url = new URL(config.url);
273
+ const headers = {
274
+ 'Accept': 'text/event-stream',
275
+ 'Cache-Control': 'no-cache',
276
+ ...config.headers,
277
+ };
278
+ // Use native fetch for SSE
279
+ const controller = new AbortController();
280
+ const timeout = setTimeout(() => {
281
+ controller.abort();
282
+ reject(new Error(`SSE connection to ${name} timed out`));
283
+ }, config.timeout || 30000);
284
+ let resolved = false;
285
+ // Simple SSE client using fetch
286
+ fetch(config.url, {
287
+ method: 'GET',
288
+ headers,
289
+ signal: controller.signal,
290
+ }).then(async (response) => {
291
+ clearTimeout(timeout);
292
+ if (!response.ok) {
293
+ throw new Error(`SSE connection failed: ${response.status} ${response.statusText}`);
294
+ }
295
+ const reader = response.body?.getReader();
296
+ if (!reader)
297
+ throw new Error('No response body for SSE');
298
+ const decoder = new TextDecoder();
299
+ let buffer = '';
300
+ let eventType = '';
301
+ let eventData = '';
302
+ // Store the close handler
303
+ this.eventSource.set(name, {
304
+ close: () => {
305
+ controller.abort();
306
+ reader.cancel().catch(() => { });
307
+ }
308
+ });
309
+ this.setConnectionState(name, 'connected');
310
+ resolved = true;
311
+ resolve();
312
+ try {
313
+ while (true) {
314
+ const { done, value } = await reader.read();
315
+ if (done)
316
+ break;
317
+ buffer += decoder.decode(value, { stream: true });
318
+ const lines = buffer.split('\n');
319
+ buffer = lines.pop() || '';
320
+ for (const line of lines) {
321
+ if (line.startsWith('event: ')) {
322
+ eventType = line.slice(7).trim();
323
+ }
324
+ else if (line.startsWith('data: ')) {
325
+ eventData = line.slice(6);
326
+ }
327
+ else if (line === '' && eventData) {
328
+ // End of event
329
+ this.handleSSEEvent(name, eventType, eventData);
330
+ eventType = '';
331
+ eventData = '';
332
+ }
333
+ }
334
+ }
335
+ }
336
+ catch (error) {
337
+ if (!controller.signal.aborted) {
338
+ this.setConnectionState(name, 'error', `SSE stream error: ${error instanceof Error ? error.message : String(error)}`);
339
+ if (this.servers.get(name)?.autoReconnect !== false) {
340
+ this.attemptReconnect(name, this.servers.get(name));
341
+ }
342
+ }
343
+ }
344
+ }).catch((error) => {
345
+ clearTimeout(timeout);
346
+ this.setConnectionState(name, 'error', error.message);
347
+ if (!resolved)
348
+ reject(error);
349
+ else if (this.servers.get(name)?.autoReconnect !== false) {
350
+ this.attemptReconnect(name, this.servers.get(name));
351
+ }
352
+ });
353
+ }
354
+ catch (error) {
355
+ reject(error);
356
+ }
357
+ });
358
+ }
359
+ handleSSEEvent(serverName, eventType, data) {
360
+ try {
361
+ const parsed = JSON.parse(data);
362
+ if (parsed.jsonrpc && parsed.id !== undefined) {
363
+ this.handleResponse(serverName, parsed);
364
+ }
365
+ }
366
+ catch {
367
+ // Not a JSON-RPC response, might be a server notification
368
+ const handler = this.sseEventHandlers.get(serverName);
369
+ if (handler)
370
+ handler(eventType, data);
371
+ }
372
+ }
373
+ /**
374
+ * Register a custom SSE event handler
375
+ */
376
+ onSSEEvent(serverName, handler) {
377
+ this.sseEventHandlers.set(serverName, handler);
378
+ }
379
+ // --- Disconnect ---
380
+ async disconnect(name) {
381
+ this.stopHealthCheck(name);
382
+ const reconnectTimer = this.reconnectTimers.get(name);
383
+ if (reconnectTimer) {
384
+ clearTimeout(reconnectTimer);
385
+ this.reconnectTimers.delete(name);
386
+ }
387
+ const es = this.eventSource.get(name);
388
+ if (es) {
389
+ es.close();
390
+ this.eventSource.delete(name);
391
+ }
392
+ const proc = this.processes.get(name);
393
+ if (proc) {
394
+ proc.kill();
395
+ this.processes.delete(name);
396
+ }
397
+ this.tools.delete(name);
398
+ this.resources.delete(name);
399
+ this.prompts.delete(name);
400
+ this.servers.delete(name);
401
+ this.inputBuffers.delete(name);
402
+ this.sseEventHandlers.delete(name);
403
+ this.setConnectionState(name, 'disconnected');
404
+ }
405
+ async disconnectAll() {
406
+ for (const name of Array.from(this.processes.keys()))
407
+ await this.disconnect(name);
408
+ for (const name of Array.from(this.eventSource.keys()))
409
+ await this.disconnect(name);
410
+ }
411
+ // --- Initialize ---
412
+ async initialize(name) {
413
+ try {
414
+ await this.sendRequest(name, 'initialize', {
415
+ protocolVersion: '2024-11-05',
416
+ capabilities: {
417
+ roots: { listChanged: true },
418
+ sampling: {},
419
+ },
420
+ clientInfo: { name: 'neuro-cli', version: '2.0.0' },
421
+ });
422
+ await this.sendNotification(name, 'notifications/initialized', {});
423
+ // List tools
424
+ try {
425
+ const toolsResult = await this.sendRequest(name, 'tools/list', {});
426
+ this.tools.set(name, toolsResult.tools || []);
427
+ }
428
+ catch { }
429
+ // List resources
430
+ try {
431
+ const resourcesResult = await this.sendRequest(name, 'resources/list', {});
432
+ this.resources.set(name, resourcesResult.resources || []);
433
+ }
434
+ catch { }
435
+ // List prompts
436
+ try {
437
+ const promptsResult = await this.sendRequest(name, 'prompts/list', {});
438
+ this.prompts.set(name, promptsResult.prompts || []);
439
+ }
440
+ catch { }
441
+ this.setConnectionState(name, 'connected');
442
+ }
443
+ catch (error) {
444
+ console.error(`MCP: Failed to initialize ${name}: ${error instanceof Error ? error.message : String(error)}`);
445
+ this.setConnectionState(name, 'error', error instanceof Error ? error.message : String(error));
446
+ }
447
+ }
448
+ // --- Tool Calls ---
449
+ async callTool(serverName, toolName, args) {
450
+ const config = this.servers.get(serverName);
451
+ if (!config)
452
+ throw new Error(`MCP server ${serverName} not found`);
453
+ if (config.transport === 'http') {
454
+ return this.callToolHTTP(serverName, toolName, args);
455
+ }
456
+ return this.sendRequest(serverName, 'tools/call', { name: toolName, arguments: args });
457
+ }
458
+ async callToolHTTP(serverName, toolName, args) {
459
+ const config = this.servers.get(serverName);
460
+ if (!config?.url)
461
+ throw new Error(`No URL for MCP server ${serverName}`);
462
+ const response = await fetch(config.url, {
463
+ method: 'POST',
464
+ headers: {
465
+ 'Content-Type': 'application/json',
466
+ ...config.headers,
467
+ },
468
+ body: JSON.stringify({
469
+ jsonrpc: '2.0',
470
+ id: this.nextId++,
471
+ method: 'tools/call',
472
+ params: { name: toolName, arguments: args },
473
+ }),
474
+ });
475
+ const data = await response.json();
476
+ if (data.error)
477
+ throw new Error(`MCP error from ${serverName}: ${data.error.message}`);
478
+ return data.result;
479
+ }
480
+ // --- Resource Access ---
481
+ async readResource(serverName, uri) {
482
+ const config = this.servers.get(serverName);
483
+ if (!config)
484
+ throw new Error(`MCP server ${serverName} not found`);
485
+ if (config.transport === 'http') {
486
+ const response = await fetch(config.url, {
487
+ method: 'POST',
488
+ headers: { 'Content-Type': 'application/json', ...config.headers },
489
+ body: JSON.stringify({
490
+ jsonrpc: '2.0',
491
+ id: this.nextId++,
492
+ method: 'resources/read',
493
+ params: { uri },
494
+ }),
495
+ });
496
+ const data = await response.json();
497
+ if (data.error)
498
+ throw new Error(`MCP resource error: ${data.error.message}`);
499
+ return data.result;
500
+ }
501
+ return this.sendRequest(serverName, 'resources/read', { uri });
502
+ }
503
+ // --- Prompt Access ---
504
+ async getPrompt(serverName, promptName, args) {
505
+ const config = this.servers.get(serverName);
506
+ if (!config)
507
+ throw new Error(`MCP server ${serverName} not found`);
508
+ if (config.transport === 'http') {
509
+ const response = await fetch(config.url, {
510
+ method: 'POST',
511
+ headers: { 'Content-Type': 'application/json', ...config.headers },
512
+ body: JSON.stringify({
513
+ jsonrpc: '2.0',
514
+ id: this.nextId++,
515
+ method: 'prompts/get',
516
+ params: { name: promptName, arguments: args || {} },
517
+ }),
518
+ });
519
+ const data = await response.json();
520
+ if (data.error)
521
+ throw new Error(`MCP prompt error: ${data.error.message}`);
522
+ return data.result;
523
+ }
524
+ return this.sendRequest(serverName, 'prompts/get', { name: promptName, arguments: args || {} });
525
+ }
526
+ // --- JSON-RPC Transport ---
527
+ sendRequest(serverName, method, params) {
528
+ const proc = this.processes.get(serverName);
529
+ if (!proc || !proc.stdin?.writable) {
530
+ // Try SSE
531
+ if (this.eventSource.has(serverName)) {
532
+ return this.sendRequestSSE(serverName, method, params);
533
+ }
534
+ return Promise.reject(new Error(`MCP server ${serverName} not connected`));
535
+ }
536
+ const id = this.nextId++;
537
+ const request = { jsonrpc: '2.0', id, method, params };
538
+ return new Promise((resolve, reject) => {
539
+ const timeout = setTimeout(() => {
540
+ this.pendingRequests.delete(id);
541
+ reject(new Error(`MCP request to ${serverName} timed out`));
542
+ }, this.servers.get(serverName)?.timeout || 30000);
543
+ this.pendingRequests.set(id, { resolve, reject, timer: timeout });
544
+ if (proc.stdin) {
545
+ proc.stdin.write(JSON.stringify(request) + '\n');
546
+ }
547
+ else {
548
+ clearTimeout(timeout);
549
+ this.pendingRequests.delete(id);
550
+ reject(new Error(`MCP server ${serverName} stdin not available`));
551
+ }
552
+ });
553
+ }
554
+ async sendRequestSSE(serverName, method, params) {
555
+ const config = this.servers.get(serverName);
556
+ if (!config?.url)
557
+ throw new Error(`No URL for SSE MCP server ${serverName}`);
558
+ // For SSE transport, we use a separate POST request for sending
559
+ // The response will come back through the SSE stream
560
+ const id = this.nextId++;
561
+ const request = { jsonrpc: '2.0', id, method, params };
562
+ // Create a pending request that will be resolved by the SSE handler
563
+ return new Promise((resolve, reject) => {
564
+ const timeout = setTimeout(() => {
565
+ this.pendingRequests.delete(id);
566
+ reject(new Error(`MCP SSE request to ${serverName} timed out`));
567
+ }, config.timeout || 30000);
568
+ this.pendingRequests.set(id, { resolve, reject, timer: timeout });
569
+ // Send via HTTP POST to the same endpoint
570
+ const postUrl = (config.url || '').replace(/\/sse$/, '/message') || config.url || '';
571
+ fetch(postUrl, {
572
+ method: 'POST',
573
+ headers: { 'Content-Type': 'application/json', ...config.headers },
574
+ body: JSON.stringify(request),
575
+ }).catch((error) => {
576
+ clearTimeout(timeout);
577
+ this.pendingRequests.delete(id);
578
+ reject(error);
579
+ });
580
+ });
581
+ }
582
+ async sendNotification(serverName, method, params) {
583
+ const proc = this.processes.get(serverName);
584
+ if (proc && proc.stdin?.writable) {
585
+ proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n');
586
+ }
587
+ // For SSE/HTTP, notifications are best-effort
588
+ }
589
+ handleResponse(_serverName, response) {
590
+ const pending = this.pendingRequests.get(response.id);
591
+ if (!pending)
592
+ return;
593
+ clearTimeout(pending.timer);
594
+ this.pendingRequests.delete(response.id);
595
+ if (response.error)
596
+ pending.reject(new Error(response.error.message));
597
+ else
598
+ pending.resolve(response.result);
599
+ }
600
+ // --- Tool Definitions for AI ---
601
+ getAllTools() {
602
+ const result = [];
603
+ for (const [serverName, tools] of this.tools) {
604
+ for (const tool of tools)
605
+ result.push({ serverName, tool });
606
+ }
607
+ return result;
608
+ }
609
+ getToolDefinitions() {
610
+ const definitions = [];
611
+ for (const [serverName, tools] of this.tools) {
612
+ for (const tool of tools) {
613
+ definitions.push({
614
+ name: `mcp_${serverName}__${tool.name}`,
615
+ description: `[MCP:${serverName}] ${tool.description}`,
616
+ parameters: {
617
+ type: 'object',
618
+ properties: (tool.inputSchema.properties || {}),
619
+ required: tool.inputSchema.required || [],
620
+ },
621
+ });
622
+ }
623
+ }
624
+ return definitions;
625
+ }
626
+ getAllResources() {
627
+ const result = [];
628
+ for (const [serverName, resources] of this.resources) {
629
+ for (const resource of resources)
630
+ result.push({ serverName, resource });
631
+ }
632
+ return result;
633
+ }
634
+ getAllPrompts() {
635
+ const result = [];
636
+ for (const [serverName, prompts] of this.prompts) {
637
+ for (const prompt of prompts)
638
+ result.push({ serverName, prompt });
639
+ }
640
+ return result;
641
+ }
642
+ parseMCPToolName(fullName) {
643
+ if (!fullName.startsWith('mcp_'))
644
+ return null;
645
+ const rest = fullName.slice(4);
646
+ const sep = rest.indexOf('__');
647
+ if (sep === -1)
648
+ return null;
649
+ return { serverName: rest.slice(0, sep), toolName: rest.slice(sep + 2) };
650
+ }
651
+ isConnected(name) {
652
+ return this.processes.has(name) || this.eventSource.has(name);
653
+ }
654
+ getServerNames() {
655
+ return Array.from(this.servers.keys());
656
+ }
657
+ // --- Diagnostics ---
658
+ /**
659
+ * Print a health report of all MCP connections
660
+ */
661
+ healthReport() {
662
+ const states = this.getConnectionStates();
663
+ if (states.length === 0) {
664
+ console.log(chalk.gray(' No MCP servers configured.'));
665
+ return;
666
+ }
667
+ console.log(chalk.bold('\n MCP Health Report:\n'));
668
+ for (const state of states) {
669
+ const statusIcon = state.status === 'connected' ? chalk.green('●') :
670
+ state.status === 'connecting' ? chalk.yellow('◎') :
671
+ state.status === 'reconnecting' ? chalk.yellow('↻') :
672
+ chalk.red('○');
673
+ const lastConnected = state.lastConnected
674
+ ? new Date(state.lastConnected).toLocaleTimeString()
675
+ : 'never';
676
+ console.log(` ${statusIcon} ${chalk.cyan(state.name.padEnd(20))} ${state.status.padEnd(15)} tools: ${chalk.green(String(state.toolCount))} resources: ${chalk.green(String(state.resourceCount))} prompts: ${chalk.green(String(state.promptCount))} last: ${chalk.gray(lastConnected)}`);
677
+ if (state.lastError) {
678
+ console.log(` ${chalk.red('⚠ ' + state.lastError)}`);
679
+ }
680
+ }
681
+ console.log();
682
+ }
683
+ }
684
+ //# sourceMappingURL=client.js.map