nova-terminal-assistant 0.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.
Potentially problematic release.
This version of nova-terminal-assistant might be problematic. Click here for more details.
- package/README.md +358 -0
- package/bin/nova +38 -0
- package/bin/nova.js +12 -0
- package/package.json +67 -0
- package/src/cli/commands/SmartCompletion.ts +458 -0
- package/src/cli/index.ts +5 -0
- package/src/cli/startup/IFlowRepl.ts +212 -0
- package/src/cli/startup/InkBasedRepl.ts +1056 -0
- package/src/cli/startup/InteractiveRepl.ts +2833 -0
- package/src/cli/startup/NovaApp.ts +1861 -0
- package/src/cli/startup/index.ts +4 -0
- package/src/cli/startup/parseArgs.ts +293 -0
- package/src/cli/test-modules.ts +27 -0
- package/src/cli/ui/IFlowDropdown.ts +425 -0
- package/src/cli/ui/ModernReplUI.ts +276 -0
- package/src/cli/ui/SimpleSelector2.ts +215 -0
- package/src/cli/ui/components/ConfirmDialog.ts +176 -0
- package/src/cli/ui/components/ErrorPanel.ts +364 -0
- package/src/cli/ui/components/InkAppRunner.tsx +67 -0
- package/src/cli/ui/components/InkComponents.tsx +613 -0
- package/src/cli/ui/components/NovaInkApp.tsx +312 -0
- package/src/cli/ui/components/ProgressBar.ts +177 -0
- package/src/cli/ui/components/ProgressIndicator.ts +298 -0
- package/src/cli/ui/components/QuickActions.ts +396 -0
- package/src/cli/ui/components/SimpleErrorPanel.ts +231 -0
- package/src/cli/ui/components/StatusBar.ts +194 -0
- package/src/cli/ui/components/ThinkingBlockRenderer.ts +401 -0
- package/src/cli/ui/components/index.ts +27 -0
- package/src/cli/ui/ink-prototype.tsx +347 -0
- package/src/cli/utils/CliUI.ts +336 -0
- package/src/cli/utils/CompletionHelper.ts +388 -0
- package/src/cli/utils/EnhancedCompleter.test.ts +226 -0
- package/src/cli/utils/EnhancedCompleter.ts +513 -0
- package/src/cli/utils/ErrorEnhancer.ts +429 -0
- package/src/cli/utils/OutputFormatter.ts +193 -0
- package/src/cli/utils/index.ts +9 -0
- package/src/core/agents/AgentOrchestrator.ts +515 -0
- package/src/core/agents/index.ts +17 -0
- package/src/core/audit/AuditLogger.ts +509 -0
- package/src/core/audit/index.ts +11 -0
- package/src/core/auth/AuthManager.d.ts.map +1 -0
- package/src/core/auth/AuthManager.ts +138 -0
- package/src/core/auth/index.d.ts.map +1 -0
- package/src/core/auth/index.ts +2 -0
- package/src/core/config/ConfigManager.d.ts.map +1 -0
- package/src/core/config/ConfigManager.test.ts +183 -0
- package/src/core/config/ConfigManager.ts +1219 -0
- package/src/core/config/index.d.ts.map +1 -0
- package/src/core/config/index.ts +1 -0
- package/src/core/context/ContextBuilder.d.ts.map +1 -0
- package/src/core/context/ContextBuilder.ts +171 -0
- package/src/core/context/ContextCompressor.d.ts.map +1 -0
- package/src/core/context/ContextCompressor.ts +642 -0
- package/src/core/context/LayeredMemoryManager.ts +657 -0
- package/src/core/context/MemoryDiscovery.d.ts.map +1 -0
- package/src/core/context/MemoryDiscovery.ts +175 -0
- package/src/core/context/defaultSystemPrompt.d.ts.map +1 -0
- package/src/core/context/defaultSystemPrompt.ts +35 -0
- package/src/core/context/index.d.ts.map +1 -0
- package/src/core/context/index.ts +22 -0
- package/src/core/extensions/SkillGenerator.ts +421 -0
- package/src/core/extensions/SkillInstaller.d.ts.map +1 -0
- package/src/core/extensions/SkillInstaller.ts +257 -0
- package/src/core/extensions/SkillRegistry.d.ts.map +1 -0
- package/src/core/extensions/SkillRegistry.ts +361 -0
- package/src/core/extensions/SkillValidator.ts +525 -0
- package/src/core/extensions/index.ts +15 -0
- package/src/core/index.d.ts.map +1 -0
- package/src/core/index.ts +42 -0
- package/src/core/mcp/McpManager.d.ts.map +1 -0
- package/src/core/mcp/McpManager.ts +632 -0
- package/src/core/mcp/index.d.ts.map +1 -0
- package/src/core/mcp/index.ts +2 -0
- package/src/core/model/ModelClient.d.ts.map +1 -0
- package/src/core/model/ModelClient.ts +217 -0
- package/src/core/model/ModelConnectionTester.ts +363 -0
- package/src/core/model/ModelValidator.ts +348 -0
- package/src/core/model/index.d.ts.map +1 -0
- package/src/core/model/index.ts +6 -0
- package/src/core/model/providers/AnthropicProvider.d.ts.map +1 -0
- package/src/core/model/providers/AnthropicProvider.ts +279 -0
- package/src/core/model/providers/CodingPlanProvider.d.ts.map +1 -0
- package/src/core/model/providers/CodingPlanProvider.ts +210 -0
- package/src/core/model/providers/OllamaCloudProvider.d.ts.map +1 -0
- package/src/core/model/providers/OllamaCloudProvider.ts +405 -0
- package/src/core/model/providers/OllamaManager.d.ts.map +1 -0
- package/src/core/model/providers/OllamaManager.ts +201 -0
- package/src/core/model/providers/OllamaProvider.d.ts.map +1 -0
- package/src/core/model/providers/OllamaProvider.ts +73 -0
- package/src/core/model/providers/OpenAICompatibleProvider.d.ts.map +1 -0
- package/src/core/model/providers/OpenAICompatibleProvider.ts +327 -0
- package/src/core/model/providers/OpenAIProvider.d.ts.map +1 -0
- package/src/core/model/providers/OpenAIProvider.ts +29 -0
- package/src/core/model/providers/index.d.ts.map +1 -0
- package/src/core/model/providers/index.ts +12 -0
- package/src/core/model/types.d.ts.map +1 -0
- package/src/core/model/types.ts +77 -0
- package/src/core/security/ApprovalManager.d.ts.map +1 -0
- package/src/core/security/ApprovalManager.ts +174 -0
- package/src/core/security/FileFilter.d.ts.map +1 -0
- package/src/core/security/FileFilter.ts +141 -0
- package/src/core/security/HookExecutor.d.ts.map +1 -0
- package/src/core/security/HookExecutor.ts +178 -0
- package/src/core/security/SandboxExecutor.ts +447 -0
- package/src/core/security/index.d.ts.map +1 -0
- package/src/core/security/index.ts +8 -0
- package/src/core/session/AgentLoop.d.ts.map +1 -0
- package/src/core/session/AgentLoop.ts +501 -0
- package/src/core/session/SessionManager.d.ts.map +1 -0
- package/src/core/session/SessionManager.test.ts +183 -0
- package/src/core/session/SessionManager.ts +460 -0
- package/src/core/session/index.d.ts.map +1 -0
- package/src/core/session/index.ts +3 -0
- package/src/core/telemetry/Telemetry.d.ts.map +1 -0
- package/src/core/telemetry/Telemetry.ts +90 -0
- package/src/core/telemetry/TelemetryService.ts +531 -0
- package/src/core/telemetry/index.d.ts.map +1 -0
- package/src/core/telemetry/index.ts +12 -0
- package/src/core/testing/AutoFixer.ts +385 -0
- package/src/core/testing/ErrorAnalyzer.ts +499 -0
- package/src/core/testing/TestRunner.ts +265 -0
- package/src/core/testing/agent-cli-tests.ts +538 -0
- package/src/core/testing/index.ts +11 -0
- package/src/core/tools/ToolRegistry.d.ts.map +1 -0
- package/src/core/tools/ToolRegistry.test.ts +206 -0
- package/src/core/tools/ToolRegistry.ts +260 -0
- package/src/core/tools/impl/EditFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/EditFileTool.ts +97 -0
- package/src/core/tools/impl/ListDirectoryTool.d.ts.map +1 -0
- package/src/core/tools/impl/ListDirectoryTool.ts +142 -0
- package/src/core/tools/impl/MemoryTool.d.ts.map +1 -0
- package/src/core/tools/impl/MemoryTool.ts +102 -0
- package/src/core/tools/impl/ReadFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/ReadFileTool.ts +58 -0
- package/src/core/tools/impl/SearchContentTool.d.ts.map +1 -0
- package/src/core/tools/impl/SearchContentTool.ts +94 -0
- package/src/core/tools/impl/SearchFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/SearchFileTool.ts +61 -0
- package/src/core/tools/impl/ShellTool.d.ts.map +1 -0
- package/src/core/tools/impl/ShellTool.ts +118 -0
- package/src/core/tools/impl/TaskTool.d.ts.map +1 -0
- package/src/core/tools/impl/TaskTool.ts +207 -0
- package/src/core/tools/impl/TodoTool.d.ts.map +1 -0
- package/src/core/tools/impl/TodoTool.ts +122 -0
- package/src/core/tools/impl/WebFetchTool.d.ts.map +1 -0
- package/src/core/tools/impl/WebFetchTool.ts +103 -0
- package/src/core/tools/impl/WebSearchTool.d.ts.map +1 -0
- package/src/core/tools/impl/WebSearchTool.ts +89 -0
- package/src/core/tools/impl/WriteFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/WriteFileTool.ts +49 -0
- package/src/core/tools/impl/index.d.ts.map +1 -0
- package/src/core/tools/impl/index.ts +16 -0
- package/src/core/tools/index.d.ts.map +1 -0
- package/src/core/tools/index.ts +7 -0
- package/src/core/tools/schemas/execution.d.ts.map +1 -0
- package/src/core/tools/schemas/execution.ts +42 -0
- package/src/core/tools/schemas/file.d.ts.map +1 -0
- package/src/core/tools/schemas/file.ts +119 -0
- package/src/core/tools/schemas/index.d.ts.map +1 -0
- package/src/core/tools/schemas/index.ts +11 -0
- package/src/core/tools/schemas/memory.d.ts.map +1 -0
- package/src/core/tools/schemas/memory.ts +52 -0
- package/src/core/tools/schemas/orchestration.d.ts.map +1 -0
- package/src/core/tools/schemas/orchestration.ts +44 -0
- package/src/core/tools/schemas/search.d.ts.map +1 -0
- package/src/core/tools/schemas/search.ts +112 -0
- package/src/core/tools/schemas/todo.d.ts.map +1 -0
- package/src/core/tools/schemas/todo.ts +32 -0
- package/src/core/tools/schemas/web.d.ts.map +1 -0
- package/src/core/tools/schemas/web.ts +86 -0
- package/src/core/types/config.d.ts.map +1 -0
- package/src/core/types/config.ts +200 -0
- package/src/core/types/errors.d.ts.map +1 -0
- package/src/core/types/errors.ts +204 -0
- package/src/core/types/index.d.ts.map +1 -0
- package/src/core/types/index.ts +8 -0
- package/src/core/types/session.d.ts.map +1 -0
- package/src/core/types/session.ts +216 -0
- package/src/core/types/tools.d.ts.map +1 -0
- package/src/core/types/tools.ts +157 -0
- package/src/core/utils/CheckpointManager.d.ts.map +1 -0
- package/src/core/utils/CheckpointManager.ts +327 -0
- package/src/core/utils/Logger.d.ts.map +1 -0
- package/src/core/utils/Logger.ts +98 -0
- package/src/core/utils/RetryManager.ts +471 -0
- package/src/core/utils/TokenCounter.d.ts.map +1 -0
- package/src/core/utils/TokenCounter.ts +414 -0
- package/src/core/utils/VectorMemoryStore.ts +440 -0
- package/src/core/utils/helpers.d.ts.map +1 -0
- package/src/core/utils/helpers.ts +89 -0
- package/src/core/utils/index.d.ts.map +1 -0
- package/src/core/utils/index.ts +19 -0
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// MCPManager v2 - Enhanced Model Context Protocol server management
|
|
3
|
+
// Adds: namespace isolation, notification handling, reconnection, resource support
|
|
4
|
+
// ============================================================================
|
|
5
|
+
|
|
6
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
7
|
+
import { EventEmitter } from 'node:events';
|
|
8
|
+
import type { ToolDefinition } from '../types/tools.js';
|
|
9
|
+
import { McpError } from '../types/errors.js';
|
|
10
|
+
|
|
11
|
+
export interface McpServerConfig {
|
|
12
|
+
name: string;
|
|
13
|
+
command: string;
|
|
14
|
+
args?: string[];
|
|
15
|
+
env?: Record<string, string>;
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
timeout?: number;
|
|
18
|
+
/** Transport type: stdio (default) or http/sse (future) */
|
|
19
|
+
transport?: 'stdio' | 'http' | 'sse';
|
|
20
|
+
/** HTTP URL for http/sse transport */
|
|
21
|
+
url?: string;
|
|
22
|
+
/** Headers for HTTP transport */
|
|
23
|
+
headers?: Record<string, string>;
|
|
24
|
+
/** Auto-reconnect on disconnect */
|
|
25
|
+
autoReconnect?: boolean;
|
|
26
|
+
/** Reconnect interval in ms (default: 5000) */
|
|
27
|
+
reconnectInterval?: number;
|
|
28
|
+
/** Maximum reconnect attempts (default: 3) */
|
|
29
|
+
maxReconnectAttempts?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface McpToolDefinition {
|
|
33
|
+
name: string;
|
|
34
|
+
description: string;
|
|
35
|
+
inputSchema: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface McpResource {
|
|
39
|
+
uri: string;
|
|
40
|
+
name: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
mimeType?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface McpNotification {
|
|
46
|
+
method: string;
|
|
47
|
+
params?: Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface McpServerStatus {
|
|
51
|
+
name: string;
|
|
52
|
+
toolCount: number;
|
|
53
|
+
resourceCount: number;
|
|
54
|
+
connected: boolean;
|
|
55
|
+
reconnectAttempts: number;
|
|
56
|
+
lastError?: string;
|
|
57
|
+
lastActivity: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type JsonRpcMessage = {
|
|
61
|
+
jsonrpc: '2.0';
|
|
62
|
+
id?: number;
|
|
63
|
+
method?: string;
|
|
64
|
+
params?: unknown;
|
|
65
|
+
result?: unknown;
|
|
66
|
+
error?: { code: number; message: string; data?: unknown };
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// --- MCP Client with namespace isolation ---
|
|
70
|
+
|
|
71
|
+
interface McpClientEntry {
|
|
72
|
+
process: ChildProcess;
|
|
73
|
+
tools: McpToolDefinition[];
|
|
74
|
+
resources: McpResource[];
|
|
75
|
+
connected: boolean;
|
|
76
|
+
requestId: number;
|
|
77
|
+
pendingRequests: Map<number, {
|
|
78
|
+
resolve: (value: unknown) => void;
|
|
79
|
+
reject: (error: Error) => void;
|
|
80
|
+
timer: ReturnType<typeof setTimeout>;
|
|
81
|
+
}>;
|
|
82
|
+
buffer: string;
|
|
83
|
+
reconnectAttempts: number;
|
|
84
|
+
lastActivity: number;
|
|
85
|
+
lastError?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- Enhanced McpManager ---
|
|
89
|
+
|
|
90
|
+
export class McpManager extends EventEmitter {
|
|
91
|
+
private servers = new Map<string, McpClientEntry>();
|
|
92
|
+
private configs = new Map<string, McpServerConfig>();
|
|
93
|
+
private toolServerMap = new Map<string, string>(); // toolName -> serverName
|
|
94
|
+
|
|
95
|
+
/** Connect to an MCP server */
|
|
96
|
+
async connect(config: McpServerConfig): Promise<ToolDefinition[]> {
|
|
97
|
+
if (this.servers.has(config.name)) {
|
|
98
|
+
const existing = this.servers.get(config.name)!;
|
|
99
|
+
if (existing.connected) {
|
|
100
|
+
return this.getToolsForServer(config.name);
|
|
101
|
+
}
|
|
102
|
+
// Server exists but disconnected, try reconnect
|
|
103
|
+
await this.disconnect(config.name);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Validate config
|
|
107
|
+
if (!config.command && config.transport !== 'http' && config.transport !== 'sse') {
|
|
108
|
+
throw new McpError(`MCP server "${config.name}": command is required for stdio transport`, config.name);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.configs.set(config.name, {
|
|
112
|
+
transport: 'stdio',
|
|
113
|
+
autoReconnect: true,
|
|
114
|
+
reconnectInterval: 5000,
|
|
115
|
+
maxReconnectAttempts: 3,
|
|
116
|
+
...config,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Connect with spawn error handling
|
|
120
|
+
let processRef: any = null;
|
|
121
|
+
try {
|
|
122
|
+
const entry = await this.createClient(config);
|
|
123
|
+
processRef = entry.process;
|
|
124
|
+
this.servers.set(config.name, entry);
|
|
125
|
+
|
|
126
|
+
// Set up event handler for auto-reconnect on exit
|
|
127
|
+
entry.process.on('exit', () => {
|
|
128
|
+
this.handleDisconnect(config.name);
|
|
129
|
+
});
|
|
130
|
+
// Set up error handler for runtime process errors
|
|
131
|
+
entry.process.on('error', (err: Error) => {
|
|
132
|
+
this.handleProcessError(config.name, err);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const tools = this.getToolsForServer(config.name);
|
|
136
|
+
this.emit('connected', { serverName: config.name, toolCount: tools.length });
|
|
137
|
+
return tools;
|
|
138
|
+
} catch (err) {
|
|
139
|
+
// If process was spawned but init failed, clean up
|
|
140
|
+
if (processRef) {
|
|
141
|
+
try { processRef.kill(); } catch {}
|
|
142
|
+
}
|
|
143
|
+
throw new McpError(
|
|
144
|
+
`Failed to connect to MCP server "${config.name}": ${(err as Error).message}`,
|
|
145
|
+
config.name
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Call a tool on a specific MCP server */
|
|
151
|
+
async callTool(serverName: string, toolName: string, input: Record<string, unknown>): Promise<string> {
|
|
152
|
+
const entry = this.servers.get(serverName);
|
|
153
|
+
if (!entry) {
|
|
154
|
+
throw new McpError(`MCP server "${serverName}" not found`, serverName);
|
|
155
|
+
}
|
|
156
|
+
if (!entry.connected) {
|
|
157
|
+
throw new McpError(`MCP server "${serverName}" is not connected`, serverName);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const result = await this.sendJsonRpc(entry, 'tools/call', {
|
|
162
|
+
name: toolName,
|
|
163
|
+
arguments: input,
|
|
164
|
+
});
|
|
165
|
+
entry.lastActivity = Date.now();
|
|
166
|
+
return JSON.stringify(result);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
throw new McpError(
|
|
169
|
+
`MCP tool call failed on "${serverName}": ${(err as Error).message}`,
|
|
170
|
+
serverName
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Call a tool by its namespaced name (e.g., "filesystem:read_file").
|
|
177
|
+
* Automatically resolves the server.
|
|
178
|
+
*/
|
|
179
|
+
async callToolByNamespacedName(namespacedName: string, input: Record<string, unknown>): Promise<string> {
|
|
180
|
+
const colonIdx = namespacedName.indexOf(':');
|
|
181
|
+
if (colonIdx < 0) {
|
|
182
|
+
throw new McpError(`Invalid namespaced tool name "${namespacedName}", expected format "server:tool"`, 'unknown');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const serverName = namespacedName.slice(0, colonIdx);
|
|
186
|
+
const toolName = namespacedName.slice(colonIdx + 1);
|
|
187
|
+
|
|
188
|
+
// Check tool-server map first
|
|
189
|
+
const mappedServer = this.toolServerMap.get(namespacedName);
|
|
190
|
+
if (mappedServer) {
|
|
191
|
+
return this.callTool(mappedServer, toolName, input);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Fallback: try the server name extracted from the namespace
|
|
195
|
+
return this.callTool(serverName, toolName, input);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Read a resource from an MCP server */
|
|
199
|
+
async readResource(serverName: string, uri: string): Promise<{ contents: Array<{ uri: string; mimeType?: string; text?: string; blob?: string }> }> {
|
|
200
|
+
const entry = this.servers.get(serverName);
|
|
201
|
+
if (!entry || !entry.connected) {
|
|
202
|
+
throw new McpError(`MCP server "${serverName}" not connected`, serverName);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const result = await this.sendJsonRpc(entry, 'resources/read', { uri });
|
|
206
|
+
entry.lastActivity = Date.now();
|
|
207
|
+
return result as any;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** List available resources from an MCP server */
|
|
211
|
+
async listResources(serverName: string): Promise<McpResource[]> {
|
|
212
|
+
const entry = this.servers.get(serverName);
|
|
213
|
+
if (!entry || !entry.connected) {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const result = await this.sendJsonRpc(entry, 'resources/list', {});
|
|
219
|
+
entry.resources = (result as any)?.resources || [];
|
|
220
|
+
return entry.resources;
|
|
221
|
+
} catch {
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Subscribe to resource changes */
|
|
227
|
+
async subscribeResource(serverName: string, uri: string): Promise<void> {
|
|
228
|
+
const entry = this.servers.get(serverName);
|
|
229
|
+
if (!entry || !entry.connected) {
|
|
230
|
+
throw new McpError(`MCP server "${serverName}" not connected`, serverName);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
await this.sendJsonRpc(entry, 'resources/subscribe', { uri });
|
|
234
|
+
entry.lastActivity = Date.now();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Disconnect from an MCP server */
|
|
238
|
+
async disconnect(serverName: string): Promise<void> {
|
|
239
|
+
const entry = this.servers.get(serverName);
|
|
240
|
+
if (entry) {
|
|
241
|
+
try {
|
|
242
|
+
// Try graceful shutdown via notification
|
|
243
|
+
if (entry.process.stdin && !entry.process.stdin.destroyed) {
|
|
244
|
+
entry.process.stdin.write(JSON.stringify({
|
|
245
|
+
jsonrpc: '2.0',
|
|
246
|
+
method: 'notifications/cancelled',
|
|
247
|
+
params: {},
|
|
248
|
+
}) + '\n');
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
// Ignore write errors during shutdown
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Clean up pending requests
|
|
255
|
+
for (const [, pending] of entry.pendingRequests) {
|
|
256
|
+
clearTimeout(pending.timer);
|
|
257
|
+
pending.reject(new Error('Server disconnected'));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
entry.process.kill();
|
|
262
|
+
} catch {
|
|
263
|
+
// Process may have already exited
|
|
264
|
+
}
|
|
265
|
+
this.servers.delete(serverName);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Disconnect from all servers */
|
|
270
|
+
async disconnectAll(): Promise<void> {
|
|
271
|
+
const names = Array.from(this.servers.keys());
|
|
272
|
+
await Promise.all(names.map((name) => this.disconnect(name)));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** List connected servers with status */
|
|
276
|
+
listServers(): McpServerStatus[] {
|
|
277
|
+
return Array.from(this.servers.entries()).map(([name, entry]) => ({
|
|
278
|
+
name,
|
|
279
|
+
toolCount: entry.tools.length,
|
|
280
|
+
resourceCount: entry.resources.length,
|
|
281
|
+
connected: entry.connected,
|
|
282
|
+
reconnectAttempts: entry.reconnectAttempts,
|
|
283
|
+
lastError: entry.lastError,
|
|
284
|
+
lastActivity: entry.lastActivity,
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Get all tools from all connected servers, with namespace isolation.
|
|
290
|
+
* Tool names are prefixed with "server:" to avoid conflicts.
|
|
291
|
+
*/
|
|
292
|
+
getAllTools(): ToolDefinition[] {
|
|
293
|
+
const allTools: ToolDefinition[] = [];
|
|
294
|
+
|
|
295
|
+
for (const [serverName, entry] of this.servers) {
|
|
296
|
+
if (!entry.connected) continue;
|
|
297
|
+
|
|
298
|
+
for (const mcpTool of entry.tools) {
|
|
299
|
+
const namespacedName = `${serverName}:${mcpTool.name}`;
|
|
300
|
+
const tool: ToolDefinition = {
|
|
301
|
+
name: namespacedName,
|
|
302
|
+
description: `[${serverName}] ${mcpTool.description}`,
|
|
303
|
+
category: 'mcp',
|
|
304
|
+
inputSchema: mcpTool.inputSchema,
|
|
305
|
+
requiresApproval: true,
|
|
306
|
+
riskLevel: 'medium',
|
|
307
|
+
tags: ['mcp', serverName],
|
|
308
|
+
};
|
|
309
|
+
allTools.push(tool);
|
|
310
|
+
this.toolServerMap.set(namespacedName, serverName);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return allTools;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Get tools for a specific server (without namespace prefix) */
|
|
318
|
+
getToolsForServer(serverName: string): ToolDefinition[] {
|
|
319
|
+
const entry = this.servers.get(serverName);
|
|
320
|
+
if (!entry) return [];
|
|
321
|
+
|
|
322
|
+
return entry.tools.map((mcpTool) => ({
|
|
323
|
+
name: mcpTool.name,
|
|
324
|
+
description: mcpTool.description,
|
|
325
|
+
category: 'mcp' as const,
|
|
326
|
+
inputSchema: mcpTool.inputSchema,
|
|
327
|
+
requiresApproval: true,
|
|
328
|
+
riskLevel: 'medium' as const,
|
|
329
|
+
tags: ['mcp', serverName],
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ========================================================================
|
|
334
|
+
// Private: Client Lifecycle
|
|
335
|
+
// ========================================================================
|
|
336
|
+
|
|
337
|
+
private async createClient(config: McpServerConfig): Promise<McpClientEntry> {
|
|
338
|
+
let proc: any;
|
|
339
|
+
try {
|
|
340
|
+
proc = spawn(config.command, config.args || [], {
|
|
341
|
+
env: { ...process.env, ...config.env },
|
|
342
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
343
|
+
windowsHide: true,
|
|
344
|
+
});
|
|
345
|
+
} catch (err) {
|
|
346
|
+
throw new McpError(
|
|
347
|
+
`Failed to spawn MCP server "${config.name}": ${(err as Error).message}`,
|
|
348
|
+
config.name
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const entry: McpClientEntry = {
|
|
353
|
+
process: proc,
|
|
354
|
+
tools: [],
|
|
355
|
+
resources: [],
|
|
356
|
+
connected: false,
|
|
357
|
+
requestId: 0,
|
|
358
|
+
pendingRequests: new Map(),
|
|
359
|
+
buffer: '',
|
|
360
|
+
reconnectAttempts: 0,
|
|
361
|
+
lastActivity: Date.now(),
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// Set up stdout handler
|
|
365
|
+
proc.stdout?.on('data', (data: Buffer) => {
|
|
366
|
+
this.handleStdoutData(entry, data);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
370
|
+
// MCP servers may log to stderr
|
|
371
|
+
const text = data.toString().trim();
|
|
372
|
+
if (text) {
|
|
373
|
+
this.emit('stderr', { serverName: config.name, data: text });
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Initialize the connection - use Promise.race to handle async spawn errors (e.g., ENOENT)
|
|
378
|
+
const timeout = config.timeout || 15000;
|
|
379
|
+
const initPromise = this.initializeConnection(entry, timeout);
|
|
380
|
+
const spawnErrorPromise = new Promise<never>((_, reject) => {
|
|
381
|
+
proc.on('error', (err: Error) => {
|
|
382
|
+
reject(new McpError(
|
|
383
|
+
`Failed to spawn MCP server "${config.name}": ${err.message}`,
|
|
384
|
+
config.name
|
|
385
|
+
));
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
await Promise.race([initPromise, spawnErrorPromise]);
|
|
390
|
+
|
|
391
|
+
entry.connected = true;
|
|
392
|
+
return entry;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private async initializeConnection(entry: McpClientEntry, timeout: number): Promise<void> {
|
|
396
|
+
return new Promise((resolve, reject) => {
|
|
397
|
+
const timer = setTimeout(() => {
|
|
398
|
+
entry.process.kill();
|
|
399
|
+
reject(new McpError('MCP server initialization timed out'));
|
|
400
|
+
}, timeout);
|
|
401
|
+
|
|
402
|
+
let initReceived = false;
|
|
403
|
+
let toolsReceived = false;
|
|
404
|
+
|
|
405
|
+
const completionHandler = () => {
|
|
406
|
+
if (initReceived && toolsReceived) {
|
|
407
|
+
clearTimeout(timer);
|
|
408
|
+
resolve();
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// Override stdout handler during initialization
|
|
413
|
+
const originalHandler = entry.process.stdout?.listeners('data');
|
|
414
|
+
// Remove existing listeners
|
|
415
|
+
for (const listener of (originalHandler || [])) {
|
|
416
|
+
entry.process.stdout?.removeListener('data', listener as (...args: any[]) => void);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
entry.process.stdout?.on('data', (data: Buffer) => {
|
|
420
|
+
entry.buffer += data.toString();
|
|
421
|
+
const lines = entry.buffer.split('\n');
|
|
422
|
+
entry.buffer = lines.pop() || '';
|
|
423
|
+
|
|
424
|
+
for (const line of lines) {
|
|
425
|
+
if (!line.trim()) continue;
|
|
426
|
+
try {
|
|
427
|
+
const response: JsonRpcMessage = JSON.parse(line);
|
|
428
|
+
|
|
429
|
+
if (!initReceived && response.id === 1 && response.result) {
|
|
430
|
+
initReceived = true;
|
|
431
|
+
|
|
432
|
+
// Send tools/list request
|
|
433
|
+
const toolsRequest: JsonRpcMessage = {
|
|
434
|
+
jsonrpc: '2.0',
|
|
435
|
+
id: 2,
|
|
436
|
+
method: 'tools/list',
|
|
437
|
+
params: {},
|
|
438
|
+
};
|
|
439
|
+
entry.process.stdin?.write(JSON.stringify(toolsRequest) + '\n');
|
|
440
|
+
completionHandler();
|
|
441
|
+
} else if (!toolsReceived && response.id === 2 && response.result) {
|
|
442
|
+
toolsReceived = true;
|
|
443
|
+
entry.tools = (response.result as any)?.tools || [];
|
|
444
|
+
completionHandler();
|
|
445
|
+
|
|
446
|
+
// Also request resources/list
|
|
447
|
+
const resourcesRequest: JsonRpcMessage = {
|
|
448
|
+
jsonrpc: '2.0',
|
|
449
|
+
id: 3,
|
|
450
|
+
method: 'resources/list',
|
|
451
|
+
params: {},
|
|
452
|
+
};
|
|
453
|
+
entry.process.stdin?.write(JSON.stringify(resourcesRequest) + '\n');
|
|
454
|
+
} else if (response.id === 3 && response.result) {
|
|
455
|
+
entry.resources = (response.result as any)?.resources || [];
|
|
456
|
+
|
|
457
|
+
// Restore normal data handler
|
|
458
|
+
for (const listener of (originalHandler || [])) {
|
|
459
|
+
entry.process.stdout?.on('data', listener as (...args: any[]) => void);
|
|
460
|
+
}
|
|
461
|
+
// Remove our temp listener
|
|
462
|
+
entry.process.stdout?.removeAllListeners('data');
|
|
463
|
+
for (const listener of (originalHandler || [])) {
|
|
464
|
+
entry.process.stdout?.on('data', listener as (...args: any[]) => void);
|
|
465
|
+
}
|
|
466
|
+
} else if (response.id && entry.pendingRequests.has(response.id)) {
|
|
467
|
+
// Route to pending request handler
|
|
468
|
+
const pending = entry.pendingRequests.get(response.id)!;
|
|
469
|
+
clearTimeout(pending.timer);
|
|
470
|
+
entry.pendingRequests.delete(response.id);
|
|
471
|
+
|
|
472
|
+
if (response.error) {
|
|
473
|
+
pending.reject(new Error(response.error.message));
|
|
474
|
+
} else {
|
|
475
|
+
pending.resolve(response.result);
|
|
476
|
+
}
|
|
477
|
+
} else if (!response.id && response.method) {
|
|
478
|
+
// This is a notification from the server
|
|
479
|
+
this.handleNotification(entry, response);
|
|
480
|
+
}
|
|
481
|
+
} catch {
|
|
482
|
+
// Ignore unparseable lines
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// Send initialize request
|
|
488
|
+
const initRequest: JsonRpcMessage = {
|
|
489
|
+
jsonrpc: '2.0',
|
|
490
|
+
id: 1,
|
|
491
|
+
method: 'initialize',
|
|
492
|
+
params: {
|
|
493
|
+
protocolVersion: '2024-11-05',
|
|
494
|
+
capabilities: {
|
|
495
|
+
tools: {},
|
|
496
|
+
resources: { subscribe: true },
|
|
497
|
+
prompts: {},
|
|
498
|
+
},
|
|
499
|
+
clientInfo: { name: 'nova-cli', version: '0.2.0' },
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
entry.process.stdin?.write(JSON.stringify(initRequest) + '\n');
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private handleStdoutData(entry: McpClientEntry, data: Buffer): void {
|
|
507
|
+
entry.buffer += data.toString();
|
|
508
|
+
const lines = entry.buffer.split('\n');
|
|
509
|
+
entry.buffer = lines.pop() || '';
|
|
510
|
+
|
|
511
|
+
for (const line of lines) {
|
|
512
|
+
if (!line.trim()) continue;
|
|
513
|
+
try {
|
|
514
|
+
const response: JsonRpcMessage = JSON.parse(line);
|
|
515
|
+
|
|
516
|
+
if (response.id && entry.pendingRequests.has(response.id)) {
|
|
517
|
+
const pending = entry.pendingRequests.get(response.id)!;
|
|
518
|
+
clearTimeout(pending.timer);
|
|
519
|
+
entry.pendingRequests.delete(response.id);
|
|
520
|
+
|
|
521
|
+
if (response.error) {
|
|
522
|
+
pending.reject(new Error(response.error.message));
|
|
523
|
+
} else {
|
|
524
|
+
pending.resolve(response.result);
|
|
525
|
+
}
|
|
526
|
+
} else if (!response.id) {
|
|
527
|
+
// Notification from server
|
|
528
|
+
this.handleNotification(entry, response);
|
|
529
|
+
}
|
|
530
|
+
} catch {
|
|
531
|
+
// Ignore unparseable lines
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private handleNotification(entry: McpClientEntry, notification: JsonRpcMessage): void {
|
|
537
|
+
const method = notification.method || '';
|
|
538
|
+
entry.lastActivity = Date.now();
|
|
539
|
+
|
|
540
|
+
// Handle resource update notifications
|
|
541
|
+
if (method === 'notifications/resources/updated') {
|
|
542
|
+
const uri = (notification.params as any)?.uri;
|
|
543
|
+
this.emit('resource-updated', { serverName: '', uri }); // Would need serverName from entry
|
|
544
|
+
// Refresh resources list
|
|
545
|
+
this.listResources('').catch(() => {}); // Best effort
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Emit generic notification event
|
|
549
|
+
this.emit('notification', {
|
|
550
|
+
method,
|
|
551
|
+
params: notification.params,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private handleDisconnect(serverName: string): void {
|
|
556
|
+
const entry = this.servers.get(serverName);
|
|
557
|
+
if (!entry) return;
|
|
558
|
+
|
|
559
|
+
entry.connected = false;
|
|
560
|
+
this.emit('disconnected', { serverName });
|
|
561
|
+
|
|
562
|
+
// Try auto-reconnect
|
|
563
|
+
const config = this.configs.get(serverName);
|
|
564
|
+
if (config?.autoReconnect && entry.reconnectAttempts < (config.maxReconnectAttempts || 3)) {
|
|
565
|
+
entry.reconnectAttempts++;
|
|
566
|
+
entry.lastError = `Process exited unexpectedly (attempt ${entry.reconnectAttempts})`;
|
|
567
|
+
|
|
568
|
+
const interval = config.reconnectInterval || 5000;
|
|
569
|
+
this.emit('reconnecting', {
|
|
570
|
+
serverName,
|
|
571
|
+
attempt: entry.reconnectAttempts,
|
|
572
|
+
maxAttempts: config.maxReconnectAttempts || 3,
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
setTimeout(async () => {
|
|
576
|
+
try {
|
|
577
|
+
await this.disconnect(serverName);
|
|
578
|
+
const newEntry = await this.createClient(config);
|
|
579
|
+
newEntry.reconnectAttempts = entry.reconnectAttempts;
|
|
580
|
+
this.servers.set(serverName, newEntry);
|
|
581
|
+
|
|
582
|
+
newEntry.process.on('exit', () => this.handleDisconnect(serverName));
|
|
583
|
+
newEntry.process.on('error', (err) => this.handleProcessError(serverName, err));
|
|
584
|
+
|
|
585
|
+
this.emit('reconnected', { serverName, toolCount: newEntry.tools.length });
|
|
586
|
+
} catch (err) {
|
|
587
|
+
this.emit('reconnect-failed', {
|
|
588
|
+
serverName,
|
|
589
|
+
error: (err as Error).message,
|
|
590
|
+
attempt: entry.reconnectAttempts,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}, interval);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
private handleProcessError(serverName: string, err: Error): void {
|
|
598
|
+
const entry = this.servers.get(serverName);
|
|
599
|
+
if (entry) {
|
|
600
|
+
entry.lastError = err.message;
|
|
601
|
+
}
|
|
602
|
+
// Use 'serverError' instead of 'error' to avoid EventEmitter unhandled error crash
|
|
603
|
+
this.emit('serverError', { serverName, error: err.message });
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ========================================================================
|
|
607
|
+
// Private: JSON-RPC
|
|
608
|
+
// ========================================================================
|
|
609
|
+
|
|
610
|
+
private sendJsonRpc(entry: McpClientEntry, method: string, params: unknown): Promise<unknown> {
|
|
611
|
+
return new Promise((resolve, reject) => {
|
|
612
|
+
const id = ++entry.requestId;
|
|
613
|
+
const request: JsonRpcMessage = { jsonrpc: '2.0', id, method, params };
|
|
614
|
+
|
|
615
|
+
const timer = setTimeout(() => {
|
|
616
|
+
entry.pendingRequests.delete(id);
|
|
617
|
+
reject(new Error(`MCP request "${method}" timed out after 30s`));
|
|
618
|
+
}, 30000);
|
|
619
|
+
|
|
620
|
+
entry.pendingRequests.set(id, { resolve, reject, timer });
|
|
621
|
+
|
|
622
|
+
if (!entry.process.stdin || entry.process.stdin.destroyed) {
|
|
623
|
+
clearTimeout(timer);
|
|
624
|
+
entry.pendingRequests.delete(id);
|
|
625
|
+
reject(new Error('MCP server process stdin is closed'));
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
entry.process.stdin.write(JSON.stringify(request) + '\n');
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModelClient.d.ts","sourceRoot":"","sources":["ModelClient.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,OAAO,EAKP,SAAS,EACV,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAMxD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AAChG,OAAO,KAAK,EAAiB,mBAAmB,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEjG,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,WAAW,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,cAAc,GAAG,aAAa,GAAG,QAAQ,CAAC;IAClG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;CACzC;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,WAAW,CAAS;gBAEhB,OAAO,EAAE,wBAAwB;IA8F7C,8CAA8C;IACxC,QAAQ,CACZ,QAAQ,EAAE,OAAO,EAAE,EACnB,KAAK,EAAE,cAAc,EAAE,EACvB,SAAS,EAAE,SAAS,EACpB,OAAO,CAAC,EAAE,OAAO,CAAC,mBAAmB,CAAC,GACrC,OAAO,CAAC,aAAa,CAAC;IAwBzB,0CAA0C;IACnC,MAAM,CACX,QAAQ,EAAE,OAAO,EAAE,EACnB,KAAK,EAAE,cAAc,EAAE,EACvB,SAAS,EAAE,SAAS,EACpB,OAAO,CAAC,EAAE,OAAO,CAAC,mBAAmB,CAAC,GACrC,cAAc,CAAC,WAAW,CAAC;IAyB9B,qDAAqD;IAC/C,WAAW,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAIvD,iCAAiC;IACjC,QAAQ,IAAI,MAAM;IAIlB,4BAA4B;IAC5B,eAAe,IAAI,MAAM;IAIzB,8BAA8B;IAC9B,aAAa,CAAC,OAAO,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;CAK3F"}
|