@darkiceinteractive/mcp-conductor 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 (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +558 -0
  3. package/dist/bin/cli.d.ts +8 -0
  4. package/dist/bin/cli.d.ts.map +1 -0
  5. package/dist/bin/cli.js +940 -0
  6. package/dist/bin/cli.js.map +1 -0
  7. package/dist/bridge/http-server.d.ts +161 -0
  8. package/dist/bridge/http-server.d.ts.map +1 -0
  9. package/dist/bridge/http-server.js +367 -0
  10. package/dist/bridge/http-server.js.map +1 -0
  11. package/dist/bridge/index.d.ts +5 -0
  12. package/dist/bridge/index.d.ts.map +1 -0
  13. package/dist/bridge/index.js +5 -0
  14. package/dist/bridge/index.js.map +1 -0
  15. package/dist/config/defaults.d.ts +29 -0
  16. package/dist/config/defaults.d.ts.map +1 -0
  17. package/dist/config/defaults.js +60 -0
  18. package/dist/config/defaults.js.map +1 -0
  19. package/dist/config/index.d.ts +7 -0
  20. package/dist/config/index.d.ts.map +1 -0
  21. package/dist/config/index.js +7 -0
  22. package/dist/config/index.js.map +1 -0
  23. package/dist/config/loader.d.ts +49 -0
  24. package/dist/config/loader.d.ts.map +1 -0
  25. package/dist/config/loader.js +272 -0
  26. package/dist/config/loader.js.map +1 -0
  27. package/dist/config/schema.d.ts +93 -0
  28. package/dist/config/schema.d.ts.map +1 -0
  29. package/dist/config/schema.js +5 -0
  30. package/dist/config/schema.js.map +1 -0
  31. package/dist/hub/index.d.ts +5 -0
  32. package/dist/hub/index.d.ts.map +1 -0
  33. package/dist/hub/index.js +5 -0
  34. package/dist/hub/index.js.map +1 -0
  35. package/dist/hub/mcp-hub.d.ts +176 -0
  36. package/dist/hub/mcp-hub.d.ts.map +1 -0
  37. package/dist/hub/mcp-hub.js +550 -0
  38. package/dist/hub/mcp-hub.js.map +1 -0
  39. package/dist/index.d.ts +9 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +45 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/metrics/index.d.ts +5 -0
  44. package/dist/metrics/index.d.ts.map +1 -0
  45. package/dist/metrics/index.js +5 -0
  46. package/dist/metrics/index.js.map +1 -0
  47. package/dist/metrics/metrics-collector.d.ts +211 -0
  48. package/dist/metrics/metrics-collector.d.ts.map +1 -0
  49. package/dist/metrics/metrics-collector.js +437 -0
  50. package/dist/metrics/metrics-collector.js.map +1 -0
  51. package/dist/modes/index.d.ts +5 -0
  52. package/dist/modes/index.d.ts.map +1 -0
  53. package/dist/modes/index.js +5 -0
  54. package/dist/modes/index.js.map +1 -0
  55. package/dist/modes/mode-handler.d.ts +132 -0
  56. package/dist/modes/mode-handler.d.ts.map +1 -0
  57. package/dist/modes/mode-handler.js +252 -0
  58. package/dist/modes/mode-handler.js.map +1 -0
  59. package/dist/runtime/executor.d.ts +57 -0
  60. package/dist/runtime/executor.d.ts.map +1 -0
  61. package/dist/runtime/executor.js +700 -0
  62. package/dist/runtime/executor.js.map +1 -0
  63. package/dist/runtime/index.d.ts +5 -0
  64. package/dist/runtime/index.d.ts.map +1 -0
  65. package/dist/runtime/index.js +5 -0
  66. package/dist/runtime/index.js.map +1 -0
  67. package/dist/server/index.d.ts +5 -0
  68. package/dist/server/index.d.ts.map +1 -0
  69. package/dist/server/index.js +5 -0
  70. package/dist/server/index.js.map +1 -0
  71. package/dist/server/mcp-server.d.ts +62 -0
  72. package/dist/server/mcp-server.d.ts.map +1 -0
  73. package/dist/server/mcp-server.js +1272 -0
  74. package/dist/server/mcp-server.js.map +1 -0
  75. package/dist/skills/index.d.ts +5 -0
  76. package/dist/skills/index.d.ts.map +1 -0
  77. package/dist/skills/index.js +5 -0
  78. package/dist/skills/index.js.map +1 -0
  79. package/dist/skills/skills-engine.d.ts +157 -0
  80. package/dist/skills/skills-engine.d.ts.map +1 -0
  81. package/dist/skills/skills-engine.js +405 -0
  82. package/dist/skills/skills-engine.js.map +1 -0
  83. package/dist/streaming/execution-stream.d.ts +158 -0
  84. package/dist/streaming/execution-stream.d.ts.map +1 -0
  85. package/dist/streaming/execution-stream.js +320 -0
  86. package/dist/streaming/execution-stream.js.map +1 -0
  87. package/dist/streaming/index.d.ts +5 -0
  88. package/dist/streaming/index.d.ts.map +1 -0
  89. package/dist/streaming/index.js +5 -0
  90. package/dist/streaming/index.js.map +1 -0
  91. package/dist/utils/errors.d.ts +36 -0
  92. package/dist/utils/errors.d.ts.map +1 -0
  93. package/dist/utils/errors.js +68 -0
  94. package/dist/utils/errors.js.map +1 -0
  95. package/dist/utils/helpers.d.ts +44 -0
  96. package/dist/utils/helpers.d.ts.map +1 -0
  97. package/dist/utils/helpers.js +95 -0
  98. package/dist/utils/helpers.js.map +1 -0
  99. package/dist/utils/index.d.ts +9 -0
  100. package/dist/utils/index.d.ts.map +1 -0
  101. package/dist/utils/index.js +9 -0
  102. package/dist/utils/index.js.map +1 -0
  103. package/dist/utils/logger.d.ts +13 -0
  104. package/dist/utils/logger.d.ts.map +1 -0
  105. package/dist/utils/logger.js +48 -0
  106. package/dist/utils/logger.js.map +1 -0
  107. package/dist/utils/permissions.d.ts +97 -0
  108. package/dist/utils/permissions.d.ts.map +1 -0
  109. package/dist/utils/permissions.js +165 -0
  110. package/dist/utils/permissions.js.map +1 -0
  111. package/dist/utils/rate-limiter.d.ts +87 -0
  112. package/dist/utils/rate-limiter.d.ts.map +1 -0
  113. package/dist/utils/rate-limiter.js +187 -0
  114. package/dist/utils/rate-limiter.js.map +1 -0
  115. package/dist/watcher/config-watcher.d.ts +67 -0
  116. package/dist/watcher/config-watcher.d.ts.map +1 -0
  117. package/dist/watcher/config-watcher.js +150 -0
  118. package/dist/watcher/config-watcher.js.map +1 -0
  119. package/dist/watcher/index.d.ts +5 -0
  120. package/dist/watcher/index.d.ts.map +1 -0
  121. package/dist/watcher/index.js +5 -0
  122. package/dist/watcher/index.js.map +1 -0
  123. package/package.json +86 -0
  124. package/templates/CLAUDE.md +137 -0
  125. package/templates/skill-mcp-conductor.md +64 -0
@@ -0,0 +1,1272 @@
1
+ /**
2
+ * MCP Executor Server
3
+ *
4
+ * Main MCP server that exposes the execute_code tool and other utilities.
5
+ */
6
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
+ import * as z from 'zod';
9
+ import { logger } from '../utils/index.js';
10
+ import { HttpBridge } from '../bridge/index.js';
11
+ import { DenoExecutor } from '../runtime/index.js';
12
+ import { MCPHub } from '../hub/index.js';
13
+ import { ModeHandler } from '../modes/index.js';
14
+ import { MetricsCollector } from '../metrics/index.js';
15
+ import { loadConductorConfig, saveConductorConfig, getDefaultConductorConfigPath } from '../config/index.js';
16
+ /**
17
+ * MCP Executor Server
18
+ */
19
+ export class MCPExecutorServer {
20
+ server;
21
+ bridge;
22
+ executor;
23
+ hub;
24
+ skills = null;
25
+ modeHandler;
26
+ metricsCollector;
27
+ config;
28
+ useMockServers;
29
+ currentMode;
30
+ // Mock server data for testing when no real servers configured
31
+ mockServers = new Map();
32
+ constructor(config, options) {
33
+ this.config = config;
34
+ this.useMockServers = options?.useMockServers ?? false;
35
+ this.currentMode = config.execution.mode;
36
+ // Initialise mode handler
37
+ this.modeHandler = new ModeHandler({
38
+ defaultMode: config.execution.mode,
39
+ hybridToolCallThreshold: 3,
40
+ hybridDataThreshold: 5,
41
+ });
42
+ // Initialise metrics collector
43
+ this.metricsCollector = new MetricsCollector(config.metrics);
44
+ // Initialise MCP server with metadata
45
+ this.server = new McpServer({
46
+ name: 'mcp-conductor',
47
+ title: 'MCP Conductor',
48
+ version: '0.1.0',
49
+ websiteUrl: 'https://github.com/darkiceinteractive/mcp-conductor',
50
+ icons: [
51
+ {
52
+ // Conductor baton/orchestrator icon (SVG data URI)
53
+ src: 'data:image/svg+xml;base64,' + Buffer.from(`
54
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
55
+ <circle cx="24" cy="24" r="22" fill="#1a1a2e" stroke="#6366f1" stroke-width="2"/>
56
+ <circle cx="24" cy="14" r="4" fill="#6366f1"/>
57
+ <path d="M24 18 L24 34" stroke="#6366f1" stroke-width="3" stroke-linecap="round"/>
58
+ <path d="M16 26 L24 22 L32 26" stroke="#818cf8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
59
+ <path d="M12 32 L24 26 L36 32" stroke="#a5b4fc" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
60
+ <circle cx="12" cy="32" r="2" fill="#22d3ee"/>
61
+ <circle cx="24" cy="26" r="2" fill="#22d3ee"/>
62
+ <circle cx="36" cy="32" r="2" fill="#22d3ee"/>
63
+ <circle cx="16" cy="26" r="1.5" fill="#34d399"/>
64
+ <circle cx="32" cy="26" r="1.5" fill="#34d399"/>
65
+ </svg>
66
+ `).toString('base64'),
67
+ mimeType: 'image/svg+xml',
68
+ sizes: ['48x48', 'any'],
69
+ },
70
+ ],
71
+ });
72
+ // Initialise HTTP bridge
73
+ this.bridge = new HttpBridge(config.bridge);
74
+ // Initialise Deno executor
75
+ this.executor = new DenoExecutor(config.sandbox);
76
+ // Initialise MCP Hub for real server connections
77
+ this.hub = new MCPHub({
78
+ servers: config.servers,
79
+ autoReconnect: true,
80
+ reconnectDelayMs: 5000,
81
+ maxReconnectAttempts: 3,
82
+ });
83
+ // Set up mock servers for fallback/testing
84
+ this.setupMockServers();
85
+ // Register tools
86
+ this.registerTools();
87
+ }
88
+ /**
89
+ * Set up mock servers for testing/fallback
90
+ */
91
+ setupMockServers() {
92
+ // Mock filesystem server
93
+ this.mockServers.set('filesystem', {
94
+ tools: [
95
+ { name: 'read_file', description: 'Read the contents of a file' },
96
+ { name: 'write_file', description: 'Write content to a file' },
97
+ { name: 'list_directory', description: 'List files in a directory' },
98
+ { name: 'search_files', description: 'Search for files matching a pattern' },
99
+ ],
100
+ });
101
+ // Mock echo server for testing
102
+ this.mockServers.set('echo', {
103
+ tools: [
104
+ { name: 'echo', description: 'Echo back the input message' },
105
+ { name: 'reverse', description: 'Reverse the input string' },
106
+ ],
107
+ });
108
+ }
109
+ /**
110
+ * Register all MCP tools
111
+ */
112
+ registerTools() {
113
+ // Register execute_code tool
114
+ this.server.registerTool('execute_code', {
115
+ title: 'Execute Code',
116
+ description: `Execute TypeScript/JavaScript code to perform MCP operations efficiently.
117
+
118
+ **Token Savings:** 90-98% vs individual tool calls. Batch operations in a single execution.
119
+
120
+ **API:** \`mcp.server('name').call('tool', params)\` | \`mcp.searchTools('query')\` | \`mcp.log('msg')\`
121
+
122
+ **Example:** \`const files = await mcp.filesystem.call('list_directory', { path: '/src' }); return files;\`
123
+
124
+ Use passthrough_call only for debugging - it has HIGH token cost.`,
125
+ inputSchema: {
126
+ code: z.string().describe('TypeScript/JavaScript code to execute. Must include a return statement.'),
127
+ servers: z.array(z.string()).optional().describe('Optional: List of MCP server names to load.'),
128
+ timeout_ms: z.number().optional().describe('Maximum execution time in milliseconds. Default: 30000.'),
129
+ stream: z.boolean().optional().describe('If true, stream progress updates. Default: false.'),
130
+ verbose: z.boolean().optional().describe('If true, include detailed metrics in response. Default: false.'),
131
+ },
132
+ outputSchema: {
133
+ success: z.boolean(),
134
+ result: z.unknown().optional(),
135
+ error: z.object({
136
+ type: z.enum(['syntax', 'runtime', 'timeout', 'security']),
137
+ message: z.string(),
138
+ stack: z.string().optional(),
139
+ line: z.number().optional(),
140
+ }).optional(),
141
+ metrics: z.object({
142
+ execution_time_ms: z.number(),
143
+ tool_calls: z.number(),
144
+ data_processed_bytes: z.number(),
145
+ result_size_bytes: z.number(),
146
+ estimated_tokens_saved: z.number(),
147
+ }).optional(),
148
+ logs: z.array(z.string()).optional(),
149
+ },
150
+ }, async ({ code, servers, timeout_ms, stream, verbose }) => {
151
+ const timeoutMs = Math.min(timeout_ms || this.config.execution.defaultTimeoutMs, this.config.execution.maxTimeoutMs);
152
+ logger.info('Executing code', {
153
+ codeLength: code.length,
154
+ timeout: timeoutMs,
155
+ servers: servers || 'all',
156
+ });
157
+ const result = await this.executor.execute(code, {
158
+ timeoutMs,
159
+ bridgeUrl: this.bridge.getUrl(),
160
+ servers: servers || [],
161
+ });
162
+ // Record execution with enhanced metrics collector
163
+ const executionMetrics = this.metricsCollector.recordExecution({
164
+ executionId: result.executionId,
165
+ code,
166
+ result: result.result,
167
+ success: result.success,
168
+ durationMs: result.metrics.executionTimeMs,
169
+ toolCalls: result.metrics.toolCalls,
170
+ dataProcessedBytes: result.metrics.dataProcessedBytes,
171
+ resultSizeBytes: result.metrics.resultSizeBytes,
172
+ mode: 'execution',
173
+ serversUsed: servers || [],
174
+ errorType: result.error?.type,
175
+ });
176
+ // Track with mode handler
177
+ this.modeHandler.recordExecutionCall(executionMetrics.estimatedTokensSaved);
178
+ const output = {
179
+ success: result.success,
180
+ result: result.result,
181
+ error: result.error,
182
+ };
183
+ // Only include metrics when verbose is true (saves ~150-300 tokens per response)
184
+ if (verbose) {
185
+ output.metrics = {
186
+ execution_time_ms: result.metrics.executionTimeMs,
187
+ tool_calls: result.metrics.toolCalls,
188
+ data_processed_bytes: result.metrics.dataProcessedBytes,
189
+ result_size_bytes: result.metrics.resultSizeBytes,
190
+ estimated_tokens_saved: executionMetrics.estimatedTokensSaved,
191
+ savings_percent: executionMetrics.savingsPercent,
192
+ };
193
+ output.logs = result.logs;
194
+ }
195
+ return {
196
+ content: [{ type: 'text', text: JSON.stringify(output) }],
197
+ structuredContent: output,
198
+ };
199
+ });
200
+ // Register list_servers tool
201
+ this.server.registerTool('list_servers', {
202
+ title: 'List Servers',
203
+ description: 'List all MCP servers connected through MCP Executor.',
204
+ inputSchema: {
205
+ include_tools: z.boolean().optional().describe('If true, include list of tool names.'),
206
+ },
207
+ outputSchema: {
208
+ servers: z.array(z.object({
209
+ name: z.string(),
210
+ status: z.enum(['connected', 'disconnected', 'error']),
211
+ tool_count: z.number(),
212
+ tools: z.array(z.string()).optional(),
213
+ })),
214
+ total_servers: z.number(),
215
+ total_tools: z.number(),
216
+ },
217
+ }, async ({ include_tools }) => {
218
+ let servers;
219
+ if (this.useMockServers) {
220
+ // Use mock servers for testing
221
+ servers = Array.from(this.mockServers.entries()).map(([name, data]) => ({
222
+ name,
223
+ status: 'connected',
224
+ tool_count: data.tools.length,
225
+ tools: include_tools ? data.tools.map((t) => t.name) : undefined,
226
+ }));
227
+ }
228
+ else {
229
+ // Use real hub servers
230
+ servers = this.hub.listServers().map((s) => ({
231
+ name: s.name,
232
+ status: s.status === 'connected' ? 'connected' : s.status === 'error' ? 'error' : 'disconnected',
233
+ tool_count: s.toolCount,
234
+ tools: include_tools ? this.hub.getServerTools(s.name).map((t) => t.name) : undefined,
235
+ }));
236
+ }
237
+ const totalTools = servers.reduce((sum, s) => sum + s.tool_count, 0);
238
+ const output = {
239
+ servers,
240
+ total_servers: servers.length,
241
+ total_tools: totalTools,
242
+ };
243
+ return {
244
+ content: [{ type: 'text', text: JSON.stringify(output) }],
245
+ structuredContent: output,
246
+ };
247
+ });
248
+ // Register discover_tools tool
249
+ this.server.registerTool('discover_tools', {
250
+ title: 'Discover Tools',
251
+ description: 'Search for available tools across all connected MCP servers.',
252
+ inputSchema: {
253
+ query: z.string().optional().describe('Search query. Matches against tool names and descriptions.'),
254
+ server: z.string().optional().describe('Optional: limit search to a specific server.'),
255
+ limit: z.number().optional().describe('Maximum results to return. Default: 20.'),
256
+ },
257
+ outputSchema: {
258
+ results: z.array(z.object({
259
+ server: z.string(),
260
+ tool: z.string(),
261
+ description: z.string(),
262
+ relevance: z.number(),
263
+ })),
264
+ total_matches: z.number(),
265
+ servers_searched: z.number(),
266
+ },
267
+ }, async ({ query, server, limit }) => {
268
+ const maxResults = limit || 20;
269
+ const results = [];
270
+ const searchLower = (query || '').toLowerCase();
271
+ let serversSearched = 0;
272
+ if (this.useMockServers) {
273
+ // Search mock servers
274
+ for (const [serverName, data] of this.mockServers.entries()) {
275
+ if (server && serverName !== server)
276
+ continue;
277
+ serversSearched++;
278
+ for (const tool of data.tools) {
279
+ const nameMatch = tool.name.toLowerCase().includes(searchLower);
280
+ const descMatch = tool.description.toLowerCase().includes(searchLower);
281
+ if (!query || nameMatch || descMatch) {
282
+ const relevance = nameMatch ? 1.0 : descMatch ? 0.7 : 0.5;
283
+ results.push({
284
+ server: serverName,
285
+ tool: tool.name,
286
+ description: tool.description,
287
+ relevance,
288
+ });
289
+ }
290
+ }
291
+ }
292
+ }
293
+ else {
294
+ // Search real hub servers
295
+ const hubServers = this.hub.listServers();
296
+ for (const hubServer of hubServers) {
297
+ if (server && hubServer.name !== server)
298
+ continue;
299
+ serversSearched++;
300
+ const tools = this.hub.getServerTools(hubServer.name);
301
+ for (const tool of tools) {
302
+ const nameMatch = tool.name.toLowerCase().includes(searchLower);
303
+ const descMatch = (tool.description || '').toLowerCase().includes(searchLower);
304
+ if (!query || nameMatch || descMatch) {
305
+ const relevance = nameMatch ? 1.0 : descMatch ? 0.7 : 0.5;
306
+ results.push({
307
+ server: hubServer.name,
308
+ tool: tool.name,
309
+ description: tool.description || '',
310
+ relevance,
311
+ });
312
+ }
313
+ }
314
+ }
315
+ }
316
+ // Sort by relevance and limit
317
+ results.sort((a, b) => b.relevance - a.relevance);
318
+ const limited = results.slice(0, maxResults);
319
+ const output = {
320
+ results: limited,
321
+ total_matches: results.length,
322
+ servers_searched: serversSearched,
323
+ };
324
+ return {
325
+ content: [{ type: 'text', text: JSON.stringify(output) }],
326
+ structuredContent: output,
327
+ };
328
+ });
329
+ // Register get_metrics tool
330
+ this.server.registerTool('get_metrics', {
331
+ title: 'Get Metrics',
332
+ description: 'Get detailed aggregated metrics for the current session including token savings, performance, and usage patterns.',
333
+ inputSchema: {
334
+ reset: z.boolean().optional().describe('Reset metrics after returning.'),
335
+ include_details: z.boolean().optional().describe('Include detailed breakdowns (servers, tools, recent executions).'),
336
+ },
337
+ outputSchema: {
338
+ session: z.object({
339
+ session_id: z.string(),
340
+ session_start: z.string(),
341
+ uptime_ms: z.number(),
342
+ }),
343
+ executions: z.object({
344
+ total: z.number(),
345
+ successful: z.number(),
346
+ failed: z.number(),
347
+ }),
348
+ tokens: z.object({
349
+ total_saved: z.number(),
350
+ average_saved: z.number(),
351
+ average_savings_percent: z.number(),
352
+ }),
353
+ performance: z.object({
354
+ average_duration_ms: z.number(),
355
+ min_duration_ms: z.number(),
356
+ max_duration_ms: z.number(),
357
+ }),
358
+ data: z.object({
359
+ total_processed_bytes: z.number(),
360
+ total_result_bytes: z.number(),
361
+ }),
362
+ mode_breakdown: z.object({
363
+ execution_calls: z.number(),
364
+ passthrough_calls: z.number(),
365
+ }),
366
+ current_mode: z.enum(['execution', 'passthrough', 'hybrid']),
367
+ details: z.object({
368
+ top_servers: z.array(z.object({ server: z.string(), calls: z.number() })).optional(),
369
+ top_tools: z.array(z.object({ tool: z.string(), calls: z.number() })).optional(),
370
+ recent_executions: z.array(z.object({
371
+ execution_id: z.string(),
372
+ success: z.boolean(),
373
+ duration_ms: z.number(),
374
+ tokens_saved: z.number(),
375
+ })).optional(),
376
+ }).optional(),
377
+ },
378
+ }, async ({ reset, include_details }) => {
379
+ const sessionMetrics = this.metricsCollector.getSessionMetrics();
380
+ const output = {
381
+ session: {
382
+ session_id: sessionMetrics.sessionId,
383
+ session_start: sessionMetrics.sessionStart.toISOString(),
384
+ uptime_ms: sessionMetrics.uptime,
385
+ },
386
+ executions: {
387
+ total: sessionMetrics.totalExecutions,
388
+ successful: sessionMetrics.successfulExecutions,
389
+ failed: sessionMetrics.failedExecutions,
390
+ },
391
+ tokens: {
392
+ total_saved: sessionMetrics.totalTokensSaved,
393
+ average_saved: Math.round(sessionMetrics.averageTokensSaved),
394
+ average_savings_percent: Math.round(sessionMetrics.averageSavingsPercent),
395
+ },
396
+ performance: {
397
+ average_duration_ms: Math.round(sessionMetrics.averageDurationMs),
398
+ min_duration_ms: sessionMetrics.minDurationMs,
399
+ max_duration_ms: sessionMetrics.maxDurationMs,
400
+ },
401
+ data: {
402
+ total_processed_bytes: sessionMetrics.totalDataProcessedBytes,
403
+ total_result_bytes: sessionMetrics.totalResultBytes,
404
+ },
405
+ mode_breakdown: {
406
+ execution_calls: sessionMetrics.executionModeCount,
407
+ passthrough_calls: sessionMetrics.passthroughModeCount,
408
+ },
409
+ current_mode: this.currentMode,
410
+ };
411
+ // Include detailed breakdowns if requested
412
+ if (include_details) {
413
+ const topServers = this.metricsCollector.getTopServers(5);
414
+ const topTools = this.metricsCollector.getTopTools(5);
415
+ const recentExecutions = this.metricsCollector.getRecentExecutions(5);
416
+ output['details'] = {
417
+ top_servers: topServers,
418
+ top_tools: topTools,
419
+ recent_executions: recentExecutions.map(e => ({
420
+ execution_id: e.executionId,
421
+ success: e.success,
422
+ duration_ms: e.durationMs,
423
+ tokens_saved: e.estimatedTokensSaved,
424
+ })),
425
+ };
426
+ }
427
+ if (reset) {
428
+ this.metricsCollector.reset();
429
+ }
430
+ return {
431
+ content: [{ type: 'text', text: JSON.stringify(output) }],
432
+ structuredContent: output,
433
+ };
434
+ });
435
+ // Register set_mode tool
436
+ this.server.registerTool('set_mode', {
437
+ title: 'Set Operation Mode',
438
+ description: `Switch between operation modes:
439
+ - execution: All requests go through the code executor (default, maximum token savings)
440
+ - passthrough: Direct tool calls without code execution (for debugging/comparison)
441
+ - hybrid: Automatic selection based on task complexity`,
442
+ inputSchema: {
443
+ mode: z.enum(['execution', 'passthrough', 'hybrid']).describe('The operation mode to switch to.'),
444
+ },
445
+ outputSchema: {
446
+ previous_mode: z.enum(['execution', 'passthrough', 'hybrid']),
447
+ current_mode: z.enum(['execution', 'passthrough', 'hybrid']),
448
+ message: z.string(),
449
+ },
450
+ }, async ({ mode }) => {
451
+ const previousMode = this.currentMode;
452
+ this.currentMode = mode;
453
+ this.modeHandler.setMode(mode);
454
+ const output = {
455
+ previous_mode: previousMode,
456
+ current_mode: mode,
457
+ message: `Mode changed from '${previousMode}' to '${mode}'`,
458
+ };
459
+ return {
460
+ content: [{ type: 'text', text: JSON.stringify(output) }],
461
+ structuredContent: output,
462
+ };
463
+ });
464
+ // Register reload_servers tool
465
+ this.server.registerTool('reload_servers', {
466
+ title: 'Reload Servers',
467
+ description: 'Reload MCP server configurations. Useful after modifying claude_desktop_config.json.',
468
+ inputSchema: {},
469
+ outputSchema: {
470
+ added: z.array(z.string()),
471
+ removed: z.array(z.string()),
472
+ total_servers: z.number(),
473
+ message: z.string(),
474
+ },
475
+ }, async () => {
476
+ const result = await this.reloadServers();
477
+ const output = {
478
+ added: result.added,
479
+ removed: result.removed,
480
+ total_servers: this.hub.getStats().total,
481
+ message: `Reloaded servers. Added: ${result.added.length}, Removed: ${result.removed.length}`,
482
+ };
483
+ return {
484
+ content: [{ type: 'text', text: JSON.stringify(output) }],
485
+ structuredContent: output,
486
+ };
487
+ });
488
+ // Register get_capabilities tool
489
+ this.server.registerTool('get_capabilities', {
490
+ title: 'Get Capabilities',
491
+ description: 'Get detailed information about MCP Executor capabilities and configuration.',
492
+ inputSchema: {},
493
+ outputSchema: {
494
+ version: z.string(),
495
+ current_mode: z.enum(['execution', 'passthrough', 'hybrid']),
496
+ features: z.object({
497
+ streaming: z.boolean(),
498
+ hot_reload: z.boolean(),
499
+ skills: z.boolean(),
500
+ }),
501
+ limits: z.object({
502
+ max_timeout_ms: z.number(),
503
+ default_timeout_ms: z.number(),
504
+ max_memory_mb: z.number(),
505
+ }),
506
+ servers: z.object({
507
+ total: z.number(),
508
+ connected: z.number(),
509
+ }),
510
+ skills: z.object({
511
+ loaded: z.number(),
512
+ categories: z.array(z.string()),
513
+ }),
514
+ },
515
+ }, async () => {
516
+ const hubStats = this.hub.getStats();
517
+ const skillsInfo = this.skills
518
+ ? { loaded: this.skills.getSkillCount(), categories: this.skills.getCategories() }
519
+ : { loaded: 0, categories: [] };
520
+ const output = {
521
+ version: '0.1.0',
522
+ current_mode: this.currentMode,
523
+ features: {
524
+ streaming: this.config.execution.streamingEnabled,
525
+ hot_reload: this.config.hotReload.enabled,
526
+ skills: this.skills !== null && this.skills.isLoaded(),
527
+ },
528
+ limits: {
529
+ max_timeout_ms: this.config.execution.maxTimeoutMs,
530
+ default_timeout_ms: this.config.execution.defaultTimeoutMs,
531
+ max_memory_mb: this.config.sandbox.maxMemoryMb,
532
+ },
533
+ servers: {
534
+ total: hubStats.total,
535
+ connected: hubStats.connected,
536
+ },
537
+ skills: skillsInfo,
538
+ };
539
+ return {
540
+ content: [{ type: 'text', text: JSON.stringify(output) }],
541
+ structuredContent: output,
542
+ };
543
+ });
544
+ // Register compare_modes tool
545
+ this.server.registerTool('compare_modes', {
546
+ title: 'Compare Modes',
547
+ description: `Analyse how a task would be handled in different modes.
548
+ Returns estimated token usage and approach for each mode.`,
549
+ inputSchema: {
550
+ task_description: z.string().describe('Description of the task to analyse.'),
551
+ estimated_tool_calls: z.number().optional().describe('Estimated number of tool calls needed.'),
552
+ estimated_data_kb: z.number().optional().describe('Estimated data to process in KB.'),
553
+ },
554
+ outputSchema: {
555
+ task: z.string(),
556
+ modes: z.object({
557
+ execution: z.object({
558
+ approach: z.string(),
559
+ estimated_tokens: z.number(),
560
+ advantages: z.array(z.string()),
561
+ }),
562
+ passthrough: z.object({
563
+ approach: z.string(),
564
+ estimated_tokens: z.number(),
565
+ advantages: z.array(z.string()),
566
+ }),
567
+ }),
568
+ recommendation: z.string(),
569
+ token_savings_percent: z.number(),
570
+ },
571
+ }, async ({ task_description, estimated_tool_calls, estimated_data_kb }) => {
572
+ const toolCalls = estimated_tool_calls || 5;
573
+ const dataKb = estimated_data_kb || 10;
574
+ // Estimate tokens for passthrough mode
575
+ // Each tool call involves request + response in context
576
+ const tokensPerToolCall = 200; // Average tokens per tool call overhead
577
+ const tokensPerKb = 250; // Approximate tokens per KB of data
578
+ const passthroughTokens = (toolCalls * tokensPerToolCall) + (dataKb * tokensPerKb);
579
+ // Estimate tokens for execution mode
580
+ // Code + summarised result
581
+ const codeTokens = 100; // Typical code block
582
+ const resultTokens = 50; // Summarised result
583
+ const executionTokens = codeTokens + resultTokens;
584
+ const savingsPercent = Math.round(((passthroughTokens - executionTokens) / passthroughTokens) * 100);
585
+ const output = {
586
+ task: task_description,
587
+ modes: {
588
+ execution: {
589
+ approach: 'Write code that processes data and returns only the relevant summary',
590
+ estimated_tokens: executionTokens,
591
+ advantages: [
592
+ 'Minimal context usage',
593
+ 'Data processing happens in sandbox',
594
+ 'Only final result returned to context',
595
+ 'Can handle large datasets efficiently',
596
+ ],
597
+ },
598
+ passthrough: {
599
+ approach: 'Make direct tool calls with full results in context',
600
+ estimated_tokens: passthroughTokens,
601
+ advantages: [
602
+ 'Simpler for quick, single tool calls',
603
+ 'No code writing overhead',
604
+ 'Direct access to full tool responses',
605
+ 'Better for debugging',
606
+ ],
607
+ },
608
+ },
609
+ recommendation: savingsPercent > 50
610
+ ? 'Use execution mode for significant token savings'
611
+ : savingsPercent > 20
612
+ ? 'Execution mode recommended for moderate savings'
613
+ : 'Passthrough mode may be simpler for this task',
614
+ token_savings_percent: Math.max(0, savingsPercent),
615
+ };
616
+ return {
617
+ content: [{ type: 'text', text: JSON.stringify(output) }],
618
+ structuredContent: output,
619
+ };
620
+ });
621
+ // Register passthrough_call tool for direct tool invocations
622
+ this.server.registerTool('passthrough_call', {
623
+ title: 'Passthrough Call',
624
+ description: `⚠️ DEBUGGING TOOL - Direct MCP tool call. HIGH TOKEN COST (10-100x vs execute_code).
625
+
626
+ Only use for debugging raw tool input/output. Use execute_code for all normal operations.`,
627
+ inputSchema: {
628
+ server: z.string().describe('Name of the MCP server to call.'),
629
+ tool: z.string().describe('Name of the tool to invoke.'),
630
+ params: z.record(z.unknown()).optional().describe('Parameters to pass to the tool.'),
631
+ },
632
+ outputSchema: {
633
+ success: z.boolean(),
634
+ result: z.unknown().optional(),
635
+ error: z.string().optional(),
636
+ metrics: z.object({
637
+ duration_ms: z.number(),
638
+ mode: z.enum(['passthrough', 'hybrid']),
639
+ }),
640
+ },
641
+ }, async ({ server, tool, params }) => {
642
+ const startTime = Date.now();
643
+ const passthroughId = `passthrough_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
644
+ // Check mode - warn if in execution mode
645
+ if (this.currentMode === 'execution') {
646
+ logger.warn('passthrough_call used in execution mode', { server, tool });
647
+ }
648
+ try {
649
+ let result;
650
+ if (this.useMockServers) {
651
+ // Use mock handlers for testing
652
+ const mockServer = this.mockServers.get(server);
653
+ if (!mockServer) {
654
+ throw new Error(`Server not found: ${server}`);
655
+ }
656
+ const mockTool = mockServer.tools.find((t) => t.name === tool);
657
+ if (!mockTool) {
658
+ throw new Error(`Tool not found: ${server}.${tool}`);
659
+ }
660
+ // Mock implementations
661
+ if (server === 'echo' && tool === 'echo') {
662
+ result = { message: params?.['message'] || '' };
663
+ }
664
+ else if (server === 'echo' && tool === 'reverse') {
665
+ const msg = String(params?.['message'] || '');
666
+ result = { reversed: msg.split('').reverse().join('') };
667
+ }
668
+ else if (server === 'filesystem') {
669
+ if (tool === 'list_directory') {
670
+ result = { entries: [{ name: 'file1.ts', type: 'file' }] };
671
+ }
672
+ else if (tool === 'read_file') {
673
+ result = { content: '// Mock file content' };
674
+ }
675
+ }
676
+ else {
677
+ throw new Error(`Mock not implemented: ${server}.${tool}`);
678
+ }
679
+ }
680
+ else {
681
+ // Use real hub
682
+ result = await this.hub.callTool(server, tool, params || {});
683
+ }
684
+ const durationMs = Date.now() - startTime;
685
+ // Record successful passthrough execution
686
+ const resultStr = JSON.stringify(result);
687
+ this.metricsCollector.recordExecution({
688
+ executionId: passthroughId,
689
+ code: '', // No code for passthrough
690
+ result,
691
+ success: true,
692
+ durationMs,
693
+ toolCalls: 1,
694
+ dataProcessedBytes: resultStr.length,
695
+ resultSizeBytes: resultStr.length,
696
+ mode: 'passthrough',
697
+ serversUsed: [server],
698
+ toolsUsed: [`${server}.${tool}`],
699
+ });
700
+ // Track with mode handler
701
+ this.modeHandler.recordPassthroughCall({
702
+ server,
703
+ tool,
704
+ params: params || {},
705
+ success: true,
706
+ durationMs,
707
+ });
708
+ const output = {
709
+ success: true,
710
+ result,
711
+ metrics: {
712
+ duration_ms: durationMs,
713
+ mode: this.currentMode === 'hybrid' ? 'hybrid' : 'passthrough',
714
+ },
715
+ };
716
+ // Add warning when used in execution mode
717
+ if (this.currentMode === 'execution') {
718
+ output['warning'] = '⚠️ INEFFICIENT: You used passthrough_call in execution mode. Use execute_code instead for 90%+ token savings. Each passthrough_call adds full request/response JSON to context.';
719
+ }
720
+ return {
721
+ content: [{ type: 'text', text: JSON.stringify(output) }],
722
+ structuredContent: output,
723
+ };
724
+ }
725
+ catch (error) {
726
+ const durationMs = Date.now() - startTime;
727
+ // Record failed passthrough execution
728
+ this.metricsCollector.recordExecution({
729
+ executionId: passthroughId,
730
+ code: '', // No code for passthrough
731
+ result: null,
732
+ success: false,
733
+ durationMs,
734
+ toolCalls: 1,
735
+ dataProcessedBytes: 0,
736
+ resultSizeBytes: 0,
737
+ mode: 'passthrough',
738
+ serversUsed: [server],
739
+ toolsUsed: [`${server}.${tool}`],
740
+ errorType: 'runtime',
741
+ });
742
+ // Track with mode handler
743
+ this.modeHandler.recordPassthroughCall({
744
+ server,
745
+ tool,
746
+ params: params || {},
747
+ success: false,
748
+ error: String(error),
749
+ durationMs,
750
+ });
751
+ const output = {
752
+ success: false,
753
+ error: String(error),
754
+ metrics: {
755
+ duration_ms: durationMs,
756
+ mode: this.currentMode === 'hybrid' ? 'hybrid' : 'passthrough',
757
+ },
758
+ };
759
+ return {
760
+ content: [{ type: 'text', text: JSON.stringify(output) }],
761
+ structuredContent: output,
762
+ };
763
+ }
764
+ });
765
+ // Register brave_web_search tool - direct access to Brave Search API
766
+ // This provides a token-efficient alternative to native WebSearch
767
+ this.server.registerTool('brave_web_search', {
768
+ title: 'Brave Web Search',
769
+ description: `Web search via Brave Search API. Uses 90% fewer tokens than native WebSearch.
770
+
771
+ Routes to brave-search MCP server internally. Requires brave-search server to be configured.`,
772
+ inputSchema: {
773
+ query: z.string().describe('Search query (max 400 chars, 50 words).'),
774
+ count: z.number().optional().describe('Number of results (1-20, default 10).'),
775
+ },
776
+ outputSchema: {
777
+ success: z.boolean(),
778
+ results: z.array(z.object({
779
+ title: z.string(),
780
+ description: z.string(),
781
+ url: z.string(),
782
+ })).optional(),
783
+ error: z.string().optional(),
784
+ },
785
+ }, async ({ query, count }) => {
786
+ const resultCount = Math.min(count || 10, 20);
787
+ try {
788
+ // Route to brave-search MCP server
789
+ const rawResult = await this.hub.callTool('brave-search', 'brave_web_search', {
790
+ query,
791
+ count: resultCount,
792
+ });
793
+ // Parse the text response from brave-search into structured results
794
+ const parseResults = (text) => {
795
+ if (typeof text !== 'string' || text.startsWith('Error:'))
796
+ return [];
797
+ return text.split(/\n\nTitle:/).map((block, i) => {
798
+ const b = i === 0 ? block : 'Title:' + block;
799
+ const title = b.match(/Title:\s*([^\n]+)/)?.[1]?.trim() || '';
800
+ const url = b.match(/URL:\s*([^\n]+)/)?.[1]?.trim() || '';
801
+ const desc = b.match(/Description:\s*([^\n]+)/)?.[1]?.trim() || '';
802
+ return title && url ? { title, description: desc, url } : null;
803
+ }).filter((r) => r !== null);
804
+ };
805
+ const results = parseResults(rawResult);
806
+ const output = {
807
+ success: true,
808
+ results,
809
+ };
810
+ return {
811
+ content: [{ type: 'text', text: JSON.stringify(output) }],
812
+ structuredContent: output,
813
+ };
814
+ }
815
+ catch (error) {
816
+ const output = {
817
+ success: false,
818
+ error: `Brave search failed: ${String(error)}. Ensure brave-search server is configured.`,
819
+ };
820
+ return {
821
+ content: [{ type: 'text', text: JSON.stringify(output) }],
822
+ structuredContent: output,
823
+ };
824
+ }
825
+ });
826
+ // Register add_server tool for runtime server management
827
+ this.server.registerTool('add_server', {
828
+ title: 'Add Server',
829
+ description: `Add a new MCP server to conductor config and connect immediately.
830
+
831
+ Saves the server configuration to ~/.mcp-conductor.json and triggers a reload.
832
+ Use this to dynamically add servers without restarting Claude.`,
833
+ inputSchema: {
834
+ name: z.string().describe('Unique server name (e.g., "github", "filesystem").'),
835
+ command: z.string().describe('Command to run the server (e.g., "npx", "node", "python").'),
836
+ args: z.array(z.string()).optional().describe('Command arguments (e.g., ["-y", "@modelcontextprotocol/server-github"]).'),
837
+ env: z.record(z.string()).optional().describe('Environment variables for the server (e.g., { "GITHUB_TOKEN": "..." }).'),
838
+ },
839
+ outputSchema: {
840
+ success: z.boolean(),
841
+ server_name: z.string(),
842
+ config_path: z.string(),
843
+ message: z.string(),
844
+ servers_after: z.number(),
845
+ },
846
+ }, async ({ name, command, args, env }) => {
847
+ try {
848
+ // Load existing conductor config or create new one
849
+ let config = loadConductorConfig();
850
+ if (!config) {
851
+ config = {
852
+ exclusive: false,
853
+ servers: {},
854
+ };
855
+ }
856
+ // Check if server already exists
857
+ if (config.servers[name]) {
858
+ return {
859
+ content: [{ type: 'text', text: JSON.stringify({
860
+ success: false,
861
+ server_name: name,
862
+ config_path: getDefaultConductorConfigPath(),
863
+ message: `Server '${name}' already exists. Use remove_server first to replace it.`,
864
+ servers_after: Object.keys(config.servers).length,
865
+ }, null, 2) }],
866
+ };
867
+ }
868
+ // Add new server
869
+ config.servers[name] = {
870
+ command,
871
+ args: args || [],
872
+ env: env || {},
873
+ };
874
+ // Save config
875
+ const saveResult = saveConductorConfig(config);
876
+ if (!saveResult.success) {
877
+ return {
878
+ content: [{ type: 'text', text: JSON.stringify({
879
+ success: false,
880
+ server_name: name,
881
+ config_path: saveResult.path,
882
+ message: `Failed to save config: ${saveResult.error}`,
883
+ servers_after: Object.keys(config.servers).length - 1,
884
+ }, null, 2) }],
885
+ };
886
+ }
887
+ // Reload servers to connect to the new one
888
+ const reloadResult = await this.reloadServers();
889
+ const output = {
890
+ success: true,
891
+ server_name: name,
892
+ config_path: saveResult.path,
893
+ message: `Server '${name}' added successfully. ${reloadResult.added.includes(name) ? 'Connected.' : 'Will connect on next initialisation.'}`,
894
+ servers_after: Object.keys(config.servers).length,
895
+ };
896
+ return {
897
+ content: [{ type: 'text', text: JSON.stringify(output) }],
898
+ structuredContent: output,
899
+ };
900
+ }
901
+ catch (error) {
902
+ return {
903
+ content: [{ type: 'text', text: JSON.stringify({
904
+ success: false,
905
+ server_name: name,
906
+ config_path: getDefaultConductorConfigPath(),
907
+ message: `Error adding server: ${String(error)}`,
908
+ servers_after: 0,
909
+ }, null, 2) }],
910
+ };
911
+ }
912
+ });
913
+ // Register remove_server tool for runtime server management
914
+ this.server.registerTool('remove_server', {
915
+ title: 'Remove Server',
916
+ description: `Remove an MCP server from conductor config and disconnect it.
917
+
918
+ Removes the server configuration from ~/.mcp-conductor.json and triggers a reload.
919
+ Use this to dynamically remove servers without restarting Claude.`,
920
+ inputSchema: {
921
+ name: z.string().describe('Name of the server to remove.'),
922
+ },
923
+ outputSchema: {
924
+ success: z.boolean(),
925
+ server_name: z.string(),
926
+ config_path: z.string(),
927
+ message: z.string(),
928
+ servers_after: z.number(),
929
+ },
930
+ }, async ({ name }) => {
931
+ try {
932
+ // Load existing conductor config
933
+ const config = loadConductorConfig();
934
+ if (!config) {
935
+ return {
936
+ content: [{ type: 'text', text: JSON.stringify({
937
+ success: false,
938
+ server_name: name,
939
+ config_path: getDefaultConductorConfigPath(),
940
+ message: 'No conductor config found. Nothing to remove.',
941
+ servers_after: 0,
942
+ }, null, 2) }],
943
+ };
944
+ }
945
+ // Check if server exists
946
+ if (!config.servers[name]) {
947
+ return {
948
+ content: [{ type: 'text', text: JSON.stringify({
949
+ success: false,
950
+ server_name: name,
951
+ config_path: getDefaultConductorConfigPath(),
952
+ message: `Server '${name}' not found in conductor config.`,
953
+ servers_after: Object.keys(config.servers).length,
954
+ }, null, 2) }],
955
+ };
956
+ }
957
+ // Remove server
958
+ delete config.servers[name];
959
+ // Save config
960
+ const saveResult = saveConductorConfig(config);
961
+ if (!saveResult.success) {
962
+ return {
963
+ content: [{ type: 'text', text: JSON.stringify({
964
+ success: false,
965
+ server_name: name,
966
+ config_path: saveResult.path,
967
+ message: `Failed to save config: ${saveResult.error}`,
968
+ servers_after: Object.keys(config.servers).length + 1,
969
+ }, null, 2) }],
970
+ };
971
+ }
972
+ // Reload servers to disconnect the removed one
973
+ const reloadResult = await this.reloadServers();
974
+ const output = {
975
+ success: true,
976
+ server_name: name,
977
+ config_path: saveResult.path,
978
+ message: `Server '${name}' removed successfully. ${reloadResult.removed.includes(name) ? 'Disconnected.' : 'Will be removed on next initialisation.'}`,
979
+ servers_after: Object.keys(config.servers).length,
980
+ };
981
+ return {
982
+ content: [{ type: 'text', text: JSON.stringify(output) }],
983
+ structuredContent: output,
984
+ };
985
+ }
986
+ catch (error) {
987
+ return {
988
+ content: [{ type: 'text', text: JSON.stringify({
989
+ success: false,
990
+ server_name: name,
991
+ config_path: getDefaultConductorConfigPath(),
992
+ message: `Error removing server: ${String(error)}`,
993
+ servers_after: 0,
994
+ }, null, 2) }],
995
+ };
996
+ }
997
+ });
998
+ // Register update_server tool for updating server config (e.g., API keys)
999
+ this.server.registerTool('update_server', {
1000
+ title: 'Update Server',
1001
+ description: `Update an existing MCP server's configuration (command, args, or env vars).
1002
+
1003
+ Use this to update API keys or other settings without removing and re-adding the server.
1004
+ Triggers a reload to apply changes immediately.`,
1005
+ inputSchema: {
1006
+ name: z.string().describe('Name of the server to update.'),
1007
+ command: z.string().optional().describe('New command (optional, keeps existing if not provided).'),
1008
+ args: z.array(z.string()).optional().describe('New arguments (optional, keeps existing if not provided).'),
1009
+ env: z.record(z.string()).optional().describe('Environment variables to update (merges with existing).'),
1010
+ replace_env: z.boolean().optional().describe('If true, replace all env vars instead of merging (default: false).'),
1011
+ },
1012
+ outputSchema: {
1013
+ success: z.boolean(),
1014
+ server_name: z.string(),
1015
+ config_path: z.string(),
1016
+ message: z.string(),
1017
+ updated_fields: z.array(z.string()),
1018
+ },
1019
+ }, async ({ name, command, args, env, replace_env }) => {
1020
+ try {
1021
+ // Load existing conductor config
1022
+ const config = loadConductorConfig();
1023
+ if (!config) {
1024
+ return {
1025
+ content: [{ type: 'text', text: JSON.stringify({
1026
+ success: false,
1027
+ server_name: name,
1028
+ config_path: getDefaultConductorConfigPath(),
1029
+ message: 'No conductor config found.',
1030
+ updated_fields: [],
1031
+ }, null, 2) }],
1032
+ };
1033
+ }
1034
+ // Check if server exists
1035
+ if (!config.servers[name]) {
1036
+ return {
1037
+ content: [{ type: 'text', text: JSON.stringify({
1038
+ success: false,
1039
+ server_name: name,
1040
+ config_path: getDefaultConductorConfigPath(),
1041
+ message: `Server '${name}' not found. Use add_server to create it first.`,
1042
+ updated_fields: [],
1043
+ }, null, 2) }],
1044
+ };
1045
+ }
1046
+ const updatedFields = [];
1047
+ const serverConfig = config.servers[name];
1048
+ // Update command if provided
1049
+ if (command !== undefined) {
1050
+ serverConfig.command = command;
1051
+ updatedFields.push('command');
1052
+ }
1053
+ // Update args if provided
1054
+ if (args !== undefined) {
1055
+ serverConfig.args = args;
1056
+ updatedFields.push('args');
1057
+ }
1058
+ // Update env vars
1059
+ if (env !== undefined) {
1060
+ if (replace_env) {
1061
+ serverConfig.env = env;
1062
+ updatedFields.push('env (replaced)');
1063
+ }
1064
+ else {
1065
+ serverConfig.env = { ...serverConfig.env, ...env };
1066
+ updatedFields.push(`env (merged: ${Object.keys(env).join(', ')})`);
1067
+ }
1068
+ }
1069
+ if (updatedFields.length === 0) {
1070
+ return {
1071
+ content: [{ type: 'text', text: JSON.stringify({
1072
+ success: false,
1073
+ server_name: name,
1074
+ config_path: getDefaultConductorConfigPath(),
1075
+ message: 'No fields to update. Provide command, args, or env.',
1076
+ updated_fields: [],
1077
+ }, null, 2) }],
1078
+ };
1079
+ }
1080
+ // Save config
1081
+ const saveResult = saveConductorConfig(config);
1082
+ if (!saveResult.success) {
1083
+ return {
1084
+ content: [{ type: 'text', text: JSON.stringify({
1085
+ success: false,
1086
+ server_name: name,
1087
+ config_path: saveResult.path,
1088
+ message: `Failed to save config: ${saveResult.error}`,
1089
+ updated_fields: [],
1090
+ }, null, 2) }],
1091
+ };
1092
+ }
1093
+ // Reload servers to apply changes
1094
+ await this.reloadServers();
1095
+ const output = {
1096
+ success: true,
1097
+ server_name: name,
1098
+ config_path: saveResult.path,
1099
+ message: `Server '${name}' updated successfully. Changes applied.`,
1100
+ updated_fields: updatedFields,
1101
+ };
1102
+ return {
1103
+ content: [{ type: 'text', text: JSON.stringify(output) }],
1104
+ structuredContent: output,
1105
+ };
1106
+ }
1107
+ catch (error) {
1108
+ return {
1109
+ content: [{ type: 'text', text: JSON.stringify({
1110
+ success: false,
1111
+ server_name: name,
1112
+ config_path: getDefaultConductorConfigPath(),
1113
+ message: `Error updating server: ${String(error)}`,
1114
+ updated_fields: [],
1115
+ }, null, 2) }],
1116
+ };
1117
+ }
1118
+ });
1119
+ }
1120
+ /**
1121
+ * Start the server
1122
+ */
1123
+ async start() {
1124
+ // Initialise the MCP Hub (connect to real servers)
1125
+ if (!this.useMockServers) {
1126
+ logger.info('Initialising MCP Hub...');
1127
+ await this.hub.initialise();
1128
+ const stats = this.hub.getStats();
1129
+ logger.info('MCP Hub initialised', {
1130
+ connected: stats.connected,
1131
+ total: stats.total,
1132
+ });
1133
+ }
1134
+ // Set up bridge handlers
1135
+ const handlers = {
1136
+ callTool: async (serverName, toolName, params) => {
1137
+ if (this.useMockServers) {
1138
+ // Use mock implementations for testing
1139
+ if (serverName === 'echo') {
1140
+ if (toolName === 'echo') {
1141
+ return { message: params['message'] || '' };
1142
+ }
1143
+ if (toolName === 'reverse') {
1144
+ const msg = String(params['message'] || '');
1145
+ return { reversed: msg.split('').reverse().join('') };
1146
+ }
1147
+ }
1148
+ // Mock filesystem responses
1149
+ if (serverName === 'filesystem') {
1150
+ if (toolName === 'list_directory') {
1151
+ return {
1152
+ entries: [
1153
+ { name: 'file1.ts', type: 'file' },
1154
+ { name: 'file2.ts', type: 'file' },
1155
+ { name: 'src', type: 'directory' },
1156
+ ],
1157
+ };
1158
+ }
1159
+ if (toolName === 'read_file') {
1160
+ return { content: '// Mock file content' };
1161
+ }
1162
+ }
1163
+ throw new Error(`Unknown tool: ${serverName}.${toolName}`);
1164
+ }
1165
+ else {
1166
+ // Use real hub for tool calls
1167
+ return await this.hub.callTool(serverName, toolName, params);
1168
+ }
1169
+ },
1170
+ listServers: () => {
1171
+ if (this.useMockServers) {
1172
+ return Array.from(this.mockServers.entries()).map(([name, data]) => ({
1173
+ name,
1174
+ toolCount: data.tools.length,
1175
+ status: 'connected',
1176
+ }));
1177
+ }
1178
+ else {
1179
+ return this.hub.listServers().map((s) => ({
1180
+ name: s.name,
1181
+ toolCount: s.toolCount,
1182
+ status: s.status === 'connected' ? 'connected' : s.status === 'error' ? 'error' : 'disconnected',
1183
+ }));
1184
+ }
1185
+ },
1186
+ listTools: (serverName) => {
1187
+ if (this.useMockServers) {
1188
+ const server = this.mockServers.get(serverName);
1189
+ return server?.tools || [];
1190
+ }
1191
+ else {
1192
+ return this.hub.getServerTools(serverName).map((t) => ({
1193
+ name: t.name,
1194
+ description: t.description || '',
1195
+ }));
1196
+ }
1197
+ },
1198
+ searchTools: (query) => {
1199
+ if (this.useMockServers) {
1200
+ const results = [];
1201
+ const searchLower = query.toLowerCase();
1202
+ for (const [serverName, data] of this.mockServers.entries()) {
1203
+ for (const tool of data.tools) {
1204
+ if (tool.name.toLowerCase().includes(searchLower) ||
1205
+ tool.description.toLowerCase().includes(searchLower)) {
1206
+ results.push({
1207
+ server: serverName,
1208
+ tool: tool.name,
1209
+ description: tool.description,
1210
+ });
1211
+ }
1212
+ }
1213
+ }
1214
+ return results;
1215
+ }
1216
+ else {
1217
+ return this.hub.searchTools(query);
1218
+ }
1219
+ },
1220
+ };
1221
+ this.bridge.setHandlers(handlers);
1222
+ // Start bridge server
1223
+ await this.bridge.start();
1224
+ // Connect to stdio transport
1225
+ const transport = new StdioServerTransport();
1226
+ await this.server.connect(transport);
1227
+ logger.info('MCP Executor server started');
1228
+ }
1229
+ /**
1230
+ * Stop the server
1231
+ */
1232
+ async stop() {
1233
+ // Shutdown hub connections
1234
+ if (!this.useMockServers) {
1235
+ await this.hub.shutdown();
1236
+ }
1237
+ await this.bridge.stop();
1238
+ logger.info('MCP Executor server stopped');
1239
+ }
1240
+ /**
1241
+ * Get the MCP Hub instance (for advanced usage)
1242
+ */
1243
+ getHub() {
1244
+ return this.hub;
1245
+ }
1246
+ /**
1247
+ * Reload server configurations
1248
+ */
1249
+ async reloadServers() {
1250
+ if (this.useMockServers) {
1251
+ return { added: [], removed: [] };
1252
+ }
1253
+ return await this.hub.reload();
1254
+ }
1255
+ /**
1256
+ * Get registered tools for testing purposes.
1257
+ * WARNING: This is an internal API for testing only.
1258
+ * @internal
1259
+ */
1260
+ getRegisteredTools() {
1261
+ // Access the private _registeredTools object from McpServer for testing
1262
+ // SDK stores tools as an object, not a Map, so we convert it
1263
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1264
+ const toolsObj = this.server._registeredTools;
1265
+ const toolsMap = new Map();
1266
+ for (const [name, tool] of Object.entries(toolsObj)) {
1267
+ toolsMap.set(name, tool);
1268
+ }
1269
+ return toolsMap;
1270
+ }
1271
+ }
1272
+ //# sourceMappingURL=mcp-server.js.map