daemora 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.
Files changed (115) hide show
  1. package/README.md +666 -0
  2. package/SOUL.md +104 -0
  3. package/config/hooks.json +14 -0
  4. package/config/mcp.json +145 -0
  5. package/package.json +86 -0
  6. package/skills/.gitkeep +0 -0
  7. package/skills/apple-notes.md +193 -0
  8. package/skills/apple-reminders.md +189 -0
  9. package/skills/camsnap.md +162 -0
  10. package/skills/coding.md +14 -0
  11. package/skills/documents.md +13 -0
  12. package/skills/email.md +13 -0
  13. package/skills/gif-search.md +196 -0
  14. package/skills/healthcheck.md +225 -0
  15. package/skills/image-gen.md +147 -0
  16. package/skills/model-usage.md +182 -0
  17. package/skills/obsidian.md +207 -0
  18. package/skills/pdf.md +211 -0
  19. package/skills/research.md +13 -0
  20. package/skills/skill-creator.md +142 -0
  21. package/skills/spotify.md +149 -0
  22. package/skills/summarize.md +230 -0
  23. package/skills/things.md +199 -0
  24. package/skills/tmux.md +204 -0
  25. package/skills/trello.md +183 -0
  26. package/skills/video-frames.md +202 -0
  27. package/skills/weather.md +127 -0
  28. package/src/a2a/A2AClient.js +136 -0
  29. package/src/a2a/A2AServer.js +316 -0
  30. package/src/a2a/AgentCard.js +79 -0
  31. package/src/agents/SubAgentManager.js +369 -0
  32. package/src/agents/Supervisor.js +192 -0
  33. package/src/channels/BaseChannel.js +104 -0
  34. package/src/channels/DiscordChannel.js +288 -0
  35. package/src/channels/EmailChannel.js +172 -0
  36. package/src/channels/GoogleChatChannel.js +316 -0
  37. package/src/channels/HttpChannel.js +26 -0
  38. package/src/channels/LineChannel.js +168 -0
  39. package/src/channels/SignalChannel.js +186 -0
  40. package/src/channels/SlackChannel.js +329 -0
  41. package/src/channels/TeamsChannel.js +272 -0
  42. package/src/channels/TelegramChannel.js +347 -0
  43. package/src/channels/WhatsAppChannel.js +219 -0
  44. package/src/channels/index.js +198 -0
  45. package/src/cli.js +1267 -0
  46. package/src/config/agentProfiles.js +120 -0
  47. package/src/config/channels.js +32 -0
  48. package/src/config/default.js +206 -0
  49. package/src/config/models.js +123 -0
  50. package/src/config/permissions.js +167 -0
  51. package/src/core/AgentLoop.js +446 -0
  52. package/src/core/Compaction.js +143 -0
  53. package/src/core/CostTracker.js +116 -0
  54. package/src/core/EventBus.js +46 -0
  55. package/src/core/Task.js +67 -0
  56. package/src/core/TaskQueue.js +206 -0
  57. package/src/core/TaskRunner.js +226 -0
  58. package/src/daemon/DaemonManager.js +301 -0
  59. package/src/hooks/HookRunner.js +230 -0
  60. package/src/index.js +482 -0
  61. package/src/mcp/MCPAgentRunner.js +112 -0
  62. package/src/mcp/MCPClient.js +186 -0
  63. package/src/mcp/MCPManager.js +412 -0
  64. package/src/models/ModelRouter.js +180 -0
  65. package/src/safety/AuditLog.js +135 -0
  66. package/src/safety/CircuitBreaker.js +126 -0
  67. package/src/safety/FilesystemGuard.js +169 -0
  68. package/src/safety/GitRollback.js +139 -0
  69. package/src/safety/HumanApproval.js +156 -0
  70. package/src/safety/InputSanitizer.js +72 -0
  71. package/src/safety/PermissionGuard.js +83 -0
  72. package/src/safety/Sandbox.js +70 -0
  73. package/src/safety/SecretScanner.js +100 -0
  74. package/src/safety/SecretVault.js +250 -0
  75. package/src/scheduler/Heartbeat.js +115 -0
  76. package/src/scheduler/Scheduler.js +228 -0
  77. package/src/services/models/outputSchema.js +15 -0
  78. package/src/services/openai.js +25 -0
  79. package/src/services/sessions.js +65 -0
  80. package/src/setup/theme.js +110 -0
  81. package/src/setup/wizard.js +788 -0
  82. package/src/skills/SkillLoader.js +168 -0
  83. package/src/storage/TaskStore.js +69 -0
  84. package/src/systemPrompt.js +526 -0
  85. package/src/tenants/TenantContext.js +19 -0
  86. package/src/tenants/TenantManager.js +379 -0
  87. package/src/tools/ToolRegistry.js +141 -0
  88. package/src/tools/applyPatch.js +144 -0
  89. package/src/tools/browserAutomation.js +223 -0
  90. package/src/tools/createDocument.js +265 -0
  91. package/src/tools/cronTool.js +105 -0
  92. package/src/tools/editFile.js +139 -0
  93. package/src/tools/executeCommand.js +123 -0
  94. package/src/tools/glob.js +67 -0
  95. package/src/tools/grep.js +121 -0
  96. package/src/tools/imageAnalysis.js +120 -0
  97. package/src/tools/index.js +173 -0
  98. package/src/tools/listDirectory.js +47 -0
  99. package/src/tools/manageAgents.js +47 -0
  100. package/src/tools/manageMCP.js +159 -0
  101. package/src/tools/memory.js +478 -0
  102. package/src/tools/messageChannel.js +45 -0
  103. package/src/tools/projectTracker.js +259 -0
  104. package/src/tools/readFile.js +52 -0
  105. package/src/tools/screenCapture.js +112 -0
  106. package/src/tools/searchContent.js +76 -0
  107. package/src/tools/searchFiles.js +75 -0
  108. package/src/tools/sendEmail.js +118 -0
  109. package/src/tools/sendFile.js +63 -0
  110. package/src/tools/textToSpeech.js +161 -0
  111. package/src/tools/transcribeAudio.js +82 -0
  112. package/src/tools/useMCP.js +29 -0
  113. package/src/tools/webFetch.js +150 -0
  114. package/src/tools/webSearch.js +134 -0
  115. package/src/tools/writeFile.js +26 -0
