Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 59 additions & 135 deletions src-api/src/extensions/mcp/sandbox-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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',
Expand All @@ -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' },
Expand Down Expand Up @@ -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' },
Expand Down Expand Up @@ -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() {
Expand All @@ -304,4 +228,4 @@ async function main() {
main().catch((error) => {
console.error('[Sandbox MCP] Fatal error:', error);
process.exit(1);
});
});