kiku-agent 1.0.0 → 1.0.4
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 +293 -122
- package/package.json +60 -61
- package/server/agent/agentCore.js +892 -782
- package/server/agent/appPaths.js +21 -0
- package/server/agent/audioPlayer.js +739 -673
- package/server/agent/cli.js +1414 -1111
- package/server/agent/commandRegistry.js +74 -58
- package/server/agent/commands.js +1694 -788
- package/server/agent/config.js +264 -90
- package/server/agent/contextCompactor.js +79 -0
- package/server/agent/doctor.js +69 -0
- package/server/agent/eventBus.js +107 -103
- package/server/agent/gpuMetrics.js +189 -0
- package/server/agent/llmClient.js +369 -200
- package/server/agent/mcpClient.js +364 -0
- package/server/agent/memory.js +670 -566
- package/server/agent/modelManager.js +77 -0
- package/server/agent/research.js +202 -0
- package/server/agent/session.js +244 -239
- package/server/agent/subagent.js +116 -0
- package/server/agent/theme.js +125 -124
- package/server/agent/toolRouter.js +201 -0
- package/server/agent/tools.js +1679 -1528
- package/server/agent/ui.js +760 -469
- package/server/agent/youtubeAuth.js +144 -65
- package/server/mcp/claude-desktop-config.example.json +1 -1
- package/server/mcp/data/discography.js +36 -36
- package/server/mcp/dataAdapter.js +102 -0
- package/server/mcp/examples/claude-code.mcp.example.json +19 -0
- package/server/mcp/examples/claude-desktop.example.json +11 -0
- package/server/mcp/examples/cline.example.json +13 -0
- package/server/mcp/examples/cursor.example.json +14 -0
- package/server/mcp/examples/gemini-cli.example.json +17 -0
- package/server/mcp/examples/opencode.example.json +21 -0
- package/server/mcp/examples/vscode.example.json +19 -0
- package/server/mcp/examples/windsurf.example.json +14 -0
- package/server/mcp/handler.js +85 -159
- package/server/mcp/security.js +140 -138
- package/server/mcp/server.js +129 -100
- package/server/mcp/stdio.js +4 -2
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
4
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
5
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
6
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
7
|
+
import { KIKU_DATA_DIR } from './appPaths.js';
|
|
8
|
+
|
|
9
|
+
export class McpClientManager {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.clientInfo = options.clientInfo || { name: 'kiku-agent', version: '1.0.4' };
|
|
12
|
+
this.autoLoad = options.autoLoad !== false;
|
|
13
|
+
this.servers = new Map();
|
|
14
|
+
this.toolRegistry = new Map();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async connect(serverName, serverConfig = {}, { persist = true } = {}) {
|
|
18
|
+
const name = String(serverName || '').trim();
|
|
19
|
+
if (!name) throw new Error('Server name is required to connect to MCP.');
|
|
20
|
+
|
|
21
|
+
if (this.servers.has(name)) {
|
|
22
|
+
await this.disconnect(name, { persist: false });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let transport;
|
|
26
|
+
let cmd = serverConfig.command;
|
|
27
|
+
let args = serverConfig.args || [];
|
|
28
|
+
if (Array.isArray(cmd)) {
|
|
29
|
+
args = [...cmd.slice(1), ...args];
|
|
30
|
+
cmd = cmd[0];
|
|
31
|
+
}
|
|
32
|
+
const isStdio = Boolean(cmd || serverConfig.type === 'stdio' || serverConfig.type === 'local');
|
|
33
|
+
const isSse = serverConfig.type === 'sse';
|
|
34
|
+
|
|
35
|
+
if (isStdio) {
|
|
36
|
+
transport = new StdioClientTransport({
|
|
37
|
+
command: cmd,
|
|
38
|
+
args: args,
|
|
39
|
+
env: { ...process.env, ...(serverConfig.env || serverConfig.environment || {}) },
|
|
40
|
+
cwd: serverConfig.cwd || process.cwd(),
|
|
41
|
+
stderr: 'pipe',
|
|
42
|
+
});
|
|
43
|
+
} else if (isSse) {
|
|
44
|
+
const url = new URL(serverConfig.url);
|
|
45
|
+
transport = new SSEClientTransport(url, {
|
|
46
|
+
eventSourceInit: serverConfig.headers ? { headers: serverConfig.headers } : undefined,
|
|
47
|
+
requestInit: serverConfig.headers ? { headers: serverConfig.headers } : undefined,
|
|
48
|
+
});
|
|
49
|
+
} else {
|
|
50
|
+
const targetUrl = serverConfig.url || serverConfig.serverUrl || serverConfig.httpUrl;
|
|
51
|
+
const url = new URL(targetUrl);
|
|
52
|
+
const transportOpts = {};
|
|
53
|
+
if (serverConfig.headers) transportOpts.headers = serverConfig.headers;
|
|
54
|
+
transport = new StreamableHTTPClientTransport(url, transportOpts);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const client = new Client(this.clientInfo, {
|
|
58
|
+
capabilities: {
|
|
59
|
+
tools: {},
|
|
60
|
+
resources: {},
|
|
61
|
+
prompts: {},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
await client.connect(transport);
|
|
67
|
+
|
|
68
|
+
let tools = [];
|
|
69
|
+
let resources = [];
|
|
70
|
+
let prompts = [];
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const toolsResult = await client.listTools();
|
|
74
|
+
tools = toolsResult.tools || [];
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.warn(`[MCP] Failed to list tools from ${name}: ${err.message}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const resResult = await client.listResources();
|
|
81
|
+
resources = resResult.resources || [];
|
|
82
|
+
} catch {}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const promptResult = await client.listPrompts();
|
|
86
|
+
prompts = promptResult.prompts || [];
|
|
87
|
+
} catch {}
|
|
88
|
+
|
|
89
|
+
const serverEntry = {
|
|
90
|
+
name,
|
|
91
|
+
config: serverConfig,
|
|
92
|
+
client,
|
|
93
|
+
transport,
|
|
94
|
+
tools,
|
|
95
|
+
resources,
|
|
96
|
+
prompts,
|
|
97
|
+
connectedAt: new Date().toISOString(),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
this.servers.set(name, serverEntry);
|
|
101
|
+
this._rebuildToolRegistry();
|
|
102
|
+
|
|
103
|
+
if (persist) {
|
|
104
|
+
await this.savePersistedConfig().catch(() => {});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
success: true,
|
|
109
|
+
serverName: name,
|
|
110
|
+
toolsCount: tools.length,
|
|
111
|
+
resourcesCount: resources.length,
|
|
112
|
+
promptsCount: prompts.length,
|
|
113
|
+
tools,
|
|
114
|
+
};
|
|
115
|
+
} catch (err) {
|
|
116
|
+
await transport.close().catch(() => {});
|
|
117
|
+
throw new Error(`Failed connecting to MCP server "${name}": ${err.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async disconnect(serverName, { persist = true } = {}) {
|
|
122
|
+
const entry = this.servers.get(serverName);
|
|
123
|
+
if (!entry) return false;
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
await entry.client.close().catch(() => {});
|
|
127
|
+
await entry.transport.close().catch(() => {});
|
|
128
|
+
if (entry.transport?._process) {
|
|
129
|
+
try {
|
|
130
|
+
entry.transport._process.kill('SIGKILL');
|
|
131
|
+
entry.transport._process.stdout?.destroy?.();
|
|
132
|
+
entry.transport._process.stderr?.destroy?.();
|
|
133
|
+
entry.transport._process.stdin?.destroy?.();
|
|
134
|
+
entry.transport._process.unref?.();
|
|
135
|
+
} catch {}
|
|
136
|
+
}
|
|
137
|
+
} catch {}
|
|
138
|
+
|
|
139
|
+
this.servers.delete(serverName);
|
|
140
|
+
this._rebuildToolRegistry();
|
|
141
|
+
|
|
142
|
+
if (persist) {
|
|
143
|
+
await this.savePersistedConfig().catch(() => {});
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async disconnectAll() {
|
|
149
|
+
const names = [...this.servers.keys()];
|
|
150
|
+
for (const name of names) {
|
|
151
|
+
await this.disconnect(name, { persist: false });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
_rebuildToolRegistry() {
|
|
156
|
+
this.toolRegistry.clear();
|
|
157
|
+
|
|
158
|
+
for (const [serverName, serverEntry] of this.servers.entries()) {
|
|
159
|
+
for (const tool of serverEntry.tools) {
|
|
160
|
+
const directName = `mcp_${serverName}_${tool.name}`;
|
|
161
|
+
this.toolRegistry.set(directName, {
|
|
162
|
+
serverName,
|
|
163
|
+
originalName: tool.name,
|
|
164
|
+
schema: tool,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (!this.toolRegistry.has(tool.name)) {
|
|
168
|
+
this.toolRegistry.set(tool.name, {
|
|
169
|
+
serverName,
|
|
170
|
+
originalName: tool.name,
|
|
171
|
+
schema: tool,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
isMcpTool(toolName) {
|
|
179
|
+
return this.toolRegistry.has(toolName);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
getToolsSchema() {
|
|
183
|
+
const schemas = [];
|
|
184
|
+
const seenNames = new Set();
|
|
185
|
+
|
|
186
|
+
for (const [serverName, serverEntry] of this.servers.entries()) {
|
|
187
|
+
for (const tool of serverEntry.tools) {
|
|
188
|
+
const toolName = `mcp_${serverName}_${tool.name}`;
|
|
189
|
+
if (seenNames.has(toolName)) continue;
|
|
190
|
+
seenNames.add(toolName);
|
|
191
|
+
|
|
192
|
+
schemas.push({
|
|
193
|
+
type: 'function',
|
|
194
|
+
function: {
|
|
195
|
+
name: toolName,
|
|
196
|
+
description: `[MCP Server: ${serverName}] ${tool.description || `Tool from ${serverName}`}`,
|
|
197
|
+
parameters: tool.inputSchema || { type: 'object', properties: {} },
|
|
198
|
+
},
|
|
199
|
+
_mcp: {
|
|
200
|
+
serverName,
|
|
201
|
+
originalName: tool.name,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return schemas;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async callTool(toolName, args = {}) {
|
|
211
|
+
const registration = this.toolRegistry.get(toolName);
|
|
212
|
+
if (!registration) {
|
|
213
|
+
return {
|
|
214
|
+
status: 'error',
|
|
215
|
+
message: `MCP tool "${toolName}" is not registered on any active MCP server.`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const serverEntry = this.servers.get(registration.serverName);
|
|
220
|
+
if (!serverEntry) {
|
|
221
|
+
return {
|
|
222
|
+
status: 'error',
|
|
223
|
+
message: `MCP server "${registration.serverName}" is disconnected.`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const response = await serverEntry.client.callTool({
|
|
229
|
+
name: registration.originalName,
|
|
230
|
+
arguments: args,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (response.isError) {
|
|
234
|
+
const errText = response.content?.map(c => c.text).filter(Boolean).join('\n') || 'Unknown MCP tool error';
|
|
235
|
+
return {
|
|
236
|
+
status: 'error',
|
|
237
|
+
server: registration.serverName,
|
|
238
|
+
tool: registration.originalName,
|
|
239
|
+
message: errText,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let parsedPayload = null;
|
|
244
|
+
let textContent = '';
|
|
245
|
+
if (response.content && Array.isArray(response.content)) {
|
|
246
|
+
for (const item of response.content) {
|
|
247
|
+
if (item.type === 'text' && item.text) {
|
|
248
|
+
textContent += item.text + '\n';
|
|
249
|
+
try {
|
|
250
|
+
if (!parsedPayload && item.text.trim().startsWith('{') && item.text.trim().endsWith('}')) {
|
|
251
|
+
parsedPayload = JSON.parse(item.text);
|
|
252
|
+
}
|
|
253
|
+
} catch {}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
status: 'success',
|
|
260
|
+
server: registration.serverName,
|
|
261
|
+
tool: registration.originalName,
|
|
262
|
+
...(parsedPayload && typeof parsedPayload === 'object' ? parsedPayload : {}),
|
|
263
|
+
content: textContent.trim(),
|
|
264
|
+
_rawMcp: response.content,
|
|
265
|
+
};
|
|
266
|
+
} catch (err) {
|
|
267
|
+
return {
|
|
268
|
+
status: 'error',
|
|
269
|
+
server: registration.serverName,
|
|
270
|
+
tool: registration.originalName,
|
|
271
|
+
message: `Failed executing MCP tool "${registration.originalName}": ${err.message}`,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
getStatus() {
|
|
277
|
+
const list = [];
|
|
278
|
+
for (const [name, server] of this.servers.entries()) {
|
|
279
|
+
list.push({
|
|
280
|
+
name,
|
|
281
|
+
type: server.config.type || (server.config.command ? 'stdio' : 'streamable-http'),
|
|
282
|
+
toolsCount: server.tools.length,
|
|
283
|
+
resourcesCount: server.resources.length,
|
|
284
|
+
promptsCount: server.prompts.length,
|
|
285
|
+
connectedAt: server.connectedAt,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
activeServersCount: this.servers.size,
|
|
290
|
+
totalToolsCount: this.getToolsSchema().length,
|
|
291
|
+
servers: list,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async savePersistedConfig() {
|
|
296
|
+
try {
|
|
297
|
+
const dir = KIKU_DATA_DIR;
|
|
298
|
+
await fs.mkdir(dir, { recursive: true });
|
|
299
|
+
const filePath = path.join(dir, 'mcp_servers.json');
|
|
300
|
+
|
|
301
|
+
const serversObj = {};
|
|
302
|
+
for (const [name, entry] of this.servers.entries()) {
|
|
303
|
+
serversObj[name] = entry.config;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
await fs.writeFile(filePath, JSON.stringify({ mcpServers: serversObj }, null, 2), 'utf8');
|
|
307
|
+
return true;
|
|
308
|
+
} catch {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async loadPersistedServers() {
|
|
314
|
+
if (this.autoLoad === false) return [];
|
|
315
|
+
const results = [];
|
|
316
|
+
const candidates = [
|
|
317
|
+
path.join(KIKU_DATA_DIR, 'mcp_servers.json'),
|
|
318
|
+
path.join(process.cwd(), 'opencode.json'),
|
|
319
|
+
path.join(process.cwd(), '.mcp.json'),
|
|
320
|
+
path.join(process.cwd(), 'mcp.json'),
|
|
321
|
+
];
|
|
322
|
+
|
|
323
|
+
for (const filePath of candidates) {
|
|
324
|
+
try {
|
|
325
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
326
|
+
const parsed = JSON.parse(content);
|
|
327
|
+
const serversObj = parsed.mcpServers || parsed.mcp || {};
|
|
328
|
+
for (const [name, cfg] of Object.entries(serversObj)) {
|
|
329
|
+
if (cfg.enabled === false || cfg.disabled === true) continue;
|
|
330
|
+
if (this.servers.has(name)) continue;
|
|
331
|
+
try {
|
|
332
|
+
const res = await this.connect(name, cfg, { persist: false });
|
|
333
|
+
results.push(res);
|
|
334
|
+
} catch (err) {
|
|
335
|
+
results.push({ success: false, serverName: name, error: err.message });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
} catch {}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return results;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async loadConfigFile(configPath) {
|
|
345
|
+
const resolvedPath = path.resolve(process.cwd(), configPath);
|
|
346
|
+
const content = await fs.readFile(resolvedPath, 'utf8');
|
|
347
|
+
const parsed = JSON.parse(content);
|
|
348
|
+
|
|
349
|
+
const serversObj = parsed.mcpServers || parsed.mcp || {};
|
|
350
|
+
const results = [];
|
|
351
|
+
|
|
352
|
+
for (const [name, cfg] of Object.entries(serversObj)) {
|
|
353
|
+
if (cfg.enabled === false || cfg.disabled === true) continue;
|
|
354
|
+
try {
|
|
355
|
+
const res = await this.connect(name, cfg);
|
|
356
|
+
results.push(res);
|
|
357
|
+
} catch (err) {
|
|
358
|
+
results.push({ success: false, serverName: name, error: err.message });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return results;
|
|
363
|
+
}
|
|
364
|
+
}
|