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.
Files changed (84) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +22 -0
  3. package/README.md +390 -0
  4. package/dist/cli/index.d.ts +3 -0
  5. package/dist/cli/index.d.ts.map +1 -0
  6. package/dist/cli/index.js +1615 -0
  7. package/dist/cli/index.js.map +1 -0
  8. package/dist/server/index.d.ts +3 -0
  9. package/dist/server/index.d.ts.map +1 -0
  10. package/dist/server/index.js +19 -0
  11. package/dist/server/index.js.map +1 -0
  12. package/dist/server/mcp-handler.d.ts +34 -0
  13. package/dist/server/mcp-handler.d.ts.map +1 -0
  14. package/dist/server/mcp-handler.js +1524 -0
  15. package/dist/server/mcp-handler.js.map +1 -0
  16. package/dist/server/metrics-collector.d.ts +30 -0
  17. package/dist/server/metrics-collector.d.ts.map +1 -0
  18. package/dist/server/metrics-collector.js +85 -0
  19. package/dist/server/metrics-collector.js.map +1 -0
  20. package/dist/server/schema-converter.d.ts +9 -0
  21. package/dist/server/schema-converter.d.ts.map +1 -0
  22. package/dist/server/schema-converter.js +82 -0
  23. package/dist/server/schema-converter.js.map +1 -0
  24. package/dist/server/worker-manager.d.ts +48 -0
  25. package/dist/server/worker-manager.d.ts.map +1 -0
  26. package/dist/server/worker-manager.js +1746 -0
  27. package/dist/server/worker-manager.js.map +1 -0
  28. package/dist/types/index.d.ts +3 -0
  29. package/dist/types/index.d.ts.map +1 -0
  30. package/dist/types/index.js +3 -0
  31. package/dist/types/index.js.map +1 -0
  32. package/dist/types/mcp.d.ts +495 -0
  33. package/dist/types/mcp.d.ts.map +1 -0
  34. package/dist/types/mcp.js +80 -0
  35. package/dist/types/mcp.js.map +1 -0
  36. package/dist/types/worker.d.ts +35 -0
  37. package/dist/types/worker.d.ts.map +1 -0
  38. package/dist/types/worker.js +2 -0
  39. package/dist/types/worker.js.map +1 -0
  40. package/dist/utils/config-manager.d.ts +64 -0
  41. package/dist/utils/config-manager.d.ts.map +1 -0
  42. package/dist/utils/config-manager.js +556 -0
  43. package/dist/utils/config-manager.js.map +1 -0
  44. package/dist/utils/env-selector.d.ts +4 -0
  45. package/dist/utils/env-selector.d.ts.map +1 -0
  46. package/dist/utils/env-selector.js +127 -0
  47. package/dist/utils/env-selector.js.map +1 -0
  48. package/dist/utils/errors.d.ts +19 -0
  49. package/dist/utils/errors.d.ts.map +1 -0
  50. package/dist/utils/errors.js +37 -0
  51. package/dist/utils/errors.js.map +1 -0
  52. package/dist/utils/logger.d.ts +4 -0
  53. package/dist/utils/logger.d.ts.map +1 -0
  54. package/dist/utils/logger.js +27 -0
  55. package/dist/utils/logger.js.map +1 -0
  56. package/dist/utils/mcp-registry.d.ts +108 -0
  57. package/dist/utils/mcp-registry.d.ts.map +1 -0
  58. package/dist/utils/mcp-registry.js +298 -0
  59. package/dist/utils/mcp-registry.js.map +1 -0
  60. package/dist/utils/progress-indicator.d.ts +14 -0
  61. package/dist/utils/progress-indicator.d.ts.map +1 -0
  62. package/dist/utils/progress-indicator.js +82 -0
  63. package/dist/utils/progress-indicator.js.map +1 -0
  64. package/dist/utils/settings-manager.d.ts +19 -0
  65. package/dist/utils/settings-manager.d.ts.map +1 -0
  66. package/dist/utils/settings-manager.js +78 -0
  67. package/dist/utils/settings-manager.js.map +1 -0
  68. package/dist/utils/token-calculator.d.ts +34 -0
  69. package/dist/utils/token-calculator.d.ts.map +1 -0
  70. package/dist/utils/token-calculator.js +167 -0
  71. package/dist/utils/token-calculator.js.map +1 -0
  72. package/dist/utils/validation.d.ts +4 -0
  73. package/dist/utils/validation.d.ts.map +1 -0
  74. package/dist/utils/validation.js +36 -0
  75. package/dist/utils/validation.js.map +1 -0
  76. package/dist/utils/wrangler-formatter.d.ts +37 -0
  77. package/dist/utils/wrangler-formatter.d.ts.map +1 -0
  78. package/dist/utils/wrangler-formatter.js +302 -0
  79. package/dist/utils/wrangler-formatter.js.map +1 -0
  80. package/dist/worker/runtime.d.ts +34 -0
  81. package/dist/worker/runtime.d.ts.map +1 -0
  82. package/dist/worker/runtime.js +166 -0
  83. package/dist/worker/runtime.js.map +1 -0
  84. package/package.json +81 -0
