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.
- package/LICENSE +21 -0
- package/README.md +510 -0
- package/dist/advisor/advisor.d.ts +50 -0
- package/dist/advisor/advisor.js +178 -0
- package/dist/agents/base.d.ts +62 -0
- package/dist/agents/base.js +215 -0
- package/dist/agents/orchestrator.d.ts +46 -0
- package/dist/agents/orchestrator.js +192 -0
- package/dist/agents/team.d.ts +51 -0
- package/dist/agents/team.js +210 -0
- package/dist/api/models.d.ts +23 -0
- package/dist/api/models.js +514 -0
- package/dist/api/ollama.d.ts +153 -0
- package/dist/api/ollama.js +751 -0
- package/dist/api/openrouter.d.ts +55 -0
- package/dist/api/openrouter.js +223 -0
- package/dist/commands/commands.d.ts +43 -0
- package/dist/commands/commands.js +308 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.js +311 -0
- package/dist/context/compaction.d.ts +54 -0
- package/dist/context/compaction.js +251 -0
- package/dist/context/custom-agents.d.ts +107 -0
- package/dist/context/custom-agents.js +397 -0
- package/dist/context/custom-tools.d.ts +120 -0
- package/dist/context/custom-tools.js +564 -0
- package/dist/context/git-checkpoint.d.ts +52 -0
- package/dist/context/git-checkpoint.js +240 -0
- package/dist/context/neuro-md.d.ts +48 -0
- package/dist/context/neuro-md.js +202 -0
- package/dist/context/neuroignore.d.ts +102 -0
- package/dist/context/neuroignore.js +441 -0
- package/dist/context/repo-map.d.ts +38 -0
- package/dist/context/repo-map.js +220 -0
- package/dist/context/skill-standard.d.ts +262 -0
- package/dist/context/skill-standard.js +1156 -0
- package/dist/context/skill-system.d.ts +75 -0
- package/dist/context/skill-system.js +578 -0
- package/dist/context/tree-sitter.d.ts +99 -0
- package/dist/context/tree-sitter.js +1956 -0
- package/dist/core/acp.d.ts +325 -0
- package/dist/core/acp.js +1498 -0
- package/dist/core/api-server.d.ts +143 -0
- package/dist/core/api-server.js +550 -0
- package/dist/core/approval.d.ts +81 -0
- package/dist/core/approval.js +432 -0
- package/dist/core/auto-compact.d.ts +127 -0
- package/dist/core/auto-compact.js +436 -0
- package/dist/core/auto-mode.d.ts +232 -0
- package/dist/core/auto-mode.js +831 -0
- package/dist/core/background-session.d.ts +166 -0
- package/dist/core/background-session.js +696 -0
- package/dist/core/cicd.d.ts +257 -0
- package/dist/core/cicd.js +1443 -0
- package/dist/core/cloud-sync.d.ts +156 -0
- package/dist/core/cloud-sync.js +582 -0
- package/dist/core/code-review.d.ts +132 -0
- package/dist/core/code-review.js +1191 -0
- package/dist/core/completion.d.ts +49 -0
- package/dist/core/completion.js +384 -0
- package/dist/core/context.d.ts +38 -0
- package/dist/core/context.js +144 -0
- package/dist/core/diff-preview.d.ts +35 -0
- package/dist/core/diff-preview.js +173 -0
- package/dist/core/doom-loop.d.ts +51 -0
- package/dist/core/doom-loop.js +179 -0
- package/dist/core/engine.d.ts +183 -0
- package/dist/core/engine.js +942 -0
- package/dist/core/extended-thinking.d.ts +103 -0
- package/dist/core/extended-thinking.js +269 -0
- package/dist/core/fallback.d.ts +54 -0
- package/dist/core/fallback.js +104 -0
- package/dist/core/git-worktree.d.ts +139 -0
- package/dist/core/git-worktree.js +614 -0
- package/dist/core/headless.d.ts +30 -0
- package/dist/core/headless.js +101 -0
- package/dist/core/i18n.d.ts +83 -0
- package/dist/core/i18n.js +739 -0
- package/dist/core/linting.d.ts +129 -0
- package/dist/core/linting.js +699 -0
- package/dist/core/model-router.d.ts +109 -0
- package/dist/core/model-router.js +581 -0
- package/dist/core/multi-model.d.ts +243 -0
- package/dist/core/multi-model.js +1099 -0
- package/dist/core/multi-session.d.ts +144 -0
- package/dist/core/multi-session.js +442 -0
- package/dist/core/multimodal.d.ts +125 -0
- package/dist/core/multimodal.js +286 -0
- package/dist/core/observability.d.ts +93 -0
- package/dist/core/observability.js +737 -0
- package/dist/core/os-sandbox.d.ts +122 -0
- package/dist/core/os-sandbox.js +1193 -0
- package/dist/core/outcome-grading.d.ts +228 -0
- package/dist/core/outcome-grading.js +1123 -0
- package/dist/core/output-styles.d.ts +57 -0
- package/dist/core/output-styles.js +382 -0
- package/dist/core/parallel-agents.d.ts +183 -0
- package/dist/core/parallel-agents.js +563 -0
- package/dist/core/plugin-bundle.d.ts +236 -0
- package/dist/core/plugin-bundle.js +887 -0
- package/dist/core/plugin-sdk.d.ts +139 -0
- package/dist/core/plugin-sdk.js +273 -0
- package/dist/core/prompt-cache.d.ts +163 -0
- package/dist/core/prompt-cache.js +599 -0
- package/dist/core/sandbox.d.ts +127 -0
- package/dist/core/sandbox.js +369 -0
- package/dist/core/scheduled-tasks.d.ts +151 -0
- package/dist/core/scheduled-tasks.js +502 -0
- package/dist/core/security-scanner.d.ts +160 -0
- package/dist/core/security-scanner.js +1494 -0
- package/dist/core/session.d.ts +83 -0
- package/dist/core/session.js +269 -0
- package/dist/core/shell-completion.d.ts +51 -0
- package/dist/core/shell-completion.js +674 -0
- package/dist/core/smart-monitor.d.ts +146 -0
- package/dist/core/smart-monitor.js +1199 -0
- package/dist/core/spec-driven.d.ts +233 -0
- package/dist/core/spec-driven.js +1485 -0
- package/dist/core/spending-warnings.d.ts +123 -0
- package/dist/core/spending-warnings.js +456 -0
- package/dist/core/sub-agent.d.ts +298 -0
- package/dist/core/sub-agent.js +1023 -0
- package/dist/core/telemetry.d.ts +157 -0
- package/dist/core/telemetry.js +412 -0
- package/dist/core/terminal-ux.d.ts +134 -0
- package/dist/core/terminal-ux.js +649 -0
- package/dist/core/testing.d.ts +146 -0
- package/dist/core/testing.js +1200 -0
- package/dist/core/types.d.ts +439 -0
- package/dist/core/types.js +6 -0
- package/dist/core/undo-redo.d.ts +112 -0
- package/dist/core/undo-redo.js +290 -0
- package/dist/core/updater.d.ts +159 -0
- package/dist/core/updater.js +608 -0
- package/dist/core/vim-mode.d.ts +151 -0
- package/dist/core/vim-mode.js +771 -0
- package/dist/core/voice.d.ts +137 -0
- package/dist/core/voice.js +538 -0
- package/dist/core/web-dashboard.d.ts +109 -0
- package/dist/core/web-dashboard.js +484 -0
- package/dist/hooks/hooks.d.ts +74 -0
- package/dist/hooks/hooks.js +160 -0
- package/dist/hooks/llm-evaluator.d.ts +165 -0
- package/dist/hooks/llm-evaluator.js +560 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1186 -0
- package/dist/lsp/lsp-manager.d.ts +63 -0
- package/dist/lsp/lsp-manager.js +351 -0
- package/dist/mcp/client.d.ts +133 -0
- package/dist/mcp/client.js +684 -0
- package/dist/mcp/mcp-apps.d.ts +70 -0
- package/dist/mcp/mcp-apps.js +1007 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +195 -0
- package/dist/tools/browser.d.ts +92 -0
- package/dist/tools/browser.js +1570 -0
- package/dist/tools/extended.d.ts +6 -0
- package/dist/tools/extended.js +191 -0
- package/dist/tools/file.d.ts +10 -0
- package/dist/tools/file.js +382 -0
- package/dist/tools/github.d.ts +389 -0
- package/dist/tools/github.js +759 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/memory.d.ts +6 -0
- package/dist/tools/memory.js +197 -0
- package/dist/tools/registry.d.ts +29 -0
- package/dist/tools/registry.js +64 -0
- package/dist/tools/web.d.ts +6 -0
- package/dist/tools/web.js +150 -0
- package/dist/ui/renderer.d.ts +97 -0
- package/dist/ui/renderer.js +279 -0
- package/dist/ui/theme.d.ts +27 -0
- package/dist/ui/theme.js +106 -0
- 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
|