@@ -0,0 +1,186 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
4
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
+
6
+ /**
7
+ * Expand ${VAR_NAME} patterns in string values using process.env.
8
+ * Works recursively on objects: values only, keys are left untouched.
9
+ *
10
+ * This lets users write:
11
+ * "Authorization": "Bearer ${MY_API_TOKEN}"
12
+ * "url": "https://api.example.com/${TENANT_ID}/mcp"
13
+ * without storing the actual secret in mcp.json.
14
+ */
15
+ function expandEnvVars(value) {
16
+ if (typeof value === "string") {
17
+ return value.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] ?? "");
18
+ }
19
+ if (value && typeof value === "object" && !Array.isArray(value)) {
20
+ const out = {};
21
+ for (const [k, v] of Object.entries(value)) {
22
+ out[k] = expandEnvVars(v);
23
+ }
24
+ return out;
25
+ }
26
+ return value;
27
+ }
28
+
29
+ /**
30
+ * MCP Client — connects to a single MCP server.
31
+ *
32
+ * Supports three transports:
33
+ *
34
+ * stdio — local subprocess. Auth via `env` (merged into process.env for the child).
35
+ * config: { command, args, env }
36
+ *
37
+ * http — Streamable HTTP (MCP 2025-03-26 spec). Auth via `headers`.
38
+ * config: { url, headers: { "Authorization": "Bearer ${TOKEN}", ... } }
39
+ *
40
+ * sse — Legacy SSE transport. Auth via `headers` (applied to both the SSE GET
41
+ * stream and POST calls). Prefer http for new servers.
42
+ * config: { url, transport: "sse", headers: { "Authorization": "Bearer ${TOKEN}", ... } }
43
+ *
44
+ * Header values support ${VAR_NAME} expansion from process.env at connect time.
45
+ */
46
+ export class MCPClient {
47
+ constructor(name, serverConfig) {
48
+ this.name = name;
49
+ this.serverConfig = serverConfig;
50
+ this.client = null;
51
+ this.transport = null;
52
+ this.tools = [];
53
+ this.connected = false;
54
+ }
55
+
56
+ /**
57
+ * Connect to the MCP server.
58
+ */
59
+ async connect() {
60
+ try {
61
+ this.client = new Client(
62
+ { name: `daemora-${this.name}`, version: "1.0.0" },
63
+ { capabilities: { tools: {} } }
64
+ );
65
+
66
+ this.transport = this.createTransport();
67
+ await this.client.connect(this.transport);
68
+ this.connected = true;
69
+
70
+ const toolsResult = await this.client.listTools();
71
+ this.tools = toolsResult.tools || [];
72
+
73
+ console.log(
74
+ `[MCP:${this.name}] Connected — ${this.tools.length} tools: ${this.tools.map((t) => t.name).join(", ")}`
75
+ );
76
+
77
+ return this.tools;
78
+ } catch (error) {
79
+ console.log(`[MCP:${this.name}] Connection failed: ${error.message}`);
80
+ this.connected = false;
81
+ return [];
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Create the transport based on config.
87
+ *
88
+ * stdio → StdioClientTransport — env vars merged into subprocess environment
89
+ * sse → SSEClientTransport — headers in requestInit + eventSourceInit
90
+ * http → StreamableHTTPClientTransport — headers in requestInit
91
+ */
92
+ createTransport() {
93
+ const cfg = this.serverConfig;
94
+
95
+ // ── stdio ─────────────────────────────────────────────────────────────────
96
+ if (cfg.command) {
97
+ return new StdioClientTransport({
98
+ command: cfg.command,
99
+ args: cfg.args || [],
100
+ // Env vars are merged into the child process environment.
101
+ // Values support ${VAR} expansion so users can reference existing env vars.
102
+ env: { ...process.env, ...expandEnvVars(cfg.env || {}) },
103
+ });
104
+ }
105
+
106
+ if (!cfg.url) {
107
+ throw new Error(`Invalid MCP config for ${this.name}: need 'command' (stdio) or 'url' (http/sse)`);
108
+ }
109
+
110
+ const url = new URL(expandEnvVars(cfg.url));
111
+
112
+ // Expand ${VAR} in header values at connect time.
113
+ // This keeps actual secrets out of mcp.json — store them in .env / vault,
114
+ // reference them as ${MY_SECRET} in the headers config.
115
+ const rawHeaders = cfg.headers || {};
116
+ const headers = expandEnvVars(rawHeaders);
117
+
118
+ // ── SSE ───────────────────────────────────────────────────────────────────
119
+ if (cfg.transport === "sse") {
120
+ // requestInit covers POST requests (messages sent to server).
121
+ // eventSourceInit covers the GET SSE stream (messages received from server).
122
+ // Both need the auth headers.
123
+ return new SSEClientTransport(url, {
124
+ requestInit: Object.keys(headers).length > 0 ? { headers } : undefined,
125
+ eventSourceInit: Object.keys(headers).length > 0 ? { headers } : undefined,
126
+ });
127
+ }
128
+
129
+ // ── Streamable HTTP (default) ─────────────────────────────────────────────
130
+ return new StreamableHTTPClientTransport(url, {
131
+ requestInit: Object.keys(headers).length > 0 ? { headers } : undefined,
132
+ });
133
+ }
134
+
135
+ /**
136
+ * Call a tool on this server.
137
+ */
138
+ async callTool(toolName, args) {
139
+ if (!this.connected || !this.client) {
140
+ throw new Error(`MCP server ${this.name} not connected`);
141
+ }
142
+
143
+ const result = await this.client.callTool({
144
+ name: toolName,
145
+ arguments: args,
146
+ });
147
+
148
+ if (result.content && Array.isArray(result.content)) {
149
+ return result.content
150
+ .map((c) => {
151
+ if (c.type === "text") return c.text;
152
+ if (c.type === "image") return `[Image: ${c.mimeType}]`;
153
+ return JSON.stringify(c);
154
+ })
155
+ .join("\n");
156
+ }
157
+
158
+ return JSON.stringify(result);
159
+ }
160
+
161
+ /**
162
+ * Disconnect from the server.
163
+ */
164
+ async disconnect() {
165
+ if (this.client) {
166
+ try {
167
+ await this.client.close();
168
+ } catch {
169
+ // ignore close errors
170
+ }
171
+ this.connected = false;
172
+ console.log(`[MCP:${this.name}] Disconnected`);
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Get tool list for this server.
178
+ */
179
+ getTools() {
180
+ return this.tools.map((t) => ({
181
+ name: t.name,
182
+ description: t.description,
183
+ inputSchema: t.inputSchema,
184
+ }));
185
+ }
186
+ }
@@ -0,0 +1,412 @@
1
+ import { readFileSync, writeFileSync, existsSync } from "fs";
2
+ import { join } from "path";
3
+ import { config } from "../config/default.js";
4
+ import { MCPClient } from "./MCPClient.js";
5
+
6
+ /**
7
+ * MCP Manager — manages multiple MCP server connections.
8
+ *
9
+ * Reads config from config/mcp.json (same format as Claude Code's .mcp.json).
10
+ * Each server's tools are exposed as `mcp__{serverName}__{toolName}` in the agent.
11
+ *
12
+ * Config format:
13
+ * ```json
14
+ * {
15
+ * "mcpServers": {
16
+ * "github": {
17
+ * "command": "npx",
18
+ * "args": ["-y", "@modelcontextprotocol/server-github"],
19
+ * "env": { "GITHUB_TOKEN": "..." }
20
+ * },
21
+ * "memory": {
22
+ * "url": "http://localhost:3100/mcp",
23
+ * "transport": "sse"
24
+ * }
25
+ * }
26
+ * }
27
+ * ```
28
+ */
29
+ class MCPManager {
30
+ constructor() {
31
+ this.clients = new Map();
32
+ this.toolMap = new Map(); // mcp__server__tool -> { client, toolName }
33
+ this.mcpConfigPath = join(config.rootDir, "config", "mcp.json");
34
+ }
35
+
36
+ /**
37
+ * Read and parse the current mcp.json config.
38
+ */
39
+ readConfig() {
40
+ if (!existsSync(this.mcpConfigPath)) return { mcpServers: {} };
41
+ try {
42
+ return JSON.parse(readFileSync(this.mcpConfigPath, "utf-8"));
43
+ } catch {
44
+ return { mcpServers: {} };
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Write the config back to mcp.json.
50
+ */
51
+ writeConfig(mcpConfig) {
52
+ writeFileSync(this.mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
53
+ }
54
+
55
+ /**
56
+ * Connect a single server and register its tools.
57
+ */
58
+ async connectServer(name, serverConfig) {
59
+ // Disconnect existing if present
60
+ if (this.clients.has(name)) {
61
+ await this.disconnectServer(name);
62
+ }
63
+
64
+ const client = new MCPClient(name, serverConfig);
65
+ this.clients.set(name, client);
66
+
67
+ const tools = await client.connect();
68
+
69
+ for (const tool of tools) {
70
+ const fullName = `mcp__${name}__${tool.name}`;
71
+ this.toolMap.set(fullName, {
72
+ client,
73
+ toolName: tool.name,
74
+ description: tool.description,
75
+ inputSchema: tool.inputSchema,
76
+ });
77
+ }
78
+
79
+ return tools;
80
+ }
81
+
82
+ /**
83
+ * Disconnect a server and remove its tools.
84
+ */
85
+ async disconnectServer(name) {
86
+ const client = this.clients.get(name);
87
+ if (!client) return;
88
+
89
+ await client.disconnect();
90
+ this.clients.delete(name);
91
+
92
+ // Remove all tools from this server
93
+ const prefix = `mcp__${name}__`;
94
+ for (const key of this.toolMap.keys()) {
95
+ if (key.startsWith(prefix)) this.toolMap.delete(key);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Dynamically add a new MCP server — saves to config and connects.
101
+ * @param {string} name - Server name (e.g. "github")
102
+ * @param {object} serverConfig - Config object (command+args or url+transport)
103
+ * @returns {string} Result message
104
+ */
105
+ async addServer(name, serverConfig) {
106
+ if (!name || typeof name !== "string") throw new Error("name is required");
107
+ if (!serverConfig || typeof serverConfig !== "object") throw new Error("serverConfig is required");
108
+ if (!serverConfig.command && !serverConfig.url) throw new Error("serverConfig must have 'command' (stdio) or 'url' (SSE/HTTP)");
109
+
110
+ // Write to config
111
+ const mcpConfig = this.readConfig();
112
+ mcpConfig.mcpServers = mcpConfig.mcpServers || {};
113
+ mcpConfig.mcpServers[name] = { ...serverConfig, enabled: true };
114
+ this.writeConfig(mcpConfig);
115
+
116
+ // Connect immediately
117
+ const tools = await this.connectServer(name, serverConfig);
118
+ return `Server "${name}" added and connected — ${tools.length} tools: ${tools.map(t => t.name).join(", ") || "(none)"}`;
119
+ }
120
+
121
+ /**
122
+ * Dynamically remove an MCP server — disconnects and removes from config.
123
+ * @param {string} name - Server name
124
+ * @returns {string} Result message
125
+ */
126
+ async removeServer(name) {
127
+ await this.disconnectServer(name);
128
+
129
+ const mcpConfig = this.readConfig();
130
+ if (mcpConfig.mcpServers && mcpConfig.mcpServers[name]) {
131
+ delete mcpConfig.mcpServers[name];
132
+ this.writeConfig(mcpConfig);
133
+ }
134
+
135
+ return `Server "${name}" removed.`;
136
+ }
137
+
138
+ /**
139
+ * Enable or disable a server in config (does not reconnect — use reload).
140
+ */
141
+ async setEnabled(name, enabled) {
142
+ const mcpConfig = this.readConfig();
143
+ if (!mcpConfig.mcpServers?.[name]) {
144
+ throw new Error(`Server "${name}" not found in config`);
145
+ }
146
+ mcpConfig.mcpServers[name].enabled = enabled;
147
+ this.writeConfig(mcpConfig);
148
+
149
+ if (enabled) {
150
+ const tools = await this.connectServer(name, mcpConfig.mcpServers[name]);
151
+ return `Server "${name}" enabled and connected — ${tools.length} tools.`;
152
+ } else {
153
+ await this.disconnectServer(name);
154
+ return `Server "${name}" disabled and disconnected.`;
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Reload a server — disconnect, re-read config, reconnect.
160
+ */
161
+ async reloadServer(name) {
162
+ const mcpConfig = this.readConfig();
163
+ const serverConfig = mcpConfig.mcpServers?.[name];
164
+ if (!serverConfig) throw new Error(`Server "${name}" not found in config`);
165
+ if (serverConfig.enabled === false) {
166
+ await this.disconnectServer(name);
167
+ return `Server "${name}" is disabled — not reconnected.`;
168
+ }
169
+
170
+ const tools = await this.connectServer(name, serverConfig);
171
+ return `Server "${name}" reloaded — ${tools.length} tools.`;
172
+ }
173
+
174
+ /**
175
+ * Initialize and connect to all configured MCP servers.
176
+ */
177
+ async init() {
178
+ const mcpConfigPath = this.mcpConfigPath;
179
+
180
+ if (!existsSync(mcpConfigPath)) {
181
+ console.log(`[MCPManager] No config/mcp.json — MCP disabled`);
182
+ return;
183
+ }
184
+
185
+ let mcpConfig;
186
+ try {
187
+ mcpConfig = JSON.parse(readFileSync(mcpConfigPath, "utf-8"));
188
+ } catch (error) {
189
+ console.log(`[MCPManager] Error reading mcp.json: ${error.message}`);
190
+ return;
191
+ }
192
+
193
+ const servers = mcpConfig.mcpServers || {};
194
+
195
+ // Filter to only real, enabled servers
196
+ const enabledServers = Object.entries(servers).filter(
197
+ ([name, cfg]) => !name.startsWith("_comment") && typeof cfg === "object" && cfg.enabled !== false
198
+ );
199
+
200
+ if (enabledServers.length === 0) {
201
+ console.log(`[MCPManager] No MCP servers enabled`);
202
+ return;
203
+ }
204
+
205
+ console.log(
206
+ `[MCPManager] Connecting to ${enabledServers.length} server(s) in background...`
207
+ );
208
+
209
+ // Connect all servers in parallel, non-blocking
210
+ const connectAll = Promise.allSettled(
211
+ enabledServers.map(async ([serverName, serverConfig]) => {
212
+ const tools = await this.connectServer(serverName, serverConfig);
213
+ return { name: serverName, toolCount: tools.length };
214
+ })
215
+ );
216
+
217
+ // Don't block startup — log results when ready
218
+ connectAll.then((results) => {
219
+ const succeeded = results.filter((r) => r.status === "fulfilled");
220
+ const failed = results.filter((r) => r.status === "rejected");
221
+
222
+ for (const f of failed) {
223
+ console.log(`[MCPManager] Failed to connect: ${f.reason?.message || f.reason}`);
224
+ }
225
+
226
+ console.log(
227
+ `[MCPManager] Ready — ${succeeded.length}/${enabledServers.length} servers, ${this.toolMap.size} tools`
228
+ );
229
+ });
230
+ }
231
+
232
+ /**
233
+ * Get all MCP tools as functions + descriptions for the tool registry.
234
+ * Returns { functions: { name: fn }, descriptions: [string] }
235
+ */
236
+ getToolsForAgent() {
237
+ const functions = {};
238
+ const descriptions = [];
239
+
240
+ for (const [fullName, entry] of this.toolMap) {
241
+ // Create a wrapper function that calls the MCP tool
242
+ functions[fullName] = async (...args) => {
243
+ // Parse args: first arg is JSON string of arguments
244
+ let toolArgs = {};
245
+ if (args[0]) {
246
+ try {
247
+ toolArgs = typeof args[0] === "string" ? JSON.parse(args[0]) : args[0];
248
+ } catch {
249
+ // If not JSON, try to map positionally based on schema
250
+ const props = entry.inputSchema?.properties || {};
251
+ const keys = Object.keys(props);
252
+ toolArgs = {};
253
+ for (let i = 0; i < keys.length && i < args.length; i++) {
254
+ toolArgs[keys[i]] = args[i];
255
+ }
256
+ }
257
+ }
258
+
259
+ console.log(
260
+ ` [MCP:${fullName}] Calling with: ${JSON.stringify(toolArgs).slice(0, 200)}`
261
+ );
262
+ try {
263
+ const result = await entry.client.callTool(entry.toolName, toolArgs);
264
+ return result;
265
+ } catch (error) {
266
+ return `MCP tool error: ${error.message}`;
267
+ }
268
+ };
269
+
270
+ // Build description
271
+ const schema = entry.inputSchema?.properties || {};
272
+ const params = Object.entries(schema)
273
+ .map(([k, v]) => `${k}: ${v.type || "any"}`)
274
+ .join(", ");
275
+ descriptions.push(
276
+ `${fullName}(argsJson: string) - [MCP] ${entry.description || entry.toolName}. Params as JSON: {${params}}`
277
+ );
278
+ }
279
+
280
+ return { functions, descriptions };
281
+ }
282
+
283
+ /**
284
+ * Get built-in tools merged with all connected MCP tools.
285
+ * Called fresh at each task execution — always reflects current connection state.
286
+ * @param {object} builtinTools - The base toolFunctions map
287
+ * @returns {object} Merged tool functions map
288
+ */
289
+ getMergedTools(builtinTools) {
290
+ if (this.toolMap.size === 0) return builtinTools;
291
+ const { functions } = this.getToolsForAgent();
292
+ return { ...builtinTools, ...functions };
293
+ }
294
+
295
+ /**
296
+ * Get MCP tool descriptions for the system prompt.
297
+ * Returns empty string if no MCP servers connected.
298
+ */
299
+ getToolDocs() {
300
+ if (this.toolMap.size === 0) return "";
301
+
302
+ const lines = ["## MCP Server Tools\n"];
303
+ lines.push("These tools come from connected MCP servers. All params passed as a JSON string.");
304
+ lines.push("");
305
+
306
+ // Group by server
307
+ const byServer = new Map();
308
+ for (const [fullName, entry] of this.toolMap) {
309
+ const serverName = fullName.split("__")[1];
310
+ if (!byServer.has(serverName)) byServer.set(serverName, []);
311
+ byServer.get(serverName).push({ fullName, entry });
312
+ }
313
+
314
+ for (const [serverName, tools] of byServer) {
315
+ lines.push(`### Server: ${serverName}`);
316
+ for (const { fullName, entry } of tools) {
317
+ const schema = entry.inputSchema?.properties || {};
318
+ const required = entry.inputSchema?.required || [];
319
+ const params = Object.entries(schema)
320
+ .map(([k, v]) => `${k}${required.includes(k) ? "" : "?"}: ${v.type || "any"}`)
321
+ .join(", ");
322
+ const desc = entry.description || entry.toolName;
323
+ lines.push(`#### ${fullName}(argsJson: string)`);
324
+ lines.push(`${desc}`);
325
+ if (params) lines.push(`- argsJson: {${params}}`);
326
+ lines.push("");
327
+ }
328
+ }
329
+
330
+ return lines.join("\n");
331
+ }
332
+
333
+ /**
334
+ * Get callable tool functions for a specific MCP server only.
335
+ * Used by MCPAgentRunner to give specialist agents only their server's tools.
336
+ * @param {string} serverName - e.g. "github"
337
+ * @returns {object} { "mcp__server__tool": fn, ... }
338
+ */
339
+ getServerTools(serverName) {
340
+ const prefix = `mcp__${serverName}__`;
341
+ const serverTools = {};
342
+
343
+ for (const [fullName, entry] of this.toolMap) {
344
+ if (!fullName.startsWith(prefix)) continue;
345
+
346
+ serverTools[fullName] = async (...args) => {
347
+ let toolArgs = {};
348
+ if (args[0]) {
349
+ try {
350
+ toolArgs = typeof args[0] === "string" ? JSON.parse(args[0]) : args[0];
351
+ } catch {
352
+ const props = entry.inputSchema?.properties || {};
353
+ const keys = Object.keys(props);
354
+ toolArgs = {};
355
+ for (let i = 0; i < keys.length && i < args.length; i++) {
356
+ toolArgs[keys[i]] = args[i];
357
+ }
358
+ }
359
+ }
360
+ console.log(` [MCP:${fullName}] Calling with: ${JSON.stringify(toolArgs).slice(0, 200)}`);
361
+ try {
362
+ return await entry.client.callTool(entry.toolName, toolArgs);
363
+ } catch (error) {
364
+ return `MCP tool error: ${error.message}`;
365
+ }
366
+ };
367
+ }
368
+
369
+ return serverTools;
370
+ }
371
+
372
+ /**
373
+ * Get info about all connected servers — used for system prompt listing.
374
+ * @returns {Array<{name, toolCount, toolNames}>}
375
+ */
376
+ getConnectedServersInfo() {
377
+ const configServers = this.readConfig().mcpServers || {};
378
+ return [...this.clients.entries()]
379
+ .filter(([, client]) => client.connected)
380
+ .map(([name, client]) => ({
381
+ name,
382
+ description: configServers[name]?.description || "",
383
+ toolCount: client.getTools().length,
384
+ toolNames: client.getTools().map((t) => t.name),
385
+ }));
386
+ }
387
+
388
+ /**
389
+ * Disconnect all servers.
390
+ */
391
+ async shutdown() {
392
+ for (const [name, client] of this.clients) {
393
+ await client.disconnect();
394
+ }
395
+ this.clients.clear();
396
+ this.toolMap.clear();
397
+ }
398
+
399
+ /**
400
+ * List connected servers and tools.
401
+ */
402
+ list() {
403
+ return [...this.clients.entries()].map(([name, client]) => ({
404
+ name,
405
+ connected: client.connected,
406
+ tools: client.getTools().map((t) => t.name),
407
+ }));
408
+ }
409
+ }
410
+
411
+ const mcpManager = new MCPManager();
412
+ export default mcpManager;