Skip to main content
Glama
SandboxManagerImpl.ts9.75 kB
import * as fs from "fs"; import * as path from "path"; import { fileURLToPath } from "url"; import { Tool } from "@modelcontextprotocol/sdk/types.js"; import { ServerInfo } from "../../domain/types.js"; import { ISandboxManager } from "../../interfaces/ISandboxManager.js"; import { NodeVM } from "vm2"; import { convertOutputToSchema } from "../../utils.js"; import { ServersToolDatabaseImpl } from "../database/serversToolDatabaseImpl.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * SandboxManagerImpl manages code execution in an isolated sandbox * Implements dependency injection pattern for better testability */ export class SandboxManagerImpl implements ISandboxManager { private sandboxPath: string; private vm: NodeVM; private serversInfo: Map<string, ServerInfo>; private toolOutputCache: Map<string, Array<{toolName: string, output: any}>>; constructor(sandboxPath?: string) { // Calculate project root: go up from build/infrastructure/sandbox to project root const projectRoot = path.resolve(__dirname, '..', '..', '..'); // Use provided path or default to .sandbox in project root if (sandboxPath) { this.sandboxPath = path.isAbsolute(sandboxPath) ? sandboxPath : path.resolve(projectRoot, sandboxPath); } else { this.sandboxPath = path.resolve(projectRoot, '.sandbox'); } this.serversInfo = new Map(); this.toolOutputCache = new Map(); // Create NodeVM with mock module to provide injectedObject this.vm = new NodeVM({ console: 'inherit', sandbox: {}, require: { external: true, // allow external modules (local files) root: this.sandboxPath, // set root directory for requires to sandbox context: 'sandbox', // Load modules in sandbox context to make mocks work mock: { 'serversInfo': this.serversInfo, // Mock module that provides the data (read-only) 'toolOutputCache': this.toolOutputCache, // Mock module for cache (mutable) }, }, }); this.createSandboxFolder(); } /** * Initialize the sandbox with server information * @param servers - Map of server information */ initialize(servers: Map<string, ServerInfo>): void { // Clear existing entries and populate with new data this.clearAndSetServiceInfoCache(servers); this.clearAndSetToolOutCache(); // Freeze the serversInfo for the VM (read-only) this.vm.freeze(this.serversInfo, 'serversInfo'); // Note: toolOutputCache is NOT frozen, so it remains mutable // Create sandbox structure this.createSandboxStructure(servers); } /** * Execute code in the sandbox * @param code - JavaScript code to execute * @returns Promise resolving to the execution result * @throws Error if execution fails */ async execute(code: string): Promise<any> { try { // Use a virtual filename within the sandbox path for proper require resolution const filename = path.join(this.sandboxPath, 'exec.js'); const result = this.vm.run(code, filename); // If the result is a Promise, wait for it to resolve let finalResult; if (result && typeof result.then === 'function') { finalResult = await result; } else { finalResult = result; } // Save tool output schemas to database using singleton instance await this.updateOutputSchema(); // update db with output cache this.clearAndSetToolOutCache(); return finalResult; } catch (error) { console.error('✗ Error executing code in sandbox:', error); throw error; } } private async updateOutputSchema() { const toolDatabase = ServersToolDatabaseImpl.getInstance(); for (const [serverName, outputs] of this.toolOutputCache) { for (const { toolName, output } of outputs) { try { // Convert output to schema const outputSchema = convertOutputToSchema(output); if (outputSchema) { // Check if tool exists in database const existingTool = await toolDatabase.getTool(serverName, toolName); if (existingTool) { } else { throw new Error(`Tool ${toolName} not found in database for server ${serverName}`); } if (!existingTool.originalOutputSchema) { // Update existing tool's output schema await toolDatabase.updateTool( serverName, toolName, JSON.stringify(outputSchema), false // originalOutputSchema = false since we generated it from output ); // Update the tool's output schema in ServerInfo const serverInfo = this.serversInfo.get(serverName); if (serverInfo) { const tool = serverInfo.tools.find(t => t.name === toolName); if (tool) { tool.outputSchema = outputSchema; console.error(`✓ Updated output schema for ${serverName}/${toolName} in ServerInfo`); } } } } } catch (error) { console.error(`✗ Failed to save output schema for ${serverName}/${toolName}:`, error); } } } } /** * Create empty sandbox folder */ private createSandboxFolder(): void { try { if (fs.existsSync(this.sandboxPath)) { fs.rmSync(this.sandboxPath, { recursive: true, force: true }); } fs.mkdirSync(this.sandboxPath, { recursive: true }); console.error(`✓ Created empty sandbox folder at: ${this.sandboxPath}`); } catch (error) { console.error(`✗ Failed to create sandbox folder:`, error); throw error; } } /** * Create folder structure for all servers and their tools */ private createSandboxStructure(serversInfo: Map<string, ServerInfo>): void { for (const [serverName, serverInfo] of serversInfo) { try { const serverFolderPath = path.join(this.sandboxPath, serverName); fs.mkdirSync(serverFolderPath, { recursive: true }); // Get tools from server info const tools = serverInfo.tools; // Create a file for each tool for (const tool of tools) { const toolFilePath = path.join(serverFolderPath, `${tool.title}.cjs`); const toolFileContent = this.generateToolFile(serverName, tool); fs.writeFileSync(toolFilePath, toolFileContent, 'utf-8'); } console.error(`✓ Created folder structure for ${serverName} with ${tools.length} tools`); } catch (error) { console.error(`✗ Failed to create folder structure for ${serverName}:`, error); } } } /** * Generate tool file content for a given server and tool */ private generateToolFile(serverName: string, tool: Tool): string { const toolFileContent = ` const serversInfo = require('serversInfo'); const toolOutputCache = require('toolOutputCache'); async function ${tool.title}(args) { const serverInfo = serversInfo.get("${serverName}"); if (!serverInfo) { throw new Error(\`Server ${serverName} not exist\`); } if(!serverInfo.client) { throw new Error(\`Client for server ${serverName} not connected\`); } const client = serverInfo.client; const response = await client.callTool({ name: "${tool.name}", arguments: args, }); // Save full response to cache for schema generation const serverCache = toolOutputCache.get("${serverName}"); if (serverCache) { serverCache.push({ toolName: "${tool.name}", output: response }); } // Return standardized response structure // This provides predictable output that AI can work with consistently return { content: response.content || [], isError: response.isError || false, _meta: { serverName: "${serverName}", toolName: "${tool.name}" } }; } module.exports = ${tool.title}; `; return toolFileContent; } /** * Get all cached outputs for a specific server * @param serverName - Name of the server * @returns Array of tool outputs or undefined if server not found */ getServerOutputs(serverName: string): Array<{toolName: string, output: any}> | undefined { return this.toolOutputCache.get(serverName); } /** * Get the entire cache * @returns The complete tool output cache */ getAllOutputs(): Map<string, Array<{toolName: string, output: any}>> { return this.toolOutputCache; } /** * Clear the cache for a specific server * @param serverName - Name of the server */ clearServerCache(serverName: string): void { const serverCache = this.toolOutputCache.get(serverName); if (serverCache) { serverCache.length = 0; } } /** * Clear the entire cache */ clearAndSetToolOutCache(): void { this.toolOutputCache.clear(); for (const [serverName, serverInfo] of this.serversInfo) { this.toolOutputCache.set(serverName, []); } } /** * Clear and set service info cache */ clearAndSetServiceInfoCache(serversInfo: Map<string, ServerInfo>): void { this.serversInfo.clear(); for (const [serverName, serverInfo] of serversInfo) { this.serversInfo.set(serverName, serverInfo); } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/eliavamar/mcp-of-mcps'

If you have feedback or need assistance with the MCP directory API, please join our Discord server