diff --git a/deno.lock b/deno.lock index b6fa96b..0d2c115 100644 --- a/deno.lock +++ b/deno.lock @@ -21,6 +21,7 @@ "npm:@ai-sdk/provider-utils@^2.2.8": "2.2.8_zod@3.25.76", "npm:@ai-sdk/provider@2": "2.0.0", "npm:@hono/zod-openapi@~0.19.2": "0.19.10_hono@4.9.10_zod@3.25.76", + "npm:@mcpc-tech/handle-sandbox@^0.0.7": "0.0.7", "npm:@mcpc-tech/ripgrep-napi@^0.0.4": "0.0.4", "npm:@modelcontextprotocol/sdk@^1.8.0": "1.20.2_express@5.1.0_zod@3.25.76", "npm:@opentelemetry/api@^1.9.0": "1.9.0", @@ -35,7 +36,6 @@ "npm:ajv-formats@^3.0.1": "3.0.1_ajv@8.17.1", "npm:ajv@^8.17.1": "8.17.1", "npm:cheerio@1": "1.1.2", - "npm:deno@^2.5.6": "2.5.6", "npm:hono@^4.7.5": "4.9.10", "npm:json-schema-faker@~0.5.9": "0.5.9", "npm:json-schema-to-zod@^2.6.1": "2.6.1", @@ -255,6 +255,12 @@ "jsep" ] }, + "@mcpc-tech/handle-sandbox@0.0.7": { + "integrity": "sha512-KXTdUEJvNwo+1vgbj0dArVO1TdLA+SCBvbf9k615/uTAekKnEqusIw6zNDvEBij6v/q7uskReumGSiEnD8jJ/g==", + "dependencies": [ + "deno" + ] + }, "@mcpc-tech/ripgrep-napi-android-arm-eabi@0.0.4": { "integrity": "sha512-Y52S9IdbqMYvet7hDGk/DRdKFM7ED6tSQmBtrWVzdeqVWPMayZvrKdlcceBykdKjbmgnlqtulzEtHBF2iXmXwA==", "os": ["android"], @@ -1431,10 +1437,10 @@ }, "packages/plugin-code-execution": { "dependencies": [ - "jsr:@mcpc/core@~0.3.2", + "jsr:@mcpc/core@~0.3.3", "jsr:@std/assert@1", - "npm:@modelcontextprotocol/sdk@^1.8.0", - "npm:deno@^2.5.6" + "npm:@mcpc-tech/handle-sandbox@^0.0.7", + "npm:@modelcontextprotocol/sdk@^1.8.0" ] }, "packages/utils": { diff --git a/packages/core/deno.json b/packages/core/deno.json index ead7961..ed9f9c1 100644 --- a/packages/core/deno.json +++ b/packages/core/deno.json @@ -1,6 +1,6 @@ { "name": "@mcpc/core", - "version": "0.3.2", + "version": "0.3.3", "repository": { "type": "git", "url": "git+https://github.com/mcpc-tech/mcpc.git" diff --git a/packages/core/src/compose.ts b/packages/core/src/compose.ts index 9263e90..50c4e70 100644 --- a/packages/core/src/compose.ts +++ b/packages/core/src/compose.ts @@ -357,6 +357,14 @@ export class ComposableMCPServer extends Server { await this.pluginManager.dispose(); } + /** + * Close the server and ensure all plugins are disposed + */ + override async close(): Promise { + await this.disposePlugins(); + await super.close(); + } + async compose( name: string | null, description: string, diff --git a/packages/plugin-code-execution/deno.json b/packages/plugin-code-execution/deno.json index 1dc81d6..9e3177a 100644 --- a/packages/plugin-code-execution/deno.json +++ b/packages/plugin-code-execution/deno.json @@ -1,6 +1,6 @@ { "name": "@mcpc/plugin-code-execution", - "version": "0.0.2", + "version": "0.0.3", "repository": { "type": "git", "url": "git+https://github.com/mcpc-tech/mcpc.git" @@ -13,8 +13,8 @@ "imports": { "@std/assert": "jsr:@std/assert@^1.0.0", "@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.8.0", - "@mcpc/core": "jsr:@mcpc/core@^0.3.2", - "deno": "npm:deno@^2.5.6" + "@mcpc/core": "jsr:@mcpc/core@^0.3.3", + "@mcpc-tech/handle-sandbox": "npm:@mcpc-tech/handle-sandbox@^0.0.7" }, "publish": { "exclude": ["examples/**", "tests/**", "logs/**"] diff --git a/packages/plugin-code-execution/mod.ts b/packages/plugin-code-execution/mod.ts index 5cf3647..a8add06 100644 --- a/packages/plugin-code-execution/mod.ts +++ b/packages/plugin-code-execution/mod.ts @@ -17,5 +17,3 @@ export { SandboxExecutor, type ToolCallHandler, } from "./src/sandbox-executor.ts"; -export { JsonRpcHandler } from "./src/json-rpc.ts"; -export * from "./src/types.ts"; diff --git a/packages/plugin-code-execution/sandbox/runtime.ts b/packages/plugin-code-execution/sandbox/runtime.ts deleted file mode 100644 index 9da4877..0000000 --- a/packages/plugin-code-execution/sandbox/runtime.ts +++ /dev/null @@ -1,167 +0,0 @@ -/** - * Deno Sandbox Runtime - * - * This script runs inside the Deno sandbox and executes user code. - * It communicates with the host process via JSON-RPC over stdin/stdout. - */ - -import { - JsonRpcMethod, - type JsonRpcRequest, - type JsonRpcResponse, -} from "../src/types.ts"; - -// Accumulated logs from console -const logs: string[] = []; - -/** - * Send JSON-RPC request to host and wait for response - */ -async function sendRequest(method: string, params?: unknown): Promise { - const encoder = new TextEncoder(); - const decoder = new TextDecoder(); - const requestId = crypto.randomUUID(); - - // Send request - const request = JSON.stringify({ - jsonrpc: "2.0", - id: requestId, - method, - params, - }) + "\n"; - await Deno.stdout.write(encoder.encode(request)); - - // Read response line by line - const buffer = new Uint8Array(65536); - while (true) { - const n = await Deno.stdin.read(buffer); - if (n === null) throw new Error("Stdin closed"); - - const data = decoder.decode(buffer.subarray(0, n)); - const lines = data.split("\n").filter((line) => line.trim()); - - for (const line of lines) { - try { - const response: JsonRpcResponse = JSON.parse(line); - if (response.id !== requestId) continue; - - if (response.error) { - throw new Error(`JSON-RPC Error: ${response.error.message}`); - } - return response.result; - } catch (e) { - if (!(e instanceof SyntaxError)) throw e; - // Incomplete JSON, keep reading - } - } - } -} - -/** - * Call MCP tool from user code - */ -const callMCPTool = async ( - toolName: string, - params: unknown, -): Promise => { - return await sendRequest(JsonRpcMethod.CALL_TOOL, { toolName, params }); -}; - -/** - * Format value for logging - */ -function formatValue(value: unknown): string { - return typeof value === "object" && value !== null - ? JSON.stringify(value, null, 2) - : String(value); -} - -/** - * Execute user code in sandbox - */ -async function executeCode( - code: string, -): Promise<{ logs: string[]; result?: unknown; error?: string }> { - logs.length = 0; // Clear logs - - // Console that captures output - const console = { - log: (...args: unknown[]) => logs.push(args.map(formatValue).join(" ")), - error: (...args: unknown[]) => - logs.push("ERROR: " + args.map(formatValue).join(" ")), - warn: (...args: unknown[]) => - logs.push("WARN: " + args.map(formatValue).join(" ")), - info: (...args: unknown[]) => - logs.push("INFO: " + args.map(formatValue).join(" ")), - }; - - try { - const fn = new Function( - "console", - "callMCPTool", - `return (async () => { ${code} })();`, - ); - const result = await fn(console, callMCPTool); - return { logs: [...logs], result }; - } catch (error) { - return { - logs: [...logs], - error: error instanceof Error ? error.message : String(error), - }; - } -} - -/** - * Main message loop - processes JSON-RPC requests from host - */ -async function main() { - const encoder = new TextEncoder(); - const decoder = new TextDecoder(); - const buffer = new Uint8Array(65536); - - while (true) { - const n = await Deno.stdin.read(buffer); - if (n === null) break; // Stdin closed - - const data = decoder.decode(buffer.subarray(0, n)); - const lines = data.split("\n").filter((line) => line.trim()); - - for (const line of lines) { - try { - const request: JsonRpcRequest = JSON.parse(line); - let result; - - if (request.method === JsonRpcMethod.EXECUTE_CODE) { - const params = request.params as { - code: string; - hasDefinitions: string[]; - }; - result = await executeCode(params.code); - } else if (request.method === JsonRpcMethod.GET_TOOL_DEFINITIONS) { - const params = request.params as { toolNames: string[] }; - result = { received: params.toolNames }; - } else { - continue; // Unknown method, skip - } - - const response = JSON.stringify({ - jsonrpc: "2.0", - id: request.id, - result, - }) + "\n"; - - await Deno.stdout.write(encoder.encode(response)); - } catch (error) { - if (!(error instanceof SyntaxError)) { - console.error("Error processing request:", error); - } - } - } - } -} - -// Start the main loop -main().catch((error) => { - console.error("Fatal error in sandbox:", error); - Deno.exit(1); -}); diff --git a/packages/plugin-code-execution/src/json-rpc.ts b/packages/plugin-code-execution/src/json-rpc.ts deleted file mode 100644 index 6d348c1..0000000 --- a/packages/plugin-code-execution/src/json-rpc.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * JSON-RPC Message Handler - * Handles encoding/decoding and validation of JSON-RPC messages - */ - -import type { JsonRpcError, JsonRpcRequest, JsonRpcResponse } from "./types.ts"; - -export class JsonRpcHandler { - private requestId = 0; - - createRequest(method: string, params?: unknown): JsonRpcRequest { - return { jsonrpc: "2.0", id: ++this.requestId, method, params }; - } - - createResponse( - id: string | number, - result?: unknown, - error?: JsonRpcError, - ): JsonRpcResponse { - return { jsonrpc: "2.0", id, result, error }; - } - - createErrorResponse( - id: string | number, - code: number, - message: string, - data?: unknown, - ): JsonRpcResponse { - return this.createResponse(id, undefined, { code, message, data }); - } - - /** - * Parse JSON-RPC message from string - */ - parseMessage(data: string): JsonRpcRequest | JsonRpcResponse | null { - try { - const parsed = JSON.parse(data); - if (!parsed || typeof parsed !== "object" || parsed.jsonrpc !== "2.0") { - return null; - } - - if ("method" in parsed) return parsed as JsonRpcRequest; - if ("result" in parsed || "error" in parsed) { - return parsed as JsonRpcResponse; - } - - return null; - } catch { - return null; - } - } - - /** - * Serialize message to string - */ - serializeMessage(message: JsonRpcRequest | JsonRpcResponse): string { - return JSON.stringify(message); - } - - /** - * Validate JSON-RPC request - */ - validateRequest(request: JsonRpcRequest): JsonRpcError | null { - if (request.jsonrpc !== "2.0") { - return { - code: -32600, - message: "Invalid Request: jsonrpc must be '2.0'", - }; - } - - if (!request.method || typeof request.method !== "string") { - return { - code: -32600, - message: "Invalid Request: method must be a string", - }; - } - - if (request.id !== undefined) { - const idType = typeof request.id; - if (idType !== "string" && idType !== "number") { - return { - code: -32600, - message: "Invalid Request: id must be a string or number", - }; - } - } - - return null; - } -} diff --git a/packages/plugin-code-execution/src/plugin.ts b/packages/plugin-code-execution/src/plugin.ts index 0c669ca..c8ebed6 100644 --- a/packages/plugin-code-execution/src/plugin.ts +++ b/packages/plugin-code-execution/src/plugin.ts @@ -77,7 +77,7 @@ export function createCodeExecutionPlugin( const contentParts: CallToolResult["content"] = []; // Execute code - if (code && hasDefinitions.length > 0) { + if (code) { if (!executor) throw new Error("Sandbox not initialized"); const result = await executor.executeCode(code, hasDefinitions); diff --git a/packages/plugin-code-execution/src/sandbox-executor.ts b/packages/plugin-code-execution/src/sandbox-executor.ts index f7fd555..8756658 100644 --- a/packages/plugin-code-execution/src/sandbox-executor.ts +++ b/packages/plugin-code-execution/src/sandbox-executor.ts @@ -1,222 +1,42 @@ /** - * Sandbox Executor using Deno + * Sandbox Executor using @mcpc-tech/handle-sandbox * - * This executor spawns a Deno subprocess and communicates with it via JSON-RPC - * over stdin/stdout to execute user code securely. + * Wrapper around the handle-sandbox package for executing user code securely. */ -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { type ChildProcess, spawn } from "node:child_process"; -import type { Buffer } from "node:buffer"; +import { Sandbox, type SandboxConfig } from "@mcpc-tech/handle-sandbox"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { JsonRpcHandler } from "./json-rpc.ts"; -import { - type CallToolRequest, - JsonRpcErrorCode, - JsonRpcMethod, - type JsonRpcRequest, - type JsonRpcResponse, -} from "./types.ts"; - -export interface SandboxConfig { - timeout?: number; // Execution timeout in milliseconds - memoryLimit?: number; // Memory limit in MB - permissions?: string[]; // Deno permission flags, e.g., ["--allow-net", "--allow-read"] -} export interface ToolCallHandler { (toolName: string, params: unknown): Promise; } -export class SandboxExecutor { - private process: ChildProcess | null = null; - private jsonRpc = new JsonRpcHandler(); - private pendingRequests = new Map< - string | number, - { resolve: (value: unknown) => void; reject: (error: Error) => void } - >(); - private buffer = ""; +export type { SandboxConfig }; - constructor( - private config: SandboxConfig = {}, - private toolCallHandler?: ToolCallHandler, - ) {} - - /** - * Get Deno binary path from npm package - */ - private getDenoBinaryPath(): string { - const resolver = ( - import.meta as { resolve?: (specifier: string) => string } - ).resolve; - if (!resolver) throw new Error("Cannot resolve deno package"); +export class SandboxExecutor { + private sandbox: Sandbox; + private toolCallHandler?: ToolCallHandler; - const pkgUrl = resolver("deno/package.json"); - const denoDir = path.dirname(fileURLToPath(pkgUrl)); - return path.join(denoDir, "bin.cjs"); + constructor(config: SandboxConfig = {}, toolCallHandler?: ToolCallHandler) { + this.sandbox = new Sandbox(config); + this.toolCallHandler = toolCallHandler; } /** * Start Deno sandbox process */ start(): void { - if (this.process) throw new Error("Sandbox already started"); - - const runtimePath = path.join( - path.dirname(fileURLToPath(import.meta.url)), - "../sandbox/runtime.ts", - ); - - const args = ["run", "--no-prompt"]; - - if (this.config.memoryLimit) { - args.push(`--v8-flags=--max-old-space-size=${this.config.memoryLimit}`); - } - - if (this.config.permissions) { - args.push(...this.config.permissions); - } - - args.push(runtimePath); - - this.process = spawn(this.getDenoBinaryPath(), args, { - stdio: ["pipe", "pipe", "pipe"], - }); - - this.process.stdout?.on("data", (data: Buffer) => this.handleStdout(data)); - this.process.stderr?.on( - "data", - (data: Buffer) => console.error("Sandbox stderr:", data.toString()), - ); - this.process.on("error", (error: Error) => { - console.error("Sandbox error:", error); - this.cleanup(); - }); - this.process.on("exit", (code: number | null) => { - console.log("Sandbox exited:", code); - this.cleanup(); - }); - } - - /** - * Handle stdout data from sandbox - */ - private handleStdout(data: Buffer): void { - this.buffer += data.toString(); - const lines = this.buffer.split("\n"); - this.buffer = lines.pop() || ""; - - for (const line of lines) { - if (!line.trim()) continue; - - const message = this.jsonRpc.parseMessage(line); - if (!message) { - console.error("Failed to parse message:", line); - continue; - } - - // Dispatch to response or request handler - if ("result" in message || "error" in message) { - this.handleResponse(message as JsonRpcResponse); - } else if ("method" in message) { - this.handleRequest(message as JsonRpcRequest).catch((err) => - console.error("Error handling request:", err) - ); - } - } - } - - /** - * Handle JSON-RPC response from sandbox - */ - private handleResponse(response: JsonRpcResponse): void { - const pending = this.pendingRequests.get(response.id); - if (!pending) { - console.error("Unknown request ID:", response.id); - return; - } - - this.pendingRequests.delete(response.id); - response.error - ? pending.reject(new Error(response.error.message)) - : pending.resolve(response.result); - } - - /** - * Handle JSON-RPC request from sandbox (tool calls) - */ - private async handleRequest(request: JsonRpcRequest): Promise { - try { - if (request.method === JsonRpcMethod.CALL_TOOL) { - if (!this.toolCallHandler) throw new Error("No tool call handler"); - - const params = request.params as CallToolRequest; - const result = await this.toolCallHandler( - params.toolName, - params.params, - ); - this.sendMessage(this.jsonRpc.createResponse(request.id, result)); - } else { - this.sendMessage( - this.jsonRpc.createErrorResponse( - request.id, - JsonRpcErrorCode.METHOD_NOT_FOUND, - `Method not found: ${request.method}`, - ), - ); - } - } catch (error) { - this.sendMessage( - this.jsonRpc.createErrorResponse( - request.id, - JsonRpcErrorCode.INTERNAL_ERROR, - error instanceof Error ? error.message : String(error), - ), + // Register the tool call handler + if (this.toolCallHandler) { + this.sandbox.registerHandler( + "callMCPTool", + async (...args: unknown[]) => { + const [toolName, params] = args as [string, unknown]; + return await this.toolCallHandler!(toolName, params); + }, ); } - } - - /** - * Send message to sandbox - */ - private sendMessage(message: JsonRpcRequest | JsonRpcResponse): void { - if (!this.process?.stdin) throw new Error("Sandbox not started"); - this.process.stdin.write(this.jsonRpc.serializeMessage(message) + "\n"); - } - - /** - * Send request and wait for response - */ - private sendRequest( - method: string, - params?: unknown, - timeout?: number, - ): Promise { - if (!this.process) throw new Error("Sandbox not started"); - - const request = this.jsonRpc.createRequest(method, params); - const timeoutMs = timeout || this.config.timeout || 30000; - - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - this.pendingRequests.delete(request.id); - reject(new Error(`Request timeout after ${timeoutMs}ms`)); - }, timeoutMs); - - this.pendingRequests.set(request.id, { - resolve: (value) => { - clearTimeout(timer); - resolve(value); - }, - reject: (error) => { - clearTimeout(timer); - reject(error); - }, - }); - - this.sendMessage(request); - }); + this.sandbox.start(); } /** @@ -227,14 +47,7 @@ export class SandboxExecutor { hasDefinitions: string[], ): Promise { try { - const result = (await this.sendRequest(JsonRpcMethod.EXECUTE_CODE, { - code, - hasDefinitions, - })) as { - logs: string[]; - result?: CallToolResult; - error?: string; - }; + const result = await this.sandbox.execute(code, { hasDefinitions }); if (result.error) { return { @@ -246,7 +59,7 @@ export class SandboxExecutor { const content: { type: "text"; text: string }[] = []; // Add console logs if any - if (result.logs.length > 0) { + if (result.logs && result.logs.length > 0) { content.push({ type: "text", text: result.logs.join("\n"), @@ -282,40 +95,6 @@ export class SandboxExecutor { * Stop sandbox process */ stop(): void { - if (!this.process) return; - - try { - this.process.stdin?.end(); - } catch { - /* ignore */ - } - try { - this.process.kill("SIGTERM"); - } catch { - /* ignore */ - } - - this.cleanup(); - } - - /** - * Cleanup resources - */ - private cleanup(): void { - if (this.process) { - this.process.stdout?.removeAllListeners(); - this.process.stderr?.removeAllListeners(); - this.process.removeAllListeners(); - this.process.stdout?.destroy(); - this.process.stderr?.destroy(); - this.process.stdin?.destroy(); - } - - this.process = null; - - for (const pending of this.pendingRequests.values()) { - pending.reject(new Error("Sandbox terminated")); - } - this.pendingRequests.clear(); + this.sandbox.stop(); } } diff --git a/packages/plugin-code-execution/src/types.ts b/packages/plugin-code-execution/src/types.ts deleted file mode 100644 index dc1187b..0000000 --- a/packages/plugin-code-execution/src/types.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * JSON-RPC 2.0 Protocol Types - * Used for communication between host and Deno sandbox - */ - -export interface JsonRpcRequest { - jsonrpc: "2.0"; - id: string | number; - method: string; - params?: unknown; -} - -export interface JsonRpcResponse { - jsonrpc: "2.0"; - id: string | number; - result?: unknown; - error?: JsonRpcError; -} - -export interface JsonRpcError { - code: number; - message: string; - data?: unknown; -} - -export interface JsonRpcNotification { - jsonrpc: "2.0"; - method: string; - params?: unknown; -} - -// Error codes -export const JsonRpcErrorCode = { - PARSE_ERROR: -32700, - INVALID_REQUEST: -32600, - METHOD_NOT_FOUND: -32601, - INVALID_PARAMS: -32602, - INTERNAL_ERROR: -32603, - // Custom error codes - EXECUTION_ERROR: -32000, - TIMEOUT_ERROR: -32001, - TOOL_CALL_ERROR: -32002, -} as const; - -/** - * Method names for JSON-RPC communication - */ -export const JsonRpcMethod = { - // Sandbox -> Host - CALL_TOOL: "callMCPTool", - LOG: "log", - - // Host -> Sandbox - EXECUTE_CODE: "executeCode", - GET_TOOL_DEFINITIONS: "getToolDefinitions", -} as const; - -/** - * Request to execute code in sandbox - */ -export interface ExecuteCodeRequest { - code: string; - hasDefinitions: string[]; -} - -/** - * Request to call an MCP tool from sandbox - */ -export interface CallToolRequest { - toolName: string; - params: unknown; -} - -/** - * Request to get tool definitions - */ -export interface GetToolDefinitionsRequest { - toolNames: string[]; -} - -/** - * Log message from sandbox - */ -export interface LogNotification { - level: "log" | "error" | "warn" | "info"; - args: unknown[]; -} diff --git a/packages/plugin-code-execution/tests/code_execution.test.ts b/packages/plugin-code-execution/tests/code_execution.test.ts index c9df6c0..c65797f 100644 --- a/packages/plugin-code-execution/tests/code_execution.test.ts +++ b/packages/plugin-code-execution/tests/code_execution.test.ts @@ -6,152 +6,123 @@ import { assertEquals, assertStringIncludes } from "@std/assert"; import { mcpc } from "@mcpc/core"; import { createCodeExecutionPlugin } from "../mod.ts"; -Deno.test("Code execution plugin - get tool definitions", async () => { - const server = await mcpc( - [{ name: "test-code-exec", version: "1.0.0" }, { - capabilities: { tools: {} }, - }], - [{ - name: "test-agent", - description: "Test agent", - deps: { mcpServers: {} }, - plugins: [createCodeExecutionPlugin()], - }], - ); - - const result: any = await server.callTool("test-agent", { - definitionsOf: ["io_github_wonderwhy-er_desktop-commander_read_file"], - }); - - assertEquals(result.isError, undefined); - assertStringIncludes( - String(result.content[0].text), - "io_github_wonderwhy-er_desktop-commander_read_file", - ); -}); - -Deno.test("Code execution plugin - execute simple code", async () => { - const server = await mcpc( - [{ name: "test-code-exec", version: "1.0.0" }, { - capabilities: { tools: {} }, - }], - [{ - name: "test-agent", - description: "Test agent", - deps: { mcpServers: {} }, - plugins: [createCodeExecutionPlugin()], - }], - ); - - const result: any = await server.callTool("test-agent", { - code: "console.log('Hello from code execution!');", - hasDefinitions: ["io_github_wonderwhy-er_desktop-commander_read_file"], - }); - - assertEquals(result.isError, undefined); - assertStringIncludes( - String(result.content[0].text), - "Hello from code execution!", - ); -}); - -Deno.test("Code execution plugin - execute code with calculations", async () => { - const server = await mcpc( - [{ name: "test-code-exec", version: "1.0.0" }, { - capabilities: { tools: {} }, - }], - [{ - name: "test-agent", - description: "Test agent", - deps: { mcpServers: {} }, - plugins: [createCodeExecutionPlugin()], - }], - ); - - const result: any = await server.callTool("test-agent", { - code: ` - const numbers = [1, 2, 3, 4, 5]; - const sum = numbers.reduce((a, b) => a + b, 0); - const avg = sum / numbers.length; - console.log(\`Sum: \${sum}, Average: \${avg}\`); - `, - hasDefinitions: ["io_github_wonderwhy-er_desktop-commander_read_file"], - }); - - assertEquals(result.isError, undefined); - assertStringIncludes(String(result.content[0].text), "Sum: 15"); - assertStringIncludes(String(result.content[0].text), "Average: 3"); -}); - -Deno.test("Code execution plugin - handle syntax errors", async () => { - const server = await mcpc( - [{ name: "test-code-exec", version: "1.0.0" }, { - capabilities: { tools: {} }, - }], - [{ - name: "test-agent", - description: "Test agent", - deps: { mcpServers: {} }, - plugins: [createCodeExecutionPlugin()], - }], - ); - - const result: any = await server.callTool("test-agent", { - code: "const x = ; // Syntax error", - hasDefinitions: ["io_github_wonderwhy-er_desktop-commander_read_file"], - }); - - assertEquals(result.isError, true); - assertStringIncludes(String(result.content[0].text).toLowerCase(), "error"); -}); - -Deno.test("Code execution plugin - execute and get new definitions", async () => { - const server = await mcpc( - [{ name: "test-code-exec", version: "1.0.0" }, { - capabilities: { tools: {} }, - }], - [{ - name: "test-agent", - description: "Test agent", - deps: { mcpServers: {} }, - plugins: [createCodeExecutionPlugin()], - }], - ); - - const result: any = await server.callTool("test-agent", { - code: "console.log('Executing code');", - hasDefinitions: ["io_github_wonderwhy-er_desktop-commander_read_file"], - definitionsOf: ["io_github_wonderwhy-er_desktop-commander_write_file"], - }); - - assertEquals(result.isError, undefined); - assertStringIncludes( - String(result.content[0].text), - "Executing code", - ); - assertStringIncludes( - String(result.content[0].text), - "io_github_wonderwhy-er_desktop-commander_write_file", - ); -}); - -Deno.test("Code execution plugin - validation: code without hasDefinitions fails", async () => { - const server = await mcpc( - [{ name: "test-code-exec", version: "1.0.0" }, { - capabilities: { tools: {} }, - }], - [{ - name: "test-agent", - description: "Test agent", - deps: { mcpServers: {} }, - plugins: [createCodeExecutionPlugin()], - }], - ); - - const result: any = await server.callTool("test-agent", { - code: "console.log('test');", - }); - - assertEquals(result.isError, true); - assertStringIncludes(String(result.content[0].text), "hasDefinitions"); -}); +Deno.test( + "Code execution plugin - execute simple code", + async () => { + const server = await mcpc( + [ + { name: "test-code-exec", version: "1.0.0" }, + { + capabilities: { tools: {} }, + }, + ], + [ + { + name: "test-agent", + description: "Test agent", + deps: { mcpServers: {} }, + plugins: [createCodeExecutionPlugin()], + options: { + mode: "custom", + }, + }, + ], + ); + + try { + const result: any = await server.callTool("test-agent", { + code: "2 + 2", + hasDefinitions: ["test-code-execq"], + }); + + assertEquals(result.isError, undefined); + assertEquals(result.content.length > 0, true); + } finally { + await server.close?.(); + await new Promise((r) => setTimeout(r, 1000)); + } + }, +); + +Deno.test( + "Code execution plugin - execute code with calculations", + async () => { + const server = await mcpc( + [ + { name: "test-code-exec", version: "1.0.0" }, + { + capabilities: { tools: {} }, + }, + ], + [ + { + name: "test-agent", + description: "Test agent", + deps: { mcpServers: {} }, + plugins: [createCodeExecutionPlugin()], + options: { + mode: "custom", + }, + }, + ], + ); + + try { + const result: any = await server.callTool("test-agent", { + code: "const sum = 1 + 2; sum", + hasDefinitions: [], + }); + + assertEquals(result.isError, undefined); + assertEquals(result.content.length > 0, true); + } finally { + await server.close?.(); + await new Promise((r) => setTimeout(r, 1000)); + } + }, +); + +Deno.test( + "Code execution plugin - handle errors", + async () => { + const server = await mcpc( + [ + { name: "test-code-exec", version: "1.0.0" }, + { + capabilities: { tools: {} }, + }, + ], + [ + { + name: "test-agent", + description: "Test agent", + deps: { mcpServers: {} }, + plugins: [createCodeExecutionPlugin()], + options: { + mode: "custom", + }, + }, + ], + ); + + try { + const result: any = await server.callTool("test-agent", { + code: "throw new Error('Test error');", + hasDefinitions: [], + }); + + // Either result is an error or content contains error message + if (result.isError) { + assertEquals(result.isError, true); + } else { + assertStringIncludes( + String(result.content[0].text).toLowerCase(), + "error", + ); + } + } finally { + await server.close?.(); + await new Promise((r) => setTimeout(r, 1000)); + } + }, +); diff --git a/packages/plugin-code-execution/tests/json-rpc.test.ts b/packages/plugin-code-execution/tests/json-rpc.test.ts deleted file mode 100644 index 9370be4..0000000 --- a/packages/plugin-code-execution/tests/json-rpc.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Tests for JSON-RPC handler - */ - -import { assertEquals } from "@std/assert"; -import { JsonRpcHandler } from "../src/json-rpc.ts"; -import type { JsonRpcRequest } from "../src/types.ts"; - -Deno.test("JsonRpcHandler - create request", () => { - const handler = new JsonRpcHandler(); - const request = handler.createRequest("test_method", { key: "value" }); - - assertEquals(request.jsonrpc, "2.0"); - assertEquals(request.method, "test_method"); - assertEquals(request.params, { key: "value" }); - assertEquals(typeof request.id, "number"); -}); - -Deno.test("JsonRpcHandler - create response", () => { - const handler = new JsonRpcHandler(); - const response = handler.createResponse(1, { result: "success" }); - - assertEquals(response.jsonrpc, "2.0"); - assertEquals(response.id, 1); - assertEquals(response.result, { result: "success" }); -}); - -Deno.test("JsonRpcHandler - create error response", () => { - const handler = new JsonRpcHandler(); - const response = handler.createErrorResponse(1, -32600, "Invalid Request"); - - assertEquals(response.jsonrpc, "2.0"); - assertEquals(response.id, 1); - assertEquals(response.error?.code, -32600); - assertEquals(response.error?.message, "Invalid Request"); -}); - -Deno.test("JsonRpcHandler - parse message", () => { - const handler = new JsonRpcHandler(); - const message = '{"jsonrpc":"2.0","method":"test","id":1}'; - const parsed = handler.parseMessage(message) as JsonRpcRequest; - - assertEquals(parsed?.jsonrpc, "2.0"); - assertEquals(parsed?.method, "test"); - assertEquals(parsed?.id, 1); -}); - -Deno.test("JsonRpcHandler - serialize message", () => { - const handler = new JsonRpcHandler(); - const request = handler.createRequest("test_method", { key: "value" }); - const serialized = handler.serializeMessage(request); - - const parsed = JSON.parse(serialized); - assertEquals(parsed.jsonrpc, "2.0"); - assertEquals(parsed.method, "test_method"); -}); - -Deno.test("JsonRpcHandler - validate request", () => { - const handler = new JsonRpcHandler(); - const validRequest = handler.createRequest("test", {}); - const error = handler.validateRequest(validRequest); - - assertEquals(error, null); -}); - -Deno.test("JsonRpcHandler - validate invalid request", () => { - const handler = new JsonRpcHandler(); - const invalidRequest: JsonRpcRequest = { - jsonrpc: "1.0" as "2.0", - method: "test", - id: 1, - }; - const error = handler.validateRequest(invalidRequest); - - assertEquals(error?.code, -32600); -}); diff --git a/packages/plugin-code-execution/tests/plugin.test.ts b/packages/plugin-code-execution/tests/plugin.test.ts deleted file mode 100644 index f0a592c..0000000 --- a/packages/plugin-code-execution/tests/plugin.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Basic tests for Code Execution Plugin - */ - -import { assertEquals } from "@std/assert"; -import { createCodeExecutionPlugin } from "../mod.ts"; - -Deno.test("Code execution plugin - exports correctly", () => { - const plugin = createCodeExecutionPlugin(); - - assertEquals(plugin.name, "code_execution"); - assertEquals(plugin.version, "1.0.0"); - assertEquals(plugin.apply, "custom"); - assertEquals(typeof plugin.registerAgentTool, "function"); -}); - -Deno.test("Code execution plugin - accepts config options", () => { - const plugin = createCodeExecutionPlugin({ - sandbox: { - timeout: 60000, - memoryLimit: 1024, - permissions: ["--allow-net"], - }, - }); - - assertEquals(plugin.name, "code_execution"); - assertEquals(typeof plugin.registerAgentTool, "function"); -}); diff --git a/packages/plugin-code-execution/tests/sandbox-executor.test.ts b/packages/plugin-code-execution/tests/sandbox-executor.test.ts deleted file mode 100644 index c440bed..0000000 --- a/packages/plugin-code-execution/tests/sandbox-executor.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Tests for SandboxExecutor - */ - -import { assertEquals, assertExists } from "@std/assert"; -import { SandboxExecutor } from "../src/sandbox-executor.ts"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; - -Deno.test("SandboxExecutor - initialization", () => { - const executor = new SandboxExecutor(); - - assertExists(executor); - - // Cleanup - executor.stop(); -}); - -Deno.test("SandboxExecutor - execute simple code", async () => { - const executor = new SandboxExecutor( - { timeout: 5000 }, - (_toolName: string, _params: unknown): Promise => { - return Promise.resolve({ - content: [{ type: "text", text: "Tool result" }], - }); - }, - ); - - executor.start(); - - const result = await executor.executeCode( - "console.log('Hello from sandbox'); return 42;", - [], - ); - - console.log("Execution result:", result); - - assertEquals(result.isError, undefined); - assertExists(result.content); - assertEquals(result.content.length, 1); - - // Cleanup - executor.stop(); - - // Wait for cleanup to complete - await new Promise((resolve) => setTimeout(resolve, 200)); -}); - -Deno.test("SandboxExecutor - handle execution errors", async () => { - const executor = new SandboxExecutor( - { timeout: 5000 }, - (_toolName: string, _params: unknown): Promise => { - return Promise.resolve({ - content: [{ type: "text", text: "Tool result" }], - }); - }, - ); - - executor.start(); - - const result = await executor.executeCode( - "throw new Error('Test error');", - [], - ); - - console.log("Execution result:", result); - - assertEquals(result.isError, true); - assertExists(result.content); - - // Cleanup - executor.stop(); - - // Wait for cleanup to complete - await new Promise((resolve) => setTimeout(resolve, 200)); -}); - -Deno.test("SandboxExecutor - stop sandbox", () => { - const executor = new SandboxExecutor(); - - executor.start(); - - executor.stop(); - - // Should not throw - executor.stop(); -});