mcpflare 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/CHANGELOG.md +33 -0
- package/LICENSE +22 -0
- package/README.md +390 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +1615 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +19 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/mcp-handler.d.ts +34 -0
- package/dist/server/mcp-handler.d.ts.map +1 -0
- package/dist/server/mcp-handler.js +1524 -0
- package/dist/server/mcp-handler.js.map +1 -0
- package/dist/server/metrics-collector.d.ts +30 -0
- package/dist/server/metrics-collector.d.ts.map +1 -0
- package/dist/server/metrics-collector.js +85 -0
- package/dist/server/metrics-collector.js.map +1 -0
- package/dist/server/schema-converter.d.ts +9 -0
- package/dist/server/schema-converter.d.ts.map +1 -0
- package/dist/server/schema-converter.js +82 -0
- package/dist/server/schema-converter.js.map +1 -0
- package/dist/server/worker-manager.d.ts +48 -0
- package/dist/server/worker-manager.d.ts.map +1 -0
- package/dist/server/worker-manager.js +1746 -0
- package/dist/server/worker-manager.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/mcp.d.ts +495 -0
- package/dist/types/mcp.d.ts.map +1 -0
- package/dist/types/mcp.js +80 -0
- package/dist/types/mcp.js.map +1 -0
- package/dist/types/worker.d.ts +35 -0
- package/dist/types/worker.d.ts.map +1 -0
- package/dist/types/worker.js +2 -0
- package/dist/types/worker.js.map +1 -0
- package/dist/utils/config-manager.d.ts +64 -0
- package/dist/utils/config-manager.d.ts.map +1 -0
- package/dist/utils/config-manager.js +556 -0
- package/dist/utils/config-manager.js.map +1 -0
- package/dist/utils/env-selector.d.ts +4 -0
- package/dist/utils/env-selector.d.ts.map +1 -0
- package/dist/utils/env-selector.js +127 -0
- package/dist/utils/env-selector.js.map +1 -0
- package/dist/utils/errors.d.ts +19 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +37 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +27 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/mcp-registry.d.ts +108 -0
- package/dist/utils/mcp-registry.d.ts.map +1 -0
- package/dist/utils/mcp-registry.js +298 -0
- package/dist/utils/mcp-registry.js.map +1 -0
- package/dist/utils/progress-indicator.d.ts +14 -0
- package/dist/utils/progress-indicator.d.ts.map +1 -0
- package/dist/utils/progress-indicator.js +82 -0
- package/dist/utils/progress-indicator.js.map +1 -0
- package/dist/utils/settings-manager.d.ts +19 -0
- package/dist/utils/settings-manager.d.ts.map +1 -0
- package/dist/utils/settings-manager.js +78 -0
- package/dist/utils/settings-manager.js.map +1 -0
- package/dist/utils/token-calculator.d.ts +34 -0
- package/dist/utils/token-calculator.d.ts.map +1 -0
- package/dist/utils/token-calculator.js +167 -0
- package/dist/utils/token-calculator.js.map +1 -0
- package/dist/utils/validation.d.ts +4 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +36 -0
- package/dist/utils/validation.js.map +1 -0
- package/dist/utils/wrangler-formatter.d.ts +37 -0
- package/dist/utils/wrangler-formatter.d.ts.map +1 -0
- package/dist/utils/wrangler-formatter.js +302 -0
- package/dist/utils/wrangler-formatter.js.map +1 -0
- package/dist/worker/runtime.d.ts +34 -0
- package/dist/worker/runtime.d.ts.map +1 -0
- package/dist/worker/runtime.js +166 -0
- package/dist/worker/runtime.js.map +1 -0
- package/package.json +81 -0
|
@@ -0,0 +1,1746 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { createServer, } from 'node:http';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
8
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
9
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
10
|
+
import { isCommandBasedConfig } from '../types/mcp.js';
|
|
11
|
+
import { MCPConnectionError, MCPIsolateError, WorkerError, } from '../utils/errors.js';
|
|
12
|
+
import logger from '../utils/logger.js';
|
|
13
|
+
import { clearMCPSchemaCache, getCachedSchema, getIsolationConfigForMCP, saveCachedSchema, } from '../utils/mcp-registry.js';
|
|
14
|
+
import { ProgressIndicator } from '../utils/progress-indicator.js';
|
|
15
|
+
import { formatWranglerError } from '../utils/wrangler-formatter.js';
|
|
16
|
+
import { SchemaConverter } from './schema-converter.js';
|
|
17
|
+
export class WorkerManager {
|
|
18
|
+
instances = new Map();
|
|
19
|
+
mcpProcesses = new Map();
|
|
20
|
+
mcpClients = new Map();
|
|
21
|
+
wranglerProcesses = new Set();
|
|
22
|
+
schemaConverter;
|
|
23
|
+
wranglerAvailable = null;
|
|
24
|
+
schemaCache = new Map();
|
|
25
|
+
rpcServer = null;
|
|
26
|
+
rpcPort = 0;
|
|
27
|
+
rpcServerReady = null;
|
|
28
|
+
cachedWorkerEntryPoint = null;
|
|
29
|
+
projectRoot = null;
|
|
30
|
+
constructor() {
|
|
31
|
+
this.schemaConverter = new SchemaConverter();
|
|
32
|
+
this.projectRoot = this.findProjectRoot();
|
|
33
|
+
this.startRPCServer();
|
|
34
|
+
}
|
|
35
|
+
findProjectRoot() {
|
|
36
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
37
|
+
let currentDir = dirname(currentFile);
|
|
38
|
+
const maxDepth = 10;
|
|
39
|
+
let depth = 0;
|
|
40
|
+
while (depth < maxDepth) {
|
|
41
|
+
if (existsSync(join(currentDir, 'wrangler.toml')) ||
|
|
42
|
+
existsSync(join(currentDir, 'package.json'))) {
|
|
43
|
+
logger.debug({
|
|
44
|
+
projectRoot: currentDir,
|
|
45
|
+
cwd: process.cwd(),
|
|
46
|
+
sourceFile: currentFile,
|
|
47
|
+
}, 'Found project root');
|
|
48
|
+
return currentDir;
|
|
49
|
+
}
|
|
50
|
+
const parentDir = resolve(currentDir, '..');
|
|
51
|
+
if (parentDir === currentDir) {
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
currentDir = parentDir;
|
|
55
|
+
depth++;
|
|
56
|
+
}
|
|
57
|
+
logger.warn({
|
|
58
|
+
cwd: process.cwd(),
|
|
59
|
+
sourceFile: currentFile,
|
|
60
|
+
searchedFrom: dirname(currentFile),
|
|
61
|
+
}, 'Could not find project root (wrangler.toml or package.json), using cwd as fallback');
|
|
62
|
+
return process.cwd();
|
|
63
|
+
}
|
|
64
|
+
startRPCServer() {
|
|
65
|
+
if (this.rpcServer) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.rpcServer = createServer(async (req, res) => {
|
|
69
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
70
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
71
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
72
|
+
if (req.method === 'OPTIONS') {
|
|
73
|
+
res.writeHead(200);
|
|
74
|
+
res.end();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (req.method !== 'POST' || req.url !== '/mcp-rpc') {
|
|
78
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
79
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
let body = '';
|
|
84
|
+
for await (const chunk of req) {
|
|
85
|
+
body += chunk.toString();
|
|
86
|
+
}
|
|
87
|
+
const { mcpId, toolName, input } = JSON.parse(body);
|
|
88
|
+
if (!mcpId || !toolName) {
|
|
89
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
90
|
+
res.end(JSON.stringify({ error: 'Missing mcpId or toolName' }));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const client = this.mcpClients.get(mcpId);
|
|
94
|
+
if (!client) {
|
|
95
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
96
|
+
res.end(JSON.stringify({
|
|
97
|
+
error: `MCP client not found for ID: ${mcpId}`,
|
|
98
|
+
}));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
logger.debug({ mcpId, toolName, input }, 'RPC: Calling MCP tool');
|
|
102
|
+
const result = await client.callTool({
|
|
103
|
+
name: toolName,
|
|
104
|
+
arguments: input || {},
|
|
105
|
+
});
|
|
106
|
+
let toolResult = result;
|
|
107
|
+
if (result && typeof result === 'object' && 'content' in result) {
|
|
108
|
+
const content = result.content;
|
|
109
|
+
if (Array.isArray(content) && content.length > 0) {
|
|
110
|
+
const firstContent = content[0];
|
|
111
|
+
if (firstContent.type === 'text' && firstContent.text) {
|
|
112
|
+
try {
|
|
113
|
+
const text = firstContent.text.trim();
|
|
114
|
+
if (text.startsWith('{') || text.startsWith('[')) {
|
|
115
|
+
toolResult = JSON.parse(text);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
toolResult = text;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
toolResult = firstContent.text;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
toolResult = firstContent;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
131
|
+
res.end(JSON.stringify({ success: true, result: toolResult }));
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
135
|
+
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
136
|
+
logger.error({ error: errorMessage, stack: errorStack }, 'RPC: Error calling MCP tool');
|
|
137
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
138
|
+
res.end(JSON.stringify({
|
|
139
|
+
success: false,
|
|
140
|
+
error: errorMessage,
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
this.rpcServerReady = new Promise((resolve) => {
|
|
145
|
+
this.rpcServer?.listen(0, '127.0.0.1', () => {
|
|
146
|
+
const address = this.rpcServer?.address();
|
|
147
|
+
if (address && typeof address === 'object') {
|
|
148
|
+
this.rpcPort = address.port;
|
|
149
|
+
}
|
|
150
|
+
resolve();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async getRPCUrl() {
|
|
155
|
+
if (this.rpcServerReady) {
|
|
156
|
+
await this.rpcServerReady;
|
|
157
|
+
}
|
|
158
|
+
return `http://127.0.0.1:${this.rpcPort}/mcp-rpc`;
|
|
159
|
+
}
|
|
160
|
+
hashConfig(mcpName, config) {
|
|
161
|
+
const configString = JSON.stringify({ mcpName, config });
|
|
162
|
+
return createHash('sha256')
|
|
163
|
+
.update(configString)
|
|
164
|
+
.digest('hex')
|
|
165
|
+
.substring(0, 16);
|
|
166
|
+
}
|
|
167
|
+
getCacheKey(mcpName, config) {
|
|
168
|
+
return `${mcpName}:${this.hashConfig(mcpName, config)}`;
|
|
169
|
+
}
|
|
170
|
+
calculateToolSchemaSize(tool) {
|
|
171
|
+
return JSON.stringify(tool).length;
|
|
172
|
+
}
|
|
173
|
+
estimateTokens(chars) {
|
|
174
|
+
return Math.round(chars / 3.5);
|
|
175
|
+
}
|
|
176
|
+
calculateSchemaMetrics(tools, toolsCalled) {
|
|
177
|
+
const totalTools = tools.length;
|
|
178
|
+
const toolsUsedSet = new Set(toolsCalled);
|
|
179
|
+
const toolsUsed = Array.from(toolsUsedSet);
|
|
180
|
+
const schemaSizeTotal = tools.reduce((sum, tool) => sum + this.calculateToolSchemaSize(tool), 0);
|
|
181
|
+
const schemaSizeUsed = tools
|
|
182
|
+
.filter((tool) => toolsUsedSet.has(tool.name))
|
|
183
|
+
.reduce((sum, tool) => sum + this.calculateToolSchemaSize(tool), 0);
|
|
184
|
+
const schemaUtilizationPercent = schemaSizeTotal > 0 ? (schemaSizeUsed / schemaSizeTotal) * 100 : 0;
|
|
185
|
+
const schemaEfficiencyRatio = schemaSizeUsed > 0 ? schemaSizeTotal / schemaSizeUsed : 0;
|
|
186
|
+
const schemaSizeReduction = schemaSizeTotal - schemaSizeUsed;
|
|
187
|
+
const schemaSizeReductionPercent = schemaSizeTotal > 0 ? (schemaSizeReduction / schemaSizeTotal) * 100 : 0;
|
|
188
|
+
const estimatedTokensTotal = this.estimateTokens(schemaSizeTotal);
|
|
189
|
+
const estimatedTokensUsed = this.estimateTokens(schemaSizeUsed);
|
|
190
|
+
const estimatedTokensSaved = estimatedTokensTotal - estimatedTokensUsed;
|
|
191
|
+
return {
|
|
192
|
+
total_tools_available: totalTools,
|
|
193
|
+
tools_used: toolsUsed,
|
|
194
|
+
schema_size_total_chars: schemaSizeTotal,
|
|
195
|
+
schema_size_used_chars: schemaSizeUsed,
|
|
196
|
+
schema_utilization_percent: Math.round(schemaUtilizationPercent * 100) / 100,
|
|
197
|
+
schema_efficiency_ratio: Math.round(schemaEfficiencyRatio * 100) / 100,
|
|
198
|
+
schema_size_reduction_chars: schemaSizeReduction,
|
|
199
|
+
schema_size_reduction_percent: Math.round(schemaSizeReductionPercent * 100) / 100,
|
|
200
|
+
estimated_tokens_total: estimatedTokensTotal,
|
|
201
|
+
estimated_tokens_used: estimatedTokensUsed,
|
|
202
|
+
estimated_tokens_saved: estimatedTokensSaved,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
getSecurityMetrics() {
|
|
206
|
+
const networkIsolationEnabled = true;
|
|
207
|
+
const processIsolationEnabled = true;
|
|
208
|
+
const isolationType = 'worker_isolate';
|
|
209
|
+
const securityLevel = 'high';
|
|
210
|
+
const protectionSummary = [];
|
|
211
|
+
if (networkIsolationEnabled) {
|
|
212
|
+
protectionSummary.push('Network isolation (no outbound access)');
|
|
213
|
+
}
|
|
214
|
+
if (processIsolationEnabled) {
|
|
215
|
+
protectionSummary.push('Process isolation (separate Worker)');
|
|
216
|
+
}
|
|
217
|
+
protectionSummary.push('Code sandboxing (isolated execution)');
|
|
218
|
+
return {
|
|
219
|
+
network_isolation_enabled: networkIsolationEnabled,
|
|
220
|
+
process_isolation_enabled: processIsolationEnabled,
|
|
221
|
+
isolation_type: isolationType,
|
|
222
|
+
sandbox_status: 'active',
|
|
223
|
+
security_level: securityLevel,
|
|
224
|
+
protection_summary: protectionSummary,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async loadMCPSchemaOnly(mcpName, config) {
|
|
228
|
+
const cacheKey = this.getCacheKey(mcpName, config);
|
|
229
|
+
const configHash = this.hashConfig(mcpName, config);
|
|
230
|
+
const cached = this.schemaCache.get(cacheKey);
|
|
231
|
+
const hasCachedSchema = cached && cached.configHash === configHash;
|
|
232
|
+
if (hasCachedSchema &&
|
|
233
|
+
cached.tools.length === 0 &&
|
|
234
|
+
!isCommandBasedConfig(config)) {
|
|
235
|
+
const persistentCached = getCachedSchema(mcpName, configHash);
|
|
236
|
+
if (persistentCached && persistentCached.toolCount > 0) {
|
|
237
|
+
this.schemaCache.set(cacheKey, {
|
|
238
|
+
tools: persistentCached.tools,
|
|
239
|
+
typescriptApi: persistentCached.typescriptApi ||
|
|
240
|
+
this.schemaConverter.convertToTypeScript(persistentCached.tools),
|
|
241
|
+
configHash: persistentCached.configHash,
|
|
242
|
+
cachedAt: new Date(persistentCached.cachedAt),
|
|
243
|
+
});
|
|
244
|
+
logger.info({ mcpName, toolCount: persistentCached.toolCount }, 'Updated empty in-memory cache from persistent cache (transparent proxy)');
|
|
245
|
+
return persistentCached.tools;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (hasCachedSchema) {
|
|
249
|
+
logger.debug({ mcpName, cacheKey, toolCount: cached.tools.length }, 'Using cached MCP schema for transparent proxy');
|
|
250
|
+
return cached.tools;
|
|
251
|
+
}
|
|
252
|
+
let client = null;
|
|
253
|
+
let transport = null;
|
|
254
|
+
try {
|
|
255
|
+
if (isCommandBasedConfig(config)) {
|
|
256
|
+
transport = new StdioClientTransport({
|
|
257
|
+
command: config.command,
|
|
258
|
+
args: config.args || [],
|
|
259
|
+
env: config.env,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
const url = new URL(config.url);
|
|
264
|
+
const transportOptions = {};
|
|
265
|
+
if (config.headers) {
|
|
266
|
+
transportOptions.requestInit = {
|
|
267
|
+
headers: config.headers,
|
|
268
|
+
};
|
|
269
|
+
const maskedHeaders = Object.fromEntries(Object.entries(config.headers).map(([k, v]) => [
|
|
270
|
+
k,
|
|
271
|
+
k.toLowerCase().includes('auth') ? `${v.substring(0, 15)}...` : v,
|
|
272
|
+
]));
|
|
273
|
+
logger.info({ mcpName, url: config.url, headers: maskedHeaders }, 'loadMCPSchemaOnly: passing headers to StreamableHTTPClientTransport');
|
|
274
|
+
}
|
|
275
|
+
transport = new StreamableHTTPClientTransport(url, transportOptions);
|
|
276
|
+
}
|
|
277
|
+
client = new Client({
|
|
278
|
+
name: 'mcpflare',
|
|
279
|
+
version: '0.1.0',
|
|
280
|
+
}, {
|
|
281
|
+
capabilities: {},
|
|
282
|
+
});
|
|
283
|
+
await client.connect(transport, { timeout: 10000 });
|
|
284
|
+
const toolsResponse = await client.listTools();
|
|
285
|
+
logger.info({ mcpName, toolCount: toolsResponse.tools.length }, `loadMCPSchemaOnly: received ${toolsResponse.tools.length} tools`);
|
|
286
|
+
const tools = toolsResponse.tools.map((tool) => ({
|
|
287
|
+
name: tool.name,
|
|
288
|
+
description: tool.description,
|
|
289
|
+
inputSchema: {
|
|
290
|
+
type: 'object',
|
|
291
|
+
properties: (tool.inputSchema.properties || {}),
|
|
292
|
+
required: tool.inputSchema.required || [],
|
|
293
|
+
},
|
|
294
|
+
}));
|
|
295
|
+
const shouldCache = tools.length > 0 || isCommandBasedConfig(config);
|
|
296
|
+
if (shouldCache) {
|
|
297
|
+
const typescriptApi = this.schemaConverter.convertToTypeScript(tools);
|
|
298
|
+
this.schemaCache.set(cacheKey, {
|
|
299
|
+
tools,
|
|
300
|
+
typescriptApi,
|
|
301
|
+
configHash,
|
|
302
|
+
cachedAt: new Date(),
|
|
303
|
+
});
|
|
304
|
+
logger.info({ mcpName, cacheKey, toolCount: tools.length }, 'Fetched and cached MCP schema for transparent proxy');
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
logger.warn({
|
|
308
|
+
mcpName,
|
|
309
|
+
url: !isCommandBasedConfig(config) ? config.url : undefined,
|
|
310
|
+
toolCount: tools.length,
|
|
311
|
+
}, 'URL-based MCP returned 0 tools - not caching (may indicate auth issue)');
|
|
312
|
+
}
|
|
313
|
+
return tools;
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
317
|
+
const isAuthError = /401|403|Unauthorized|Forbidden/i.test(errorMessage);
|
|
318
|
+
if (isAuthError) {
|
|
319
|
+
logger.warn({ error, mcpName }, 'Authentication failed for MCP - may require OAuth or valid Authorization header');
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
logger.warn({ error, mcpName }, 'Failed to fetch MCP schema for transparent proxy');
|
|
323
|
+
}
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
finally {
|
|
327
|
+
if (client) {
|
|
328
|
+
try {
|
|
329
|
+
await client.close();
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
logger.debug({ error }, 'Error closing temporary MCP client');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (transport) {
|
|
336
|
+
try {
|
|
337
|
+
await transport.close();
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
logger.debug({ error }, 'Error closing temporary MCP transport');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async loadMCPPromptsOnly(mcpName, config) {
|
|
346
|
+
const cacheKey = this.getCacheKey(mcpName, config);
|
|
347
|
+
const configHash = this.hashConfig(mcpName, config);
|
|
348
|
+
const cached = this.schemaCache.get(cacheKey);
|
|
349
|
+
if (cached && cached.configHash === configHash && cached.prompts) {
|
|
350
|
+
logger.debug({ mcpName, cacheKey, promptCount: cached.prompts.length }, 'Using cached MCP prompts for transparent proxy');
|
|
351
|
+
return cached.prompts;
|
|
352
|
+
}
|
|
353
|
+
let client = null;
|
|
354
|
+
let transport = null;
|
|
355
|
+
try {
|
|
356
|
+
if (isCommandBasedConfig(config)) {
|
|
357
|
+
transport = new StdioClientTransport({
|
|
358
|
+
command: config.command,
|
|
359
|
+
args: config.args || [],
|
|
360
|
+
env: config.env,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
const url = new URL(config.url);
|
|
365
|
+
const transportOptions = {};
|
|
366
|
+
if (config.headers) {
|
|
367
|
+
transportOptions.requestInit = {
|
|
368
|
+
headers: config.headers,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
transport = new StreamableHTTPClientTransport(url, transportOptions);
|
|
372
|
+
}
|
|
373
|
+
client = new Client({
|
|
374
|
+
name: 'mcpflare',
|
|
375
|
+
version: '0.1.0',
|
|
376
|
+
}, {
|
|
377
|
+
capabilities: {},
|
|
378
|
+
});
|
|
379
|
+
await client.connect(transport, { timeout: 10000 });
|
|
380
|
+
const promptsResponse = await client.listPrompts();
|
|
381
|
+
const prompts = promptsResponse.prompts.map((prompt) => ({
|
|
382
|
+
name: prompt.name,
|
|
383
|
+
description: prompt.description,
|
|
384
|
+
arguments: prompt.arguments,
|
|
385
|
+
}));
|
|
386
|
+
const existingCache = this.schemaCache.get(cacheKey);
|
|
387
|
+
this.schemaCache.set(cacheKey, {
|
|
388
|
+
tools: existingCache?.tools || [],
|
|
389
|
+
typescriptApi: existingCache?.typescriptApi || '',
|
|
390
|
+
prompts,
|
|
391
|
+
configHash,
|
|
392
|
+
cachedAt: new Date(),
|
|
393
|
+
});
|
|
394
|
+
logger.info({ mcpName, cacheKey, promptCount: prompts.length }, 'Fetched and cached MCP prompts for transparent proxy');
|
|
395
|
+
return prompts;
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
399
|
+
const isAuthError = /401|403|Unauthorized|Forbidden/i.test(errorMessage);
|
|
400
|
+
if (isAuthError) {
|
|
401
|
+
logger.warn({ error, mcpName }, 'Authentication failed for MCP prompts - may require OAuth or valid Authorization header');
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
logger.warn({ error, mcpName }, 'Failed to fetch MCP prompts for transparent proxy');
|
|
405
|
+
}
|
|
406
|
+
return [];
|
|
407
|
+
}
|
|
408
|
+
finally {
|
|
409
|
+
if (client) {
|
|
410
|
+
try {
|
|
411
|
+
await client.close();
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
logger.debug({ error }, 'Error closing temporary MCP client');
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (transport) {
|
|
418
|
+
try {
|
|
419
|
+
await transport.close();
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
logger.debug({ error }, 'Error closing temporary MCP transport');
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async loadMCP(mcpName, config) {
|
|
428
|
+
const mcpId = randomUUID();
|
|
429
|
+
const cacheKey = this.getCacheKey(mcpName, config);
|
|
430
|
+
const safeConfig = isCommandBasedConfig(config)
|
|
431
|
+
? {
|
|
432
|
+
command: config.command,
|
|
433
|
+
args: config.args,
|
|
434
|
+
envKeys: config.env ? Object.keys(config.env) : undefined,
|
|
435
|
+
}
|
|
436
|
+
: { url: config.url };
|
|
437
|
+
logger.info({ mcpId, mcpName, config: safeConfig }, 'Loading MCP server');
|
|
438
|
+
try {
|
|
439
|
+
const cached = this.schemaCache.get(cacheKey);
|
|
440
|
+
const configHash = this.hashConfig(mcpName, config);
|
|
441
|
+
const hasCachedSchema = cached && cached.configHash === configHash;
|
|
442
|
+
const shouldCheckPersistentCache = !hasCachedSchema ||
|
|
443
|
+
(!isCommandBasedConfig(config) && cached && cached.tools.length === 0);
|
|
444
|
+
if (shouldCheckPersistentCache) {
|
|
445
|
+
let persistentCached = getCachedSchema(mcpName, configHash);
|
|
446
|
+
if (persistentCached && persistentCached.toolCount === 0) {
|
|
447
|
+
clearMCPSchemaCache(mcpName);
|
|
448
|
+
logger.info({ mcpId, mcpName, configHash }, 'Cleared stale zero-tool persistent cache entry');
|
|
449
|
+
persistentCached = null;
|
|
450
|
+
}
|
|
451
|
+
if (persistentCached && persistentCached.toolCount > 0) {
|
|
452
|
+
this.schemaCache.set(cacheKey, {
|
|
453
|
+
tools: persistentCached.tools,
|
|
454
|
+
typescriptApi: persistentCached.typescriptApi ||
|
|
455
|
+
this.schemaConverter.convertToTypeScript(persistentCached.tools),
|
|
456
|
+
configHash: persistentCached.configHash,
|
|
457
|
+
cachedAt: new Date(persistentCached.cachedAt),
|
|
458
|
+
});
|
|
459
|
+
logger.info({ mcpId, mcpName, toolCount: persistentCached.toolCount }, 'Loaded MCP schema from persistent cache');
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const cachedAfterLoad = this.schemaCache.get(cacheKey);
|
|
463
|
+
const hasCachedSchemaAfterLoad = cachedAfterLoad &&
|
|
464
|
+
cachedAfterLoad.configHash === configHash &&
|
|
465
|
+
(isCommandBasedConfig(config) || cachedAfterLoad.tools.length > 0);
|
|
466
|
+
if (hasCachedSchemaAfterLoad) {
|
|
467
|
+
if (isCommandBasedConfig(config)) {
|
|
468
|
+
const mcpProcess = await this.startMCPProcess(config, true);
|
|
469
|
+
this.mcpProcesses.set(mcpId, mcpProcess);
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
logger.info({ mcpId, mcpName }, 'Establishing MCP client connection for URL-based MCP with cached schema');
|
|
473
|
+
await this.connectMCPClient(mcpId, mcpName, config);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
let tools;
|
|
477
|
+
let prompts;
|
|
478
|
+
let typescriptApi;
|
|
479
|
+
if (hasCachedSchemaAfterLoad) {
|
|
480
|
+
logger.info({ mcpId, mcpName, cacheKey }, 'Using cached MCP schema');
|
|
481
|
+
tools = cachedAfterLoad?.tools;
|
|
482
|
+
prompts = cachedAfterLoad?.prompts || [];
|
|
483
|
+
typescriptApi = cachedAfterLoad?.typescriptApi;
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
const schema = await this.fetchMCPSchema(mcpName, config, mcpId);
|
|
487
|
+
tools = schema.tools;
|
|
488
|
+
prompts = schema.prompts;
|
|
489
|
+
typescriptApi = this.schemaConverter.convertToTypeScript(tools);
|
|
490
|
+
const shouldCache = tools.length > 0 || prompts.length > 0 || isCommandBasedConfig(config);
|
|
491
|
+
if (shouldCache) {
|
|
492
|
+
this.schemaCache.set(cacheKey, {
|
|
493
|
+
tools,
|
|
494
|
+
prompts,
|
|
495
|
+
typescriptApi,
|
|
496
|
+
configHash: this.hashConfig(mcpName, config),
|
|
497
|
+
cachedAt: new Date(),
|
|
498
|
+
});
|
|
499
|
+
logger.debug({
|
|
500
|
+
mcpId,
|
|
501
|
+
mcpName,
|
|
502
|
+
cacheKey,
|
|
503
|
+
toolCount: tools.length,
|
|
504
|
+
promptCount: prompts.length,
|
|
505
|
+
}, 'Cached MCP schema');
|
|
506
|
+
saveCachedSchema({
|
|
507
|
+
mcpName,
|
|
508
|
+
configHash: this.hashConfig(mcpName, config),
|
|
509
|
+
tools,
|
|
510
|
+
prompts,
|
|
511
|
+
toolNames: tools.map((t) => t.name),
|
|
512
|
+
promptNames: prompts.map((p) => p.name),
|
|
513
|
+
toolCount: tools.length,
|
|
514
|
+
promptCount: prompts.length,
|
|
515
|
+
cachedAt: new Date().toISOString(),
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
logger.warn({
|
|
520
|
+
mcpId,
|
|
521
|
+
mcpName,
|
|
522
|
+
url: !isCommandBasedConfig(config) ? config.url : undefined,
|
|
523
|
+
toolCount: tools.length,
|
|
524
|
+
promptCount: prompts.length,
|
|
525
|
+
}, 'URL-based MCP returned 0 tools and 0 prompts - not caching (may indicate auth issue)');
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
const workerId = `worker-${mcpId}`;
|
|
529
|
+
const instance = {
|
|
530
|
+
mcp_id: mcpId,
|
|
531
|
+
mcp_name: mcpName,
|
|
532
|
+
status: 'ready',
|
|
533
|
+
worker_id: workerId,
|
|
534
|
+
typescript_api: typescriptApi,
|
|
535
|
+
tools,
|
|
536
|
+
prompts,
|
|
537
|
+
created_at: new Date(),
|
|
538
|
+
uptime_ms: 0,
|
|
539
|
+
};
|
|
540
|
+
this.instances.set(mcpId, instance);
|
|
541
|
+
logger.info({ mcpId, mcpName, cached: !!cached }, 'MCP server loaded successfully');
|
|
542
|
+
return instance;
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
546
|
+
logger.error({ error, mcpId, mcpName }, 'Failed to load MCP server');
|
|
547
|
+
const mcpProcess = this.mcpProcesses.get(mcpId);
|
|
548
|
+
if (mcpProcess) {
|
|
549
|
+
try {
|
|
550
|
+
await this.killMCPProcess(mcpProcess);
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
logger.warn({ error, mcpId }, 'Error killing MCP process during load failure');
|
|
554
|
+
}
|
|
555
|
+
this.mcpProcesses.delete(mcpId);
|
|
556
|
+
}
|
|
557
|
+
throw new MCPConnectionError(`Failed to load MCP server: ${errorMessage}`, { mcpName, error });
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
async executeCode(mcpId, code, timeoutMs = 30000) {
|
|
561
|
+
const instance = this.instances.get(mcpId);
|
|
562
|
+
if (!instance) {
|
|
563
|
+
throw new WorkerError(`MCP instance not found: ${mcpId}`);
|
|
564
|
+
}
|
|
565
|
+
if (instance.status !== 'ready') {
|
|
566
|
+
throw new WorkerError(`MCP instance not ready: ${instance.status}`);
|
|
567
|
+
}
|
|
568
|
+
logger.info({ mcpId, codeLength: code.length }, 'Executing code in Worker isolate');
|
|
569
|
+
const startTime = Date.now();
|
|
570
|
+
try {
|
|
571
|
+
const result = await this.executeInIsolate(mcpId, code, timeoutMs, instance);
|
|
572
|
+
const executionTime = Date.now() - startTime;
|
|
573
|
+
const toolsCalled = result.metrics?.tools_called || [];
|
|
574
|
+
const schemaEfficiency = this.calculateSchemaMetrics(instance.tools, toolsCalled);
|
|
575
|
+
const security = this.getSecurityMetrics();
|
|
576
|
+
logger.info({ mcpId, executionTime }, 'Code executed successfully');
|
|
577
|
+
return {
|
|
578
|
+
success: true,
|
|
579
|
+
output: result.output,
|
|
580
|
+
result: result.result,
|
|
581
|
+
execution_time_ms: executionTime,
|
|
582
|
+
metrics: {
|
|
583
|
+
mcp_calls_made: result.metrics?.mcp_calls_made ?? 0,
|
|
584
|
+
tools_called: result.metrics?.tools_called,
|
|
585
|
+
schema_efficiency: schemaEfficiency,
|
|
586
|
+
security,
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
catch (error) {
|
|
591
|
+
const executionTime = Date.now() - startTime;
|
|
592
|
+
logger.error({ error, mcpId, executionTime }, 'Code execution failed');
|
|
593
|
+
const schemaEfficiency = this.calculateSchemaMetrics(instance.tools, []);
|
|
594
|
+
const security = this.getSecurityMetrics();
|
|
595
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
596
|
+
let errorDetails;
|
|
597
|
+
if (error instanceof MCPIsolateError) {
|
|
598
|
+
errorDetails = error.details;
|
|
599
|
+
logger.debug({ mcpId, hasDetails: !!errorDetails }, 'Extracted error details from MCPIsolateError');
|
|
600
|
+
}
|
|
601
|
+
else if (error instanceof WorkerError) {
|
|
602
|
+
errorDetails = error.details;
|
|
603
|
+
logger.debug({ mcpId, hasDetails: !!errorDetails }, 'Extracted error details from WorkerError');
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
logger.debug({ mcpId, errorType: error?.constructor?.name }, 'Error is not an MCPIsolateError or WorkerError');
|
|
607
|
+
}
|
|
608
|
+
return {
|
|
609
|
+
success: false,
|
|
610
|
+
error: errorMessage,
|
|
611
|
+
execution_time_ms: executionTime,
|
|
612
|
+
metrics: {
|
|
613
|
+
mcp_calls_made: 0,
|
|
614
|
+
tools_called: [],
|
|
615
|
+
schema_efficiency: schemaEfficiency,
|
|
616
|
+
security,
|
|
617
|
+
},
|
|
618
|
+
error_details: errorDetails,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async unloadMCP(mcpId) {
|
|
623
|
+
logger.info({ mcpId }, 'Unloading MCP server');
|
|
624
|
+
const instance = this.instances.get(mcpId);
|
|
625
|
+
if (!instance) {
|
|
626
|
+
throw new WorkerError(`MCP instance not found: ${mcpId}`);
|
|
627
|
+
}
|
|
628
|
+
const client = this.mcpClients.get(mcpId);
|
|
629
|
+
if (client) {
|
|
630
|
+
try {
|
|
631
|
+
const clientWithTransport = client;
|
|
632
|
+
const transport = clientWithTransport._transport;
|
|
633
|
+
if (transport && typeof transport.close === 'function') {
|
|
634
|
+
await transport.close();
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
logger.warn({ error, mcpId }, 'Error closing MCP client transport');
|
|
639
|
+
}
|
|
640
|
+
this.mcpClients.delete(mcpId);
|
|
641
|
+
}
|
|
642
|
+
const mcpProcess = this.mcpProcesses.get(mcpId);
|
|
643
|
+
if (mcpProcess) {
|
|
644
|
+
try {
|
|
645
|
+
await this.killMCPProcess(mcpProcess);
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
logger.warn({ error, mcpId }, 'Error killing MCP process during unload');
|
|
649
|
+
}
|
|
650
|
+
this.mcpProcesses.delete(mcpId);
|
|
651
|
+
}
|
|
652
|
+
this.instances.delete(mcpId);
|
|
653
|
+
logger.info({ mcpId }, 'MCP server unloaded');
|
|
654
|
+
}
|
|
655
|
+
listInstances() {
|
|
656
|
+
return Array.from(this.instances.values()).map((instance) => ({
|
|
657
|
+
...instance,
|
|
658
|
+
uptime_ms: Date.now() - instance.created_at.getTime(),
|
|
659
|
+
}));
|
|
660
|
+
}
|
|
661
|
+
getInstance(mcpId) {
|
|
662
|
+
const instance = this.instances.get(mcpId);
|
|
663
|
+
if (instance) {
|
|
664
|
+
return {
|
|
665
|
+
...instance,
|
|
666
|
+
uptime_ms: Date.now() - instance.created_at.getTime(),
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
return undefined;
|
|
670
|
+
}
|
|
671
|
+
getMCPByName(mcpName) {
|
|
672
|
+
const instances = this.listInstances();
|
|
673
|
+
return instances.find((instance) => instance.mcp_name === mcpName);
|
|
674
|
+
}
|
|
675
|
+
getMCPClient(mcpId) {
|
|
676
|
+
return this.mcpClients.get(mcpId);
|
|
677
|
+
}
|
|
678
|
+
clearSchemaCache(mcpName) {
|
|
679
|
+
let cleared = 0;
|
|
680
|
+
for (const cacheKey of this.schemaCache.keys()) {
|
|
681
|
+
if (cacheKey.startsWith(`${mcpName}:`)) {
|
|
682
|
+
this.schemaCache.delete(cacheKey);
|
|
683
|
+
cleared++;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (cleared > 0) {
|
|
687
|
+
logger.info({ mcpName, clearedEntries: cleared }, 'Cleared in-memory schema cache for MCP');
|
|
688
|
+
}
|
|
689
|
+
return cleared;
|
|
690
|
+
}
|
|
691
|
+
async startMCPProcess(config, hasCachedSchema = false) {
|
|
692
|
+
if (!isCommandBasedConfig(config)) {
|
|
693
|
+
throw new MCPConnectionError('URL-based MCP configurations use HTTP transport and do not spawn processes. Process tracking is only for command-based MCPs.');
|
|
694
|
+
}
|
|
695
|
+
return new Promise((resolve, reject) => {
|
|
696
|
+
let command = config.command;
|
|
697
|
+
const args = config.args || [];
|
|
698
|
+
if (process.platform === 'win32' && command === 'npx') {
|
|
699
|
+
command = 'npx.cmd';
|
|
700
|
+
}
|
|
701
|
+
logger.info({
|
|
702
|
+
platform: process.platform,
|
|
703
|
+
originalCommand: config.command,
|
|
704
|
+
resolvedCommand: command,
|
|
705
|
+
args: args,
|
|
706
|
+
envKeys: Object.keys(config.env || {}),
|
|
707
|
+
hasCachedSchema,
|
|
708
|
+
}, 'Spawning MCP process');
|
|
709
|
+
let mcpProcess;
|
|
710
|
+
let initialized = false;
|
|
711
|
+
try {
|
|
712
|
+
const isTestEnv = process.env.NODE_ENV === 'test' || process.env.VITEST === 'true';
|
|
713
|
+
const spawnOptions = {
|
|
714
|
+
env: { ...process.env, ...config.env },
|
|
715
|
+
stdio: isTestEnv
|
|
716
|
+
? ['pipe', 'pipe', 'ignore']
|
|
717
|
+
: ['pipe', 'pipe', 'pipe'],
|
|
718
|
+
};
|
|
719
|
+
if (process.platform === 'win32') {
|
|
720
|
+
spawnOptions.shell = true;
|
|
721
|
+
}
|
|
722
|
+
const safeSpawnOptions = {
|
|
723
|
+
...spawnOptions,
|
|
724
|
+
env: spawnOptions.env
|
|
725
|
+
? Object.keys(spawnOptions.env).reduce((acc, key) => {
|
|
726
|
+
acc[key] = '[REDACTED]';
|
|
727
|
+
return acc;
|
|
728
|
+
}, {})
|
|
729
|
+
: undefined,
|
|
730
|
+
};
|
|
731
|
+
logger.debug({ spawnOptions: safeSpawnOptions }, 'Spawning with options');
|
|
732
|
+
mcpProcess = spawn(command, args, spawnOptions);
|
|
733
|
+
logger.info({
|
|
734
|
+
pid: mcpProcess.pid,
|
|
735
|
+
command,
|
|
736
|
+
args: args.slice(0, 5),
|
|
737
|
+
}, `MCP process spawned: PID ${mcpProcess.pid}`);
|
|
738
|
+
if (hasCachedSchema) {
|
|
739
|
+
setTimeout(() => {
|
|
740
|
+
if (mcpProcess && !mcpProcess.killed) {
|
|
741
|
+
initialized = true;
|
|
742
|
+
resolve(mcpProcess);
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
reject(new MCPConnectionError('MCP process failed to start'));
|
|
746
|
+
}
|
|
747
|
+
}, 500);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (mcpProcess.stdout) {
|
|
751
|
+
mcpProcess.stdout.on('data', (data) => {
|
|
752
|
+
const output = data.toString();
|
|
753
|
+
logger.debug({ output }, 'MCP stdout');
|
|
754
|
+
if (!initialized) {
|
|
755
|
+
initialized = true;
|
|
756
|
+
setTimeout(() => resolve(mcpProcess), 200);
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
if (!isTestEnv && mcpProcess.stderr) {
|
|
761
|
+
mcpProcess.stderr.on('data', (data) => {
|
|
762
|
+
const stderrOutput = data.toString();
|
|
763
|
+
logger.debug({ error: stderrOutput }, 'MCP stderr');
|
|
764
|
+
if (!initialized && stderrOutput.trim().length > 0) {
|
|
765
|
+
initialized = true;
|
|
766
|
+
setTimeout(() => resolve(mcpProcess), 200);
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
mcpProcess.on('error', (error) => {
|
|
771
|
+
const errnoError = error;
|
|
772
|
+
logger.error({
|
|
773
|
+
error: error instanceof Error ? error.message : String(error),
|
|
774
|
+
code: errnoError.code,
|
|
775
|
+
errno: errnoError.errno,
|
|
776
|
+
syscall: errnoError.syscall,
|
|
777
|
+
command,
|
|
778
|
+
args,
|
|
779
|
+
}, 'MCP process spawn error');
|
|
780
|
+
reject(new MCPConnectionError(`Failed to start MCP process: ${error.message}`));
|
|
781
|
+
});
|
|
782
|
+
mcpProcess.on('exit', (code, signal) => {
|
|
783
|
+
logger.debug({ pid: mcpProcess.pid, code, signal, command }, 'MCP process exited naturally');
|
|
784
|
+
});
|
|
785
|
+
setTimeout(() => {
|
|
786
|
+
if (!initialized) {
|
|
787
|
+
if (mcpProcess && !mcpProcess.killed && mcpProcess.pid) {
|
|
788
|
+
logger.info({ pid: mcpProcess.pid }, 'MCP process ready (timeout - assuming ready)');
|
|
789
|
+
initialized = true;
|
|
790
|
+
resolve(mcpProcess);
|
|
791
|
+
}
|
|
792
|
+
else {
|
|
793
|
+
reject(new MCPConnectionError('MCP process initialization timeout - process not running'));
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}, 2000);
|
|
797
|
+
}
|
|
798
|
+
catch (spawnError) {
|
|
799
|
+
const errorMessage = spawnError instanceof Error ? spawnError.message : 'Unknown error';
|
|
800
|
+
const errorCode = spawnError?.code;
|
|
801
|
+
const errorErrno = spawnError?.errno;
|
|
802
|
+
const errorSyscall = spawnError?.syscall;
|
|
803
|
+
logger.error({
|
|
804
|
+
error: errorMessage,
|
|
805
|
+
code: errorCode,
|
|
806
|
+
errno: errorErrno,
|
|
807
|
+
syscall: errorSyscall,
|
|
808
|
+
command,
|
|
809
|
+
args,
|
|
810
|
+
}, 'Failed to spawn MCP process (catch block)');
|
|
811
|
+
reject(new MCPConnectionError(`Failed to spawn MCP process: ${errorMessage}`));
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
async connectMCPClient(mcpId, mcpName, config) {
|
|
816
|
+
if (isCommandBasedConfig(config)) {
|
|
817
|
+
throw new Error('connectMCPClient should only be used for URL-based MCPs');
|
|
818
|
+
}
|
|
819
|
+
const url = new URL(config.url);
|
|
820
|
+
const transportOptions = {};
|
|
821
|
+
if (config.headers) {
|
|
822
|
+
transportOptions.requestInit = {
|
|
823
|
+
headers: config.headers,
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
const transport = new StreamableHTTPClientTransport(url, transportOptions);
|
|
827
|
+
const client = new Client({ name: 'mcpflare', version: '0.1.0' }, { capabilities: {} });
|
|
828
|
+
const connectStartTime = Date.now();
|
|
829
|
+
await client.connect(transport, { timeout: 10000 });
|
|
830
|
+
const connectTime = Date.now() - connectStartTime;
|
|
831
|
+
this.mcpClients.set(mcpId, client);
|
|
832
|
+
logger.info({ mcpId, mcpName, connectTimeMs: connectTime }, 'MCP client connected for cached schema');
|
|
833
|
+
}
|
|
834
|
+
async fetchMCPSchema(mcpName, config, mcpId) {
|
|
835
|
+
logger.info({ mcpId, mcpName }, 'Fetching MCP schema using real protocol');
|
|
836
|
+
try {
|
|
837
|
+
let transport;
|
|
838
|
+
if (isCommandBasedConfig(config)) {
|
|
839
|
+
transport = new StdioClientTransport({
|
|
840
|
+
command: config.command,
|
|
841
|
+
args: config.args || [],
|
|
842
|
+
env: config.env,
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
const url = new URL(config.url);
|
|
847
|
+
const transportOptions = {};
|
|
848
|
+
if (config.headers) {
|
|
849
|
+
transportOptions.requestInit = {
|
|
850
|
+
headers: config.headers,
|
|
851
|
+
};
|
|
852
|
+
const maskedHeaders = Object.fromEntries(Object.entries(config.headers).map(([k, v]) => [
|
|
853
|
+
k,
|
|
854
|
+
k.toLowerCase().includes('auth') ? `${v.substring(0, 15)}...` : v,
|
|
855
|
+
]));
|
|
856
|
+
logger.info({ mcpId, mcpName, url: config.url, headers: maskedHeaders }, 'URL-based MCP: passing headers to StreamableHTTPClientTransport');
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
logger.info({ mcpId, mcpName, url: config.url }, 'URL-based MCP: no custom headers configured');
|
|
860
|
+
}
|
|
861
|
+
transport = new StreamableHTTPClientTransport(url, transportOptions);
|
|
862
|
+
}
|
|
863
|
+
const client = new Client({
|
|
864
|
+
name: 'mcpflare',
|
|
865
|
+
version: '0.1.0',
|
|
866
|
+
}, {
|
|
867
|
+
capabilities: {},
|
|
868
|
+
});
|
|
869
|
+
const connectStartTime = Date.now();
|
|
870
|
+
await client.connect(transport, { timeout: 10000 });
|
|
871
|
+
const connectTime = Date.now() - connectStartTime;
|
|
872
|
+
logger.info({ mcpId, mcpName, connectTimeMs: connectTime }, 'MCP client connected');
|
|
873
|
+
this.mcpClients.set(mcpId, client);
|
|
874
|
+
if (isCommandBasedConfig(config)) {
|
|
875
|
+
const transportWithProcess = transport;
|
|
876
|
+
const process = transportWithProcess._process;
|
|
877
|
+
if (process) {
|
|
878
|
+
this.mcpProcesses.set(mcpId, process);
|
|
879
|
+
if (process.pid) {
|
|
880
|
+
logger.info({
|
|
881
|
+
pid: process.pid,
|
|
882
|
+
command: config.command,
|
|
883
|
+
args: (config.args || []).slice(0, 5),
|
|
884
|
+
mcpId,
|
|
885
|
+
mcpName,
|
|
886
|
+
}, `MCP process spawned via StdioClientTransport: PID ${process.pid}`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
logger.info({ mcpId, mcpName, url: config.url }, 'MCP connected via StreamableHTTPClientTransport');
|
|
892
|
+
}
|
|
893
|
+
const listToolsStartTime = Date.now();
|
|
894
|
+
const toolsResponse = await client.listTools();
|
|
895
|
+
const listToolsTime = Date.now() - listToolsStartTime;
|
|
896
|
+
logger.info({
|
|
897
|
+
mcpId,
|
|
898
|
+
mcpName,
|
|
899
|
+
toolCount: toolsResponse.tools.length,
|
|
900
|
+
listToolsTimeMs: listToolsTime,
|
|
901
|
+
toolNames: toolsResponse.tools.slice(0, 5).map((t) => t.name),
|
|
902
|
+
}, `Fetched ${toolsResponse.tools.length} tools from MCP server`);
|
|
903
|
+
const tools = toolsResponse.tools.map((tool) => ({
|
|
904
|
+
name: tool.name,
|
|
905
|
+
description: tool.description,
|
|
906
|
+
inputSchema: {
|
|
907
|
+
type: 'object',
|
|
908
|
+
properties: (tool.inputSchema.properties || {}),
|
|
909
|
+
required: tool.inputSchema.required || [],
|
|
910
|
+
},
|
|
911
|
+
}));
|
|
912
|
+
const listPromptsStartTime = Date.now();
|
|
913
|
+
let prompts = [];
|
|
914
|
+
try {
|
|
915
|
+
const promptsResponse = await client.listPrompts();
|
|
916
|
+
const listPromptsTime = Date.now() - listPromptsStartTime;
|
|
917
|
+
logger.debug({
|
|
918
|
+
mcpId,
|
|
919
|
+
mcpName,
|
|
920
|
+
promptCount: promptsResponse.prompts.length,
|
|
921
|
+
listPromptsTimeMs: listPromptsTime,
|
|
922
|
+
}, 'Fetched prompts from MCP server');
|
|
923
|
+
prompts = promptsResponse.prompts.map((prompt) => ({
|
|
924
|
+
name: prompt.name,
|
|
925
|
+
description: prompt.description,
|
|
926
|
+
arguments: prompt.arguments,
|
|
927
|
+
}));
|
|
928
|
+
}
|
|
929
|
+
catch (error) {
|
|
930
|
+
logger.debug({ mcpId, mcpName, error }, 'MCP does not support prompts or prompts fetch failed');
|
|
931
|
+
}
|
|
932
|
+
return { tools, prompts };
|
|
933
|
+
}
|
|
934
|
+
catch (error) {
|
|
935
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
936
|
+
logger.error({ error, mcpId, mcpName }, 'Failed to fetch MCP schema');
|
|
937
|
+
throw new MCPConnectionError(`Failed to fetch MCP schema: ${errorMessage}`, { mcpName, error });
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
async generateWorkerCode(mcpId, tools, _typescriptApi, userCode, isolationConfig) {
|
|
941
|
+
const rpcUrl = await this.getRPCUrl();
|
|
942
|
+
const allowedHostsRaw = isolationConfig?.outbound.allowedHosts ?? null;
|
|
943
|
+
const allowLocalhost = isolationConfig?.outbound.allowLocalhost ?? false;
|
|
944
|
+
const allowedHosts = Array.isArray(allowedHostsRaw) && allowedHostsRaw.length > 0
|
|
945
|
+
? allowedHostsRaw
|
|
946
|
+
.map((h) => String(h).trim().toLowerCase())
|
|
947
|
+
.filter((h) => h.length > 0)
|
|
948
|
+
: [];
|
|
949
|
+
const networkEnabled = allowLocalhost || allowedHosts.length > 0;
|
|
950
|
+
logger.debug({ mcpId, toolCount: tools.length, toolNames: tools.map((t) => t.name) }, 'Generating MCP binding stubs');
|
|
951
|
+
const mcpBindingStubs = tools
|
|
952
|
+
.map((tool) => {
|
|
953
|
+
const escapedToolName = tool.name
|
|
954
|
+
.replace(/\\/g, '\\\\')
|
|
955
|
+
.replace(/'/g, "\\'")
|
|
956
|
+
.replace(/"/g, '\\"')
|
|
957
|
+
.replace(/\n/g, '\\n')
|
|
958
|
+
.replace(/\r/g, '\\r')
|
|
959
|
+
.replace(/\t/g, '\\t');
|
|
960
|
+
return ` ${tool.name}: async (input) => {
|
|
961
|
+
// Call MCP tool via Service Binding (no fetch() needed - native RPC)
|
|
962
|
+
// The Service Binding is provided by the parent Worker and bridges to Node.js RPC server
|
|
963
|
+
return await env.MCP.callTool('${escapedToolName}', input || {});
|
|
964
|
+
}`;
|
|
965
|
+
})
|
|
966
|
+
.join(',\n');
|
|
967
|
+
logger.debug({ mcpId, bindingStubsPreview: mcpBindingStubs.substring(0, 500) }, 'Generated MCP binding stubs');
|
|
968
|
+
logger.debug({
|
|
969
|
+
codeLength: userCode.length,
|
|
970
|
+
preview: userCode.substring(0, 200),
|
|
971
|
+
}, 'Embedding user code in worker script');
|
|
972
|
+
const modulePrelude = networkEnabled
|
|
973
|
+
? '// MCPflare: Fetch wrapper at module level to intercept before runtime freezes fetch\n' +
|
|
974
|
+
`const __mcpflareAllowedHosts = ${JSON.stringify(allowedHosts.join(','))};\n` +
|
|
975
|
+
`const __mcpflareAllowLocalhost = ${allowLocalhost ? '"true"' : '"false"'};\n` +
|
|
976
|
+
'const __mcpflareOriginalFetch = globalThis.fetch;\n' +
|
|
977
|
+
'const __mcpflareFetchWrapper = async (input, init) => {\n' +
|
|
978
|
+
' const headers = new Headers(init?.headers || {});\n' +
|
|
979
|
+
' headers.set("X-MCPflare-Allowed-Hosts", __mcpflareAllowedHosts);\n' +
|
|
980
|
+
' headers.set("X-MCPflare-Allow-Localhost", __mcpflareAllowLocalhost);\n' +
|
|
981
|
+
' const response = await __mcpflareOriginalFetch(input, { ...init, headers });\n' +
|
|
982
|
+
' if (response.status === 403) {\n' +
|
|
983
|
+
' try {\n' +
|
|
984
|
+
' const body = await response.clone().json();\n' +
|
|
985
|
+
' if (body.error && body.error.startsWith("MCPflare network policy:")) {\n' +
|
|
986
|
+
' throw new Error(body.error);\n' +
|
|
987
|
+
' }\n' +
|
|
988
|
+
' } catch (e) {\n' +
|
|
989
|
+
' if (e.message && e.message.startsWith("MCPflare network policy:")) {\n' +
|
|
990
|
+
' throw e;\n' +
|
|
991
|
+
' }\n' +
|
|
992
|
+
' }\n' +
|
|
993
|
+
' }\n' +
|
|
994
|
+
' return response;\n' +
|
|
995
|
+
'};\n' +
|
|
996
|
+
'// Override globalThis.fetch at module level\n' +
|
|
997
|
+
'globalThis.fetch = __mcpflareFetchWrapper;\n\n'
|
|
998
|
+
: '';
|
|
999
|
+
const workerScript = '// Dynamic Worker that executes AI-generated code\n' +
|
|
1000
|
+
'// This Worker is spawned via Worker Loader API from the parent Worker\n' +
|
|
1001
|
+
(networkEnabled
|
|
1002
|
+
? '// Network access enabled with domain allowlist enforcement\n'
|
|
1003
|
+
: '// Uses Service Bindings for secure MCP access (globalOutbound: null enabled)\n') +
|
|
1004
|
+
modulePrelude +
|
|
1005
|
+
'export default {\n' +
|
|
1006
|
+
' async fetch(request, env, ctx) {\n' +
|
|
1007
|
+
' const { code, timeout = 30000 } = await request.json();\n' +
|
|
1008
|
+
' \n' +
|
|
1009
|
+
' // Capture console output\n' +
|
|
1010
|
+
' const logs = [];\n' +
|
|
1011
|
+
' const originalLog = console.log;\n' +
|
|
1012
|
+
' const originalError = console.error;\n' +
|
|
1013
|
+
' const originalWarn = console.warn;\n' +
|
|
1014
|
+
'\n' +
|
|
1015
|
+
' console.log = (...args) => {\n' +
|
|
1016
|
+
" logs.push(args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '));\n" +
|
|
1017
|
+
' };\n' +
|
|
1018
|
+
'\n' +
|
|
1019
|
+
' console.error = (...args) => {\n' +
|
|
1020
|
+
" logs.push('ERROR: ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '));\n" +
|
|
1021
|
+
' };\n' +
|
|
1022
|
+
'\n' +
|
|
1023
|
+
' console.warn = (...args) => {\n' +
|
|
1024
|
+
" logs.push('WARN: ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '));\n" +
|
|
1025
|
+
' };\n' +
|
|
1026
|
+
'\n' +
|
|
1027
|
+
' let mcpCallCount = 0;\n' +
|
|
1028
|
+
' const toolsCalled = new Set();\n' +
|
|
1029
|
+
' let result;\n' +
|
|
1030
|
+
'\n' +
|
|
1031
|
+
' try {\n' +
|
|
1032
|
+
' // Create MCP binding implementation using Service Binding (env.MCP)\n' +
|
|
1033
|
+
' // The Service Binding is provided by the parent Worker and allows secure MCP access\n' +
|
|
1034
|
+
' // without requiring fetch() - enabling true network isolation (globalOutbound: null)\n' +
|
|
1035
|
+
' // Reference: https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/\n' +
|
|
1036
|
+
' const mcpBinding = {\n' +
|
|
1037
|
+
mcpBindingStubs +
|
|
1038
|
+
'\n' +
|
|
1039
|
+
' };\n' +
|
|
1040
|
+
'\n' +
|
|
1041
|
+
' // Create MCP proxy to track calls and tool names\n' +
|
|
1042
|
+
' // Also provide better error messages when a tool is not found\n' +
|
|
1043
|
+
' const mcp = new Proxy(mcpBinding, {\n' +
|
|
1044
|
+
' get(target, prop) {\n' +
|
|
1045
|
+
' const original = target[prop];\n' +
|
|
1046
|
+
" if (typeof original === 'function') {\n" +
|
|
1047
|
+
' return async (...args) => {\n' +
|
|
1048
|
+
' mcpCallCount++;\n' +
|
|
1049
|
+
' toolsCalled.add(String(prop));\n' +
|
|
1050
|
+
' return await original.apply(target, args);\n' +
|
|
1051
|
+
' };\n' +
|
|
1052
|
+
' }\n' +
|
|
1053
|
+
' // Tool not found - provide helpful error message\n' +
|
|
1054
|
+
" // Skip special properties like 'then' (for await) and Symbol properties\n" +
|
|
1055
|
+
" if (prop !== 'then' && typeof prop !== 'symbol') {\n" +
|
|
1056
|
+
" const availableTools = Object.keys(target).join(', ');\n" +
|
|
1057
|
+
' throw new Error("Tool \\"" + String(prop) + "\\" not found. Available tools: " + (availableTools || "none"));\n' +
|
|
1058
|
+
' }\n' +
|
|
1059
|
+
' return original;\n' +
|
|
1060
|
+
' },\n' +
|
|
1061
|
+
' });\n' +
|
|
1062
|
+
'\n' +
|
|
1063
|
+
' // Execute the user-provided code\n' +
|
|
1064
|
+
' // The user code is embedded directly in this Worker module as executable statements\n' +
|
|
1065
|
+
' // Each execution gets a fresh Worker isolate via Worker Loader API\n' +
|
|
1066
|
+
' // Note: Function constructor and eval() are disallowed in Workers (CSP), so code must be embedded directly\n' +
|
|
1067
|
+
' const executeWithTimeout = async () => {\n' +
|
|
1068
|
+
" // User code is embedded below - it has access to 'mcp' and 'env'\n" +
|
|
1069
|
+
' // fetch() is our wrapped version (set at module level)\n' +
|
|
1070
|
+
' // User code embedded below\n' +
|
|
1071
|
+
userCode +
|
|
1072
|
+
'\n' +
|
|
1073
|
+
' };\n' +
|
|
1074
|
+
'\n' +
|
|
1075
|
+
' const timeoutPromise = new Promise((_, reject) => \n' +
|
|
1076
|
+
" setTimeout(() => reject(new Error('Execution timeout')), timeout)\n" +
|
|
1077
|
+
' );\n' +
|
|
1078
|
+
'\n' +
|
|
1079
|
+
' result = await Promise.race([executeWithTimeout(), timeoutPromise]);\n' +
|
|
1080
|
+
'\n' +
|
|
1081
|
+
' return new Response(JSON.stringify({\n' +
|
|
1082
|
+
' success: true,\n' +
|
|
1083
|
+
" output: logs.join('\\\\n'),\n" +
|
|
1084
|
+
' result: result !== undefined ? result : null,\n' +
|
|
1085
|
+
' metrics: {\n' +
|
|
1086
|
+
' mcp_calls_made: mcpCallCount,\n' +
|
|
1087
|
+
' tools_called: Array.from(toolsCalled),\n' +
|
|
1088
|
+
' },\n' +
|
|
1089
|
+
' }), {\n' +
|
|
1090
|
+
" headers: { 'Content-Type': 'application/json' },\n" +
|
|
1091
|
+
' });\n' +
|
|
1092
|
+
' } catch (error) {\n' +
|
|
1093
|
+
' return new Response(JSON.stringify({\n' +
|
|
1094
|
+
' success: false,\n' +
|
|
1095
|
+
' error: error.message,\n' +
|
|
1096
|
+
' stack: error.stack,\n' +
|
|
1097
|
+
" output: logs.join('\\\\n'),\n" +
|
|
1098
|
+
' metrics: {\n' +
|
|
1099
|
+
' mcp_calls_made: mcpCallCount,\n' +
|
|
1100
|
+
' tools_called: Array.from(toolsCalled),\n' +
|
|
1101
|
+
' },\n' +
|
|
1102
|
+
' }), {\n' +
|
|
1103
|
+
' status: 500,\n' +
|
|
1104
|
+
" headers: { 'Content-Type': 'application/json' },\n" +
|
|
1105
|
+
' });\n' +
|
|
1106
|
+
' } finally {\n' +
|
|
1107
|
+
' // Restore console methods\n' +
|
|
1108
|
+
' console.log = originalLog;\n' +
|
|
1109
|
+
' console.error = originalError;\n' +
|
|
1110
|
+
' console.warn = originalWarn;\n' +
|
|
1111
|
+
' }\n' +
|
|
1112
|
+
' }\n' +
|
|
1113
|
+
'};\n';
|
|
1114
|
+
return {
|
|
1115
|
+
compatibilityDate: '2025-06-01',
|
|
1116
|
+
mainModule: 'worker.js',
|
|
1117
|
+
modules: {
|
|
1118
|
+
'worker.js': workerScript,
|
|
1119
|
+
},
|
|
1120
|
+
env: {
|
|
1121
|
+
MCP_ID: mcpId,
|
|
1122
|
+
MCP_RPC_URL: rpcUrl,
|
|
1123
|
+
NETWORK_ENABLED: networkEnabled ? 'true' : 'false',
|
|
1124
|
+
},
|
|
1125
|
+
globalOutbound: null,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
async executeInIsolate(mcpId, code, timeoutMs, instance) {
|
|
1129
|
+
if (this.wranglerAvailable === false) {
|
|
1130
|
+
throw new WorkerError('Wrangler is required for Worker execution but is not available.\n' +
|
|
1131
|
+
'Please install Wrangler to enable code execution in isolated Worker environments:\n' +
|
|
1132
|
+
' npm install -g wrangler\n' +
|
|
1133
|
+
' or ensure npx can access wrangler: npx wrangler --version');
|
|
1134
|
+
}
|
|
1135
|
+
try {
|
|
1136
|
+
return await this.executeWithWrangler(mcpId, code, timeoutMs, instance);
|
|
1137
|
+
}
|
|
1138
|
+
catch (error) {
|
|
1139
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1140
|
+
const errorCode = error?.code;
|
|
1141
|
+
const isSpawnENOENT = errorCode === 'ENOENT' ||
|
|
1142
|
+
(errorMessage.includes('ENOENT') &&
|
|
1143
|
+
(errorMessage.includes('spawn') ||
|
|
1144
|
+
errorMessage.includes('Failed to spawn') ||
|
|
1145
|
+
errorMessage.includes('npx') ||
|
|
1146
|
+
errorMessage.includes('npx.cmd')));
|
|
1147
|
+
if (isSpawnENOENT && this.wranglerAvailable === null) {
|
|
1148
|
+
this.wranglerAvailable = false;
|
|
1149
|
+
logger.error({ mcpId, error: errorMessage, errorCode }, 'Wrangler spawn failed - command not found');
|
|
1150
|
+
throw new WorkerError('Wrangler is required for Worker execution but is not available.\n' +
|
|
1151
|
+
'Wrangler provides the Cloudflare Worker isolation environment needed for safe code execution.\n' +
|
|
1152
|
+
'Please install Wrangler:\n' +
|
|
1153
|
+
' npm install -g wrangler\n' +
|
|
1154
|
+
' or ensure npx can access wrangler: npx wrangler --version\n\n' +
|
|
1155
|
+
`Error details: ${errorMessage}`);
|
|
1156
|
+
}
|
|
1157
|
+
logger.error({ mcpId, error: errorMessage, errorCode, isSpawnENOENT }, 'Wrangler execution error (not spawn failure)');
|
|
1158
|
+
throw error;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
getWorkerEntryPoint() {
|
|
1162
|
+
if (this.cachedWorkerEntryPoint !== null) {
|
|
1163
|
+
return this.cachedWorkerEntryPoint;
|
|
1164
|
+
}
|
|
1165
|
+
const cwd = process.cwd();
|
|
1166
|
+
const nodeEnv = process.env.NODE_ENV;
|
|
1167
|
+
const isNodeEnvDev = nodeEnv === 'development';
|
|
1168
|
+
const isNodeEnvProd = nodeEnv === 'production';
|
|
1169
|
+
const isRunningViaTsx = process.argv[1]?.includes('tsx') ||
|
|
1170
|
+
process.argv[0]?.includes('tsx') ||
|
|
1171
|
+
process.argv[1]?.includes('src/server/index.ts') ||
|
|
1172
|
+
process.argv[1]?.includes('src\\server\\index.ts');
|
|
1173
|
+
const isDevMode = isNodeEnvDev || (!isNodeEnvProd && isRunningViaTsx);
|
|
1174
|
+
const devPath = join(cwd, 'src', 'worker', 'runtime.ts');
|
|
1175
|
+
const prodPath = join(cwd, 'dist', 'worker', 'runtime.js');
|
|
1176
|
+
const devExists = existsSync(devPath);
|
|
1177
|
+
const prodExists = existsSync(prodPath);
|
|
1178
|
+
let entryPoint;
|
|
1179
|
+
if (isDevMode && devExists) {
|
|
1180
|
+
entryPoint = 'src/worker/runtime.ts';
|
|
1181
|
+
}
|
|
1182
|
+
else if (prodExists) {
|
|
1183
|
+
entryPoint = 'dist/worker/runtime.js';
|
|
1184
|
+
}
|
|
1185
|
+
else if (isDevMode) {
|
|
1186
|
+
entryPoint = 'src/worker/runtime.ts';
|
|
1187
|
+
logger.warn({ devPath, prodPath, cwd }, 'Dev entry point file not found, using dev path anyway');
|
|
1188
|
+
}
|
|
1189
|
+
else {
|
|
1190
|
+
entryPoint = 'dist/worker/runtime.js';
|
|
1191
|
+
logger.warn({ devPath, prodPath, cwd }, 'Production entry point file not found, using prod path anyway');
|
|
1192
|
+
}
|
|
1193
|
+
this.cachedWorkerEntryPoint = entryPoint;
|
|
1194
|
+
logger.info({
|
|
1195
|
+
entryPoint,
|
|
1196
|
+
isDevMode,
|
|
1197
|
+
nodeEnv,
|
|
1198
|
+
isRunningViaTsx,
|
|
1199
|
+
devExists,
|
|
1200
|
+
prodExists,
|
|
1201
|
+
cwd,
|
|
1202
|
+
}, 'Determined Worker entry point (cached)');
|
|
1203
|
+
return entryPoint;
|
|
1204
|
+
}
|
|
1205
|
+
async executeWithWrangler(mcpId, code, timeoutMs, instance) {
|
|
1206
|
+
const progress = new ProgressIndicator();
|
|
1207
|
+
const isCLIMode = process.env.CLI_MODE === 'true';
|
|
1208
|
+
let wranglerProcess = null;
|
|
1209
|
+
const port = Math.floor(Math.random() * 10000) + 20000;
|
|
1210
|
+
let wranglerStdout = '';
|
|
1211
|
+
let wranglerStderr = '';
|
|
1212
|
+
try {
|
|
1213
|
+
if (isCLIMode) {
|
|
1214
|
+
progress.updateStep(0, 'running');
|
|
1215
|
+
}
|
|
1216
|
+
const isolationConfig = getIsolationConfigForMCP(instance.mcp_name);
|
|
1217
|
+
const workerCode = await this.generateWorkerCode(mcpId, instance.tools, instance.typescript_api, code, isolationConfig);
|
|
1218
|
+
const isWindows = process.platform === 'win32';
|
|
1219
|
+
const npxCmd = isWindows ? 'npx.cmd' : 'npx';
|
|
1220
|
+
if (isCLIMode) {
|
|
1221
|
+
progress.updateStep(0, 'success');
|
|
1222
|
+
}
|
|
1223
|
+
if (isCLIMode) {
|
|
1224
|
+
progress.updateStep(1, 'running');
|
|
1225
|
+
}
|
|
1226
|
+
logger.debug({ mcpId, port }, 'Starting Wrangler dev server for parent Worker');
|
|
1227
|
+
const baseEntryPoint = this.getWorkerEntryPoint();
|
|
1228
|
+
const wranglerCwd = this.projectRoot || process.cwd();
|
|
1229
|
+
const entryPointPath = resolve(wranglerCwd, baseEntryPoint);
|
|
1230
|
+
const entryPointExists = existsSync(entryPointPath);
|
|
1231
|
+
const entryPointForWrangler = baseEntryPoint;
|
|
1232
|
+
logger.info({
|
|
1233
|
+
mcpId,
|
|
1234
|
+
port,
|
|
1235
|
+
baseEntryPoint,
|
|
1236
|
+
entryPointForWrangler,
|
|
1237
|
+
wranglerCwd,
|
|
1238
|
+
entryPointPath,
|
|
1239
|
+
entryPointExists,
|
|
1240
|
+
actualCwd: process.cwd(),
|
|
1241
|
+
projectRoot: this.projectRoot,
|
|
1242
|
+
}, 'Spawning Wrangler - using project root as CWD');
|
|
1243
|
+
if (!entryPointExists) {
|
|
1244
|
+
const error = new Error(`Worker entry point not found at project root.\n` +
|
|
1245
|
+
` - Project root: ${wranglerCwd}\n` +
|
|
1246
|
+
` - Entry point: ${baseEntryPoint}\n` +
|
|
1247
|
+
` - Full path: ${entryPointPath}\n` +
|
|
1248
|
+
` - Exists: ${entryPointExists}`);
|
|
1249
|
+
logger.error({ error, wranglerCwd, baseEntryPoint, entryPointPath }, 'Entry point not found');
|
|
1250
|
+
throw error;
|
|
1251
|
+
}
|
|
1252
|
+
const wranglerArgs = [
|
|
1253
|
+
'wrangler',
|
|
1254
|
+
'dev',
|
|
1255
|
+
entryPointForWrangler,
|
|
1256
|
+
'--local',
|
|
1257
|
+
'--port',
|
|
1258
|
+
port.toString(),
|
|
1259
|
+
];
|
|
1260
|
+
wranglerProcess = spawn(npxCmd, wranglerArgs, {
|
|
1261
|
+
cwd: wranglerCwd,
|
|
1262
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1263
|
+
shell: isWindows,
|
|
1264
|
+
detached: !isWindows,
|
|
1265
|
+
});
|
|
1266
|
+
let spawnError = null;
|
|
1267
|
+
let errorHandled = false;
|
|
1268
|
+
wranglerProcess.on('error', (error) => {
|
|
1269
|
+
if (errorHandled)
|
|
1270
|
+
return;
|
|
1271
|
+
spawnError = error;
|
|
1272
|
+
errorHandled = true;
|
|
1273
|
+
logger.error({
|
|
1274
|
+
error: error.message,
|
|
1275
|
+
code: error.code,
|
|
1276
|
+
command: npxCmd,
|
|
1277
|
+
args: wranglerArgs,
|
|
1278
|
+
cwd: wranglerCwd,
|
|
1279
|
+
}, 'Wrangler spawn error - command may not be found');
|
|
1280
|
+
});
|
|
1281
|
+
if (wranglerProcess?.pid) {
|
|
1282
|
+
this.wranglerProcesses.add(wranglerProcess);
|
|
1283
|
+
logger.info({
|
|
1284
|
+
pid: wranglerProcess.pid,
|
|
1285
|
+
port,
|
|
1286
|
+
mcpId,
|
|
1287
|
+
command: npxCmd,
|
|
1288
|
+
args: wranglerArgs,
|
|
1289
|
+
}, `Wrangler process spawned: PID ${wranglerProcess.pid} on port ${port}`);
|
|
1290
|
+
const trackedProcess = wranglerProcess;
|
|
1291
|
+
trackedProcess.on('exit', (code, signal) => {
|
|
1292
|
+
this.wranglerProcesses.delete(trackedProcess);
|
|
1293
|
+
logger.debug({ pid: trackedProcess.pid, code, signal }, 'Wrangler process exited');
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
if (wranglerProcess?.stdout) {
|
|
1297
|
+
wranglerProcess.stdout.on('data', (data) => {
|
|
1298
|
+
const output = data.toString();
|
|
1299
|
+
wranglerStdout += output;
|
|
1300
|
+
logger.debug({ output }, 'Wrangler stdout');
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
if (wranglerProcess?.stderr) {
|
|
1304
|
+
wranglerProcess.stderr.on('data', (data) => {
|
|
1305
|
+
const output = data.toString();
|
|
1306
|
+
wranglerStderr += output;
|
|
1307
|
+
logger.debug({ output }, 'Wrangler stderr');
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
await new Promise((resolve, reject) => {
|
|
1311
|
+
if (spawnError) {
|
|
1312
|
+
const spawnErrnoError = spawnError;
|
|
1313
|
+
const isENOENT = spawnErrnoError.code === 'ENOENT' ||
|
|
1314
|
+
spawnError.message.includes('ENOENT');
|
|
1315
|
+
if (isENOENT) {
|
|
1316
|
+
reject(new Error(`Failed to spawn Wrangler: ${spawnError.message}\n` +
|
|
1317
|
+
`Command: ${npxCmd} ${wranglerArgs.join(' ')}\n` +
|
|
1318
|
+
`This usually means npx or wrangler is not installed or not in PATH.`));
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
reject(spawnError);
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
const timeout = setTimeout(() => {
|
|
1325
|
+
const error = new Error('Wrangler dev server failed to start within 10 seconds');
|
|
1326
|
+
reject(error);
|
|
1327
|
+
}, 10000);
|
|
1328
|
+
let ready = false;
|
|
1329
|
+
let checkCount = 0;
|
|
1330
|
+
const maxChecks = 50;
|
|
1331
|
+
const checkReady = async () => {
|
|
1332
|
+
checkCount++;
|
|
1333
|
+
try {
|
|
1334
|
+
const response = await fetch(`http://localhost:${port}`, {
|
|
1335
|
+
method: 'POST',
|
|
1336
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1337
|
+
body: JSON.stringify({
|
|
1338
|
+
workerId: `mcp-${mcpId}-${Date.now()}`,
|
|
1339
|
+
workerCode: {
|
|
1340
|
+
compatibilityDate: '2025-06-01',
|
|
1341
|
+
mainModule: 'test.js',
|
|
1342
|
+
modules: {
|
|
1343
|
+
'test.js': 'export default { fetch: () => new Response("ok") }',
|
|
1344
|
+
},
|
|
1345
|
+
},
|
|
1346
|
+
executionRequest: { code: '// health check', timeout: 1000 },
|
|
1347
|
+
}),
|
|
1348
|
+
signal: AbortSignal.timeout(500),
|
|
1349
|
+
});
|
|
1350
|
+
if (response.ok || response.status === 500) {
|
|
1351
|
+
ready = true;
|
|
1352
|
+
clearTimeout(timeout);
|
|
1353
|
+
if (isCLIMode) {
|
|
1354
|
+
progress.updateStep(1, 'success');
|
|
1355
|
+
}
|
|
1356
|
+
resolve();
|
|
1357
|
+
}
|
|
1358
|
+
else if (checkCount < maxChecks) {
|
|
1359
|
+
setTimeout(checkReady, 200);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
catch (error) {
|
|
1363
|
+
if (checkCount < maxChecks &&
|
|
1364
|
+
!(error instanceof Error && error.name?.includes('AbortError'))) {
|
|
1365
|
+
setTimeout(checkReady, 200);
|
|
1366
|
+
}
|
|
1367
|
+
else if (checkCount >= maxChecks) {
|
|
1368
|
+
clearTimeout(timeout);
|
|
1369
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1370
|
+
const healthCheckError = new Error(`Wrangler health check failed after ${maxChecks} attempts. Last error: ${errorMessage}`);
|
|
1371
|
+
reject(healthCheckError);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
setTimeout(checkReady, 1000);
|
|
1376
|
+
if (wranglerProcess?.stdout) {
|
|
1377
|
+
wranglerProcess.stdout.on('data', (data) => {
|
|
1378
|
+
const output = data.toString();
|
|
1379
|
+
if ((output.includes('Ready') ||
|
|
1380
|
+
output.includes('ready') ||
|
|
1381
|
+
output.includes('Listening')) &&
|
|
1382
|
+
!ready) {
|
|
1383
|
+
ready = true;
|
|
1384
|
+
clearTimeout(timeout);
|
|
1385
|
+
if (isCLIMode) {
|
|
1386
|
+
progress.updateStep(1, 'success');
|
|
1387
|
+
}
|
|
1388
|
+
setTimeout(() => resolve(), 500);
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
wranglerProcess?.on('error', (error) => {
|
|
1393
|
+
if (errorHandled)
|
|
1394
|
+
return;
|
|
1395
|
+
errorHandled = true;
|
|
1396
|
+
clearTimeout(timeout);
|
|
1397
|
+
const isENOENT = error.code === 'ENOENT' || error.message.includes('ENOENT');
|
|
1398
|
+
if (isENOENT) {
|
|
1399
|
+
reject(new Error(`Failed to spawn Wrangler: ${error.message}\n` +
|
|
1400
|
+
`Command: ${npxCmd} ${wranglerArgs.join(' ')}\n` +
|
|
1401
|
+
`This usually means npx or wrangler is not installed or not in PATH.`));
|
|
1402
|
+
}
|
|
1403
|
+
else {
|
|
1404
|
+
reject(new Error(`Wrangler process error: ${error.message}`));
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
wranglerProcess?.on('exit', (code, signal) => {
|
|
1408
|
+
if (!ready && code !== null && code !== 0) {
|
|
1409
|
+
clearTimeout(timeout);
|
|
1410
|
+
const hasBuildError = wranglerStderr.includes('Build failed') ||
|
|
1411
|
+
wranglerStderr.includes('build failed') ||
|
|
1412
|
+
wranglerStderr.includes('✗ Build failed');
|
|
1413
|
+
const hasWorkerLoadersError = wranglerStderr.includes('worker_loaders') ||
|
|
1414
|
+
wranglerStdout.includes('worker_loaders');
|
|
1415
|
+
if (hasWorkerLoadersError) {
|
|
1416
|
+
const error = new Error('Worker Loader API configuration error. The "worker_loaders" field may not be supported in your Wrangler version.\n' +
|
|
1417
|
+
'Please ensure you have Wrangler 3.50.0 or later, or check the Wrangler documentation for the correct configuration format.\n' +
|
|
1418
|
+
'Error details: ' +
|
|
1419
|
+
(wranglerStderr || wranglerStdout)
|
|
1420
|
+
.split('\n')
|
|
1421
|
+
.find((line) => line.includes('worker_loaders')) ||
|
|
1422
|
+
'Unknown error');
|
|
1423
|
+
const buildError = error;
|
|
1424
|
+
buildError.isBuildError = true;
|
|
1425
|
+
reject(buildError);
|
|
1426
|
+
}
|
|
1427
|
+
else if (hasBuildError) {
|
|
1428
|
+
const error = new Error('TypeScript compilation failed. Check the error details below.');
|
|
1429
|
+
error.isBuildError = true;
|
|
1430
|
+
reject(error);
|
|
1431
|
+
}
|
|
1432
|
+
else {
|
|
1433
|
+
const error = new Error(`Wrangler process exited with code ${code} (signal: ${signal})`);
|
|
1434
|
+
error.code = code ?? undefined;
|
|
1435
|
+
error.signal = signal ?? undefined;
|
|
1436
|
+
reject(error);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
});
|
|
1440
|
+
});
|
|
1441
|
+
if (isCLIMode) {
|
|
1442
|
+
progress.updateStep(2, 'running');
|
|
1443
|
+
}
|
|
1444
|
+
logger.debug({ mcpId, codeLength: code.length }, 'Executing code via Worker Loader API');
|
|
1445
|
+
const workerId = `mcp-${mcpId}-${createHash('sha256').update(`${mcpId}-${code}`).digest('hex').substring(0, 16)}`;
|
|
1446
|
+
const response = await fetch(`http://localhost:${port}`, {
|
|
1447
|
+
method: 'POST',
|
|
1448
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1449
|
+
body: JSON.stringify({
|
|
1450
|
+
workerId,
|
|
1451
|
+
workerCode,
|
|
1452
|
+
executionRequest: {
|
|
1453
|
+
code,
|
|
1454
|
+
timeout: timeoutMs,
|
|
1455
|
+
},
|
|
1456
|
+
}),
|
|
1457
|
+
});
|
|
1458
|
+
if (!response.ok) {
|
|
1459
|
+
const errorText = await response.text();
|
|
1460
|
+
if (isCLIMode) {
|
|
1461
|
+
progress.updateStep(2, 'failed');
|
|
1462
|
+
progress.showFinal(2);
|
|
1463
|
+
}
|
|
1464
|
+
throw new Error(`Worker execution failed: ${response.status} ${errorText}`);
|
|
1465
|
+
}
|
|
1466
|
+
const result = (await response.json());
|
|
1467
|
+
if (isCLIMode) {
|
|
1468
|
+
progress.updateStep(2, 'success');
|
|
1469
|
+
progress.showFinal();
|
|
1470
|
+
}
|
|
1471
|
+
const metrics = result.metrics
|
|
1472
|
+
? {
|
|
1473
|
+
mcp_calls_made: result.metrics.mcp_calls_made ?? 0,
|
|
1474
|
+
tools_called: result.metrics
|
|
1475
|
+
.tools_called,
|
|
1476
|
+
}
|
|
1477
|
+
: {
|
|
1478
|
+
mcp_calls_made: 0,
|
|
1479
|
+
};
|
|
1480
|
+
return {
|
|
1481
|
+
output: result.output || '',
|
|
1482
|
+
result: result.result,
|
|
1483
|
+
metrics,
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
catch (error) {
|
|
1487
|
+
let failedStep = -1;
|
|
1488
|
+
const isCLIMode = process.env.CLI_MODE === 'true';
|
|
1489
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1490
|
+
const errorIsBuildError = error?.isBuildError === true;
|
|
1491
|
+
if (isCLIMode) {
|
|
1492
|
+
const hasWorkerLoadersError = (wranglerStderr?.includes('worker_loaders') ||
|
|
1493
|
+
wranglerStdout?.includes('worker_loaders') ||
|
|
1494
|
+
errorMessage.includes('worker_loaders')) ??
|
|
1495
|
+
false;
|
|
1496
|
+
const hasBuildError = wranglerStderr.includes('Build failed') ||
|
|
1497
|
+
wranglerStderr.includes('build failed') ||
|
|
1498
|
+
wranglerStderr.includes('✗ Build failed') ||
|
|
1499
|
+
errorMessage.includes('TypeScript compilation failed') ||
|
|
1500
|
+
errorMessage.includes('compilation failed') ||
|
|
1501
|
+
errorIsBuildError;
|
|
1502
|
+
if (hasWorkerLoadersError ||
|
|
1503
|
+
hasBuildError ||
|
|
1504
|
+
errorMessage.includes('Wrangler process') ||
|
|
1505
|
+
errorMessage.includes('Wrangler dev server') ||
|
|
1506
|
+
errorMessage.includes('health check') ||
|
|
1507
|
+
errorMessage.includes('Wrangler process exited')) {
|
|
1508
|
+
failedStep = 1;
|
|
1509
|
+
progress.updateStep(1, 'failed');
|
|
1510
|
+
}
|
|
1511
|
+
else if (errorMessage.includes('Worker execution failed') ||
|
|
1512
|
+
errorMessage.includes('execute') ||
|
|
1513
|
+
(errorMessage.includes('fetch') && errorMessage.includes('localhost'))) {
|
|
1514
|
+
failedStep = 2;
|
|
1515
|
+
progress.updateStep(2, 'failed');
|
|
1516
|
+
}
|
|
1517
|
+
else {
|
|
1518
|
+
failedStep = 0;
|
|
1519
|
+
progress.updateStep(0, 'failed');
|
|
1520
|
+
}
|
|
1521
|
+
progress.showFinal(failedStep);
|
|
1522
|
+
}
|
|
1523
|
+
const context = {
|
|
1524
|
+
mcpId,
|
|
1525
|
+
port,
|
|
1526
|
+
};
|
|
1527
|
+
const isWorkerLoadersError = wranglerStderr?.includes('worker_loaders') ||
|
|
1528
|
+
wranglerStdout?.includes('worker_loaders');
|
|
1529
|
+
const isBuildError = wranglerStderr?.includes('Build failed') ||
|
|
1530
|
+
wranglerStderr?.includes('build failed') ||
|
|
1531
|
+
wranglerStderr?.includes('✗ Build failed');
|
|
1532
|
+
if ((isBuildError || isWorkerLoadersError) && code) {
|
|
1533
|
+
context.userCode = code;
|
|
1534
|
+
}
|
|
1535
|
+
console.error('\n' +
|
|
1536
|
+
formatWranglerError(error instanceof Error ? error : new Error(String(error)), wranglerStdout || '', wranglerStderr || '', context) +
|
|
1537
|
+
'\n');
|
|
1538
|
+
const isVerbose = process.argv.includes('--verbose') || process.argv.includes('-v');
|
|
1539
|
+
if (!isCLIMode || isVerbose) {
|
|
1540
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1541
|
+
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
1542
|
+
logger.error({
|
|
1543
|
+
error: errorMsg,
|
|
1544
|
+
stack: errorStack,
|
|
1545
|
+
mcpId,
|
|
1546
|
+
port,
|
|
1547
|
+
}, 'Wrangler execution error');
|
|
1548
|
+
}
|
|
1549
|
+
throw new WorkerError(`Wrangler execution failed: ${errorMessage}`, {
|
|
1550
|
+
wrangler_stdout: wranglerStdout || '',
|
|
1551
|
+
wrangler_stderr: wranglerStderr || '',
|
|
1552
|
+
exit_code: error?.code,
|
|
1553
|
+
mcp_id: mcpId,
|
|
1554
|
+
port,
|
|
1555
|
+
fatal: true,
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
finally {
|
|
1559
|
+
if (wranglerProcess) {
|
|
1560
|
+
try {
|
|
1561
|
+
await this.killWranglerProcess(wranglerProcess);
|
|
1562
|
+
}
|
|
1563
|
+
catch (cleanupError) {
|
|
1564
|
+
logger.warn({ error: cleanupError, pid: wranglerProcess.pid }, 'Error during Wrangler process cleanup');
|
|
1565
|
+
}
|
|
1566
|
+
wranglerProcess = null;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
async killProcessTree(pid) {
|
|
1571
|
+
if (!pid || !Number.isInteger(pid) || pid <= 0) {
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
return new Promise((resolve) => {
|
|
1575
|
+
if (process.platform === 'win32') {
|
|
1576
|
+
const taskkillProcess = spawn('taskkill', ['/F', '/T', '/PID', String(pid)], {
|
|
1577
|
+
stdio: 'ignore',
|
|
1578
|
+
shell: false,
|
|
1579
|
+
});
|
|
1580
|
+
taskkillProcess.on('exit', () => {
|
|
1581
|
+
resolve();
|
|
1582
|
+
});
|
|
1583
|
+
taskkillProcess.on('error', () => {
|
|
1584
|
+
resolve();
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
else {
|
|
1588
|
+
const killWithSignal = (signal) => {
|
|
1589
|
+
try {
|
|
1590
|
+
process.kill(-pid, signal);
|
|
1591
|
+
}
|
|
1592
|
+
catch {
|
|
1593
|
+
}
|
|
1594
|
+
try {
|
|
1595
|
+
const pkillProcess = spawn('pkill', [`-${signal}`, '-P', String(pid)], { stdio: 'ignore' });
|
|
1596
|
+
pkillProcess.on('error', () => {
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
catch {
|
|
1600
|
+
}
|
|
1601
|
+
try {
|
|
1602
|
+
process.kill(pid, signal);
|
|
1603
|
+
}
|
|
1604
|
+
catch {
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
killWithSignal('SIGTERM');
|
|
1608
|
+
setTimeout(() => {
|
|
1609
|
+
killWithSignal('SIGKILL');
|
|
1610
|
+
resolve();
|
|
1611
|
+
}, 1000);
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
async killWranglerProcess(proc) {
|
|
1616
|
+
if (!proc || proc.killed) {
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
const pid = proc.pid;
|
|
1620
|
+
if (!pid) {
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
logger.info({ pid }, `Killing Wrangler process tree: PID ${pid}`);
|
|
1624
|
+
this.wranglerProcesses.delete(proc);
|
|
1625
|
+
await this.killProcessTree(pid);
|
|
1626
|
+
try {
|
|
1627
|
+
proc.kill('SIGTERM');
|
|
1628
|
+
}
|
|
1629
|
+
catch {
|
|
1630
|
+
}
|
|
1631
|
+
await new Promise((resolve) => {
|
|
1632
|
+
if (proc.killed) {
|
|
1633
|
+
resolve();
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
proc.on('exit', () => resolve());
|
|
1637
|
+
setTimeout(() => {
|
|
1638
|
+
if (proc && !proc.killed && proc.pid) {
|
|
1639
|
+
try {
|
|
1640
|
+
this.killProcessTree(proc.pid).catch(() => {
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
catch {
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
resolve();
|
|
1647
|
+
}, 3000);
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
async killMCPProcess(proc) {
|
|
1651
|
+
if (!proc || proc.killed) {
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
const pid = proc.pid;
|
|
1655
|
+
if (!pid) {
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
logger.info({ pid }, `Killing MCP process tree: PID ${pid}`);
|
|
1659
|
+
await this.killProcessTree(pid);
|
|
1660
|
+
try {
|
|
1661
|
+
proc.kill('SIGTERM');
|
|
1662
|
+
}
|
|
1663
|
+
catch {
|
|
1664
|
+
}
|
|
1665
|
+
await new Promise((resolve) => {
|
|
1666
|
+
if (proc.killed) {
|
|
1667
|
+
resolve();
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
proc.on('exit', () => resolve());
|
|
1671
|
+
setTimeout(() => {
|
|
1672
|
+
if (proc && !proc.killed && proc.pid) {
|
|
1673
|
+
try {
|
|
1674
|
+
this.killProcessTree(proc.pid).catch(() => {
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
catch {
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
resolve();
|
|
1681
|
+
}, 2000);
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
async shutdown() {
|
|
1685
|
+
logger.debug('Shutting down WorkerManager...');
|
|
1686
|
+
if (this.rpcServer) {
|
|
1687
|
+
await new Promise((resolve) => {
|
|
1688
|
+
this.rpcServer?.close(() => {
|
|
1689
|
+
logger.debug('RPC server closed');
|
|
1690
|
+
resolve();
|
|
1691
|
+
});
|
|
1692
|
+
setTimeout(() => {
|
|
1693
|
+
resolve();
|
|
1694
|
+
}, 2000);
|
|
1695
|
+
});
|
|
1696
|
+
this.rpcServer = null;
|
|
1697
|
+
}
|
|
1698
|
+
const cleanupPromises = [];
|
|
1699
|
+
for (const [mcpId, client] of this.mcpClients.entries()) {
|
|
1700
|
+
cleanupPromises.push((async () => {
|
|
1701
|
+
try {
|
|
1702
|
+
const clientWithTransport = client;
|
|
1703
|
+
const transport = clientWithTransport._transport;
|
|
1704
|
+
if (transport && typeof transport.close === 'function') {
|
|
1705
|
+
await transport.close();
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
catch (error) {
|
|
1709
|
+
logger.warn({ error, mcpId }, 'Error closing MCP client');
|
|
1710
|
+
}
|
|
1711
|
+
})());
|
|
1712
|
+
}
|
|
1713
|
+
for (const [mcpId, proc] of this.mcpProcesses.entries()) {
|
|
1714
|
+
cleanupPromises.push((async () => {
|
|
1715
|
+
try {
|
|
1716
|
+
await this.killMCPProcess(proc);
|
|
1717
|
+
}
|
|
1718
|
+
catch (error) {
|
|
1719
|
+
logger.warn({ error, mcpId }, 'Error killing MCP process');
|
|
1720
|
+
}
|
|
1721
|
+
})());
|
|
1722
|
+
}
|
|
1723
|
+
const wranglerProcesses = Array.from(this.wranglerProcesses);
|
|
1724
|
+
for (const proc of wranglerProcesses) {
|
|
1725
|
+
cleanupPromises.push((async () => {
|
|
1726
|
+
try {
|
|
1727
|
+
await this.killWranglerProcess(proc);
|
|
1728
|
+
}
|
|
1729
|
+
catch (error) {
|
|
1730
|
+
logger.warn({ error }, 'Error killing Wrangler process');
|
|
1731
|
+
}
|
|
1732
|
+
})());
|
|
1733
|
+
}
|
|
1734
|
+
await Promise.race([
|
|
1735
|
+
Promise.all(cleanupPromises),
|
|
1736
|
+
new Promise((resolve) => setTimeout(resolve, 5000)),
|
|
1737
|
+
]);
|
|
1738
|
+
this.mcpClients.clear();
|
|
1739
|
+
this.mcpProcesses.clear();
|
|
1740
|
+
this.wranglerProcesses.clear();
|
|
1741
|
+
this.instances.clear();
|
|
1742
|
+
this.schemaCache.clear();
|
|
1743
|
+
logger.debug('WorkerManager shutdown complete');
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
//# sourceMappingURL=worker-manager.js.map
|