ikie-cli 0.1.24 → 0.1.25
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/dist/agent.js +21 -0
- package/dist/mcp-manager.d.ts +85 -0
- package/dist/mcp-manager.js +336 -0
- package/dist/tools.js +303 -33
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -948,6 +948,27 @@ changes they didn't ask for.
|
|
|
948
948
|
- \`ask_user\`: Ask the user a clarifying question when you need more info to proceed.
|
|
949
949
|
The user's answer is returned as the tool result. Use sparingly — only when genuinely
|
|
950
950
|
unsure. Don't ask for confirmation on safe operations.
|
|
951
|
+
|
|
952
|
+
## MCP (Model Context Protocol) System
|
|
953
|
+
- \`mcp_list\`: List all installed MCP servers and their available tools. MCPs extend ikie
|
|
954
|
+
with specialized capabilities like GitHub API, database access, browser automation, etc.
|
|
955
|
+
- \`mcp_install\`: Install a new MCP server from npm, git URL, or local path.
|
|
956
|
+
- \`mcp_start\`: Start an MCP server to make its tools available for use.
|
|
957
|
+
- \`mcp_stop\`: Stop a running MCP server.
|
|
958
|
+
- \`mcp_call\`: Call a tool from a running MCP. Use \`mcp_list\` first to see available tools.
|
|
959
|
+
- \`mcp_uninstall\`: Remove an installed MCP (built-in MCPs cannot be uninstalled).
|
|
960
|
+
|
|
961
|
+
**Built-in MCPs:**
|
|
962
|
+
- **filesystem**: Enhanced file operations (read multiple files, directory trees)
|
|
963
|
+
- **github**: GitHub API operations (search repos, manage issues, read files from repos)
|
|
964
|
+
- **database**: Database operations for SQLite and PostgreSQL
|
|
965
|
+
- **puppeteer**: Browser automation and web scraping
|
|
966
|
+
|
|
967
|
+
**When to use MCPs:** When you need specialized functionality beyond basic tools. For example:
|
|
968
|
+
- Use GitHub MCP to interact with repositories, issues, pull requests
|
|
969
|
+
- Use database MCP to query and manage databases
|
|
970
|
+
- Use puppeteer MCP for complex web interactions
|
|
971
|
+
- Install custom MCPs for domain-specific needs (Slack, Jira, AWS, etc.)
|
|
951
972
|
`,
|
|
952
973
|
];
|
|
953
974
|
if (skillsCatalog) {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export interface MCPTool {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: string;
|
|
6
|
+
properties: Record<string, any>;
|
|
7
|
+
required?: string[];
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export interface MCPServer {
|
|
11
|
+
name: string;
|
|
12
|
+
command: string;
|
|
13
|
+
args: string[];
|
|
14
|
+
env?: Record<string, string>;
|
|
15
|
+
description: string;
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
autoStart: boolean;
|
|
18
|
+
tools?: MCPTool[];
|
|
19
|
+
}
|
|
20
|
+
export interface MCPRegistry {
|
|
21
|
+
servers: Record<string, MCPServer>;
|
|
22
|
+
builtIn: string[];
|
|
23
|
+
}
|
|
24
|
+
export declare class MCPManager {
|
|
25
|
+
private registry;
|
|
26
|
+
private processes;
|
|
27
|
+
private messageHandlers;
|
|
28
|
+
constructor();
|
|
29
|
+
private ensureMCPDir;
|
|
30
|
+
private loadRegistry;
|
|
31
|
+
private saveRegistry;
|
|
32
|
+
private initializeBuiltInServers;
|
|
33
|
+
installMCP(config: {
|
|
34
|
+
name: string;
|
|
35
|
+
source: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
env?: Record<string, string>;
|
|
38
|
+
autoStart?: boolean;
|
|
39
|
+
}): Promise<{
|
|
40
|
+
success: boolean;
|
|
41
|
+
error?: string;
|
|
42
|
+
server?: MCPServer;
|
|
43
|
+
}>;
|
|
44
|
+
uninstallMCP(name: string): {
|
|
45
|
+
success: boolean;
|
|
46
|
+
error?: string;
|
|
47
|
+
};
|
|
48
|
+
startMCP(name: string): Promise<{
|
|
49
|
+
success: boolean;
|
|
50
|
+
error?: string;
|
|
51
|
+
tools?: MCPTool[];
|
|
52
|
+
}>;
|
|
53
|
+
stopMCP(name: string): {
|
|
54
|
+
success: boolean;
|
|
55
|
+
error?: string;
|
|
56
|
+
};
|
|
57
|
+
private initializeMCP;
|
|
58
|
+
private requestMCPTools;
|
|
59
|
+
private sendMCPMessage;
|
|
60
|
+
private handleMCPMessage;
|
|
61
|
+
callMCPTool(serverName: string, toolName: string, args: Record<string, any>): Promise<{
|
|
62
|
+
success: boolean;
|
|
63
|
+
result?: any;
|
|
64
|
+
error?: string;
|
|
65
|
+
}>;
|
|
66
|
+
getAllTools(): Array<{
|
|
67
|
+
server: string;
|
|
68
|
+
tool: MCPTool;
|
|
69
|
+
}>;
|
|
70
|
+
listMCPs(): Array<{
|
|
71
|
+
name: string;
|
|
72
|
+
description: string;
|
|
73
|
+
enabled: boolean;
|
|
74
|
+
running: boolean;
|
|
75
|
+
builtIn: boolean;
|
|
76
|
+
tools?: MCPTool[];
|
|
77
|
+
}>;
|
|
78
|
+
setMCPEnabled(name: string, enabled: boolean): {
|
|
79
|
+
success: boolean;
|
|
80
|
+
error?: string;
|
|
81
|
+
};
|
|
82
|
+
startAutoStartMCPs(): Promise<void>;
|
|
83
|
+
stopAllMCPs(): void;
|
|
84
|
+
}
|
|
85
|
+
export declare function getMCPManager(): MCPManager;
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { HOME_DIR } from './config.js';
|
|
5
|
+
const MCP_REGISTRY_FILE = join(HOME_DIR, 'mcp-registry.json');
|
|
6
|
+
const MCP_DIR = join(HOME_DIR, 'mcps');
|
|
7
|
+
export class MCPManager {
|
|
8
|
+
registry;
|
|
9
|
+
processes = new Map();
|
|
10
|
+
messageHandlers = new Map();
|
|
11
|
+
constructor() {
|
|
12
|
+
this.ensureMCPDir();
|
|
13
|
+
this.registry = this.loadRegistry();
|
|
14
|
+
this.initializeBuiltInServers();
|
|
15
|
+
}
|
|
16
|
+
ensureMCPDir() {
|
|
17
|
+
if (!existsSync(MCP_DIR)) {
|
|
18
|
+
mkdirSync(MCP_DIR, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
loadRegistry() {
|
|
22
|
+
if (existsSync(MCP_REGISTRY_FILE)) {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(readFileSync(MCP_REGISTRY_FILE, 'utf-8'));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return { servers: {}, builtIn: [] };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { servers: {}, builtIn: [] };
|
|
31
|
+
}
|
|
32
|
+
saveRegistry() {
|
|
33
|
+
writeFileSync(MCP_REGISTRY_FILE, JSON.stringify(this.registry, null, 2));
|
|
34
|
+
}
|
|
35
|
+
initializeBuiltInServers() {
|
|
36
|
+
const builtIn = {
|
|
37
|
+
filesystem: {
|
|
38
|
+
name: 'filesystem',
|
|
39
|
+
command: 'node',
|
|
40
|
+
args: [join(MCP_DIR, 'filesystem-server.js')],
|
|
41
|
+
description: 'File system operations MCP',
|
|
42
|
+
enabled: true,
|
|
43
|
+
autoStart: true,
|
|
44
|
+
},
|
|
45
|
+
github: {
|
|
46
|
+
name: 'github',
|
|
47
|
+
command: 'node',
|
|
48
|
+
args: [join(MCP_DIR, 'github-server.js')],
|
|
49
|
+
env: { GITHUB_TOKEN: process.env.GITHUB_TOKEN || '' },
|
|
50
|
+
description: 'GitHub API operations MCP',
|
|
51
|
+
enabled: false,
|
|
52
|
+
autoStart: false,
|
|
53
|
+
},
|
|
54
|
+
database: {
|
|
55
|
+
name: 'database',
|
|
56
|
+
command: 'node',
|
|
57
|
+
args: [join(MCP_DIR, 'database-server.js')],
|
|
58
|
+
description: 'Database operations MCP (SQLite, PostgreSQL)',
|
|
59
|
+
enabled: false,
|
|
60
|
+
autoStart: false,
|
|
61
|
+
},
|
|
62
|
+
puppeteer: {
|
|
63
|
+
name: 'puppeteer',
|
|
64
|
+
command: 'node',
|
|
65
|
+
args: [join(MCP_DIR, 'puppeteer-server.js')],
|
|
66
|
+
description: 'Browser automation MCP',
|
|
67
|
+
enabled: false,
|
|
68
|
+
autoStart: false,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
for (const [name, server] of Object.entries(builtIn)) {
|
|
72
|
+
if (!this.registry.servers[name]) {
|
|
73
|
+
this.registry.servers[name] = server;
|
|
74
|
+
if (!this.registry.builtIn.includes(name)) {
|
|
75
|
+
this.registry.builtIn.push(name);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
this.saveRegistry();
|
|
80
|
+
}
|
|
81
|
+
async installMCP(config) {
|
|
82
|
+
try {
|
|
83
|
+
if (this.registry.servers[config.name]) {
|
|
84
|
+
return { success: false, error: `MCP '${config.name}' already installed` };
|
|
85
|
+
}
|
|
86
|
+
let command;
|
|
87
|
+
let args;
|
|
88
|
+
if (config.source.startsWith('npm:')) {
|
|
89
|
+
const pkg = config.source.slice(4);
|
|
90
|
+
const { execSync } = await import('child_process');
|
|
91
|
+
execSync(`npm install -g ${pkg}`, { cwd: MCP_DIR });
|
|
92
|
+
command = pkg;
|
|
93
|
+
args = [];
|
|
94
|
+
}
|
|
95
|
+
else if (config.source.startsWith('http://') || config.source.startsWith('https://')) {
|
|
96
|
+
const { execSync } = await import('child_process');
|
|
97
|
+
const repoName = config.source.split('/').pop()?.replace('.git', '') || config.name;
|
|
98
|
+
const repoPath = join(MCP_DIR, repoName);
|
|
99
|
+
execSync(`git clone ${config.source} ${repoPath}`, { cwd: MCP_DIR });
|
|
100
|
+
execSync('npm install', { cwd: repoPath });
|
|
101
|
+
command = 'node';
|
|
102
|
+
args = [join(repoPath, 'index.js')];
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
command = 'node';
|
|
106
|
+
args = [config.source];
|
|
107
|
+
}
|
|
108
|
+
const server = {
|
|
109
|
+
name: config.name,
|
|
110
|
+
command,
|
|
111
|
+
args,
|
|
112
|
+
env: config.env,
|
|
113
|
+
description: config.description || 'Custom MCP server',
|
|
114
|
+
enabled: true,
|
|
115
|
+
autoStart: config.autoStart ?? false,
|
|
116
|
+
};
|
|
117
|
+
this.registry.servers[config.name] = server;
|
|
118
|
+
this.saveRegistry();
|
|
119
|
+
return { success: true, server };
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
return {
|
|
123
|
+
success: false,
|
|
124
|
+
error: error instanceof Error ? error.message : String(error),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
uninstallMCP(name) {
|
|
129
|
+
if (!this.registry.servers[name]) {
|
|
130
|
+
return { success: false, error: `MCP '${name}' not found` };
|
|
131
|
+
}
|
|
132
|
+
if (this.registry.builtIn.includes(name)) {
|
|
133
|
+
return { success: false, error: `Cannot uninstall built-in MCP '${name}'` };
|
|
134
|
+
}
|
|
135
|
+
if (this.processes.has(name)) {
|
|
136
|
+
this.stopMCP(name);
|
|
137
|
+
}
|
|
138
|
+
delete this.registry.servers[name];
|
|
139
|
+
this.saveRegistry();
|
|
140
|
+
return { success: true };
|
|
141
|
+
}
|
|
142
|
+
async startMCP(name) {
|
|
143
|
+
const server = this.registry.servers[name];
|
|
144
|
+
if (!server) {
|
|
145
|
+
return { success: false, error: `MCP '${name}' not found` };
|
|
146
|
+
}
|
|
147
|
+
if (!server.enabled) {
|
|
148
|
+
return { success: false, error: `MCP '${name}' is disabled` };
|
|
149
|
+
}
|
|
150
|
+
if (this.processes.has(name)) {
|
|
151
|
+
return { success: false, error: `MCP '${name}' is already running` };
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const child = spawn(server.command, server.args, {
|
|
155
|
+
env: { ...process.env, ...server.env },
|
|
156
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
157
|
+
});
|
|
158
|
+
const mcpProcess = {
|
|
159
|
+
server,
|
|
160
|
+
process: child,
|
|
161
|
+
tools: [],
|
|
162
|
+
ready: false,
|
|
163
|
+
};
|
|
164
|
+
this.processes.set(name, mcpProcess);
|
|
165
|
+
child.stdout?.on('data', (data) => {
|
|
166
|
+
this.handleMCPMessage(name, data.toString());
|
|
167
|
+
});
|
|
168
|
+
child.stderr?.on('data', (data) => {
|
|
169
|
+
console.error(`MCP ${name} error:`, data.toString());
|
|
170
|
+
});
|
|
171
|
+
child.on('exit', (code) => {
|
|
172
|
+
console.log(`MCP ${name} exited with code ${code}`);
|
|
173
|
+
this.processes.delete(name);
|
|
174
|
+
});
|
|
175
|
+
await this.initializeMCP(name);
|
|
176
|
+
return { success: true, tools: mcpProcess.tools };
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
return {
|
|
180
|
+
success: false,
|
|
181
|
+
error: error instanceof Error ? error.message : String(error),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
stopMCP(name) {
|
|
186
|
+
const mcpProcess = this.processes.get(name);
|
|
187
|
+
if (!mcpProcess) {
|
|
188
|
+
return { success: false, error: `MCP '${name}' is not running` };
|
|
189
|
+
}
|
|
190
|
+
mcpProcess.process.kill();
|
|
191
|
+
this.processes.delete(name);
|
|
192
|
+
return { success: true };
|
|
193
|
+
}
|
|
194
|
+
async initializeMCP(name) {
|
|
195
|
+
const mcpProcess = this.processes.get(name);
|
|
196
|
+
if (!mcpProcess)
|
|
197
|
+
return;
|
|
198
|
+
this.sendMCPMessage(name, {
|
|
199
|
+
jsonrpc: '2.0',
|
|
200
|
+
id: 1,
|
|
201
|
+
method: 'initialize',
|
|
202
|
+
params: {
|
|
203
|
+
protocolVersion: '2024-11-05',
|
|
204
|
+
capabilities: {
|
|
205
|
+
roots: { listChanged: true },
|
|
206
|
+
},
|
|
207
|
+
clientInfo: {
|
|
208
|
+
name: 'ikie-cli',
|
|
209
|
+
version: '0.1.0',
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
await this.requestMCPTools(name);
|
|
214
|
+
}
|
|
215
|
+
async requestMCPTools(name) {
|
|
216
|
+
this.sendMCPMessage(name, {
|
|
217
|
+
jsonrpc: '2.0',
|
|
218
|
+
id: 2,
|
|
219
|
+
method: 'tools/list',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
sendMCPMessage(name, message) {
|
|
223
|
+
const mcpProcess = this.processes.get(name);
|
|
224
|
+
if (!mcpProcess)
|
|
225
|
+
return;
|
|
226
|
+
const json = JSON.stringify(message) + '\n';
|
|
227
|
+
mcpProcess.process.stdin?.write(json);
|
|
228
|
+
}
|
|
229
|
+
handleMCPMessage(name, data) {
|
|
230
|
+
const lines = data.split('\n').filter(Boolean);
|
|
231
|
+
for (const line of lines) {
|
|
232
|
+
try {
|
|
233
|
+
const message = JSON.parse(line);
|
|
234
|
+
if (message.result?.tools) {
|
|
235
|
+
const mcpProcess = this.processes.get(name);
|
|
236
|
+
if (mcpProcess) {
|
|
237
|
+
mcpProcess.tools = message.result.tools;
|
|
238
|
+
mcpProcess.ready = true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (message.id && this.messageHandlers.has(`${name}-${message.id}`)) {
|
|
242
|
+
const handler = this.messageHandlers.get(`${name}-${message.id}`);
|
|
243
|
+
handler?.(message);
|
|
244
|
+
this.messageHandlers.delete(`${name}-${message.id}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
console.error(`Failed to parse MCP message from ${name}:`, error);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async callMCPTool(serverName, toolName, args) {
|
|
253
|
+
const mcpProcess = this.processes.get(serverName);
|
|
254
|
+
if (!mcpProcess) {
|
|
255
|
+
return { success: false, error: `MCP '${serverName}' is not running` };
|
|
256
|
+
}
|
|
257
|
+
if (!mcpProcess.ready) {
|
|
258
|
+
return { success: false, error: `MCP '${serverName}' is not ready` };
|
|
259
|
+
}
|
|
260
|
+
return new Promise((resolve) => {
|
|
261
|
+
const id = Date.now();
|
|
262
|
+
const handlerKey = `${serverName}-${id}`;
|
|
263
|
+
const timeout = setTimeout(() => {
|
|
264
|
+
this.messageHandlers.delete(handlerKey);
|
|
265
|
+
resolve({ success: false, error: 'MCP tool call timeout' });
|
|
266
|
+
}, 30000);
|
|
267
|
+
this.messageHandlers.set(handlerKey, (message) => {
|
|
268
|
+
clearTimeout(timeout);
|
|
269
|
+
if (message.error) {
|
|
270
|
+
resolve({ success: false, error: message.error.message });
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
resolve({ success: true, result: message.result });
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
this.sendMCPMessage(serverName, {
|
|
277
|
+
jsonrpc: '2.0',
|
|
278
|
+
id,
|
|
279
|
+
method: 'tools/call',
|
|
280
|
+
params: {
|
|
281
|
+
name: toolName,
|
|
282
|
+
arguments: args,
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
getAllTools() {
|
|
288
|
+
const tools = [];
|
|
289
|
+
for (const [name, mcpProcess] of this.processes) {
|
|
290
|
+
if (mcpProcess.ready) {
|
|
291
|
+
for (const tool of mcpProcess.tools) {
|
|
292
|
+
tools.push({ server: name, tool });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return tools;
|
|
297
|
+
}
|
|
298
|
+
listMCPs() {
|
|
299
|
+
return Object.entries(this.registry.servers).map(([name, server]) => ({
|
|
300
|
+
name,
|
|
301
|
+
description: server.description,
|
|
302
|
+
enabled: server.enabled,
|
|
303
|
+
running: this.processes.has(name),
|
|
304
|
+
builtIn: this.registry.builtIn.includes(name),
|
|
305
|
+
tools: this.processes.get(name)?.tools,
|
|
306
|
+
}));
|
|
307
|
+
}
|
|
308
|
+
setMCPEnabled(name, enabled) {
|
|
309
|
+
const server = this.registry.servers[name];
|
|
310
|
+
if (!server) {
|
|
311
|
+
return { success: false, error: `MCP '${name}' not found` };
|
|
312
|
+
}
|
|
313
|
+
server.enabled = enabled;
|
|
314
|
+
this.saveRegistry();
|
|
315
|
+
return { success: true };
|
|
316
|
+
}
|
|
317
|
+
async startAutoStartMCPs() {
|
|
318
|
+
for (const [name, server] of Object.entries(this.registry.servers)) {
|
|
319
|
+
if (server.enabled && server.autoStart) {
|
|
320
|
+
await this.startMCP(name);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
stopAllMCPs() {
|
|
325
|
+
for (const name of this.processes.keys()) {
|
|
326
|
+
this.stopMCP(name);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
let mcpManager = null;
|
|
331
|
+
export function getMCPManager() {
|
|
332
|
+
if (!mcpManager) {
|
|
333
|
+
mcpManager = new MCPManager();
|
|
334
|
+
}
|
|
335
|
+
return mcpManager;
|
|
336
|
+
}
|
package/dist/tools.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { exec, spawn } from 'child_process';
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, mkdirSync } from 'fs';
|
|
3
|
-
import { dirname, join, resolve } from 'path';
|
|
3
|
+
import { dirname, join, relative, resolve } from 'path';
|
|
4
4
|
import { promisify } from 'util';
|
|
5
5
|
import { glob } from 'glob';
|
|
6
6
|
const execAsync = promisify(exec);
|
|
@@ -328,15 +328,102 @@ export const TOOL_DEFS = [
|
|
|
328
328
|
},
|
|
329
329
|
},
|
|
330
330
|
},
|
|
331
|
+
{
|
|
332
|
+
type: 'function',
|
|
333
|
+
function: {
|
|
334
|
+
name: 'mcp_install',
|
|
335
|
+
description: 'Install a new MCP (Model Context Protocol) server to extend capabilities. MCPs provide specialized tools like GitHub API, database access, browser automation, etc.',
|
|
336
|
+
parameters: {
|
|
337
|
+
type: 'object',
|
|
338
|
+
properties: {
|
|
339
|
+
name: { type: 'string', description: 'Unique name for this MCP' },
|
|
340
|
+
source: { type: 'string', description: 'Source: npm package (npm:package-name), git URL, or local path' },
|
|
341
|
+
description: { type: 'string', description: 'Description of what this MCP does' },
|
|
342
|
+
autoStart: { type: 'boolean', description: 'Start automatically on ikie launch (default: false)' },
|
|
343
|
+
},
|
|
344
|
+
required: ['name', 'source'],
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
type: 'function',
|
|
350
|
+
function: {
|
|
351
|
+
name: 'mcp_list',
|
|
352
|
+
description: 'List all installed MCP servers and their available tools.',
|
|
353
|
+
parameters: {
|
|
354
|
+
type: 'object',
|
|
355
|
+
properties: {},
|
|
356
|
+
required: [],
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
type: 'function',
|
|
362
|
+
function: {
|
|
363
|
+
name: 'mcp_start',
|
|
364
|
+
description: 'Start an installed MCP server to make its tools available.',
|
|
365
|
+
parameters: {
|
|
366
|
+
type: 'object',
|
|
367
|
+
properties: {
|
|
368
|
+
name: { type: 'string', description: 'Name of the MCP server to start' },
|
|
369
|
+
},
|
|
370
|
+
required: ['name'],
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
type: 'function',
|
|
376
|
+
function: {
|
|
377
|
+
name: 'mcp_stop',
|
|
378
|
+
description: 'Stop a running MCP server.',
|
|
379
|
+
parameters: {
|
|
380
|
+
type: 'object',
|
|
381
|
+
properties: {
|
|
382
|
+
name: { type: 'string', description: 'Name of the MCP server to stop' },
|
|
383
|
+
},
|
|
384
|
+
required: ['name'],
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
type: 'function',
|
|
390
|
+
function: {
|
|
391
|
+
name: 'mcp_call',
|
|
392
|
+
description: 'Call a tool from a running MCP server. Use mcp_list first to see available tools and their parameters.',
|
|
393
|
+
parameters: {
|
|
394
|
+
type: 'object',
|
|
395
|
+
properties: {
|
|
396
|
+
server: { type: 'string', description: 'MCP server name' },
|
|
397
|
+
tool: { type: 'string', description: 'Tool name from the MCP server' },
|
|
398
|
+
arguments: { type: 'object', description: 'Arguments to pass to the tool' },
|
|
399
|
+
},
|
|
400
|
+
required: ['server', 'tool', 'arguments'],
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
type: 'function',
|
|
406
|
+
function: {
|
|
407
|
+
name: 'mcp_uninstall',
|
|
408
|
+
description: 'Uninstall an MCP server (cannot uninstall built-in MCPs).',
|
|
409
|
+
parameters: {
|
|
410
|
+
type: 'object',
|
|
411
|
+
properties: {
|
|
412
|
+
name: { type: 'string', description: 'Name of the MCP server to uninstall' },
|
|
413
|
+
},
|
|
414
|
+
required: ['name'],
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
},
|
|
331
418
|
];
|
|
332
419
|
// ─── Safe tools (auto-approved) ───────────────────────────────────────────────
|
|
333
420
|
// spawn_agent is "safe" at the dispatch layer — the tools the sub-agent itself
|
|
334
421
|
// runs go through their own approval inside the sub-agent loop.
|
|
335
|
-
export const SAFE_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', 'memory_write', 'spawn_agent', 'ask_user', 'fetch_url', 'web_search', 'git_status', 'git_diff', 'git_log', 'git_branch', 'use_skill', 'install_skill', 'remove_skill']);
|
|
422
|
+
export const SAFE_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', 'memory_write', 'spawn_agent', 'ask_user', 'fetch_url', 'web_search', 'git_status', 'git_diff', 'git_log', 'git_branch', 'use_skill', 'install_skill', 'remove_skill', 'mcp_list', 'mcp_start', 'mcp_stop', 'mcp_call']);
|
|
336
423
|
// Tools available in PLAN mode — read-only exploration plus delegation/questions.
|
|
337
424
|
// Everything that mutates the filesystem or runs commands (write_file, edit_file,
|
|
338
425
|
// bash, memory_write) is intentionally excluded so plan mode can only research.
|
|
339
|
-
export const PLAN_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', 'spawn_agent', 'ask_user', 'fetch_url', 'web_search', 'git_status', 'git_diff', 'git_log', 'git_branch', 'use_skill', 'install_skill', 'remove_skill']);
|
|
426
|
+
export const PLAN_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', 'spawn_agent', 'ask_user', 'fetch_url', 'web_search', 'git_status', 'git_diff', 'git_log', 'git_branch', 'use_skill', 'install_skill', 'remove_skill', 'mcp_list', 'mcp_call']);
|
|
340
427
|
// Paths that may contain secrets, credentials, or system configuration.
|
|
341
428
|
// Reading these requires explicit user permission even though read_file is normally safe.
|
|
342
429
|
const RESTRICTED_PATTERNS = [
|
|
@@ -424,14 +511,84 @@ export function formatToolArgs(name, input) {
|
|
|
424
511
|
return JSON.stringify(input).slice(0, 80);
|
|
425
512
|
}
|
|
426
513
|
}
|
|
514
|
+
// ─── Security Validation Functions ───────────────────────────────────────────
|
|
515
|
+
/**
|
|
516
|
+
* Validates that a path is safe and within allowed boundaries
|
|
517
|
+
*/
|
|
518
|
+
function validatePathSafety(userPath) {
|
|
519
|
+
try {
|
|
520
|
+
const resolved = resolve(userPath);
|
|
521
|
+
const cwd = process.cwd();
|
|
522
|
+
const rel = relative(cwd, resolved);
|
|
523
|
+
if (rel.startsWith('..') || resolve(rel) !== rel) {
|
|
524
|
+
return { safe: false, resolved, error: 'Path traversal detected' };
|
|
525
|
+
}
|
|
526
|
+
if (!resolved.startsWith(cwd)) {
|
|
527
|
+
return { safe: false, resolved, error: 'Path outside working directory' };
|
|
528
|
+
}
|
|
529
|
+
return { safe: true, resolved };
|
|
530
|
+
}
|
|
531
|
+
catch (e) {
|
|
532
|
+
return { safe: false, resolved: '', error: `Invalid path: ${e}` };
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Validates bash command for safety
|
|
537
|
+
*/
|
|
538
|
+
function validateBashCommand(command) {
|
|
539
|
+
const trimmed = command.trim();
|
|
540
|
+
const dangerousPatterns = [
|
|
541
|
+
{ pattern: /[;&|`](?!.*\bin\b.*[\"'])/g, desc: 'shell metacharacters' },
|
|
542
|
+
{ pattern: /\$\(/g, desc: 'command substitution' },
|
|
543
|
+
{ pattern: />\s*\/(?:dev|etc|proc|sys)\b/g, desc: 'system file access' },
|
|
544
|
+
];
|
|
545
|
+
for (const { pattern, desc } of dangerousPatterns) {
|
|
546
|
+
if (pattern.test(trimmed)) {
|
|
547
|
+
return { safe: false, error: `Potentially dangerous: ${desc}` };
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return { safe: true };
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Sanitizes git branch names
|
|
554
|
+
*/
|
|
555
|
+
function sanitizeGitBranchName(name) {
|
|
556
|
+
const trimmed = name.trim();
|
|
557
|
+
const dangerous = /[~^:\\@{}]|\.\.|\/{2,}|^[\/\.\-]|[\/\.]$/;
|
|
558
|
+
if (dangerous.test(trimmed)) {
|
|
559
|
+
return { safe: false, sanitized: '', error: 'Invalid branch name characters' };
|
|
560
|
+
}
|
|
561
|
+
if (trimmed.length === 0 || trimmed.length > 255) {
|
|
562
|
+
return { safe: false, sanitized: '', error: 'Branch name length must be 1-255 characters' };
|
|
563
|
+
}
|
|
564
|
+
return { safe: true, sanitized: trimmed };
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Sanitizes error messages to prevent information leakage
|
|
568
|
+
*/
|
|
569
|
+
function sanitizeError(error) {
|
|
570
|
+
if (error instanceof Error) {
|
|
571
|
+
return error.message.split('\n')[0];
|
|
572
|
+
}
|
|
573
|
+
return String(error).split('\n')[0];
|
|
574
|
+
}
|
|
427
575
|
// ─── Executors ────────────────────────────────────────────────────────────────
|
|
428
576
|
function readFile(input) {
|
|
429
577
|
if (!input.path || input.path === 'undefined')
|
|
430
578
|
return 'Error: path is required for read_file';
|
|
431
|
-
const
|
|
579
|
+
const pathCheck = validatePathSafety(input.path);
|
|
580
|
+
if (!pathCheck.safe) {
|
|
581
|
+
return `Error: ${pathCheck.error}`;
|
|
582
|
+
}
|
|
583
|
+
const abs = pathCheck.resolved;
|
|
432
584
|
if (!existsSync(abs))
|
|
433
585
|
return `Error: File not found: ${input.path}`;
|
|
434
586
|
try {
|
|
587
|
+
const stats = statSync(abs);
|
|
588
|
+
const maxSize = 10 * 1024 * 1024;
|
|
589
|
+
if (stats.size > maxSize) {
|
|
590
|
+
return `Error: File too large (${(stats.size / 1024 / 1024).toFixed(2)}MB). Maximum: 10MB`;
|
|
591
|
+
}
|
|
435
592
|
const content = readFileSync(abs, 'utf-8');
|
|
436
593
|
const lines = content.split('\n');
|
|
437
594
|
const start = (input.start_line ?? 1) - 1;
|
|
@@ -439,7 +596,7 @@ function readFile(input) {
|
|
|
439
596
|
return lines.slice(start, end).map((l, i) => `${String(i + start + 1).padStart(4)} │ ${l}`).join('\n');
|
|
440
597
|
}
|
|
441
598
|
catch (e) {
|
|
442
|
-
return `Error: ${e}`;
|
|
599
|
+
return `Error: ${sanitizeError(e)}`;
|
|
443
600
|
}
|
|
444
601
|
}
|
|
445
602
|
function writeFile(input) {
|
|
@@ -447,22 +604,35 @@ function writeFile(input) {
|
|
|
447
604
|
return 'Error: path is required for write_file';
|
|
448
605
|
if (input.content == null)
|
|
449
606
|
return 'Error: content is required for write_file';
|
|
450
|
-
const
|
|
607
|
+
const pathCheck = validatePathSafety(input.path);
|
|
608
|
+
if (!pathCheck.safe) {
|
|
609
|
+
return `Error: ${pathCheck.error}`;
|
|
610
|
+
}
|
|
611
|
+
const abs = pathCheck.resolved;
|
|
612
|
+
const maxSize = 5 * 1024 * 1024;
|
|
613
|
+
const contentSize = Buffer.byteLength(input.content, 'utf-8');
|
|
614
|
+
if (contentSize > maxSize) {
|
|
615
|
+
return `Error: Content too large (${(contentSize / 1024 / 1024).toFixed(2)}MB). Maximum: 5MB`;
|
|
616
|
+
}
|
|
451
617
|
const dir = dirname(abs);
|
|
452
|
-
if (!existsSync(dir))
|
|
453
|
-
mkdirSync(dir, { recursive: true });
|
|
454
618
|
try {
|
|
619
|
+
if (!existsSync(dir))
|
|
620
|
+
mkdirSync(dir, { recursive: true });
|
|
455
621
|
writeFileSync(abs, input.content);
|
|
456
622
|
return `Written ${input.content.split('\n').length} lines to ${input.path}`;
|
|
457
623
|
}
|
|
458
624
|
catch (e) {
|
|
459
|
-
return `Error: ${e}`;
|
|
625
|
+
return `Error: ${sanitizeError(e)}`;
|
|
460
626
|
}
|
|
461
627
|
}
|
|
462
628
|
function editFile(input) {
|
|
463
629
|
if (!input.path || input.path === 'undefined')
|
|
464
630
|
return 'Error: path is required for edit_file';
|
|
465
|
-
const
|
|
631
|
+
const pathCheck = validatePathSafety(input.path);
|
|
632
|
+
if (!pathCheck.safe) {
|
|
633
|
+
return `Error: ${pathCheck.error}`;
|
|
634
|
+
}
|
|
635
|
+
const abs = pathCheck.resolved;
|
|
466
636
|
if (!existsSync(abs))
|
|
467
637
|
return `Error: File not found: ${input.path}`;
|
|
468
638
|
try {
|
|
@@ -476,13 +646,23 @@ function editFile(input) {
|
|
|
476
646
|
return `Edited ${input.path} — replaced 1 occurrence`;
|
|
477
647
|
}
|
|
478
648
|
catch (e) {
|
|
479
|
-
return `Error: ${e}`;
|
|
649
|
+
return `Error: ${sanitizeError(e)}`;
|
|
480
650
|
}
|
|
481
651
|
}
|
|
482
652
|
async function bash(input) {
|
|
483
|
-
|
|
653
|
+
let cwd = process.cwd();
|
|
654
|
+
if (input.cwd) {
|
|
655
|
+
const cwdCheck = validatePathSafety(input.cwd);
|
|
656
|
+
if (!cwdCheck.safe) {
|
|
657
|
+
return `Error: ${cwdCheck.error}`;
|
|
658
|
+
}
|
|
659
|
+
cwd = cwdCheck.resolved;
|
|
660
|
+
}
|
|
484
661
|
const command = input.command.trim();
|
|
485
|
-
|
|
662
|
+
const validation = validateBashCommand(command);
|
|
663
|
+
if (!validation.safe) {
|
|
664
|
+
return `Error: ${validation.error}. Command blocked for security.`;
|
|
665
|
+
}
|
|
486
666
|
if (command.endsWith('&')) {
|
|
487
667
|
const bgCmd = command.slice(0, -1).trim();
|
|
488
668
|
try {
|
|
@@ -500,13 +680,15 @@ async function bash(input) {
|
|
|
500
680
|
return `Started background process (PID: ${child.pid})`;
|
|
501
681
|
}
|
|
502
682
|
catch (err) {
|
|
503
|
-
return `Error starting background process: ${err}`;
|
|
683
|
+
return `Error starting background process: ${sanitizeError(err)}`;
|
|
504
684
|
}
|
|
505
685
|
}
|
|
686
|
+
const maxTimeout = 60000;
|
|
687
|
+
const timeout = Math.min(input.timeout_ms ?? 30000, maxTimeout);
|
|
506
688
|
try {
|
|
507
689
|
const { stdout, stderr } = await execAsync(command, {
|
|
508
690
|
cwd,
|
|
509
|
-
timeout
|
|
691
|
+
timeout,
|
|
510
692
|
maxBuffer: 2 * 1024 * 1024,
|
|
511
693
|
});
|
|
512
694
|
const parts = [];
|
|
@@ -524,12 +706,15 @@ async function bash(input) {
|
|
|
524
706
|
if (e.stderr?.trim())
|
|
525
707
|
parts.push(`[stderr]\n${e.stderr.trim()}`);
|
|
526
708
|
if (!parts.length)
|
|
527
|
-
parts.push(e.message ?? 'Command failed');
|
|
709
|
+
parts.push(sanitizeError(e.message ?? 'Command failed'));
|
|
528
710
|
return `Exit ${e.code ?? 1}\n${parts.join('\n')}`;
|
|
529
711
|
}
|
|
530
712
|
}
|
|
531
713
|
function listDir(input) {
|
|
532
|
-
const
|
|
714
|
+
const check = validatePathSafety(input.path ?? '.');
|
|
715
|
+
if (!check.safe)
|
|
716
|
+
return `Error: ${check.error}`;
|
|
717
|
+
const root = check.resolved;
|
|
533
718
|
if (!existsSync(root))
|
|
534
719
|
return `Error: Not found: ${input.path}`;
|
|
535
720
|
const maxDepth = input.max_depth ?? 3;
|
|
@@ -582,6 +767,8 @@ async function grepFiles(input) {
|
|
|
582
767
|
const flags = input.ignore_case ? 'i' : '';
|
|
583
768
|
let re;
|
|
584
769
|
try {
|
|
770
|
+
if (input.pattern.length > 200)
|
|
771
|
+
return 'Error: Regex pattern too long (max 200 characters)';
|
|
585
772
|
re = new RegExp(input.pattern, flags);
|
|
586
773
|
}
|
|
587
774
|
catch {
|
|
@@ -680,8 +867,7 @@ async function memoryWrite(input) {
|
|
|
680
867
|
return `Saved to ${scope} memory.`;
|
|
681
868
|
}
|
|
682
869
|
catch (err) {
|
|
683
|
-
|
|
684
|
-
return `Error saving to ${scope} memory: ${msg}`;
|
|
870
|
+
return `Error saving to ${scope} memory: ${sanitizeError(err)}`;
|
|
685
871
|
}
|
|
686
872
|
}
|
|
687
873
|
// ─── Git ──────────────────────────────────────────────────────────────────────
|
|
@@ -693,7 +879,7 @@ async function gitStatus(input) {
|
|
|
693
879
|
}
|
|
694
880
|
catch (err) {
|
|
695
881
|
const e = err;
|
|
696
|
-
return `Error: ${e.stderr
|
|
882
|
+
return `Error: ${sanitizeError(e.stderr ?? e.message ?? err)}`;
|
|
697
883
|
}
|
|
698
884
|
}
|
|
699
885
|
async function gitDiff(input) {
|
|
@@ -706,7 +892,7 @@ async function gitDiff(input) {
|
|
|
706
892
|
}
|
|
707
893
|
catch (err) {
|
|
708
894
|
const e = err;
|
|
709
|
-
return `Error: ${e.stderr
|
|
895
|
+
return `Error: ${sanitizeError(e.stderr ?? e.message ?? err)}`;
|
|
710
896
|
}
|
|
711
897
|
}
|
|
712
898
|
async function gitLog(input) {
|
|
@@ -717,7 +903,7 @@ async function gitLog(input) {
|
|
|
717
903
|
}
|
|
718
904
|
catch (err) {
|
|
719
905
|
const e = err;
|
|
720
|
-
return `Error: ${e.stderr
|
|
906
|
+
return `Error: ${sanitizeError(e.stderr ?? e.message ?? err)}`;
|
|
721
907
|
}
|
|
722
908
|
}
|
|
723
909
|
async function gitCommit(input) {
|
|
@@ -740,7 +926,7 @@ async function gitCommit(input) {
|
|
|
740
926
|
parts.push(e.stdout.trim());
|
|
741
927
|
if (e.stderr?.trim())
|
|
742
928
|
parts.push(e.stderr.trim());
|
|
743
|
-
return `Error: ${parts.join('\n') || (e
|
|
929
|
+
return `Error: ${parts.join('\n') || sanitizeError(e)}`;
|
|
744
930
|
}
|
|
745
931
|
}
|
|
746
932
|
async function gitBranch(input) {
|
|
@@ -750,16 +936,21 @@ async function gitBranch(input) {
|
|
|
750
936
|
const { stdout } = await execAsync('git branch -a', { cwd });
|
|
751
937
|
return stdout.trim() || '(no branches)';
|
|
752
938
|
}
|
|
939
|
+
const validation = sanitizeGitBranchName(input.name);
|
|
940
|
+
if (!validation.safe) {
|
|
941
|
+
return `Error: ${validation.error}`;
|
|
942
|
+
}
|
|
943
|
+
const safeName = validation.sanitized;
|
|
753
944
|
if (input.checkout) {
|
|
754
|
-
const { stdout, stderr } = await execAsync(`git checkout -b
|
|
755
|
-
return (stdout + stderr).trim() || `Switched to new branch '${
|
|
945
|
+
const { stdout, stderr } = await execAsync(`git checkout -b ${JSON.stringify(safeName)}`, { cwd });
|
|
946
|
+
return (stdout + stderr).trim() || `Switched to new branch '${safeName}'`;
|
|
756
947
|
}
|
|
757
|
-
await execAsync(`git branch
|
|
758
|
-
return `Created branch '${
|
|
948
|
+
await execAsync(`git branch ${JSON.stringify(safeName)}`, { cwd });
|
|
949
|
+
return `Created branch '${safeName}'`;
|
|
759
950
|
}
|
|
760
951
|
catch (err) {
|
|
761
952
|
const e = err;
|
|
762
|
-
return `Error: ${e.stderr
|
|
953
|
+
return `Error: ${sanitizeError(e.stderr ?? e.message ?? err)}`;
|
|
763
954
|
}
|
|
764
955
|
}
|
|
765
956
|
// ─── Web ──────────────────────────────────────────────────────────────────────
|
|
@@ -823,7 +1014,7 @@ async function fetchUrl(input) {
|
|
|
823
1014
|
const e = err;
|
|
824
1015
|
if (e.name === 'AbortError')
|
|
825
1016
|
return `Error: request to ${url} timed out after 15s`;
|
|
826
|
-
return `Error fetching ${url}: ${e
|
|
1017
|
+
return `Error fetching ${url}: ${sanitizeError(e)}`;
|
|
827
1018
|
}
|
|
828
1019
|
finally {
|
|
829
1020
|
clearTimeout(timer);
|
|
@@ -834,7 +1025,6 @@ async function webSearch(input) {
|
|
|
834
1025
|
if (!query || query === 'undefined')
|
|
835
1026
|
return 'Error: query is required for web_search';
|
|
836
1027
|
const count = Math.min(Math.max(input.count ?? 5, 1), 10);
|
|
837
|
-
// Load account key at call time (same pattern as memoryWrite's dynamic import).
|
|
838
1028
|
const { loadConfig, getApiKey, IKIE_API_BASE } = await import('./config.js');
|
|
839
1029
|
const apiKey = getApiKey(loadConfig());
|
|
840
1030
|
if (!apiKey) {
|
|
@@ -855,8 +1045,7 @@ async function webSearch(input) {
|
|
|
855
1045
|
return formatSearchResults(query, data.results ?? []);
|
|
856
1046
|
}
|
|
857
1047
|
catch (err) {
|
|
858
|
-
|
|
859
|
-
return `Error during web_search: ${e.message ?? err}`;
|
|
1048
|
+
return `Error during web_search: ${sanitizeError(err)}`;
|
|
860
1049
|
}
|
|
861
1050
|
}
|
|
862
1051
|
function formatSearchResults(query, results) {
|
|
@@ -901,7 +1090,7 @@ async function installSkill(input) {
|
|
|
901
1090
|
return lines.length ? lines.join('\n') : 'Nothing to install.';
|
|
902
1091
|
}
|
|
903
1092
|
catch (err) {
|
|
904
|
-
return `Error: ${
|
|
1093
|
+
return `Error: ${sanitizeError(err)}`;
|
|
905
1094
|
}
|
|
906
1095
|
}
|
|
907
1096
|
async function removeSkill(input) {
|
|
@@ -918,6 +1107,81 @@ async function removeSkill(input) {
|
|
|
918
1107
|
const removed = doRemove(name);
|
|
919
1108
|
return removed ? `Removed skill "${name}".` : `No skill named "${name}" found.`;
|
|
920
1109
|
}
|
|
1110
|
+
// ─── MCP Functions ────────────────────────────────────────────────────────────
|
|
1111
|
+
async function mcpInstall(input) {
|
|
1112
|
+
const { getMCPManager } = await import('./mcp-manager.js');
|
|
1113
|
+
const manager = getMCPManager();
|
|
1114
|
+
const result = await manager.installMCP({
|
|
1115
|
+
name: input.name,
|
|
1116
|
+
source: input.source,
|
|
1117
|
+
description: input.description,
|
|
1118
|
+
autoStart: input.autoStart,
|
|
1119
|
+
});
|
|
1120
|
+
if (!result.success) {
|
|
1121
|
+
return `Error installing MCP: ${result.error}`;
|
|
1122
|
+
}
|
|
1123
|
+
return `✓ Installed MCP "${input.name}".\nRun mcp_start to activate it.`;
|
|
1124
|
+
}
|
|
1125
|
+
async function mcpList() {
|
|
1126
|
+
const { getMCPManager } = await import('./mcp-manager.js');
|
|
1127
|
+
const manager = getMCPManager();
|
|
1128
|
+
const mcps = manager.listMCPs();
|
|
1129
|
+
if (!mcps.length) {
|
|
1130
|
+
return 'No MCPs installed. Use mcp_install to add MCP servers.';
|
|
1131
|
+
}
|
|
1132
|
+
const lines = ['Available MCPs:\n'];
|
|
1133
|
+
for (const mcp of mcps) {
|
|
1134
|
+
const status = mcp.running ? '🟢 Running' : mcp.enabled ? '⚪ Stopped' : '⚫ Disabled';
|
|
1135
|
+
const builtIn = mcp.builtIn ? ' [Built-in]' : '';
|
|
1136
|
+
lines.push(`${status} ${mcp.name}${builtIn}`);
|
|
1137
|
+
lines.push(` ${mcp.description}`);
|
|
1138
|
+
if (mcp.running && mcp.tools) {
|
|
1139
|
+
lines.push(` Tools: ${mcp.tools.map(t => t.name).join(', ')}`);
|
|
1140
|
+
}
|
|
1141
|
+
lines.push('');
|
|
1142
|
+
}
|
|
1143
|
+
return lines.join('\n');
|
|
1144
|
+
}
|
|
1145
|
+
async function mcpStart(input) {
|
|
1146
|
+
const { getMCPManager } = await import('./mcp-manager.js');
|
|
1147
|
+
const manager = getMCPManager();
|
|
1148
|
+
const result = await manager.startMCP(input.name);
|
|
1149
|
+
if (!result.success) {
|
|
1150
|
+
return `Error starting MCP: ${result.error}`;
|
|
1151
|
+
}
|
|
1152
|
+
const toolCount = result.tools?.length || 0;
|
|
1153
|
+
const toolNames = result.tools?.map(t => t.name).join(', ') || 'none';
|
|
1154
|
+
return `✓ Started MCP "${input.name}".\nAvailable tools (${toolCount}): ${toolNames}`;
|
|
1155
|
+
}
|
|
1156
|
+
async function mcpStop(input) {
|
|
1157
|
+
const { getMCPManager } = await import('./mcp-manager.js');
|
|
1158
|
+
const manager = getMCPManager();
|
|
1159
|
+
const result = manager.stopMCP(input.name);
|
|
1160
|
+
if (!result.success) {
|
|
1161
|
+
return `Error stopping MCP: ${result.error}`;
|
|
1162
|
+
}
|
|
1163
|
+
return `✓ Stopped MCP "${input.name}".`;
|
|
1164
|
+
}
|
|
1165
|
+
async function mcpCall(input) {
|
|
1166
|
+
const { getMCPManager } = await import('./mcp-manager.js');
|
|
1167
|
+
const manager = getMCPManager();
|
|
1168
|
+
const result = await manager.callMCPTool(input.server, input.tool, input.arguments);
|
|
1169
|
+
if (!result.success) {
|
|
1170
|
+
return `Error calling MCP tool: ${result.error}`;
|
|
1171
|
+
}
|
|
1172
|
+
return typeof result.result === 'string'
|
|
1173
|
+
? result.result
|
|
1174
|
+
: JSON.stringify(result.result, null, 2);
|
|
1175
|
+
}
|
|
1176
|
+
async function mcpUninstall(input) {
|
|
1177
|
+
const { getMCPManager } = await import('./mcp-manager.js');
|
|
1178
|
+
const manager = getMCPManager();
|
|
1179
|
+
const result = manager.uninstallMCP(input.name);
|
|
1180
|
+
if (!result.success) {
|
|
1181
|
+
return `Error uninstalling MCP: ${result.error}`;
|
|
1182
|
+
}
|
|
1183
|
+
return `✓ Uninstalled MCP "${input.name}".`;
|
|
1184
|
+
}
|
|
921
1185
|
// ─── Dispatcher ───────────────────────────────────────────────────────────────
|
|
922
1186
|
export async function executeTool(name, input) {
|
|
923
1187
|
switch (name) {
|
|
@@ -940,6 +1204,12 @@ export async function executeTool(name, input) {
|
|
|
940
1204
|
case 'use_skill': return useSkill(input);
|
|
941
1205
|
case 'install_skill': return installSkill(input);
|
|
942
1206
|
case 'remove_skill': return removeSkill(input);
|
|
1207
|
+
case 'mcp_install': return mcpInstall(input);
|
|
1208
|
+
case 'mcp_list': return mcpList();
|
|
1209
|
+
case 'mcp_start': return mcpStart(input);
|
|
1210
|
+
case 'mcp_stop': return mcpStop(input);
|
|
1211
|
+
case 'mcp_call': return mcpCall(input);
|
|
1212
|
+
case 'mcp_uninstall': return mcpUninstall(input);
|
|
943
1213
|
default: return `Unknown tool: ${name}`;
|
|
944
1214
|
}
|
|
945
1215
|
}
|