@@ -0,0 +1,1524 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
4
+ import { ExecuteCodeRequestSchema, LoadMCPRequestSchema, } from '../types/mcp.js';
5
+ import { ConfigManager } from '../utils/config-manager.js';
6
+ import { MCPConnectionError, MCPIsolateError } from '../utils/errors.js';
7
+ import logger from '../utils/logger.js';
8
+ import { cleanupSchemaCache, getCachedSchema } from '../utils/mcp-registry.js';
9
+ import { validateInput, validateTypeScriptCode } from '../utils/validation.js';
10
+ import { MetricsCollector } from './metrics-collector.js';
11
+ import { WorkerManager } from './worker-manager.js';
12
+ export class MCPHandler {
13
+ server;
14
+ workerManager;
15
+ metricsCollector;
16
+ configManager;
17
+ discoveredMCPTools = new Map();
18
+ discoveredMCPPrompts = new Map();
19
+ constructor() {
20
+ this.server = new Server({
21
+ name: 'mcpflare',
22
+ version: '0.1.0',
23
+ }, {
24
+ capabilities: {
25
+ tools: {},
26
+ prompts: {},
27
+ },
28
+ });
29
+ this.workerManager = new WorkerManager();
30
+ this.metricsCollector = new MetricsCollector();
31
+ this.configManager = new ConfigManager();
32
+ cleanupSchemaCache();
33
+ this.setupHandlers();
34
+ }
35
+ parseToolNamespace(namespacedName) {
36
+ const parts = namespacedName.split('::');
37
+ if (parts.length === 2) {
38
+ return { mcpName: parts[0], toolName: parts[1] };
39
+ }
40
+ return null;
41
+ }
42
+ async discoverConfiguredMCPs() {
43
+ const allMCPs = this.configManager.getAllConfiguredMCPs();
44
+ const mcpMap = new Map();
45
+ for (const [name, entry] of Object.entries(allMCPs)) {
46
+ mcpMap.set(name, entry);
47
+ }
48
+ return mcpMap;
49
+ }
50
+ async ensureMCPToolsLoaded(mcpName) {
51
+ if (this.discoveredMCPTools.has(mcpName)) {
52
+ return;
53
+ }
54
+ const configuredMCPs = await this.discoverConfiguredMCPs();
55
+ const entry = configuredMCPs.get(mcpName);
56
+ if (!entry) {
57
+ return;
58
+ }
59
+ try {
60
+ const resolvedConfig = this.configManager.resolveEnvVarsInObject(entry.config);
61
+ const tools = await this.workerManager.loadMCPSchemaOnly(mcpName, resolvedConfig);
62
+ if (tools.length > 0) {
63
+ this.discoveredMCPTools.set(mcpName, tools);
64
+ logger.debug({ mcpName, toolCount: tools.length }, 'Lazy-loaded MCP tools for transparent proxy');
65
+ }
66
+ }
67
+ catch (error) {
68
+ logger.warn({ error, mcpName }, 'Failed to lazy-load MCP tools for transparent proxy');
69
+ }
70
+ }
71
+ async ensureMCPPromptsLoaded(mcpName) {
72
+ if (this.discoveredMCPPrompts.has(mcpName)) {
73
+ return;
74
+ }
75
+ const configuredMCPs = await this.discoverConfiguredMCPs();
76
+ const entry = configuredMCPs.get(mcpName);
77
+ if (!entry) {
78
+ return;
79
+ }
80
+ try {
81
+ const resolvedConfig = this.configManager.resolveEnvVarsInObject(entry.config);
82
+ const prompts = await this.workerManager.loadMCPPromptsOnly(mcpName, resolvedConfig);
83
+ if (prompts.length > 0) {
84
+ this.discoveredMCPPrompts.set(mcpName, prompts);
85
+ logger.debug({ mcpName, promptCount: prompts.length }, 'Lazy-loaded MCP prompts for transparent proxy');
86
+ }
87
+ }
88
+ catch (error) {
89
+ logger.warn({ error, mcpName }, 'Failed to lazy-load MCP prompts for transparent proxy');
90
+ }
91
+ }
92
+ setupHandlers() {
93
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
94
+ logger.debug('Listing available tools');
95
+ const mcpflareTools = [
96
+ {
97
+ name: 'connect',
98
+ description: 'Manually connect to an MCP server and load it into a secure isolated Worker environment. Usually not needed - call_mcp will auto-connect when needed. Use this if you need to pre-connect to an MCP or connect with a custom configuration. Can use a saved configuration or load with a new configuration. Automatically saves the configuration unless auto_save is false.',
99
+ inputSchema: {
100
+ type: 'object',
101
+ properties: {
102
+ mcp_name: {
103
+ type: 'string',
104
+ description: 'Unique identifier for this MCP instance (alphanumeric, hyphens, underscores only)',
105
+ },
106
+ mcp_config: {
107
+ type: 'object',
108
+ description: 'MCP server connection configuration. Required if use_saved is false. Can use $' +
109
+ '{VAR_NAME} syntax for environment variables.',
110
+ properties: {
111
+ command: {
112
+ type: 'string',
113
+ description: 'Command to launch the MCP server (e.g., "npx", "node", "python")',
114
+ },
115
+ args: {
116
+ type: 'array',
117
+ items: { type: 'string' },
118
+ description: 'Arguments for the MCP server command',
119
+ },
120
+ env: {
121
+ type: 'object',
122
+ description: 'Environment variables for the MCP server. Use $' +
123
+ '{VAR_NAME} syntax to reference .env variables.',
124
+ },
125
+ },
126
+ required: ['command'],
127
+ },
128
+ use_saved: {
129
+ type: 'boolean',
130
+ description: 'If true, load configuration from saved configs instead of using mcp_config. Default: false',
131
+ default: false,
132
+ },
133
+ auto_save: {
134
+ type: 'boolean',
135
+ description: 'If true, automatically save the configuration after loading. Default: true',
136
+ default: true,
137
+ },
138
+ },
139
+ required: ['mcp_name'],
140
+ },
141
+ },
142
+ {
143
+ name: 'call_mcp',
144
+ description: `PRIMARY tool for interacting with MCPs. Auto-connects to MCPs from IDE config if needed. Use this instead of calling MCP tools directly. All MCP operations should go through this tool for secure isolation and efficiency.
145
+
146
+ The executed code receives two parameters:
147
+ 1. 'mcp' - An object containing all available MCP tools as async functions
148
+ 2. 'env' - Worker environment variables (contains MCP_ID and other bindings)
149
+
150
+ Usage pattern:
151
+ - Call MCP tools: await mcp.toolName({ param1: value1, param2: value2 })
152
+ - Output results: console.log(JSON.stringify(result, null, 2))
153
+ - Handle errors: try/catch blocks around MCP calls
154
+ - Chain operations: const result1 = await mcp.tool1({...}); const result2 = await mcp.tool2({...})
155
+
156
+ Example:
157
+ \`\`\`typescript
158
+ // Search for repositories (MCP auto-connects if not already connected)
159
+ const repos = await mcp.search_repositories({ query: 'typescript', page: 1 });
160
+ console.log('Found repositories:', JSON.stringify(repos, null, 2));
161
+
162
+ // Create an issue
163
+ const issue = await mcp.create_issue({
164
+ owner: 'owner',
165
+ repo: 'repo',
166
+ title: 'New Issue',
167
+ body: 'Issue description'
168
+ });
169
+ console.log('Created issue:', JSON.stringify(issue, null, 2));
170
+ \`\`\`
171
+
172
+ The 'mcp' object is automatically generated from the MCP's tool schema. Each tool is an async function that takes an input object matching the tool's inputSchema and returns a Promise with the tool's result.
173
+
174
+ Use console.log() to output results - all console output is captured and returned in the response.
175
+
176
+ IMPORTANT: Provide either mcp_name (to auto-connect from IDE config) or mcp_id (if already connected). Using mcp_name is recommended as it automatically connects to the MCP when needed.`,
177
+ inputSchema: {
178
+ type: 'object',
179
+ properties: {
180
+ mcp_id: {
181
+ type: 'string',
182
+ description: 'UUID of the connected MCP server (returned from connect). Use if MCP is already connected. Otherwise, use mcp_name to auto-connect.',
183
+ },
184
+ mcp_name: {
185
+ type: 'string',
186
+ description: 'Name of the MCP server from IDE config (e.g., "github", "filesystem"). Auto-connects to the MCP if not already connected. Use search_mcp_tools to discover available MCPs.',
187
+ },
188
+ code: {
189
+ type: 'string',
190
+ description: `TypeScript code to execute. The code receives 'mcp' and 'env' as parameters.
191
+
192
+ Example code structure:
193
+ \`\`\`typescript
194
+ // Access MCP tools via the 'mcp' parameter
195
+ const result = await mcp.toolName({ param: value });
196
+ console.log(JSON.stringify(result, null, 2));
197
+ \`\`\`
198
+
199
+ The code runs in an isolated Worker environment with no network access. All MCP communication happens through the 'mcp' binding object.`,
200
+ },
201
+ timeout_ms: {
202
+ type: 'number',
203
+ description: 'Execution timeout in milliseconds (default: 30000, max: 60000)',
204
+ default: 30000,
205
+ },
206
+ },
207
+ required: ['code'],
208
+ },
209
+ },
210
+ {
211
+ name: 'list_available_mcps',
212
+ description: 'List all MCP servers currently connected in Worker isolates. Returns a list with MCP IDs, names, status, and tool counts. Use get_mcp_by_name to find a specific MCP by name. Note: MCPs are auto-connected when call_mcp is called with mcp_name, so this shows actively connected MCPs.',
213
+ inputSchema: {
214
+ type: 'object',
215
+ properties: {},
216
+ },
217
+ },
218
+ {
219
+ name: 'get_mcp_by_name',
220
+ description: 'Find a connected MCP server by its name. Returns the MCP ID and basic information for quick lookup. This is more efficient than calling list_available_mcps and searching manually.',
221
+ inputSchema: {
222
+ type: 'object',
223
+ properties: {
224
+ mcp_name: {
225
+ type: 'string',
226
+ description: 'Name of the MCP server to find (the name used when loading the MCP)',
227
+ },
228
+ },
229
+ required: ['mcp_name'],
230
+ },
231
+ },
232
+ {
233
+ name: 'get_mcp_schema',
234
+ description: 'Get the TypeScript API definition for a connected MCP server',
235
+ inputSchema: {
236
+ type: 'object',
237
+ properties: {
238
+ mcp_id: {
239
+ type: 'string',
240
+ description: 'UUID of the loaded MCP server',
241
+ },
242
+ },
243
+ required: ['mcp_id'],
244
+ },
245
+ },
246
+ {
247
+ name: 'disconnect',
248
+ description: 'Disconnect from an MCP server and clean up its Worker isolate. Optionally remove from saved configurations.',
249
+ inputSchema: {
250
+ type: 'object',
251
+ properties: {
252
+ mcp_id: {
253
+ type: 'string',
254
+ description: 'UUID of the loaded MCP server to unload',
255
+ },
256
+ remove_from_saved: {
257
+ type: 'boolean',
258
+ description: 'If true, also remove the MCP configuration from the IDE config file (Claude Code or Cursor). Default: false',
259
+ default: false,
260
+ },
261
+ },
262
+ required: ['mcp_id'],
263
+ },
264
+ },
265
+ {
266
+ name: 'get_metrics',
267
+ description: 'Get performance metrics and statistics for MCP operations',
268
+ inputSchema: {
269
+ type: 'object',
270
+ properties: {},
271
+ },
272
+ },
273
+ {
274
+ name: 'import_configs',
275
+ description: 'Refresh/import MCP configurations from IDE configuration file (Claude Code, GitHub Copilot, or Cursor). Automatically discovers config location or uses provided path. Checks IDEs in priority order.',
276
+ inputSchema: {
277
+ type: 'object',
278
+ properties: {
279
+ cursor_config_path: {
280
+ type: 'string',
281
+ description: 'Optional: Path to IDE config file. If not provided, will search default locations for Claude Code, GitHub Copilot, and Cursor.',
282
+ },
283
+ },
284
+ },
285
+ },
286
+ {
287
+ name: 'search_mcp_tools',
288
+ description: 'Search and discover MCP servers configured in your IDE. Returns all configured MCPs (except mcpflare) with their status and available tools. Use this to find which MCPs are available before calling call_mcp. Implements the search_tools pattern for progressive disclosure - discover tools on-demand rather than loading all definitions upfront.',
289
+ inputSchema: {
290
+ type: 'object',
291
+ properties: {
292
+ query: {
293
+ type: 'string',
294
+ description: 'Optional search term to filter by MCP name or tool name. If not provided, returns all configured MCPs.',
295
+ },
296
+ detail_level: {
297
+ type: 'string',
298
+ enum: ['summary', 'tools', 'full'],
299
+ description: 'Level of detail to return. summary: just MCP names and status. tools: includes tool names. full: includes full tool schemas (only for loaded MCPs). Default: summary',
300
+ default: 'summary',
301
+ },
302
+ },
303
+ },
304
+ },
305
+ {
306
+ name: 'guard',
307
+ description: "Guard MCP servers by routing them through MCPflare's secure isolation. This prevents the IDE from loading their tools into the context window unnecessarily, maximizing efficiency and ensuring all tool calls are protected. Can guard specific MCPs or all MCPs except mcpflare.",
308
+ inputSchema: {
309
+ type: 'object',
310
+ properties: {
311
+ mcp_names: {
312
+ type: 'array',
313
+ items: { type: 'string' },
314
+ description: 'Array of MCP names to disable. If not provided or empty, disables all MCPs except mcpflare.',
315
+ },
316
+ },
317
+ },
318
+ },
319
+ ];
320
+ const aggregatedTools = [...mcpflareTools];
321
+ logger.debug({
322
+ mcpflareToolsCount: mcpflareTools.length,
323
+ totalToolsCount: aggregatedTools.length,
324
+ note: 'MCP tools are loaded lazily on-demand for efficiency',
325
+ }, 'Returning MCPflare tools (MCP tools loaded lazily)');
326
+ return {
327
+ tools: aggregatedTools,
328
+ };
329
+ });
330
+ this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
331
+ logger.debug('Listing available prompts');
332
+ const configuredMCPs = await this.discoverConfiguredMCPs();
333
+ const guardedMCPs = [];
334
+ for (const [mcpName, entry] of configuredMCPs.entries()) {
335
+ if (entry.status === 'disabled') {
336
+ guardedMCPs.push(mcpName);
337
+ }
338
+ }
339
+ const promptLoadPromises = guardedMCPs.map((mcpName) => this.ensureMCPPromptsLoaded(mcpName));
340
+ await Promise.all(promptLoadPromises);
341
+ const aggregatedPrompts = [];
342
+ for (const mcpName of guardedMCPs) {
343
+ const prompts = this.discoveredMCPPrompts.get(mcpName);
344
+ if (prompts && prompts.length > 0) {
345
+ for (const prompt of prompts) {
346
+ let namespacedName = prompt.name;
347
+ if (!namespacedName.includes(':') &&
348
+ !namespacedName.includes('/')) {
349
+ namespacedName = `${mcpName}:${prompt.name}`;
350
+ }
351
+ else if (namespacedName.includes('/')) {
352
+ const parts = namespacedName.split('/');
353
+ if (parts.length === 2 && parts[0] === mcpName) {
354
+ namespacedName = `${parts[0]}:${parts[1]}`;
355
+ }
356
+ else {
357
+ namespacedName = `${mcpName}:${prompt.name}`;
358
+ }
359
+ }
360
+ aggregatedPrompts.push({
361
+ name: namespacedName,
362
+ description: prompt.description,
363
+ arguments: prompt.arguments,
364
+ });
365
+ }
366
+ }
367
+ }
368
+ logger.debug({
369
+ guardedMCPsCount: guardedMCPs.length,
370
+ totalPromptsCount: aggregatedPrompts.length,
371
+ promptNames: aggregatedPrompts.map((p) => p.name),
372
+ }, 'Returning aggregated prompts from guarded MCPs');
373
+ return {
374
+ prompts: aggregatedPrompts,
375
+ };
376
+ });
377
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
378
+ const { name, arguments: args } = request.params;
379
+ logger.info({ tool: name, args }, 'Tool called');
380
+ try {
381
+ const namespace = this.parseToolNamespace(name);
382
+ if (namespace) {
383
+ await this.ensureMCPToolsLoaded(namespace.mcpName);
384
+ return await this.routeToolCall(namespace.mcpName, namespace.toolName, args);
385
+ }
386
+ switch (name) {
387
+ case 'connect':
388
+ return await this.handleLoadMCP(args);
389
+ case 'call_mcp':
390
+ return await this.handleExecuteCode(args);
391
+ case 'list_available_mcps':
392
+ return await this.handleListMCPs();
393
+ case 'get_mcp_by_name':
394
+ return await this.handleGetMCPByName(args);
395
+ case 'get_mcp_schema':
396
+ return await this.handleGetSchema(args);
397
+ case 'disconnect':
398
+ return await this.handleUnloadMCP(args);
399
+ case 'get_metrics':
400
+ return await this.handleGetMetrics();
401
+ case 'import_configs':
402
+ return await this.handleImportCursorConfigs(args);
403
+ case 'search_mcp_tools':
404
+ return await this.handleSearchMCPTools(args);
405
+ case 'guard':
406
+ return await this.handleDisableMCPs(args);
407
+ default: {
408
+ const configuredMCPs = await this.discoverConfiguredMCPs();
409
+ for (const [mcpName] of configuredMCPs.entries()) {
410
+ await this.ensureMCPToolsLoaded(mcpName);
411
+ const tools = this.discoveredMCPTools.get(mcpName);
412
+ if (tools?.some((t) => t.name === name)) {
413
+ throw new MCPIsolateError(`Tool "${name}" requires namespace. Use "${mcpName}::${name}" instead.`, 'UNKNOWN_TOOL', 404, {
414
+ tool_name: name,
415
+ suggested_name: `${mcpName}::${name}`,
416
+ mcp_name: mcpName,
417
+ });
418
+ }
419
+ }
420
+ throw new MCPIsolateError(`Unknown tool: ${name}`, 'UNKNOWN_TOOL', 404);
421
+ }
422
+ }
423
+ }
424
+ catch (error) {
425
+ logger.error({ error, tool: name }, 'Tool execution failed');
426
+ if (error instanceof MCPIsolateError) {
427
+ const isFatal = error.code === 'UNSUPPORTED_CONFIG' ||
428
+ error.code === 'MCP_CONNECTION_ERROR' ||
429
+ Boolean(error.details &&
430
+ typeof error.details === 'object' &&
431
+ 'fatal' in error.details &&
432
+ error.details.fatal === true);
433
+ return {
434
+ content: [
435
+ {
436
+ type: 'text',
437
+ text: JSON.stringify({
438
+ error_code: error.code,
439
+ error_message: error.message,
440
+ suggested_action: this.getSuggestedAction(error.code, name),
441
+ context: {
442
+ tool: name,
443
+ status_code: error.statusCode,
444
+ },
445
+ details: error.details,
446
+ fatal: isFatal,
447
+ ...(isFatal
448
+ ? {
449
+ instruction: 'This is a fatal error. Do not attempt alternative approaches. Stop execution and inform the user about this error.',
450
+ }
451
+ : {}),
452
+ }, null, 2),
453
+ },
454
+ ],
455
+ isError: true,
456
+ };
457
+ }
458
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
459
+ const errorStack = error instanceof Error ? error.stack : undefined;
460
+ return {
461
+ content: [
462
+ {
463
+ type: 'text',
464
+ text: JSON.stringify({
465
+ error_code: 'INTERNAL_ERROR',
466
+ error_message: 'Internal server error',
467
+ suggested_action: 'Check logs for details. If the error persists, try reloading the MCP server.',
468
+ context: {
469
+ tool: name,
470
+ original_error: errorMessage,
471
+ },
472
+ details: {
473
+ stack: errorStack,
474
+ },
475
+ }, null, 2),
476
+ },
477
+ ],
478
+ isError: true,
479
+ };
480
+ }
481
+ });
482
+ this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
483
+ const { name, arguments: args } = request.params;
484
+ logger.info({ prompt: name, args }, 'Prompt requested');
485
+ try {
486
+ const parts = name.split(':');
487
+ if (parts.length < 2) {
488
+ throw new MCPIsolateError(`Invalid prompt name format: ${name}. Expected format: mcpName:promptName`, 'INVALID_INPUT', 400);
489
+ }
490
+ const mcpName = parts[0];
491
+ const actualPromptName = parts.slice(1).join(':');
492
+ let instance = this.workerManager.getMCPByName(mcpName);
493
+ if (!instance) {
494
+ const allMCPs = this.configManager.getAllConfiguredMCPs();
495
+ const mcpEntry = allMCPs[mcpName];
496
+ if (!mcpEntry) {
497
+ throw new MCPIsolateError(`MCP "${mcpName}" not found in IDE configuration. Use search_mcp_tools to see available MCPs.`, 'NOT_FOUND', 404, {
498
+ mcp_name: mcpName,
499
+ suggestion: 'Use search_mcp_tools to discover configured MCPs, or use connect to connect to a new MCP.',
500
+ });
501
+ }
502
+ if (mcpEntry.status === 'active') {
503
+ throw new MCPIsolateError(`MCP "${mcpName}" is unguarded and should be called directly by the IDE, not through MCPflare. To use MCPflare, first guard this MCP in your IDE configuration.`, 'UNGUARDED_MCP', 400, {
504
+ mcp_name: mcpName,
505
+ status: 'active',
506
+ suggestion: 'This MCP is not guarded. Either call its prompts directly, or guard it first using the VS Code extension or by moving it to _mcpflare_disabled in your IDE config.',
507
+ });
508
+ }
509
+ const resolvedConfig = this.configManager.resolveEnvVarsInObject(mcpEntry.config);
510
+ logger.info({ mcp_name: mcpName }, 'Auto-connecting MCP for prompt request');
511
+ const startTime = Date.now();
512
+ try {
513
+ const loadedInstance = await this.workerManager.loadMCP(mcpName, resolvedConfig);
514
+ const loadTime = Date.now() - startTime;
515
+ this.metricsCollector.recordMCPLoad(loadedInstance.mcp_id, loadTime);
516
+ instance = loadedInstance;
517
+ logger.info({
518
+ mcp_name: mcpName,
519
+ mcp_id: instance.mcp_id,
520
+ load_time_ms: loadTime,
521
+ }, 'MCP auto-loaded for prompt request');
522
+ }
523
+ catch (error) {
524
+ if (error instanceof MCPConnectionError) {
525
+ throw new MCPConnectionError(error.message, {
526
+ mcp_name: mcpName,
527
+ original_error: error.details,
528
+ fatal: true,
529
+ });
530
+ }
531
+ throw error;
532
+ }
533
+ }
534
+ const client = this.workerManager.getMCPClient(instance.mcp_id);
535
+ if (!client) {
536
+ throw new MCPIsolateError(`MCP client not found for ID: ${instance.mcp_id}`, 'NOT_FOUND', 404);
537
+ }
538
+ logger.debug({ mcpName, promptName: actualPromptName, originalName: name, args }, 'Calling getPrompt on MCP client');
539
+ let promptResponse;
540
+ let lastError;
541
+ try {
542
+ promptResponse = await client.getPrompt({
543
+ name: actualPromptName,
544
+ arguments: args,
545
+ });
546
+ }
547
+ catch (error) {
548
+ lastError = error;
549
+ const nameWithSlash = `${mcpName}/${actualPromptName}`;
550
+ try {
551
+ logger.debug({ mcpName, attemptedName: nameWithSlash }, 'Stripped name failed, trying with slash namespace');
552
+ promptResponse = await client.getPrompt({
553
+ name: nameWithSlash,
554
+ arguments: args,
555
+ });
556
+ }
557
+ catch (error2) {
558
+ lastError = error2;
559
+ try {
560
+ logger.debug({ mcpName, attemptedName: name }, 'Slash namespace failed, trying with colon namespace');
561
+ promptResponse = await client.getPrompt({
562
+ name: name,
563
+ arguments: args,
564
+ });
565
+ }
566
+ catch (_error3) {
567
+ throw lastError;
568
+ }
569
+ }
570
+ }
571
+ logger.info({ mcpName, promptName: actualPromptName }, 'Prompt retrieved successfully');
572
+ if (promptResponse &&
573
+ typeof promptResponse === 'object' &&
574
+ ('description' in promptResponse || 'messages' in promptResponse)) {
575
+ return promptResponse;
576
+ }
577
+ return promptResponse;
578
+ }
579
+ catch (error) {
580
+ logger.error({ error, prompt: name }, 'Prompt request failed');
581
+ if (error instanceof MCPIsolateError) {
582
+ const isFatal = error.code === 'UNSUPPORTED_CONFIG' ||
583
+ error.code === 'MCP_CONNECTION_ERROR' ||
584
+ Boolean(error.details &&
585
+ typeof error.details === 'object' &&
586
+ 'fatal' in error.details &&
587
+ error.details.fatal === true);
588
+ return {
589
+ content: [
590
+ {
591
+ type: 'text',
592
+ text: JSON.stringify({
593
+ error_code: error.code,
594
+ error_message: error.message,
595
+ suggested_action: this.getSuggestedAction(error.code, name),
596
+ context: {
597
+ prompt: name,
598
+ status_code: error.statusCode,
599
+ },
600
+ details: error.details,
601
+ fatal: isFatal,
602
+ }, null, 2),
603
+ },
604
+ ],
605
+ isError: true,
606
+ };
607
+ }
608
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
609
+ const errorStack = error instanceof Error ? error.stack : undefined;
610
+ return {
611
+ content: [
612
+ {
613
+ type: 'text',
614
+ text: JSON.stringify({
615
+ error_code: 'INTERNAL_ERROR',
616
+ error_message: 'Internal server error',
617
+ suggested_action: 'Check logs for details. If the error persists, try reloading the MCP server.',
618
+ context: {
619
+ prompt: name,
620
+ original_error: errorMessage,
621
+ },
622
+ details: {
623
+ stack: errorStack,
624
+ },
625
+ }, null, 2),
626
+ },
627
+ ],
628
+ isError: true,
629
+ };
630
+ }
631
+ });
632
+ }
633
+ async handleLoadMCP(args) {
634
+ if (!args ||
635
+ typeof args !== 'object' ||
636
+ !('mcp_name' in args) ||
637
+ typeof args.mcp_name !== 'string') {
638
+ throw new MCPIsolateError('mcp_name is required and must be a string', 'INVALID_INPUT', 400);
639
+ }
640
+ const { mcp_name, mcp_config, use_saved = false, auto_save = true, } = args;
641
+ let configToUse;
642
+ if (use_saved) {
643
+ const savedConfig = this.configManager.getSavedConfig(mcp_name);
644
+ if (!savedConfig) {
645
+ throw new MCPIsolateError(`No saved configuration found for MCP: ${mcp_name}. Use search_mcp_tools to see available MCPs.`, 'NOT_FOUND', 404);
646
+ }
647
+ configToUse = savedConfig;
648
+ }
649
+ else {
650
+ if (!mcp_config) {
651
+ throw new MCPIsolateError('mcp_config is required when use_saved is false', 'INVALID_INPUT', 400);
652
+ }
653
+ const validated = validateInput(LoadMCPRequestSchema, {
654
+ mcp_name,
655
+ mcp_config,
656
+ });
657
+ configToUse = validated.mcp_config;
658
+ }
659
+ const resolvedConfig = this.configManager.resolveEnvVarsInObject(configToUse);
660
+ const startTime = Date.now();
661
+ const instance = await this.workerManager.loadMCP(mcp_name, resolvedConfig);
662
+ const loadTime = Date.now() - startTime;
663
+ this.metricsCollector.recordMCPLoad(instance.mcp_id, loadTime);
664
+ if (auto_save && !use_saved) {
665
+ try {
666
+ this.configManager.saveConfig(mcp_name, configToUse);
667
+ }
668
+ catch (error) {
669
+ logger.warn({ error, mcp_name }, 'Failed to auto-save MCP config');
670
+ }
671
+ }
672
+ const usageExample = this.generateUsageExample(instance.tools);
673
+ const exampleCode = this.generateExampleCode(instance.tools);
674
+ return {
675
+ content: [
676
+ {
677
+ type: 'text',
678
+ text: JSON.stringify({
679
+ success: true,
680
+ mcp_id: instance.mcp_id,
681
+ mcp_name: instance.mcp_name,
682
+ status: instance.status,
683
+ tools_count: instance.tools.length,
684
+ typescript_api: instance.typescript_api,
685
+ available_tools: instance.tools.map((t) => t.name),
686
+ load_time_ms: loadTime,
687
+ usage_example: usageExample,
688
+ example_code: exampleCode,
689
+ config_saved: auto_save && !use_saved,
690
+ }, null, 2),
691
+ },
692
+ ],
693
+ };
694
+ }
695
+ async handleExecuteCode(args) {
696
+ let validated;
697
+ try {
698
+ const result = validateInput(ExecuteCodeRequestSchema, args);
699
+ validated = {
700
+ ...result,
701
+ timeout_ms: result.timeout_ms ?? 30000,
702
+ };
703
+ }
704
+ catch (error) {
705
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
706
+ const errorDetails = error && typeof error === 'object' && 'errors' in error
707
+ ? error.errors || error
708
+ : error;
709
+ throw new MCPIsolateError(`Invalid input: ${errorMessage}`, 'VALIDATION_ERROR', 400, { validation_errors: errorDetails });
710
+ }
711
+ try {
712
+ validateTypeScriptCode(validated.code);
713
+ }
714
+ catch (error) {
715
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
716
+ throw new MCPIsolateError(`Code validation failed: ${errorMessage}`, 'SECURITY_ERROR', 403, { code_length: validated.code.length });
717
+ }
718
+ let mcpId;
719
+ let instance = this.workerManager.getInstance(validated.mcp_id || '');
720
+ if (validated.mcp_name) {
721
+ const existingInstance = this.workerManager.getMCPByName(validated.mcp_name);
722
+ if (existingInstance) {
723
+ mcpId = existingInstance.mcp_id;
724
+ instance = existingInstance;
725
+ }
726
+ else {
727
+ const allMCPs = this.configManager.getAllConfiguredMCPs();
728
+ const mcpEntry = allMCPs[validated.mcp_name];
729
+ if (!mcpEntry) {
730
+ throw new MCPIsolateError(`MCP "${validated.mcp_name}" not found in IDE configuration. Use search_mcp_tools to see available MCPs.`, 'NOT_FOUND', 404, {
731
+ mcp_name: validated.mcp_name,
732
+ suggestion: 'Use search_mcp_tools to discover configured MCPs, or use connect to connect to a new MCP.',
733
+ });
734
+ }
735
+ if (mcpEntry.status === 'active') {
736
+ throw new MCPIsolateError(`MCP "${validated.mcp_name}" is unguarded and should be called directly by the LLM, not through MCPflare. To use MCPflare isolation, first guard this MCP in your IDE configuration.`, 'UNGUARDED_MCP', 400, {
737
+ mcp_name: validated.mcp_name,
738
+ status: 'active',
739
+ suggestion: 'This MCP is not guarded. Either call its tools directly, or guard it first using the VS Code extension or by moving it to _mcpflare_disabled in your IDE config.',
740
+ });
741
+ }
742
+ const resolvedConfig = this.configManager.resolveEnvVarsInObject(mcpEntry.config);
743
+ logger.info({ mcp_name: validated.mcp_name }, 'Auto-connecting MCP for call_mcp');
744
+ const startTime = Date.now();
745
+ try {
746
+ const loadedInstance = await this.workerManager.loadMCP(validated.mcp_name, resolvedConfig);
747
+ const loadTime = Date.now() - startTime;
748
+ this.metricsCollector.recordMCPLoad(loadedInstance.mcp_id, loadTime);
749
+ mcpId = loadedInstance.mcp_id;
750
+ instance = loadedInstance;
751
+ logger.info({
752
+ mcp_name: validated.mcp_name,
753
+ mcp_id: mcpId,
754
+ load_time_ms: loadTime,
755
+ }, 'MCP auto-loaded successfully');
756
+ }
757
+ catch (error) {
758
+ if (error instanceof MCPConnectionError) {
759
+ throw new MCPConnectionError(error.message, {
760
+ mcp_name: validated.mcp_name,
761
+ original_error: error.details,
762
+ fatal: true,
763
+ });
764
+ }
765
+ throw error;
766
+ }
767
+ }
768
+ }
769
+ else if (validated.mcp_id) {
770
+ mcpId = validated.mcp_id;
771
+ instance = this.workerManager.getInstance(mcpId);
772
+ if (!instance) {
773
+ throw new MCPIsolateError(`MCP instance not found: ${mcpId}`, 'NOT_FOUND', 404, {
774
+ mcp_id: mcpId,
775
+ suggestion: 'Use list_available_mcps or get_mcp_by_name to find the correct MCP ID, or use mcp_name instead to auto-load.',
776
+ });
777
+ }
778
+ }
779
+ else {
780
+ throw new MCPIsolateError('Either mcp_id or mcp_name must be provided', 'INVALID_INPUT', 400);
781
+ }
782
+ const result = await this.workerManager.executeCode(mcpId, validated.code, validated.timeout_ms ?? 30000);
783
+ this.metricsCollector.recordExecution(mcpId, result.execution_time_ms, result.success, result.metrics?.mcp_calls_made || 0);
784
+ if (!result.success) {
785
+ const errorMessage = result.error || '';
786
+ const errorDetails = result.error_details;
787
+ const hasWranglerError = errorMessage.includes('Wrangler execution failed') ||
788
+ errorMessage.includes('Wrangler process') ||
789
+ errorMessage.includes('Wrangler dev server') ||
790
+ Boolean(errorDetails &&
791
+ typeof errorDetails === 'object' &&
792
+ ('wrangler_stderr' in errorDetails ||
793
+ 'wrangler_stdout' in errorDetails));
794
+ const isFatal = errorMessage.includes('MCP_CONNECTION_ERROR') ||
795
+ errorMessage.includes('URL-based MCP') ||
796
+ errorMessage.includes('cannot be loaded') ||
797
+ hasWranglerError ||
798
+ Boolean(errorDetails &&
799
+ typeof errorDetails === 'object' &&
800
+ 'fatal' in errorDetails &&
801
+ errorDetails.fatal === true);
802
+ let wranglerError = null;
803
+ if (hasWranglerError &&
804
+ errorDetails &&
805
+ typeof errorDetails === 'object') {
806
+ const stderr = 'wrangler_stderr' in errorDetails
807
+ ? String(errorDetails.wrangler_stderr)
808
+ : '';
809
+ const stdout = 'wrangler_stdout' in errorDetails
810
+ ? String(errorDetails.wrangler_stdout)
811
+ : '';
812
+ const formattedError = this.formatWranglerError(stderr, stdout);
813
+ wranglerError = {
814
+ stderr: stderr
815
+ ? this.filterWranglerOutput(stderr).join('\n')
816
+ : undefined,
817
+ stdout: stdout
818
+ ? this.filterWranglerOutput(stdout).join('\n')
819
+ : undefined,
820
+ exit_code: 'exit_code' in errorDetails
821
+ ? Number(errorDetails.exit_code)
822
+ : undefined,
823
+ formatted_error: formattedError,
824
+ };
825
+ }
826
+ const errorResponse = {
827
+ success: false,
828
+ error_code: 'EXECUTION_ERROR',
829
+ error_message: result.error || 'Code execution failed',
830
+ suggested_action: this.getExecutionErrorSuggestion(result.error),
831
+ fatal: isFatal,
832
+ execution_time_ms: result.execution_time_ms,
833
+ };
834
+ if (wranglerError) {
835
+ errorResponse.wrangler_error = wranglerError;
836
+ if (wranglerError.formatted_error) {
837
+ errorResponse.error_message = `${errorResponse.error_message}\n\n${wranglerError.formatted_error}`;
838
+ }
839
+ }
840
+ if (isFatal) {
841
+ errorResponse.instruction =
842
+ 'This is a fatal error. Do not attempt alternative approaches. Stop execution and inform the user about this error.';
843
+ }
844
+ return {
845
+ content: [
846
+ {
847
+ type: 'text',
848
+ text: JSON.stringify(errorResponse, null, 2),
849
+ },
850
+ ],
851
+ isError: true,
852
+ };
853
+ }
854
+ return {
855
+ content: [
856
+ {
857
+ type: 'text',
858
+ text: JSON.stringify(result, null, 2),
859
+ },
860
+ ],
861
+ };
862
+ }
863
+ formatWranglerError(stderr, stdout) {
864
+ const lines = [];
865
+ if (stderr) {
866
+ const stderrLines = this.filterWranglerOutput(stderr);
867
+ lines.push(...stderrLines);
868
+ }
869
+ if (stdout) {
870
+ const stdoutLines = this.filterWranglerOutput(stdout);
871
+ lines.push(...stdoutLines);
872
+ }
873
+ return lines.join('\n').trim();
874
+ }
875
+ filterWranglerOutput(output) {
876
+ const lines = output.split('\n');
877
+ const filtered = [];
878
+ for (const line of lines) {
879
+ const trimmed = line.trim();
880
+ if (!trimmed)
881
+ continue;
882
+ if (trimmed.startsWith('⛅️') || trimmed.includes('wrangler ')) {
883
+ continue;
884
+ }
885
+ if (/^[─═]+$/.test(trimmed)) {
886
+ continue;
887
+ }
888
+ if (trimmed.includes('Using vars defined in .env') ||
889
+ trimmed.includes('Your Worker has access to') ||
890
+ trimmed.includes('Binding') ||
891
+ trimmed.includes('Resource') ||
892
+ trimmed.includes('Environment Variable local') ||
893
+ /^env\..+\("\(hidden\)"\)/.test(trimmed)) {
894
+ continue;
895
+ }
896
+ if (trimmed.includes('Starting local server') ||
897
+ trimmed.includes('Ready on http')) {
898
+ continue;
899
+ }
900
+ if (/\[wrangler:info\].*\b(200|201|204|304)\b/.test(trimmed)) {
901
+ continue;
902
+ }
903
+ if (trimmed.includes('[ERROR]') ||
904
+ trimmed.includes('✗') ||
905
+ trimmed.includes('Build failed') ||
906
+ /\b(4\d\d|5\d\d)\b/.test(trimmed) ||
907
+ trimmed.includes('Error:') ||
908
+ trimmed.includes('at ')) {
909
+ filtered.push(line);
910
+ continue;
911
+ }
912
+ if (/\w+\.(ts|js|tsx|jsx):\d+:\d+/.test(trimmed)) {
913
+ filtered.push(line);
914
+ continue;
915
+ }
916
+ if (filtered.length < 20) {
917
+ filtered.push(line);
918
+ }
919
+ }
920
+ return filtered;
921
+ }
922
+ getExecutionErrorSuggestion(error) {
923
+ if (!error) {
924
+ return 'Review the code and try again. Check that all MCP tool calls are correct.';
925
+ }
926
+ const errorLower = error.toLowerCase();
927
+ if (errorLower.includes('wrangler')) {
928
+ if (errorLower.includes('missing entry-point') ||
929
+ errorLower.includes('entry-point')) {
930
+ return 'Wrangler configuration error: Missing entry point. This is a fatal error - MCPflare cannot execute code without a properly configured Worker runtime. Check that src/worker/runtime.ts exists and wrangler.toml is correctly configured.';
931
+ }
932
+ if (errorLower.includes('exited with code')) {
933
+ return 'Wrangler execution failed. This is a fatal error - MCPflare cannot execute code. Check the error_details for Wrangler stderr/stdout output to diagnose the issue. Common causes: missing dependencies, configuration errors, or Wrangler version incompatibility.';
934
+ }
935
+ return 'Wrangler execution error. This is a fatal error - MCPflare cannot execute code. Check the error_details for detailed Wrangler output. Ensure Wrangler is installed (npx wrangler --version) and the Worker runtime is properly configured.';
936
+ }
937
+ if (errorLower.includes('timeout')) {
938
+ return 'Execution timed out. Try increasing timeout_ms or optimizing the code to run faster.';
939
+ }
940
+ if (errorLower.includes('not defined') ||
941
+ errorLower.includes('undefined')) {
942
+ return 'A variable or function is not defined. Check that all MCP tool calls use the correct tool names from the available_tools list.';
943
+ }
944
+ if (errorLower.includes('rpc') || errorLower.includes('binding')) {
945
+ return 'MCP RPC binding error. The MCP server may not be ready or the RPC mechanism needs to be implemented.';
946
+ }
947
+ if (errorLower.includes('syntax')) {
948
+ return 'Syntax error in the code. Review the TypeScript syntax and ensure all brackets, parentheses, and quotes are properly closed.';
949
+ }
950
+ return 'Review the error message and code. Ensure MCP tools are called correctly with the right parameters. Use get_mcp_schema to see the tool definitions.';
951
+ }
952
+ async handleListMCPs() {
953
+ const instances = this.workerManager.listInstances();
954
+ const mcpNameToId = {};
955
+ instances.forEach((instance) => {
956
+ mcpNameToId[instance.mcp_name] = instance.mcp_id;
957
+ });
958
+ return {
959
+ content: [
960
+ {
961
+ type: 'text',
962
+ text: JSON.stringify({
963
+ mcps: instances.map((instance) => ({
964
+ mcp_id: instance.mcp_id,
965
+ mcp_name: instance.mcp_name,
966
+ status: instance.status,
967
+ uptime_ms: instance.uptime_ms,
968
+ tools_count: instance.tools.length,
969
+ created_at: instance.created_at.toISOString(),
970
+ })),
971
+ total_count: instances.length,
972
+ mcp_name_to_id: mcpNameToId,
973
+ }, null, 2),
974
+ },
975
+ ],
976
+ };
977
+ }
978
+ async handleGetSchema(args) {
979
+ if (!args ||
980
+ typeof args !== 'object' ||
981
+ !('mcp_id' in args) ||
982
+ typeof args.mcp_id !== 'string') {
983
+ throw new MCPIsolateError('mcp_id is required and must be a string', 'INVALID_INPUT', 400);
984
+ }
985
+ const { mcp_id } = args;
986
+ if (!mcp_id) {
987
+ throw new MCPIsolateError('mcp_id is required and must be a string', 'INVALID_INPUT', 400);
988
+ }
989
+ const instance = this.workerManager.getInstance(mcp_id);
990
+ if (!instance) {
991
+ throw new MCPIsolateError(`MCP instance not found: ${mcp_id}`, 'NOT_FOUND', 404);
992
+ }
993
+ const commonPatterns = this.generateCommonPatterns(instance.tools);
994
+ return {
995
+ content: [
996
+ {
997
+ type: 'text',
998
+ text: JSON.stringify({
999
+ mcp_id: instance.mcp_id,
1000
+ mcp_name: instance.mcp_name,
1001
+ typescript_api: instance.typescript_api,
1002
+ tools: instance.tools,
1003
+ common_patterns: commonPatterns,
1004
+ }, null, 2),
1005
+ },
1006
+ ],
1007
+ };
1008
+ }
1009
+ async handleGetMCPByName(args) {
1010
+ if (!args ||
1011
+ typeof args !== 'object' ||
1012
+ !('mcp_name' in args) ||
1013
+ typeof args.mcp_name !== 'string') {
1014
+ throw new MCPIsolateError('mcp_name is required and must be a string', 'INVALID_INPUT', 400);
1015
+ }
1016
+ const { mcp_name } = args;
1017
+ if (!mcp_name) {
1018
+ throw new MCPIsolateError('mcp_name is required and must be a string', 'INVALID_INPUT', 400);
1019
+ }
1020
+ const instance = this.workerManager.getMCPByName(mcp_name);
1021
+ if (!instance) {
1022
+ throw new MCPIsolateError(`MCP not found with name: ${mcp_name}`, 'NOT_FOUND', 404);
1023
+ }
1024
+ return {
1025
+ content: [
1026
+ {
1027
+ type: 'text',
1028
+ text: JSON.stringify({
1029
+ mcp_id: instance.mcp_id,
1030
+ mcp_name: instance.mcp_name,
1031
+ status: instance.status,
1032
+ tools_count: instance.tools.length,
1033
+ available_tools: instance.tools.map((t) => t.name),
1034
+ uptime_ms: instance.uptime_ms,
1035
+ created_at: instance.created_at.toISOString(),
1036
+ }, null, 2),
1037
+ },
1038
+ ],
1039
+ };
1040
+ }
1041
+ async handleUnloadMCP(args) {
1042
+ if (!args ||
1043
+ typeof args !== 'object' ||
1044
+ !('mcp_id' in args) ||
1045
+ typeof args.mcp_id !== 'string') {
1046
+ throw new MCPIsolateError('mcp_id is required and must be a string', 'INVALID_INPUT', 400);
1047
+ }
1048
+ const { mcp_id, remove_from_saved = false } = args;
1049
+ if (!mcp_id) {
1050
+ throw new MCPIsolateError('mcp_id is required and must be a string', 'INVALID_INPUT', 400);
1051
+ }
1052
+ const instance = this.workerManager.getInstance(mcp_id);
1053
+ const mcpName = instance?.mcp_name;
1054
+ await this.workerManager.unloadMCP(mcp_id);
1055
+ let configRemoved = false;
1056
+ if (remove_from_saved && mcpName) {
1057
+ try {
1058
+ configRemoved = this.configManager.deleteConfig(mcpName);
1059
+ }
1060
+ catch (error) {
1061
+ logger.warn({ error, mcpName }, 'Failed to remove config from IDE config file');
1062
+ }
1063
+ }
1064
+ return {
1065
+ content: [
1066
+ {
1067
+ type: 'text',
1068
+ text: JSON.stringify({
1069
+ success: true,
1070
+ message: `MCP server ${mcp_id} unloaded successfully`,
1071
+ config_removed: configRemoved,
1072
+ }, null, 2),
1073
+ },
1074
+ ],
1075
+ };
1076
+ }
1077
+ async handleGetMetrics() {
1078
+ const metrics = this.metricsCollector.getMetrics();
1079
+ return {
1080
+ content: [
1081
+ {
1082
+ type: 'text',
1083
+ text: JSON.stringify(metrics, null, 2),
1084
+ },
1085
+ ],
1086
+ };
1087
+ }
1088
+ getSuggestedAction(errorCode, toolName) {
1089
+ const suggestions = {
1090
+ NOT_FOUND: `MCP not found. Use list_available_mcps to see loaded MCPs, or use get_mcp_by_name to find by name.`,
1091
+ INVALID_INPUT: `Invalid input provided. Check the tool's inputSchema for required parameters and their types.`,
1092
+ VALIDATION_ERROR: `Input validation failed. Review the error details and ensure all required fields are provided with correct types.`,
1093
+ WORKER_ERROR: `Worker execution error. The MCP may not be ready. Check the MCP status with list_available_mcps.`,
1094
+ MCP_CONNECTION_ERROR: `Failed to connect to MCP server. This is a fatal error - do not attempt alternative approaches. Verify the MCP configuration (command, args, env for command-based, or url, headers for URL-based) and ensure the MCP server is accessible.`,
1095
+ UNSUPPORTED_CONFIG: `This MCP configuration is not supported. Check the configuration format and ensure it matches the expected schema.`,
1096
+ SECURITY_ERROR: `Code validation failed. The code contains prohibited patterns. Review the code and remove any dangerous operations.`,
1097
+ UNKNOWN_TOOL: `Unknown tool: ${toolName}. Check available tools with list_available_mcps.`,
1098
+ };
1099
+ return (suggestions[errorCode] ||
1100
+ 'Review the error details and try again. If the issue persists, check the logs.');
1101
+ }
1102
+ generateUsageExample(tools) {
1103
+ if (tools.length === 0) {
1104
+ return 'No tools available.';
1105
+ }
1106
+ const firstTool = tools[0];
1107
+ const toolName = firstTool.name;
1108
+ const params = firstTool.inputSchema.properties || {};
1109
+ const paramKeys = Object.keys(params).slice(0, 2);
1110
+ const exampleParams = {};
1111
+ paramKeys.forEach((key) => {
1112
+ const schema = params[key];
1113
+ if (schema?.type === 'string') {
1114
+ exampleParams[key] = 'example_value';
1115
+ }
1116
+ else if (schema?.type === 'number') {
1117
+ exampleParams[key] = 1;
1118
+ }
1119
+ else if (schema?.type === 'boolean') {
1120
+ exampleParams[key] = true;
1121
+ }
1122
+ });
1123
+ return `To use this MCP, call call_mcp with code like:
1124
+
1125
+ \`\`\`typescript
1126
+ const result = await mcp.${toolName}(${JSON.stringify(exampleParams, null, 2)});
1127
+ console.log(JSON.stringify(result, null, 2));
1128
+ \`\`\`
1129
+
1130
+ Available tools: ${tools.map((t) => t.name).join(', ')}`;
1131
+ }
1132
+ generateExampleCode(tools) {
1133
+ if (tools.length === 0) {
1134
+ return '// No tools available';
1135
+ }
1136
+ const firstTool = tools[0];
1137
+ const toolName = firstTool.name;
1138
+ const params = firstTool.inputSchema.properties || {};
1139
+ const required = firstTool.inputSchema.required || [];
1140
+ const paramKeys = required.length > 0
1141
+ ? required.slice(0, 2)
1142
+ : Object.keys(params).slice(0, 2);
1143
+ const exampleParams = {};
1144
+ paramKeys.forEach((key) => {
1145
+ const schema = params[key];
1146
+ if (schema?.type === 'string') {
1147
+ exampleParams[key] = schema.description?.toLowerCase().includes('query')
1148
+ ? 'typescript'
1149
+ : 'example';
1150
+ }
1151
+ else if (schema?.type === 'number') {
1152
+ exampleParams[key] = 1;
1153
+ }
1154
+ else if (schema?.type === 'boolean') {
1155
+ exampleParams[key] = true;
1156
+ }
1157
+ });
1158
+ return `// Example: Call ${toolName} tool
1159
+ const result = await mcp.${toolName}(${JSON.stringify(exampleParams, null, 2)});
1160
+ console.log(JSON.stringify(result, null, 2));`;
1161
+ }
1162
+ generateCommonPatterns(tools) {
1163
+ const patterns = [];
1164
+ if (tools.length > 0) {
1165
+ patterns.push(`// Call a tool: const result = await mcp.${tools[0].name}({ ... });`);
1166
+ patterns.push(`// Output results: console.log(JSON.stringify(result, null, 2));`);
1167
+ }
1168
+ if (tools.length > 1) {
1169
+ patterns.push(`// Chain multiple calls: const r1 = await mcp.${tools[0].name}({...}); const r2 = await mcp.${tools[1].name}({...});`);
1170
+ }
1171
+ patterns.push(`// Error handling: try { const result = await mcp.toolName({...}); } catch (error) { console.error('Error:', error.message); }`);
1172
+ return patterns;
1173
+ }
1174
+ async handleDisableMCPs(args) {
1175
+ const typedArgs = args && typeof args === 'object' ? args : {};
1176
+ const { mcp_names } = typedArgs;
1177
+ const configPath = this.configManager.getCursorConfigPath();
1178
+ if (!configPath) {
1179
+ throw new MCPIsolateError('No IDE MCP configuration file found. Please add MCPflare to your IDE config first.', 'CONFIG_NOT_FOUND', 404);
1180
+ }
1181
+ const sourceName = this.configManager.getConfigSourceDisplayName();
1182
+ const result = {
1183
+ disabled: [],
1184
+ alreadyDisabled: [],
1185
+ failed: [],
1186
+ mcpflareRestored: false,
1187
+ };
1188
+ if (mcp_names && mcp_names.length > 0) {
1189
+ for (const mcpName of mcp_names) {
1190
+ if (mcpName.toLowerCase() === 'mcpflare') {
1191
+ continue;
1192
+ }
1193
+ if (this.configManager.isMCPDisabled(mcpName)) {
1194
+ result.alreadyDisabled.push(mcpName);
1195
+ }
1196
+ else if (this.configManager.disableMCP(mcpName)) {
1197
+ result.disabled.push(mcpName);
1198
+ }
1199
+ else {
1200
+ result.failed.push(mcpName);
1201
+ }
1202
+ }
1203
+ }
1204
+ else {
1205
+ const disableResult = this.configManager.disableAllExceptMCPflare();
1206
+ result.disabled = disableResult.disabled;
1207
+ result.alreadyDisabled = disableResult.alreadyDisabled;
1208
+ result.failed = disableResult.failed;
1209
+ result.mcpflareRestored = disableResult.mcpflareRestored;
1210
+ }
1211
+ const response = {
1212
+ success: true,
1213
+ message: `MCPs disabled in ${sourceName} configuration`,
1214
+ source: sourceName,
1215
+ disabled: result.disabled,
1216
+ alreadyDisabled: result.alreadyDisabled,
1217
+ failed: result.failed,
1218
+ mcpflareRestored: result.mcpflareRestored,
1219
+ };
1220
+ if (result.disabled.length === 0 && result.alreadyDisabled.length === 0) {
1221
+ response.message = `All specified MCPs are already disabled in ${sourceName} config`;
1222
+ }
1223
+ if (result.failed.length > 0) {
1224
+ response.note = `Some MCPs could not be disabled. They may not exist in the configuration.`;
1225
+ }
1226
+ if (result.mcpflareRestored) {
1227
+ response.note = `${response.note ? `${response.note} ` : ''}MCPflare was restored to active config.`;
1228
+ }
1229
+ logger.info({
1230
+ disabled: result.disabled,
1231
+ alreadyDisabled: result.alreadyDisabled,
1232
+ failed: result.failed,
1233
+ source: sourceName,
1234
+ }, 'MCPs guarded via guard tool');
1235
+ return {
1236
+ content: [
1237
+ {
1238
+ type: 'text',
1239
+ text: JSON.stringify(response, null, 2),
1240
+ },
1241
+ ],
1242
+ };
1243
+ }
1244
+ async handleImportCursorConfigs(args) {
1245
+ const typedArgs = args && typeof args === 'object' && 'cursor_config_path' in args
1246
+ ? args
1247
+ : {};
1248
+ const { cursor_config_path } = typedArgs;
1249
+ const result = this.configManager.importConfigs(cursor_config_path);
1250
+ const configPath = this.configManager.getCursorConfigPath();
1251
+ const configSource = this.configManager.getConfigSource();
1252
+ return {
1253
+ content: [
1254
+ {
1255
+ type: 'text',
1256
+ text: JSON.stringify({
1257
+ success: result.errors.length === 0,
1258
+ imported_count: result.imported,
1259
+ errors: result.errors,
1260
+ config_path: configPath,
1261
+ config_source: configSource,
1262
+ }, null, 2),
1263
+ },
1264
+ ],
1265
+ };
1266
+ }
1267
+ async routeToolCall(mcpName, toolName, args) {
1268
+ logger.info({ mcpName, toolName }, 'Routing tool call through transparent proxy');
1269
+ let instance = this.workerManager.getMCPByName(mcpName);
1270
+ if (!instance) {
1271
+ const savedConfig = this.configManager.getSavedConfig(mcpName);
1272
+ if (!savedConfig) {
1273
+ throw new MCPIsolateError(`MCP "${mcpName}" not found in IDE configuration. Use search_mcp_tools to see available MCPs.`, 'NOT_FOUND', 404, {
1274
+ mcp_name: mcpName,
1275
+ suggestion: 'Use search_mcp_tools to discover configured MCPs, or use connect to connect to a new MCP.',
1276
+ });
1277
+ }
1278
+ const resolvedConfig = this.configManager.resolveEnvVarsInObject(savedConfig);
1279
+ logger.info({ mcp_name: mcpName }, 'Auto-loading MCP for transparent proxy tool call');
1280
+ const startTime = Date.now();
1281
+ try {
1282
+ instance = await this.workerManager.loadMCP(mcpName, resolvedConfig);
1283
+ const loadTime = Date.now() - startTime;
1284
+ this.metricsCollector.recordMCPLoad(instance.mcp_id, loadTime);
1285
+ logger.info({
1286
+ mcp_name: mcpName,
1287
+ mcp_id: instance.mcp_id,
1288
+ load_time_ms: loadTime,
1289
+ }, 'MCP auto-loaded for transparent proxy');
1290
+ }
1291
+ catch (error) {
1292
+ if (error instanceof MCPConnectionError) {
1293
+ throw new MCPConnectionError(error.message, {
1294
+ mcp_name: mcpName,
1295
+ original_error: error.details,
1296
+ fatal: true,
1297
+ });
1298
+ }
1299
+ throw error;
1300
+ }
1301
+ }
1302
+ const tool = instance.tools.find((t) => t.name === toolName);
1303
+ if (!tool) {
1304
+ throw new MCPIsolateError(`Tool "${toolName}" not found in MCP "${mcpName}". Available tools: ${instance.tools.map((t) => t.name).join(', ')}`, 'NOT_FOUND', 404, {
1305
+ mcp_name: mcpName,
1306
+ tool_name: toolName,
1307
+ available_tools: instance.tools.map((t) => t.name),
1308
+ });
1309
+ }
1310
+ const argsJson = JSON.stringify(args || {});
1311
+ const code = `const result = await mcp.${toolName}(${argsJson});
1312
+ console.log(JSON.stringify(result, null, 2));
1313
+ return result;`;
1314
+ logger.debug({ mcpName, toolName, mcp_id: instance.mcp_id }, 'Executing tool call in isolation');
1315
+ const result = await this.workerManager.executeCode(instance.mcp_id, code, 30000);
1316
+ this.metricsCollector.recordExecution(instance.mcp_id, result.execution_time_ms, result.success, result.metrics?.mcp_calls_made || 0);
1317
+ if (!result.success) {
1318
+ const errorMessage = result.error || '';
1319
+ const errorDetails = result.error_details;
1320
+ const hasWranglerError = errorMessage.includes('Wrangler execution failed') ||
1321
+ errorMessage.includes('Wrangler process') ||
1322
+ errorMessage.includes('Wrangler dev server') ||
1323
+ Boolean(errorDetails &&
1324
+ typeof errorDetails === 'object' &&
1325
+ ('wrangler_stderr' in errorDetails ||
1326
+ 'wrangler_stdout' in errorDetails));
1327
+ const isFatal = errorMessage.includes('MCP_CONNECTION_ERROR') ||
1328
+ errorMessage.includes('URL-based MCP') ||
1329
+ errorMessage.includes('cannot be loaded') ||
1330
+ hasWranglerError ||
1331
+ Boolean(errorDetails &&
1332
+ typeof errorDetails === 'object' &&
1333
+ 'fatal' in errorDetails &&
1334
+ errorDetails.fatal === true);
1335
+ return {
1336
+ content: [
1337
+ {
1338
+ type: 'text',
1339
+ text: JSON.stringify({
1340
+ error_code: 'EXECUTION_ERROR',
1341
+ error_message: result.error || 'Tool execution failed',
1342
+ suggested_action: this.getExecutionErrorSuggestion(result.error),
1343
+ context: {
1344
+ mcp_name: mcpName,
1345
+ tool_name: toolName,
1346
+ mcp_id: instance.mcp_id,
1347
+ },
1348
+ execution_time_ms: result.execution_time_ms,
1349
+ metrics: result.metrics,
1350
+ output: result.output,
1351
+ error_details: result.error_details,
1352
+ fatal: isFatal,
1353
+ ...(isFatal
1354
+ ? {
1355
+ instruction: 'This is a fatal error. Do not attempt alternative approaches. Stop execution and inform the user about this error.',
1356
+ }
1357
+ : {}),
1358
+ }, null, 2),
1359
+ },
1360
+ ],
1361
+ isError: true,
1362
+ };
1363
+ }
1364
+ let parsedResult = result.output;
1365
+ try {
1366
+ const outputLines = result.output?.split('\n') || [];
1367
+ for (const line of outputLines) {
1368
+ if (line.trim().startsWith('{') || line.trim().startsWith('[')) {
1369
+ try {
1370
+ parsedResult = JSON.parse(line.trim());
1371
+ break;
1372
+ }
1373
+ catch {
1374
+ }
1375
+ }
1376
+ }
1377
+ }
1378
+ catch {
1379
+ }
1380
+ return {
1381
+ content: [
1382
+ {
1383
+ type: 'text',
1384
+ text: JSON.stringify(parsedResult, null, 2),
1385
+ },
1386
+ ],
1387
+ };
1388
+ }
1389
+ async handleSearchMCPTools(args) {
1390
+ const typedArgs = args && typeof args === 'object'
1391
+ ? args
1392
+ : {};
1393
+ const { query, detail_level = 'summary' } = typedArgs;
1394
+ const guardedConfigs = this.configManager.getGuardedMCPConfigs();
1395
+ const loadedInstances = this.workerManager.listInstances();
1396
+ const disabledMCPs = this.configManager.getDisabledMCPs();
1397
+ const configSource = this.configManager.getConfigSource();
1398
+ const configPath = this.configManager.getCursorConfigPath();
1399
+ const results = [];
1400
+ for (const [mcpName, entry] of Object.entries(guardedConfigs)) {
1401
+ if (query) {
1402
+ const queryLower = query.toLowerCase();
1403
+ const nameMatches = mcpName.toLowerCase().includes(queryLower);
1404
+ const loadedInstance = loadedInstances.find((inst) => inst.mcp_name === mcpName);
1405
+ const toolMatches = loadedInstance?.tools.some((tool) => tool.name.toLowerCase().includes(queryLower)) || false;
1406
+ if (!nameMatches && !toolMatches) {
1407
+ continue;
1408
+ }
1409
+ }
1410
+ const loadedInstance = loadedInstances.find((inst) => inst.mcp_name === mcpName);
1411
+ const isGuarded = disabledMCPs.includes(mcpName);
1412
+ let cachedTools;
1413
+ let cachedToolNames;
1414
+ if (!loadedInstance && isGuarded) {
1415
+ const config = entry.config;
1416
+ const configHash = this.workerManager.hashConfig(mcpName, config);
1417
+ const persistentCached = getCachedSchema(mcpName, configHash);
1418
+ if (persistentCached) {
1419
+ cachedTools = persistentCached.tools;
1420
+ cachedToolNames = persistentCached.toolNames;
1421
+ logger.debug({ mcpName, toolCount: persistentCached.toolCount }, 'Using persistent cache for tool discovery');
1422
+ }
1423
+ }
1424
+ const result = {
1425
+ mcp_name: mcpName,
1426
+ is_guarded: isGuarded,
1427
+ status: isGuarded
1428
+ ? loadedInstance
1429
+ ? 'loaded'
1430
+ : 'not_loaded'
1431
+ : 'unguarded',
1432
+ config_source: entry.source,
1433
+ };
1434
+ if (loadedInstance) {
1435
+ result.mcp_id = loadedInstance.mcp_id;
1436
+ result.tools_count = loadedInstance.tools.length;
1437
+ if (detail_level === 'tools' || detail_level === 'full') {
1438
+ result.tool_names = loadedInstance.tools.map((t) => t.name);
1439
+ }
1440
+ if (detail_level === 'full') {
1441
+ result.tools = loadedInstance.tools;
1442
+ }
1443
+ }
1444
+ else {
1445
+ result.tools_count = cachedTools?.length || 0;
1446
+ if (cachedToolNames &&
1447
+ (detail_level === 'tools' || detail_level === 'full')) {
1448
+ result.tool_names = cachedToolNames;
1449
+ }
1450
+ if (detail_level === 'full' && cachedTools) {
1451
+ result.tools = cachedTools;
1452
+ }
1453
+ }
1454
+ let nextAction;
1455
+ if (!result.is_guarded) {
1456
+ nextAction = `This MCP is not guarded by MCPflare. Your IDE loads it directly - use its tools directly (not via call_mcp).`;
1457
+ }
1458
+ else if (result.status === 'loaded') {
1459
+ const toolHint = result.tool_names && result.tool_names.length > 0
1460
+ ? ` Available tools: ${result.tool_names.join(', ')}`
1461
+ : '';
1462
+ nextAction = `This MCP is guarded. Call call_mcp with mcp_name='${mcpName}' to execute tools.${toolHint}`;
1463
+ }
1464
+ else {
1465
+ nextAction = `This MCP is guarded. Call call_mcp with mcp_name='${mcpName}' to auto-connect and execute tools.`;
1466
+ }
1467
+ result.next_action = nextAction;
1468
+ results.push(result);
1469
+ }
1470
+ return {
1471
+ content: [
1472
+ {
1473
+ type: 'text',
1474
+ text: JSON.stringify({
1475
+ instructions: 'INSTRUCTIONS FOR USING MCPs:\n' +
1476
+ "1. Locate the MCP you need in the 'mcps' array below\n" +
1477
+ "2. Check the 'is_guarded' field:\n" +
1478
+ ' - If is_guarded=true: This MCP is protected by MCPflare. Use call_mcp with mcp_name to access it securely\n' +
1479
+ ' - If is_guarded=false: This MCP is loaded directly by your IDE. Use its tools directly (not via call_mcp)\n' +
1480
+ "3. For guarded MCPs, check 'status' field:\n" +
1481
+ " - If 'loaded': Call call_mcp immediately to execute tools\n" +
1482
+ " - If 'not_loaded': Call call_mcp to auto-connect and execute tools\n" +
1483
+ "4. Use the 'next_action' field in each MCP result for specific guidance",
1484
+ mcps: results,
1485
+ total_count: results.length,
1486
+ loaded_count: results.filter((r) => r.status === 'loaded').length,
1487
+ not_loaded_count: results.filter((r) => r.status === 'not_loaded')
1488
+ .length,
1489
+ disabled_count: results.filter((r) => r.status === 'unguarded')
1490
+ .length,
1491
+ config_path: configPath,
1492
+ config_source: configSource,
1493
+ note: `These MCPs are configured from your ${configSource} IDE config. Guarded MCPs (is_guarded=true) are protected by MCPflare.`,
1494
+ }, null, 2),
1495
+ },
1496
+ ],
1497
+ };
1498
+ }
1499
+ async start() {
1500
+ const transport = new StdioServerTransport();
1501
+ await this.server.connect(transport);
1502
+ logger.info('MCPflare server started');
1503
+ const shutdown = async () => {
1504
+ logger.info('Shutting down gracefully...');
1505
+ try {
1506
+ await Promise.race([
1507
+ this.server.close(),
1508
+ new Promise((resolve) => setTimeout(resolve, 2000)),
1509
+ ]);
1510
+ await this.workerManager.shutdown();
1511
+ logger.info('Shutdown complete');
1512
+ }
1513
+ catch (error) {
1514
+ logger.error({ error }, 'Error during shutdown');
1515
+ }
1516
+ finally {
1517
+ process.exit(0);
1518
+ }
1519
+ };
1520
+ process.on('SIGINT', shutdown);
1521
+ process.on('SIGTERM', shutdown);
1522
+ }
1523
+ }
1524
+ //# sourceMappingURL=mcp-handler.js.map