deeper-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +254 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +12067 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +415 -0
- package/dist/index.js +1599 -0
- package/dist/index.js.map +1 -0
- package/docs/superpowers/plans/2026-05-14-deepercode-implementation.md +24 -0
- package/docs/superpowers/plans/2026-05-14-deepercode-plan.md +1248 -0
- package/docs/superpowers/specs/2026-05-14-deepercode-design.md +560 -0
- package/package.json +60 -0
- package/src/cli/bootstrap.ts +69 -0
- package/src/cli/chat-repl.ts +932 -0
- package/src/cli/commands/chat.ts +39 -0
- package/src/cli/commands/chat.tsx +39 -0
- package/src/cli/commands/config.ts +133 -0
- package/src/cli/commands/mcp.ts +172 -0
- package/src/cli/commands/run.ts +147 -0
- package/src/cli/commands/skill.ts +152 -0
- package/src/cli/index.ts +184 -0
- package/src/core/bugscan.ts +145 -0
- package/src/core/config.ts +285 -0
- package/src/core/constants.ts +49 -0
- package/src/core/eventbus.ts +202 -0
- package/src/core/logger.ts +109 -0
- package/src/core/storage.ts +96 -0
- package/src/index.ts +26 -0
- package/src/mcp/ConfigLoader.ts +74 -0
- package/src/mcp/MCPClient.ts +326 -0
- package/src/mcp/ResourceAdapter.ts +58 -0
- package/src/mcp/SSETransport.ts +133 -0
- package/src/mcp/StdioTransport.ts +116 -0
- package/src/mcp/ToolAdapter.ts +71 -0
- package/src/mcp/types.ts +58 -0
- package/src/memory/xmemory.ts +275 -0
- package/src/model/DeepSeekClient.ts +292 -0
- package/src/model/MessageBuilder.ts +155 -0
- package/src/model/RetryManager.ts +82 -0
- package/src/model/StreamHandler.ts +158 -0
- package/src/model/types.ts +86 -0
- package/src/skills/SkillCreator.ts +153 -0
- package/src/skills/SkillEngine.ts +158 -0
- package/src/skills/SkillExecutor.ts +107 -0
- package/src/skills/SkillLoader.ts +182 -0
- package/src/skills/SkillRegistry.ts +73 -0
- package/src/skills/SkillTrigger.ts +82 -0
- package/src/skills/types.ts +28 -0
- package/src/tools/ToolExecutor.ts +103 -0
- package/src/tools/ToolRegistry.ts +71 -0
- package/src/tools/ToolValidator.ts +103 -0
- package/src/tools/builtin/ai/context_summarize.ts +76 -0
- package/src/tools/builtin/ai/memory_store.ts +86 -0
- package/src/tools/builtin/ai/prompt_template.ts +71 -0
- package/src/tools/builtin/ai/skill_create.ts +53 -0
- package/src/tools/builtin/ai/subagent.ts +39 -0
- package/src/tools/builtin/ai/todo_manager.ts +157 -0
- package/src/tools/builtin/ai/token_count.ts +196 -0
- package/src/tools/builtin/ai/tool_create.ts +52 -0
- package/src/tools/builtin/code/analyze_deps.ts +72 -0
- package/src/tools/builtin/code/bug_scan.ts +80 -0
- package/src/tools/builtin/code/code_metrics.ts +111 -0
- package/src/tools/builtin/code/extract_function.ts +86 -0
- package/src/tools/builtin/code/format_code.ts +57 -0
- package/src/tools/builtin/code/generate_code.ts +75 -0
- package/src/tools/builtin/code/import_organizer.ts +82 -0
- package/src/tools/builtin/code/lint_code.ts +48 -0
- package/src/tools/builtin/code/parse_ast.ts +86 -0
- package/src/tools/builtin/code/refactor_code.ts +63 -0
- package/src/tools/builtin/code/type_check.ts +48 -0
- package/src/tools/builtin/data/chart_generate.ts +62 -0
- package/src/tools/builtin/data/csv_parse.ts +56 -0
- package/src/tools/builtin/data/data_diff.ts +79 -0
- package/src/tools/builtin/data/data_transform.ts +74 -0
- package/src/tools/builtin/data/data_validate.ts +75 -0
- package/src/tools/builtin/data/json_parse.ts +71 -0
- package/src/tools/builtin/data/template_render.ts +58 -0
- package/src/tools/builtin/data/toml_parse.ts +42 -0
- package/src/tools/builtin/data/xml_parse.ts +79 -0
- package/src/tools/builtin/data/yaml_parse.ts +42 -0
- package/src/tools/builtin/database/db_backup.ts +53 -0
- package/src/tools/builtin/database/db_restore.ts +51 -0
- package/src/tools/builtin/database/db_schema.ts +66 -0
- package/src/tools/builtin/database/nosql_query.ts +50 -0
- package/src/tools/builtin/database/orm_generate.ts +66 -0
- package/src/tools/builtin/database/redis_command.ts +46 -0
- package/src/tools/builtin/database/sql_migrate.ts +55 -0
- package/src/tools/builtin/database/sql_query.ts +60 -0
- package/src/tools/builtin/filesystem/batch_read.ts +56 -0
- package/src/tools/builtin/filesystem/batch_write.ts +67 -0
- package/src/tools/builtin/filesystem/copy_file.ts +36 -0
- package/src/tools/builtin/filesystem/create_dir.ts +30 -0
- package/src/tools/builtin/filesystem/delete_file.ts +30 -0
- package/src/tools/builtin/filesystem/diff_files.ts +47 -0
- package/src/tools/builtin/filesystem/edit_file.ts +47 -0
- package/src/tools/builtin/filesystem/file_info.ts +52 -0
- package/src/tools/builtin/filesystem/glob_find.ts +44 -0
- package/src/tools/builtin/filesystem/list_dir.ts +51 -0
- package/src/tools/builtin/filesystem/merge_files.ts +44 -0
- package/src/tools/builtin/filesystem/move_file.ts +37 -0
- package/src/tools/builtin/filesystem/read_file.ts +55 -0
- package/src/tools/builtin/filesystem/watch_file.ts +33 -0
- package/src/tools/builtin/filesystem/write_file.ts +45 -0
- package/src/tools/builtin/index.ts +244 -0
- package/src/tools/builtin/network/api_call.ts +79 -0
- package/src/tools/builtin/network/browser_action.ts +54 -0
- package/src/tools/builtin/network/check_url.ts +59 -0
- package/src/tools/builtin/network/download_file.ts +64 -0
- package/src/tools/builtin/network/graphql_query.ts +46 -0
- package/src/tools/builtin/network/http_request.ts +61 -0
- package/src/tools/builtin/network/parse_html.ts +101 -0
- package/src/tools/builtin/network/proxy_request.ts +53 -0
- package/src/tools/builtin/network/screenshot_page.ts +58 -0
- package/src/tools/builtin/network/web_fetch.ts +70 -0
- package/src/tools/builtin/network/web_search.ts +128 -0
- package/src/tools/builtin/network/websocket_connect.ts +70 -0
- package/src/tools/builtin/project/build_project.ts +68 -0
- package/src/tools/builtin/project/config_manage.ts +99 -0
- package/src/tools/builtin/project/coverage_report.ts +59 -0
- package/src/tools/builtin/project/docker_manage.ts +90 -0
- package/src/tools/builtin/project/env_manage.ts +88 -0
- package/src/tools/builtin/project/npm_manage.ts +71 -0
- package/src/tools/builtin/project/project_init.ts +59 -0
- package/src/tools/builtin/project/run_test.ts +74 -0
- package/src/tools/builtin/search/codebase_search.ts +76 -0
- package/src/tools/builtin/search/find_definition.ts +84 -0
- package/src/tools/builtin/search/find_references.ts +75 -0
- package/src/tools/builtin/search/fuzzy_find.ts +75 -0
- package/src/tools/builtin/search/grep_search.ts +90 -0
- package/src/tools/builtin/search/regex_find.ts +91 -0
- package/src/tools/builtin/search/search_docs.ts +51 -0
- package/src/tools/builtin/search/search_package.ts +50 -0
- package/src/tools/builtin/search/symbol_search.ts +82 -0
- package/src/tools/builtin/search/text_search.ts +63 -0
- package/src/tools/builtin/security/decrypt_file.ts +54 -0
- package/src/tools/builtin/security/encrypt_file.ts +52 -0
- package/src/tools/builtin/security/hash_generate.ts +48 -0
- package/src/tools/builtin/security/jwt_decode.ts +53 -0
- package/src/tools/builtin/security/secret_scan.ts +82 -0
- package/src/tools/builtin/security/vulnerability_check.ts +71 -0
- package/src/tools/builtin/shell/background_terminal.ts +38 -0
- package/src/tools/builtin/shell/check_status.ts +48 -0
- package/src/tools/builtin/shell/interactive_terminal.ts +31 -0
- package/src/tools/builtin/shell/kill_terminal.ts +29 -0
- package/src/tools/builtin/shell/list_terminals.ts +61 -0
- package/src/tools/builtin/shell/pipe_commands.ts +55 -0
- package/src/tools/builtin/shell/process-pool.ts +150 -0
- package/src/tools/builtin/shell/run_async.ts +73 -0
- package/src/tools/builtin/shell/run_command.ts +60 -0
- package/src/tools/builtin/shell/send_ctrl_keys.ts +43 -0
- package/src/tools/builtin/shell/send_keys.ts +36 -0
- package/src/tools/builtin/shell/send_text.ts +35 -0
- package/src/tools/builtin/shell/shell_script.ts +65 -0
- package/src/tools/builtin/shell/stop_command.ts +40 -0
- package/src/tools/builtin/shell/terminal_resize.ts +31 -0
- package/src/tools/builtin/shell/terminal_screenshot.ts +28 -0
- package/src/tools/builtin/system/log_viewer.ts +89 -0
- package/src/tools/builtin/system/notify_user.ts +55 -0
- package/src/tools/builtin/system/process_list.ts +66 -0
- package/src/tools/builtin/system/resource_monitor.ts +66 -0
- package/src/tools/builtin/system/system_info.ts +41 -0
- package/src/tools/tool-types.ts +97 -0
- package/src/ui/AgentTree.tsx +98 -0
- package/src/ui/App.tsx +46 -0
- package/src/ui/ChatView.tsx +278 -0
- package/src/ui/ConfirmDialog.tsx +68 -0
- package/src/ui/DiffView.tsx +64 -0
- package/src/ui/FilePreview.tsx +59 -0
- package/src/ui/InputBox.tsx +267 -0
- package/src/ui/MessageBubble.tsx +30 -0
- package/src/ui/Spinner.tsx +35 -0
- package/src/ui/StatusBar.tsx +41 -0
- package/src/ui/ToolCallCard.tsx +73 -0
- package/src/ui/ansi.ts +50 -0
- package/src/ui/markdown.ts +238 -0
- package/src/ui/themes/dark.ts +4 -0
- package/src/ui/themes/default.ts +25 -0
- package/src/ui/themes/light.ts +14 -0
- package/tests/unit/BuiltinTools.test.ts +129 -0
- package/tests/unit/BuiltinToolsIntegration.test.ts +111 -0
- package/tests/unit/FilesystemTools.test.ts +211 -0
- package/tests/unit/SkillLoader.test.ts +141 -0
- package/tests/unit/SkillRegistry.test.ts +113 -0
- package/tests/unit/ToolExecutor.test.ts +160 -0
- package/tests/unit/ToolRegistry.test.ts +103 -0
- package/tests/unit/ToolValidator.test.ts +137 -0
- package/tsconfig.json +28 -0
- package/tsup.config.ts +17 -0
- package/vitest.config.ts +20 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { EventBus, Events } from '../core/eventbus.js';
|
|
2
|
+
import { StdioTransport } from './StdioTransport.js';
|
|
3
|
+
import { SSETransport } from './SSETransport.js';
|
|
4
|
+
import { ToolAdapter } from './ToolAdapter.js';
|
|
5
|
+
import { ResourceAdapter } from './ResourceAdapter.js';
|
|
6
|
+
import type {
|
|
7
|
+
MCPServerConfig,
|
|
8
|
+
MCPTool,
|
|
9
|
+
MCPResource,
|
|
10
|
+
MCPTransport,
|
|
11
|
+
JSONRPCMessage,
|
|
12
|
+
JSONRPCResponse,
|
|
13
|
+
} from './types.js';
|
|
14
|
+
import type { Tool, ToolResult } from '../tools/tool-types.js';
|
|
15
|
+
import { MCP_CONNECTION_TIMEOUT_MS } from '../core/constants.js';
|
|
16
|
+
|
|
17
|
+
interface ServerConnection {
|
|
18
|
+
config: MCPServerConfig;
|
|
19
|
+
transport: MCPTransport;
|
|
20
|
+
tools: MCPTool[];
|
|
21
|
+
resources: MCPResource[];
|
|
22
|
+
connected: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class MCPClient {
|
|
26
|
+
private servers = new Map<string, ServerConnection>();
|
|
27
|
+
private eventbus: EventBus;
|
|
28
|
+
private toolAdapter: ToolAdapter;
|
|
29
|
+
private resourceAdapter: ResourceAdapter;
|
|
30
|
+
private requestIdCounter = 0;
|
|
31
|
+
|
|
32
|
+
constructor(eventbus?: EventBus) {
|
|
33
|
+
this.eventbus = eventbus || new EventBus();
|
|
34
|
+
this.toolAdapter = new ToolAdapter(this);
|
|
35
|
+
this.resourceAdapter = new ResourceAdapter(this);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async connect(config: MCPServerConfig): Promise<void> {
|
|
39
|
+
if (this.servers.has(config.name)) {
|
|
40
|
+
await this.disconnect(config.name);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const transport: MCPTransport = config.type === 'stdio'
|
|
44
|
+
? new StdioTransport()
|
|
45
|
+
: new SSETransport();
|
|
46
|
+
|
|
47
|
+
const connection: ServerConnection = {
|
|
48
|
+
config,
|
|
49
|
+
transport,
|
|
50
|
+
tools: [],
|
|
51
|
+
resources: [],
|
|
52
|
+
connected: false,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
this.servers.set(config.name, connection);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const connectPromise = transport.connect(config);
|
|
59
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
60
|
+
setTimeout(() => reject(new Error(`Connection timeout: ${config.name}`)), MCP_CONNECTION_TIMEOUT_MS);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await Promise.race([connectPromise, timeoutPromise]);
|
|
64
|
+
|
|
65
|
+
connection.connected = true;
|
|
66
|
+
|
|
67
|
+
transport.onMessage((message: JSONRPCMessage) => {
|
|
68
|
+
this.handleMessage(config.name, message);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await this.initializeServer(config.name);
|
|
72
|
+
|
|
73
|
+
this.eventbus.emit(Events.MCP_CONNECTED, {
|
|
74
|
+
serverName: config.name,
|
|
75
|
+
type: config.type,
|
|
76
|
+
});
|
|
77
|
+
} catch (error) {
|
|
78
|
+
transport.disconnect();
|
|
79
|
+
this.servers.delete(config.name);
|
|
80
|
+
|
|
81
|
+
this.eventbus.emit(Events.MCP_ERROR, {
|
|
82
|
+
serverName: config.name,
|
|
83
|
+
error: error instanceof Error ? error.message : String(error),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
disconnect(name: string): void {
|
|
91
|
+
const connection = this.servers.get(name);
|
|
92
|
+
if (!connection) return;
|
|
93
|
+
|
|
94
|
+
connection.transport.disconnect();
|
|
95
|
+
connection.connected = false;
|
|
96
|
+
this.servers.delete(name);
|
|
97
|
+
|
|
98
|
+
this.eventbus.emit(Events.MCP_DISCONNECTED, {
|
|
99
|
+
serverName: name,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
disconnectAll(): void {
|
|
104
|
+
for (const name of this.servers.keys()) {
|
|
105
|
+
this.disconnect(name);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
listTools(serverName?: string): MCPTool[] {
|
|
110
|
+
if (serverName) {
|
|
111
|
+
const connection = this.servers.get(serverName);
|
|
112
|
+
return connection ? [...connection.tools] : [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const allTools: MCPTool[] = [];
|
|
116
|
+
for (const [, conn] of this.servers) {
|
|
117
|
+
allTools.push(...conn.tools);
|
|
118
|
+
}
|
|
119
|
+
return allTools;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
getAdaptedTools(): Tool[] {
|
|
123
|
+
const tools: Tool[] = [];
|
|
124
|
+
for (const [name, conn] of this.servers) {
|
|
125
|
+
tools.push(...this.toolAdapter.adaptBatch(conn.tools, name));
|
|
126
|
+
}
|
|
127
|
+
return tools;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async callTool(
|
|
131
|
+
serverName: string,
|
|
132
|
+
toolName: string,
|
|
133
|
+
args: Record<string, unknown>,
|
|
134
|
+
): Promise<ToolResult> {
|
|
135
|
+
const connection = this.servers.get(serverName);
|
|
136
|
+
if (!connection) {
|
|
137
|
+
return {
|
|
138
|
+
success: false,
|
|
139
|
+
output: '',
|
|
140
|
+
error: `Server not connected: ${serverName}`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!connection.connected) {
|
|
145
|
+
return {
|
|
146
|
+
success: false,
|
|
147
|
+
output: '',
|
|
148
|
+
error: `Server disconnected: ${serverName}`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const requestId = ++this.requestIdCounter;
|
|
154
|
+
const response = await this.sendRequest(connection, requestId, 'tools/call', {
|
|
155
|
+
name: toolName,
|
|
156
|
+
arguments: args,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const result = response.result as Record<string, unknown> | undefined;
|
|
160
|
+
const content = result?.content;
|
|
161
|
+
|
|
162
|
+
if (Array.isArray(content)) {
|
|
163
|
+
const textParts = content
|
|
164
|
+
.filter((c: unknown) => {
|
|
165
|
+
const item = c as Record<string, unknown>;
|
|
166
|
+
return item?.type === 'text' && typeof item?.text === 'string';
|
|
167
|
+
})
|
|
168
|
+
.map((c: unknown) => (c as Record<string, string>).text);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
success: true,
|
|
172
|
+
output: textParts.join('\n'),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
success: true,
|
|
178
|
+
output: typeof content === 'string' ? content : JSON.stringify(content),
|
|
179
|
+
};
|
|
180
|
+
} catch (error) {
|
|
181
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
182
|
+
return {
|
|
183
|
+
success: false,
|
|
184
|
+
output: '',
|
|
185
|
+
error: errMsg,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async listResources(serverName: string): Promise<MCPResource[]> {
|
|
191
|
+
const connection = this.servers.get(serverName);
|
|
192
|
+
if (!connection) return [];
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const requestId = ++this.requestIdCounter;
|
|
196
|
+
const response = await this.sendRequest(connection, requestId, 'resources/list', {});
|
|
197
|
+
|
|
198
|
+
const result = response.result as Record<string, unknown> | undefined;
|
|
199
|
+
const resources = result?.resources;
|
|
200
|
+
|
|
201
|
+
if (Array.isArray(resources)) {
|
|
202
|
+
return resources as MCPResource[];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return [];
|
|
206
|
+
} catch {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async readResource(serverName: string, uri: string): Promise<string> {
|
|
212
|
+
const connection = this.servers.get(serverName);
|
|
213
|
+
if (!connection) {
|
|
214
|
+
throw new Error(`Server not connected: ${serverName}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const requestId = ++this.requestIdCounter;
|
|
218
|
+
const response = await this.sendRequest(connection, requestId, 'resources/read', { uri });
|
|
219
|
+
|
|
220
|
+
const result = response.result as Record<string, unknown> | undefined;
|
|
221
|
+
const contents = result?.contents;
|
|
222
|
+
|
|
223
|
+
if (Array.isArray(contents)) {
|
|
224
|
+
const textParts = contents
|
|
225
|
+
.filter((c: unknown) => {
|
|
226
|
+
const item = c as Record<string, unknown>;
|
|
227
|
+
return typeof item?.text === 'string';
|
|
228
|
+
})
|
|
229
|
+
.map((c: unknown) => (c as Record<string, string>).text);
|
|
230
|
+
|
|
231
|
+
return textParts.join('\n');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return JSON.stringify(contents);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
isConnected(name: string): boolean {
|
|
238
|
+
const connection = this.servers.get(name);
|
|
239
|
+
return connection ? connection.connected : false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
getConnectedServers(): string[] {
|
|
243
|
+
const names: string[] = [];
|
|
244
|
+
for (const [name, conn] of this.servers) {
|
|
245
|
+
if (conn.connected) {
|
|
246
|
+
names.push(name);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return names;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private async initializeServer(serverName: string): Promise<void> {
|
|
253
|
+
const connection = this.servers.get(serverName);
|
|
254
|
+
if (!connection) return;
|
|
255
|
+
|
|
256
|
+
const initRequestId = ++this.requestIdCounter;
|
|
257
|
+
const initResponse = await this.sendRequest(connection, initRequestId, 'initialize', {
|
|
258
|
+
protocolVersion: '2024-11-18',
|
|
259
|
+
capabilities: {
|
|
260
|
+
tools: {},
|
|
261
|
+
resources: {},
|
|
262
|
+
},
|
|
263
|
+
clientInfo: {
|
|
264
|
+
name: 'DeeperCode',
|
|
265
|
+
version: '1.0.0',
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const notifyMessage = {
|
|
270
|
+
jsonrpc: '2.0' as const,
|
|
271
|
+
method: 'notifications/initialized',
|
|
272
|
+
};
|
|
273
|
+
await connection.transport.send(notifyMessage);
|
|
274
|
+
|
|
275
|
+
const toolsRequestId = ++this.requestIdCounter;
|
|
276
|
+
const toolsResponse = await this.sendRequest(connection, toolsRequestId, 'tools/list', {});
|
|
277
|
+
|
|
278
|
+
const toolsResult = toolsResponse.result as Record<string, unknown> | undefined;
|
|
279
|
+
if (toolsResult?.tools && Array.isArray(toolsResult.tools)) {
|
|
280
|
+
connection.tools = toolsResult.tools as MCPTool[];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this.eventbus.emit(Events.MCP_TOOLS_DISCOVERED, {
|
|
284
|
+
serverName,
|
|
285
|
+
toolCount: connection.tools.length,
|
|
286
|
+
tools: connection.tools.map((t) => t.name),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private async sendRequest(
|
|
291
|
+
connection: ServerConnection,
|
|
292
|
+
requestId: number,
|
|
293
|
+
method: string,
|
|
294
|
+
params: Record<string, unknown>,
|
|
295
|
+
timeoutMs: number = 30000,
|
|
296
|
+
): Promise<JSONRPCResponse> {
|
|
297
|
+
const request = {
|
|
298
|
+
jsonrpc: '2.0' as const,
|
|
299
|
+
id: requestId,
|
|
300
|
+
method,
|
|
301
|
+
params,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const responsePromise = new Promise<JSONRPCResponse>((resolve, reject) => {
|
|
305
|
+
const timeout = setTimeout(() => {
|
|
306
|
+
cleanup();
|
|
307
|
+
reject(new Error(`Request timeout: ${method}`));
|
|
308
|
+
}, timeoutMs);
|
|
309
|
+
|
|
310
|
+
const cleanup = connection.transport.onMessage((message: JSONRPCMessage) => {
|
|
311
|
+
if ('id' in message && message.id === requestId) {
|
|
312
|
+
clearTimeout(timeout);
|
|
313
|
+
cleanup();
|
|
314
|
+
resolve(message as JSONRPCResponse);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
await connection.transport.send(request);
|
|
320
|
+
return responsePromise;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private handleMessage(_serverName: string, _message: JSONRPCMessage): void {
|
|
324
|
+
// Messages are handled through the request/response pattern in sendRequest
|
|
325
|
+
}
|
|
326
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Tool, ToolResult } from '../tools/tool-types.js';
|
|
2
|
+
import type { MCPResource } from './types.js';
|
|
3
|
+
import type { MCPClient } from './MCPClient.js';
|
|
4
|
+
|
|
5
|
+
export class ResourceAdapter {
|
|
6
|
+
private client: MCPClient;
|
|
7
|
+
|
|
8
|
+
constructor(client: MCPClient) {
|
|
9
|
+
this.client = client;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async readResource(serverName: string, uri: string): Promise<string> {
|
|
13
|
+
const content = await this.client.readResource(serverName, uri);
|
|
14
|
+
return content;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async listResources(serverName: string): Promise<MCPResource[]> {
|
|
18
|
+
return this.client.listResources(serverName);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
createResourceTools(resources: MCPResource[], serverName: string): Tool[] {
|
|
22
|
+
return resources.map((resource) => this.adaptResource(resource, serverName));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private adaptResource(resource: MCPResource, serverName: string): Tool {
|
|
26
|
+
const adaptedName = `mcp:${serverName}:resource:${resource.name}`;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
name: adaptedName,
|
|
30
|
+
description: `[MCP:${serverName}] Read resource: ${resource.name} (${resource.uri})`,
|
|
31
|
+
category: 'system',
|
|
32
|
+
parameters: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {},
|
|
35
|
+
required: [],
|
|
36
|
+
},
|
|
37
|
+
dangerous: false,
|
|
38
|
+
requiresApproval: false,
|
|
39
|
+
execute: async (_params: Record<string, unknown>, _signal?: AbortSignal): Promise<ToolResult> => {
|
|
40
|
+
try {
|
|
41
|
+
const content = await this.client.readResource(serverName, resource.uri);
|
|
42
|
+
return {
|
|
43
|
+
success: true,
|
|
44
|
+
output: content,
|
|
45
|
+
metadata: { uri: resource.uri, mimeType: resource.mimeType },
|
|
46
|
+
};
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
49
|
+
return {
|
|
50
|
+
success: false,
|
|
51
|
+
output: '',
|
|
52
|
+
error: `MCP resource error [${serverName}/${resource.name}]: ${errMsg}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { MCPServerConfig, JSONRPCMessage, MCPTransport } from './types.js';
|
|
2
|
+
|
|
3
|
+
export class SSETransport implements MCPTransport {
|
|
4
|
+
private url: string = '';
|
|
5
|
+
private headers: Record<string, string> = {};
|
|
6
|
+
private messageHandlers: Array<(message: JSONRPCMessage) => void> = [];
|
|
7
|
+
private connected: boolean = false;
|
|
8
|
+
private abortController: AbortController | null = null;
|
|
9
|
+
|
|
10
|
+
async connect(config: MCPServerConfig): Promise<void> {
|
|
11
|
+
if (!config.url) {
|
|
12
|
+
throw new Error('SSETransport requires a URL');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
this.url = config.url;
|
|
16
|
+
this.headers = config.headers || {};
|
|
17
|
+
this.abortController = new AbortController();
|
|
18
|
+
|
|
19
|
+
this.connected = true;
|
|
20
|
+
|
|
21
|
+
this.establishSSEConnection();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private async establishSSEConnection(): Promise<void> {
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetch(this.url, {
|
|
27
|
+
method: 'GET',
|
|
28
|
+
headers: {
|
|
29
|
+
Accept: 'text/event-stream',
|
|
30
|
+
...this.headers,
|
|
31
|
+
},
|
|
32
|
+
signal: this.abortController!.signal,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new Error(`SSE connection failed: ${response.status} ${response.statusText}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const reader = response.body?.getReader();
|
|
40
|
+
if (!reader) {
|
|
41
|
+
throw new Error('SSE response has no readable body');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const decoder = new TextDecoder();
|
|
45
|
+
let buffer = '';
|
|
46
|
+
|
|
47
|
+
while (this.connected) {
|
|
48
|
+
const { done, value } = await reader.read();
|
|
49
|
+
if (done) break;
|
|
50
|
+
|
|
51
|
+
buffer += decoder.decode(value, { stream: true });
|
|
52
|
+
const lines = buffer.split('\n');
|
|
53
|
+
buffer = lines.pop() || '';
|
|
54
|
+
|
|
55
|
+
let eventData = '';
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
if (line.startsWith('data: ')) {
|
|
58
|
+
eventData += line.slice(6);
|
|
59
|
+
} else if (line === '' && eventData) {
|
|
60
|
+
try {
|
|
61
|
+
const message = JSON.parse(eventData) as JSONRPCMessage;
|
|
62
|
+
for (const handler of this.messageHandlers) {
|
|
63
|
+
handler(message);
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// Ignore non-JSON SSE data
|
|
67
|
+
}
|
|
68
|
+
eventData = '';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.connected = false;
|
|
77
|
+
for (const handler of this.messageHandlers) {
|
|
78
|
+
handler({
|
|
79
|
+
jsonrpc: '2.0',
|
|
80
|
+
method: 'error',
|
|
81
|
+
params: { message: String(error) },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async send(message: JSONRPCMessage): Promise<void> {
|
|
88
|
+
if (!this.connected) {
|
|
89
|
+
throw new Error('SSETransport not connected');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const response = await fetch(this.url, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: {
|
|
95
|
+
'Content-Type': 'application/json',
|
|
96
|
+
...this.headers,
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify(message),
|
|
99
|
+
signal: this.abortController!.signal,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
throw new Error(`SSE POST failed: ${response.status} ${response.statusText}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const data = await response.json() as JSONRPCMessage;
|
|
107
|
+
for (const handler of this.messageHandlers) {
|
|
108
|
+
handler(data);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onMessage(handler: (message: JSONRPCMessage) => void): () => void {
|
|
113
|
+
this.messageHandlers.push(handler);
|
|
114
|
+
return () => {
|
|
115
|
+
const idx = this.messageHandlers.indexOf(handler);
|
|
116
|
+
if (idx !== -1) {
|
|
117
|
+
this.messageHandlers.splice(idx, 1);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
disconnect(): void {
|
|
123
|
+
this.connected = false;
|
|
124
|
+
if (this.abortController) {
|
|
125
|
+
this.abortController.abort();
|
|
126
|
+
this.abortController = null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
isConnected(): boolean {
|
|
131
|
+
return this.connected;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import type { ChildProcess } from 'node:child_process';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import type { MCPServerConfig, JSONRPCMessage, MCPTransport } from './types.js';
|
|
5
|
+
|
|
6
|
+
export class StdioTransport implements MCPTransport {
|
|
7
|
+
private process: ChildProcess | null = null;
|
|
8
|
+
private messageHandlers: Array<(message: JSONRPCMessage) => void> = [];
|
|
9
|
+
private connected: boolean = false;
|
|
10
|
+
private buffer: string = '';
|
|
11
|
+
|
|
12
|
+
async connect(config: MCPServerConfig): Promise<void> {
|
|
13
|
+
if (!config.command) {
|
|
14
|
+
throw new Error('StdioTransport requires a command');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const env = {
|
|
18
|
+
...process.env,
|
|
19
|
+
...config.env,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
this.process = spawn(config.command, config.args || [], {
|
|
23
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
24
|
+
env,
|
|
25
|
+
shell: true,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const rl = createInterface({ input: this.process.stdout! });
|
|
29
|
+
|
|
30
|
+
rl.on('line', (line: string) => {
|
|
31
|
+
try {
|
|
32
|
+
const message = JSON.parse(line) as JSONRPCMessage;
|
|
33
|
+
for (const handler of this.messageHandlers) {
|
|
34
|
+
handler(message);
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
this.buffer += line;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
this.process.stderr?.on('data', (data: Buffer) => {
|
|
42
|
+
const text = data.toString();
|
|
43
|
+
try {
|
|
44
|
+
const message = JSON.parse(text) as JSONRPCMessage;
|
|
45
|
+
for (const handler of this.messageHandlers) {
|
|
46
|
+
handler(message);
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// MCP stderr is for logging, not JSON-RPC
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.process.on('exit', (code) => {
|
|
54
|
+
this.connected = false;
|
|
55
|
+
if (code !== 0 && code !== null) {
|
|
56
|
+
for (const handler of this.messageHandlers) {
|
|
57
|
+
handler({
|
|
58
|
+
jsonrpc: '2.0',
|
|
59
|
+
method: 'exit',
|
|
60
|
+
params: { code },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
this.process.on('error', (err) => {
|
|
67
|
+
this.connected = false;
|
|
68
|
+
for (const handler of this.messageHandlers) {
|
|
69
|
+
handler({
|
|
70
|
+
jsonrpc: '2.0',
|
|
71
|
+
method: 'error',
|
|
72
|
+
params: { message: err.message },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
this.connected = true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async send(message: JSONRPCMessage): Promise<void> {
|
|
81
|
+
if (!this.process || !this.connected) {
|
|
82
|
+
throw new Error('StdioTransport not connected');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const line = JSON.stringify(message) + '\n';
|
|
86
|
+
await new Promise<void>((resolve, reject) => {
|
|
87
|
+
this.process!.stdin!.write(line, (err) => {
|
|
88
|
+
if (err) reject(err);
|
|
89
|
+
else resolve();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
onMessage(handler: (message: JSONRPCMessage) => void): () => void {
|
|
95
|
+
this.messageHandlers.push(handler);
|
|
96
|
+
return () => {
|
|
97
|
+
const idx = this.messageHandlers.indexOf(handler);
|
|
98
|
+
if (idx !== -1) {
|
|
99
|
+
this.messageHandlers.splice(idx, 1);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
disconnect(): void {
|
|
105
|
+
if (this.process) {
|
|
106
|
+
this.process.stdin?.end();
|
|
107
|
+
this.process.kill();
|
|
108
|
+
this.process = null;
|
|
109
|
+
}
|
|
110
|
+
this.connected = false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
isConnected(): boolean {
|
|
114
|
+
return this.connected && this.process !== null && !this.process.killed;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Tool, ToolResult } from '../tools/tool-types.js';
|
|
2
|
+
import type { MCPTool } from './types.js';
|
|
3
|
+
import type { MCPClient } from './MCPClient.js';
|
|
4
|
+
|
|
5
|
+
export class ToolAdapter {
|
|
6
|
+
private client: MCPClient;
|
|
7
|
+
|
|
8
|
+
constructor(client: MCPClient) {
|
|
9
|
+
this.client = client;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
adapt(mcpTool: MCPTool, serverName: string): Tool {
|
|
13
|
+
const adaptedName = `mcp:${serverName}:${mcpTool.name}`;
|
|
14
|
+
|
|
15
|
+
const tool: Tool = {
|
|
16
|
+
name: adaptedName,
|
|
17
|
+
description: `[MCP:${serverName}] ${mcpTool.description}`,
|
|
18
|
+
category: this.inferCategory(mcpTool) as string,
|
|
19
|
+
parameters: mcpTool.inputSchema,
|
|
20
|
+
dangerous: false,
|
|
21
|
+
requiresApproval: false,
|
|
22
|
+
execute: async (params: Record<string, unknown>, signal?: AbortSignal): Promise<ToolResult> => {
|
|
23
|
+
try {
|
|
24
|
+
const result = await this.client.callTool(serverName, mcpTool.name, params);
|
|
25
|
+
return result;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
28
|
+
return {
|
|
29
|
+
success: false,
|
|
30
|
+
output: '',
|
|
31
|
+
error: `MCP tool error [${serverName}/${mcpTool.name}]: ${errMsg}`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return tool;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
adaptBatch(mcpTools: MCPTool[], serverName: string): Tool[] {
|
|
41
|
+
return mcpTools.map((t) => this.adapt(t, serverName));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private inferCategory(mcpTool: MCPTool): string {
|
|
45
|
+
const desc = (mcpTool.description + ' ' + mcpTool.name).toLowerCase();
|
|
46
|
+
|
|
47
|
+
if (desc.includes('file') || desc.includes('dir') || desc.includes('path')) {
|
|
48
|
+
return 'filesystem';
|
|
49
|
+
}
|
|
50
|
+
if (desc.includes('search') || desc.includes('find') || desc.includes('query')) {
|
|
51
|
+
return 'search';
|
|
52
|
+
}
|
|
53
|
+
if (desc.includes('shell') || desc.includes('command') || desc.includes('exec')) {
|
|
54
|
+
return 'shell';
|
|
55
|
+
}
|
|
56
|
+
if (desc.includes('http') || desc.includes('api') || desc.includes('fetch') || desc.includes('url')) {
|
|
57
|
+
return 'network';
|
|
58
|
+
}
|
|
59
|
+
if (desc.includes('code') || desc.includes('lint') || desc.includes('compile')) {
|
|
60
|
+
return 'code';
|
|
61
|
+
}
|
|
62
|
+
if (desc.includes('db') || desc.includes('sql') || desc.includes('database')) {
|
|
63
|
+
return 'database';
|
|
64
|
+
}
|
|
65
|
+
if (desc.includes('security') || desc.includes('auth') || desc.includes('token')) {
|
|
66
|
+
return 'security';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return 'system';
|
|
70
|
+
}
|
|
71
|
+
}
|