@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.
- package/LICENSE +21 -0
- package/README.md +558 -0
- package/dist/bin/cli.d.ts +8 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +940 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/bridge/http-server.d.ts +161 -0
- package/dist/bridge/http-server.d.ts.map +1 -0
- package/dist/bridge/http-server.js +367 -0
- package/dist/bridge/http-server.js.map +1 -0
- package/dist/bridge/index.d.ts +5 -0
- package/dist/bridge/index.d.ts.map +1 -0
- package/dist/bridge/index.js +5 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/config/defaults.d.ts +29 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +60 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +7 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +49 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +272 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +93 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +5 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/hub/index.d.ts +5 -0
- package/dist/hub/index.d.ts.map +1 -0
- package/dist/hub/index.js +5 -0
- package/dist/hub/index.js.map +1 -0
- package/dist/hub/mcp-hub.d.ts +176 -0
- package/dist/hub/mcp-hub.d.ts.map +1 -0
- package/dist/hub/mcp-hub.js +550 -0
- package/dist/hub/mcp-hub.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/index.d.ts +5 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +5 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/metrics-collector.d.ts +211 -0
- package/dist/metrics/metrics-collector.d.ts.map +1 -0
- package/dist/metrics/metrics-collector.js +437 -0
- package/dist/metrics/metrics-collector.js.map +1 -0
- package/dist/modes/index.d.ts +5 -0
- package/dist/modes/index.d.ts.map +1 -0
- package/dist/modes/index.js +5 -0
- package/dist/modes/index.js.map +1 -0
- package/dist/modes/mode-handler.d.ts +132 -0
- package/dist/modes/mode-handler.d.ts.map +1 -0
- package/dist/modes/mode-handler.js +252 -0
- package/dist/modes/mode-handler.js.map +1 -0
- package/dist/runtime/executor.d.ts +57 -0
- package/dist/runtime/executor.d.ts.map +1 -0
- package/dist/runtime/executor.js +700 -0
- package/dist/runtime/executor.js.map +1 -0
- package/dist/runtime/index.d.ts +5 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +5 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +5 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/mcp-server.d.ts +62 -0
- package/dist/server/mcp-server.d.ts.map +1 -0
- package/dist/server/mcp-server.js +1272 -0
- package/dist/server/mcp-server.js.map +1 -0
- package/dist/skills/index.d.ts +5 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +5 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/skills-engine.d.ts +157 -0
- package/dist/skills/skills-engine.d.ts.map +1 -0
- package/dist/skills/skills-engine.js +405 -0
- package/dist/skills/skills-engine.js.map +1 -0
- package/dist/streaming/execution-stream.d.ts +158 -0
- package/dist/streaming/execution-stream.d.ts.map +1 -0
- package/dist/streaming/execution-stream.js +320 -0
- package/dist/streaming/execution-stream.js.map +1 -0
- package/dist/streaming/index.d.ts +5 -0
- package/dist/streaming/index.d.ts.map +1 -0
- package/dist/streaming/index.js +5 -0
- package/dist/streaming/index.js.map +1 -0
- package/dist/utils/errors.d.ts +36 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +68 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/helpers.d.ts +44 -0
- package/dist/utils/helpers.d.ts.map +1 -0
- package/dist/utils/helpers.js +95 -0
- package/dist/utils/helpers.js.map +1 -0
- package/dist/utils/index.d.ts +9 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +13 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +48 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/permissions.d.ts +97 -0
- package/dist/utils/permissions.d.ts.map +1 -0
- package/dist/utils/permissions.js +165 -0
- package/dist/utils/permissions.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +87 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +187 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/watcher/config-watcher.d.ts +67 -0
- package/dist/watcher/config-watcher.d.ts.map +1 -0
- package/dist/watcher/config-watcher.js +150 -0
- package/dist/watcher/config-watcher.js.map +1 -0
- package/dist/watcher/index.d.ts +5 -0
- package/dist/watcher/index.d.ts.map +1 -0
- package/dist/watcher/index.js +5 -0
- package/dist/watcher/index.js.map +1 -0
- package/package.json +86 -0
- package/templates/CLAUDE.md +137 -0
- 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
|