diff --git a/src-api/src/extensions/mcp/sandbox-server.ts b/src-api/src/extensions/mcp/sandbox-server.ts index 7fd7682..19a3045 100644 --- a/src-api/src/extensions/mcp/sandbox-server.ts +++ b/src-api/src/extensions/mcp/sandbox-server.ts @@ -6,12 +6,9 @@ * This allows the agent to run scripts in isolated containers * without needing to use curl commands. */ -import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { - CallToolRequestSchema, - ListToolsRequestSchema, -} from '@modelcontextprotocol/sdk/types.js'; +import { z } from 'zod'; import { DEFAULT_API_HOST, DEFAULT_API_PORT } from '@/config/constants'; @@ -23,7 +20,7 @@ const API_PORT = const SANDBOX_API_URL = process.env.SANDBOX_API_URL || `http://${DEFAULT_API_HOST}:${API_PORT}`; -const server = new Server( +const server = new McpServer( { name: 'sandbox', version: '1.0.0', @@ -35,103 +32,22 @@ const server = new Server( } ); -// List available tools -server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools: [ - { - name: 'run_script', - description: - 'Run a script file in an isolated sandbox container. Automatically detects the runtime (Python, Node.js, Bun) based on file extension. The script file must already exist on disk.', - inputSchema: { - type: 'object' as const, - properties: { - filePath: { - type: 'string', - description: 'Absolute path to the script file to execute', - }, - workDir: { - type: 'string', - description: - 'Working directory containing the script (use the directory where the script file is located)', - }, - args: { - type: 'array', - items: { type: 'string' }, - description: - 'Optional command line arguments to pass to the script', - }, - packages: { - type: 'array', - items: { type: 'string' }, - description: - 'Optional packages to install before running (npm packages for Node.js/Bun)', - }, - timeout: { - type: 'number', - description: 'Execution timeout in milliseconds (default: 120000)', - }, - }, - required: ['filePath', 'workDir'], - }, - }, - { - name: 'run_command', - description: - 'Execute a shell command in an isolated sandbox container. Use this for running commands that need specific dependencies or isolation.', - inputSchema: { - type: 'object' as const, - properties: { - command: { - type: 'string', - description: - "The command to execute (e.g., 'python', 'node', 'npm')", - }, - args: { - type: 'array', - items: { type: 'string' }, - description: 'Arguments for the command', - }, - workDir: { - type: 'string', - description: - 'Working directory for command execution (use absolute paths)', - }, - image: { - type: 'string', - description: - 'Container image to use (default: auto-detected, options: node:18-alpine, python:3.11-slim, oven/bun:latest)', - }, - timeout: { - type: 'number', - description: 'Execution timeout in milliseconds (default: 120000)', - }, - }, - required: ['command', 'workDir'], - }, - }, - ], -})); - -// Handle tool calls -server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params; - - try { - if (name === 'run_script') { - const { - filePath, - workDir, - args: scriptArgs, - packages, - timeout, - } = args as { - filePath: string; - workDir: string; - args?: string[]; - packages?: string[]; - timeout?: number; - }; - +// Register run_script tool +server.registerTool( + 'run_script', + { + description: + 'Run a script file in an isolated sandbox container. Automatically detects the runtime (Python, Node.js, Bun) based on file extension. The script file must already exist on disk.', + inputSchema: z.object({ + filePath: z.string().describe('Absolute path to the script file to execute'), + workDir: z.string().describe('Working directory containing the script (use the directory where the script file is located)'), + args: z.array(z.string()).optional().describe('Optional command line arguments to pass to the script'), + packages: z.array(z.string()).optional().describe('Optional packages to install before running (npm packages for Node.js/Bun)'), + timeout: z.number().optional().describe('Execution timeout in milliseconds (default: 120000)'), + }), + }, + async ({ filePath, workDir, args: scriptArgs, packages, timeout }) => { + try { const response = await fetch(`${SANDBOX_API_URL}/sandbox/run/file`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -198,23 +114,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { content: [{ type: 'text', text: output }], isError: !result.success, }; - } - - if (name === 'run_command') { - const { - command, - args: cmdArgs, - workDir, - image, - timeout, - } = args as { - command: string; - args?: string[]; - workDir: string; - image?: string; - timeout?: number; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error executing run_script: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, }; + } + } +); +// Register run_command tool +server.registerTool( + 'run_command', + { + description: + 'Execute a shell command in an isolated sandbox container. Use this for running commands that need specific dependencies or isolation.', + inputSchema: z.object({ + command: z.string().describe("The command to execute (e.g., 'python', 'node', 'npm')"), + args: z.array(z.string()).optional().describe('Arguments for the command'), + workDir: z.string().describe('Working directory for command execution (use absolute paths)'), + image: z.string().optional().describe('Container image to use (default: auto-detected, options: node:18-alpine, python:3.11-slim, oven/bun:latest)'), + timeout: z.number().optional().describe('Execution timeout in milliseconds (default: 120000)'), + }), + }, + async ({ command, args: cmdArgs, workDir, image, timeout }) => { + try { const response = await fetch(`${SANDBOX_API_URL}/sandbox/exec`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -275,24 +204,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { content: [{ type: 'text', text: output }], isError: !result.success, }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error executing run_command: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; } - - return { - content: [{ type: 'text', text: `Unknown tool: ${name}` }], - isError: true, - }; - } catch (error) { - return { - content: [ - { - type: 'text', - text: `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}`, - }, - ], - isError: true, - }; } -}); +); // Start the server async function main() { @@ -304,4 +228,4 @@ async function main() { main().catch((error) => { console.error('[Sandbox MCP] Fatal error:', error); process.exit(1); -}); +}); \ No newline at end of file