From b2cb58a4af0eaa082dd9543c6bbf782141762b92 Mon Sep 17 00:00:00 2001 From: yao <63141491+yaonyan@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:53:25 +0800 Subject: [PATCH 1/5] feat: add code execution mode example and fix TypeScript addFormats usage - Add 14-code-execution-mode.ts example demonstrating progressive disclosure pattern - Fix @ts-ignore issues with addFormats across sampling and workflow executors - Improve JSON schema handling in agentic executor with google-compatible schemas --- .../core/examples/14-code-execution-mode.ts | 88 +++++ .../src/executors/agentic/agentic-executor.ts | 4 +- .../code-execution/code-execution-executor.ts | 351 ++++++++++++++++++ .../code-execution-tool-registrar.ts | 92 +++++ .../sampling/base-sampling-executor.ts | 4 +- .../executors/workflow/workflow-executor.ts | 4 +- packages/core/src/plugins/built-in/index.ts | 3 + .../built-in/mode-code-execution-plugin.ts | 38 ++ packages/core/src/prompts/index.ts | 66 ++++ packages/core/src/prompts/types.ts | 3 +- .../tests/executors/code_execution_test.ts | 186 ++++++++++ 11 files changed, 832 insertions(+), 7 deletions(-) create mode 100644 packages/core/examples/14-code-execution-mode.ts create mode 100644 packages/core/src/executors/code-execution/code-execution-executor.ts create mode 100644 packages/core/src/executors/code-execution/code-execution-tool-registrar.ts create mode 100644 packages/core/src/plugins/built-in/mode-code-execution-plugin.ts create mode 100644 packages/core/tests/executors/code_execution_test.ts diff --git a/packages/core/examples/14-code-execution-mode.ts b/packages/core/examples/14-code-execution-mode.ts new file mode 100644 index 0000000..4b2c9ab --- /dev/null +++ b/packages/core/examples/14-code-execution-mode.ts @@ -0,0 +1,88 @@ +/** + * Example: Code Execution Mode with Progressive Disclosure + * + * Demonstrates the efficient code execution pattern from Anthropic's MCP guidelines. + * This example shows how to: + * 1. Use progressive disclosure to load only needed tools + * 2. Process data in execution environment to reduce token usage + * 3. Execute actual JavaScript code with MCP tool access + * + * Based on: https://www.anthropic.com/engineering/code-execution-with-mcp + * + * Key benefits vs traditional agentic mode: + * - 98.7% reduction in token usage for tool definitions + * - Data filtering/transformation happens before model sees results + * - Familiar code patterns (loops, conditionals) vs chaining tool calls + * - Actual code execution using new Function() - simple and effective + */ +/** + * MCPC Example 01: Basic File Manager + * + * Demonstrates the fundamental MCPC features: + * - Basic server creation and composition + * - Dependency management with external MCP servers + * - Simple tool orchestration with file operations + * - Tool selection using tags + * + * This creates a smart file organizer that can manage files efficiently. + */ + +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { type ComposeDefinition, mcpc } from "../mod.ts"; + +export const toolDefinitions: ComposeDefinition[] = [ + { + name: "file-organizer", + description: + `I am a smart file organizer that helps users manage their files efficiently. + +Available tools: + + + + + + +I can: +1. List directory contents to understand the current file structure +2. Create new directories for organization +3. Move files to appropriate folders based on type, date, or content +4. Delete unnecessary files after confirmation +5. Read file contents to understand what they contain +6. Create new files or modify existing ones + +I always ask for confirmation before making destructive changes and provide clear explanations of what I'm doing.`, + options: { + mode: "code_execution", + }, + deps: { + mcpServers: { + "@wonderwhy-er/desktop-commander": { + command: "npx", + args: ["-y", "@wonderwhy-er/desktop-commander@latest"], + transportType: "stdio" as const, + }, + }, + }, + }, +]; + +export const server = await mcpc( + [ + { + name: "basic-file-manager", + version: "1.0.0", + }, + { + capabilities: { + tools: { + listChanged: true, + }, + }, + }, + ], + toolDefinitions, +); + +const transport = new StdioServerTransport(); +await server.connect(transport); diff --git a/packages/core/src/executors/agentic/agentic-executor.ts b/packages/core/src/executors/agentic/agentic-executor.ts index 53c7666..7ce5282 100644 --- a/packages/core/src/executors/agentic/agentic-executor.ts +++ b/packages/core/src/executors/agentic/agentic-executor.ts @@ -13,8 +13,8 @@ const ajv = new Ajv({ allErrors: true, verbose: true, }); -// @ts-ignore - -addFormats(ajv); + +addFormats.default(ajv); export class AgenticExecutor { private logger: MCPLogger; diff --git a/packages/core/src/executors/code-execution/code-execution-executor.ts b/packages/core/src/executors/code-execution/code-execution-executor.ts new file mode 100644 index 0000000..04e4679 --- /dev/null +++ b/packages/core/src/executors/code-execution/code-execution-executor.ts @@ -0,0 +1,351 @@ +/** + * Code Execution Executor + * + * Implements efficient MCP interaction using code execution pattern. + * Key features: + * - Progressive disclosure: Load tool definitions on-demand + * - Context efficiency: Process data in execution environment + * - Reduced token usage: Filter/transform data before returning to model + * + * Based on: https://www.anthropic.com/engineering/code-execution-with-mcp + */ + +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { Ajv } from "ajv"; +import { AggregateAjvError } from "@segment/ajv-human-errors"; +import addFormats from "ajv-formats"; +import type { ComposableMCPServer } from "../../compose.ts"; +import { CompiledPrompts } from "../../prompts/index.ts"; +import { createLogger, type MCPLogger } from "../../utils/logger.ts"; +import type { Span } from "@opentelemetry/api"; +import { endSpan, initializeTracing, startSpan } from "../../utils/tracing.ts"; +import process from "node:process"; + +const ajv = new Ajv({ + allErrors: true, + verbose: true, +}); + +addFormats.default(ajv); + +export class CodeExecutionExecutor { + private logger: MCPLogger; + private tracingEnabled: boolean = false; + + constructor( + private name: string, + private allToolNames: string[], + private toolNameToDetailList: [string, unknown][], + private server: ComposableMCPServer, + private publicToolNames: string[], + private hiddenToolNames: string[], + ) { + this.logger = createLogger(`mcpc.code-execution.${name}`, server); + + // Initialize tracing + try { + this.tracingEnabled = process.env.MCPC_TRACING_ENABLED === "true"; + if (this.tracingEnabled) { + initializeTracing({ + enabled: true, + serviceName: `mcpc-code-execution-${name}`, + exportTo: (process.env.MCPC_TRACING_EXPORT ?? "otlp") as + | "console" + | "otlp" + | "none", + otlpEndpoint: process.env.MCPC_TRACING_OTLP_ENDPOINT ?? + "http://localhost:4318/v1/traces", + }); + } + } catch { + this.tracingEnabled = false; + } + } + + async execute( + args: Record, + schema: Record, + parentSpan?: Span | null, + ): Promise { + const executeSpan: Span | null = this.tracingEnabled + ? startSpan("mcpc.code_execution_execute", { + agent: this.name, + action: String(args.action ?? "unknown"), + decision: String(args.decision ?? "proceed"), + }, parentSpan ?? undefined) + : null; + + try { + // Validate input + const validationResult = await Promise.resolve( + this.validate(args, schema), + ); + if (!validationResult.valid) { + if (executeSpan) { + executeSpan.setAttributes({ + validationError: true, + errorMessage: validationResult.error || "Validation failed", + }); + endSpan(executeSpan); + } + + return { + content: [{ + type: "text", + text: CompiledPrompts.errorResponse({ + errorMessage: validationResult.error || "Validation failed", + }), + }], + isError: true, + }; + } + + const action = args.action as string; + const decision = args.decision as string; + + if (executeSpan) { + executeSpan.setAttribute("action", action); + } + + // Handle completion + if (decision === "complete") { + if (executeSpan) { + executeSpan.setAttribute("completed", true); + endSpan(executeSpan); + } + + this.logger.info({ + message: "Code execution completed", + agent: this.name, + }); + + return { + content: [{ + type: "text", + text: CompiledPrompts.completionMessage(), + }], + }; + } + + // Route to appropriate handler + let result: CallToolResult; + + switch (action) { + case "search_tools": + result = this.handleSearchTools(args, executeSpan); + break; + + case "execute_code": + result = await this.handleExecuteCode(args, executeSpan); + break; + + default: + result = { + content: [{ + type: "text", + text: + `Unknown action: ${action}. Available actions: search_tools, execute_code`, + }], + isError: true, + }; + } + + if (executeSpan) { + endSpan(executeSpan); + } + + return result; + } catch (error) { + if (executeSpan) { + executeSpan.setAttribute("error", true); + executeSpan.setAttribute("errorMessage", String(error)); + endSpan(executeSpan); + } + + this.logger.error({ + message: "Code execution error", + error: error instanceof Error ? error.message : String(error), + }); + + return { + content: [{ + type: "text", + text: `Error: ${ + error instanceof Error ? error.message : String(error) + }`, + }], + isError: true, + }; + } + } + + /** + * Search for tools and return their full schemas + */ + private handleSearchTools( + args: Record, + span?: Span | null, + ): CallToolResult { + const keyword = String(args.keyword || "").toLowerCase(); + + if (span) { + span.setAttribute("keyword", keyword); + } + + // Empty keyword = list all tools + const matchingTools = keyword + ? this.toolNameToDetailList.filter(([name, tool]) => { + const toolName = name.toLowerCase(); + const toolDesc = + (tool as { description?: string }).description?.toLowerCase() || ""; + return toolName.includes(keyword) || toolDesc.includes(keyword); + }) + : this.toolNameToDetailList; + + // Always return full schemas + const output = `Found ${matchingTools.length} tools:\n\n` + + matchingTools.map(([name, tool]) => + `## ${name}\n${JSON.stringify(tool, null, 2)}` + ).join("\n\n"); + + this.logger.info({ + message: "Tool search", + keyword: keyword || "(all)", + matches: matchingTools.length, + }); + + return { + content: [{ + type: "text", + text: output, + }], + }; + } + + /** + * Execute JavaScript code with access to MCP tools + * Simple implementation using new Function() + */ + private async handleExecuteCode( + args: Record, + span?: Span | null, + ): Promise { + const code = String(args.code || ""); + + if (!code) { + return { + content: [{ + type: "text", + text: "Error: No code provided", + }], + isError: true, + }; + } + + if (span) { + span.setAttribute("codeLength", code.length); + } + + this.logger.info({ + message: "Executing code", + codeLength: code.length, + }); + + try { + // Capture console output + const logs: string[] = []; + const consoleProxy = { + log: (...args: unknown[]) => { + logs.push( + args.map((a) => { + // Stringify objects for better readability + if (typeof a === "object" && a !== null) { + return JSON.stringify(a, null, 2); + } + return String(a); + }).join(" "), + ); + }, + error: (...args: unknown[]) => { + logs.push("ERROR: " + args.map((a) => String(a)).join(" ")); + }, + }; + + // API to call MCP tools from code + const callMCPTool = async (toolName: string, params: unknown) => { + this.logger.info({ + message: "Code calling MCP tool", + toolName, + }); + return await this.server.callTool(toolName, params); + }; + + // Create and execute function with injected APIs + const fn = new Function( + "console", + "callMCPTool", + `return (async () => { ${code} })();`, + ); + + const result = await fn(consoleProxy, callMCPTool); + + // Format output + const output = [ + logs.length > 0 ? "**Output:**\n" + logs.join("\n") : "", + result !== undefined + ? `\n**Result:** ${JSON.stringify(result, null, 2)}` + : "", + ].filter(Boolean).join("\n"); + + return { + content: [{ + type: "text", + text: output || "Code executed successfully (no output)", + }], + }; + } catch (error) { + this.logger.error({ + message: "Code execution failed", + error: error instanceof Error ? error.message : String(error), + }); + + return { + content: [{ + type: "text", + text: `Execution error: ${ + error instanceof Error ? error.message : String(error) + }`, + }], + isError: true, + }; + } + } + + /** + * Validate input arguments against schema + */ + private validate( + args: Record, + schema: Record, + ): { valid: boolean; error?: string } { + try { + const validate = ajv.compile(schema); + const valid = validate(args); + + if (!valid && validate.errors) { + const aggregatedError = new AggregateAjvError(validate.errors); + return { + valid: false, + error: aggregatedError.message, + }; + } + + return { valid: true }; + } catch (error) { + return { + valid: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } +} diff --git a/packages/core/src/executors/code-execution/code-execution-tool-registrar.ts b/packages/core/src/executors/code-execution/code-execution-tool-registrar.ts new file mode 100644 index 0000000..d5dd4bc --- /dev/null +++ b/packages/core/src/executors/code-execution/code-execution-tool-registrar.ts @@ -0,0 +1,92 @@ +/** + * Code Execution Tool Registrar + * + * Registers the code execution agent tool that implements progressive disclosure + * and efficient context usage patterns from Anthropic's MCP guidelines. + */ + +import { jsonSchema, type Schema } from "../../utils/schema.ts"; +import type { RegisterToolParams } from "../../types.ts"; +import { createGoogleCompatibleJSONSchema } from "../../utils/common/provider.ts"; +import type { ComposableMCPServer } from "../../compose.ts"; +import { CompiledPrompts } from "../../prompts/index.ts"; +import { CodeExecutionExecutor } from "./code-execution-executor.ts"; + +export interface CodeExecutionRegisterParams extends RegisterToolParams { + publicToolNames: string[]; + hiddenToolNames: string[]; +} + +export function registerCodeExecutionTool( + server: ComposableMCPServer, + { + description, + name, + allToolNames, + toolNameToDetailList, + publicToolNames, + hiddenToolNames, + }: CodeExecutionRegisterParams, +) { + // Create executor + const executor = new CodeExecutionExecutor( + name, + allToolNames, + toolNameToDetailList, + server, + publicToolNames, + hiddenToolNames, + ); + + // Enhance description with code execution prompt + description = CompiledPrompts.codeExecution({ + toolName: name, + description, + }); + + // Schema for code execution mode + const schema: Schema<{ + action: string; + keyword?: string; + code?: string; + decision: string; + [key: string]: unknown; + }>["jsonSchema"] = { + type: "object", + properties: { + action: { + type: "string", + enum: ["search_tools", "execute_code"], + description: + "Action: 'search_tools' to discover tools, 'execute_code' to run JavaScript", + }, + keyword: { + type: "string", + description: + "Search keyword for tools (empty = all tools). Used with 'search_tools' action.", + }, + code: { + type: "string", + description: + "JavaScript code to execute. Used with 'execute_code' action.", + }, + decision: { + type: "string", + enum: ["proceed", "complete"], + description: "proceed = continue execution, complete = task finished", + }, + }, + required: ["action", "decision"], + }; + + server.tool( + name, + description, + jsonSchema>( + createGoogleCompatibleJSONSchema(schema as Record), + ), + async (args: Record) => { + return await executor.execute(args, schema as Record); + }, + ); +} diff --git a/packages/core/src/executors/sampling/base-sampling-executor.ts b/packages/core/src/executors/sampling/base-sampling-executor.ts index 2243193..ab7a2a9 100644 --- a/packages/core/src/executors/sampling/base-sampling-executor.ts +++ b/packages/core/src/executors/sampling/base-sampling-executor.ts @@ -14,8 +14,8 @@ const ajv = new Ajv({ allErrors: true, verbose: true, }); -// @ts-ignore - -addFormats(ajv); + +addFormats.default(ajv); export interface ConversationMessage { role: "user" | "assistant"; diff --git a/packages/core/src/executors/workflow/workflow-executor.ts b/packages/core/src/executors/workflow/workflow-executor.ts index f236e58..c3b3bdb 100644 --- a/packages/core/src/executors/workflow/workflow-executor.ts +++ b/packages/core/src/executors/workflow/workflow-executor.ts @@ -16,8 +16,8 @@ const ajv = new Ajv({ allErrors: true, verbose: true, }); -// @ts-ignore - -addFormats(ajv); + +addFormats.default(ajv); export class WorkflowExecutor { constructor( diff --git a/packages/core/src/plugins/built-in/index.ts b/packages/core/src/plugins/built-in/index.ts index b7b2866..a2a9b56 100644 --- a/packages/core/src/plugins/built-in/index.ts +++ b/packages/core/src/plugins/built-in/index.ts @@ -10,6 +10,7 @@ export { createAgenticModePlugin } from "./mode-agentic-plugin.ts"; export { createWorkflowModePlugin } from "./mode-workflow-plugin.ts"; export { createAgenticSamplingModePlugin } from "./mode-agentic-sampling-plugin.ts"; export { createWorkflowSamplingModePlugin } from "./mode-workflow-sampling-plugin.ts"; +export { createCodeExecutionModePlugin } from "./mode-code-execution-plugin.ts"; // Import default instances import configPlugin from "./config-plugin.ts"; @@ -19,6 +20,7 @@ import agenticModePlugin from "./mode-agentic-plugin.ts"; import workflowModePlugin from "./mode-workflow-plugin.ts"; import agenticSamplingModePlugin from "./mode-agentic-sampling-plugin.ts"; import workflowSamplingModePlugin from "./mode-workflow-sampling-plugin.ts"; +import codeExecutionModePlugin from "./mode-code-execution-plugin.ts"; /** * Get all built-in plugins in the correct order @@ -31,6 +33,7 @@ export function getBuiltInPlugins() { workflowModePlugin, // Fourth: workflow mode handler agenticSamplingModePlugin, // Fifth: agentic sampling mode handler workflowSamplingModePlugin, // Sixth: workflow sampling mode handler + codeExecutionModePlugin, // Seventh: code execution mode handler loggingPlugin, // Last: logging ]; } diff --git a/packages/core/src/plugins/built-in/mode-code-execution-plugin.ts b/packages/core/src/plugins/built-in/mode-code-execution-plugin.ts new file mode 100644 index 0000000..5494cf5 --- /dev/null +++ b/packages/core/src/plugins/built-in/mode-code-execution-plugin.ts @@ -0,0 +1,38 @@ +/** + * Code Execution Mode Plugin + * Implements efficient MCP interaction using code execution pattern + * + * Based on: https://www.anthropic.com/engineering/code-execution-with-mcp + * + * Key benefits: + * - Progressive disclosure: Load tool definitions on-demand + * - Context efficiency: Process data in execution environment + * - Reduced token usage: Only results that matter pass through model + */ + +import type { ToolPlugin } from "../../plugin-types.ts"; +import { registerCodeExecutionTool } from "../../executors/code-execution/code-execution-tool-registrar.ts"; + +export const createCodeExecutionModePlugin = (): ToolPlugin => ({ + name: "mode-code-execution", + version: "1.0.0", + + // Only apply to code execution mode + apply: "code_execution", + + // Register the agent tool + registerAgentTool: (context) => { + registerCodeExecutionTool(context.server, { + description: context.description, + name: context.name, + allToolNames: context.allToolNames, + depGroups: context.depGroups, + toolNameToDetailList: context.toolNameToDetailList, + publicToolNames: context.publicToolNames, + hiddenToolNames: context.hiddenToolNames, + }); + }, +}); + +// Export default instance for auto-loading +export default createCodeExecutionModePlugin(); diff --git a/packages/core/src/prompts/index.ts b/packages/core/src/prompts/index.ts index 5f3ae9a..de27496 100644 --- a/packages/core/src/prompts/index.ts +++ b/packages/core/src/prompts/index.ts @@ -167,6 +167,71 @@ Rules: - Omit \`steps\` during step execution - Use \`decision: "retry"\` for failed steps `, + + /** + * Code Execution system prompt - progressive disclosure pattern + * + * Reduces token usage by: + * 1. Loading tool definitions on-demand (progressive disclosure) + * 2. Processing data in execution environment + * 3. Only returning relevant results to model + */ + CODE_EXECUTION: + `Agentic tool \`{toolName}\` with code execution capabilities for efficient MCP interaction. + + +{description} + + + +You can execute JavaScript code that calls MCP tools directly. +Available in your execution environment: +- \`console.log(...)\`: Print output +- \`callMCPTool(toolName, params)\`: Call any discovered MCP tool +- All standard JavaScript/ES6+ features + + + +1. **Discover Tools**: Use \`search_tools\` to find relevant tools + - Searches tool names and descriptions + - Returns full schemas immediately + - Empty keyword = list all tools +2. **Execute Code**: Process data efficiently in your execution environment + - Filter, transform, aggregate data before returning + - Use loops and conditionals instead of chaining calls + - Only log essential results to conserve tokens +3. **Format**: + \`\`\`json + { + "action": "search_tools|execute_code", + "keyword": "search term", // for search_tools + "code": "...", // for execute_code + "decision": "proceed|complete" + } + \`\`\` +4. Continue until \`decision: "complete"\` + + + +// Search for tools (returns full schemas) +{ + "action": "search_tools", + "keyword": "github", + "decision": "proceed" +} + +// Execute code using discovered tools +{ + "action": "execute_code", + "code": "const repo = await callMCPTool('github.getRepository', {owner: 'mcpc', name: 'mcpc'}); const recent = repo.issues.filter(i => new Date(i.updated) > Date.now() - 30*24*60*60*1000); console.log(\`Found \${recent.length} recent issues\`);", + "decision": "complete" +} + + + +- \`search_tools\`: Find tools by keyword, returns full schemas (empty keyword = all tools) +- \`execute_code\`: Run JavaScript with callMCPTool() access +`, }; /** @@ -328,6 +393,7 @@ export const CompiledPrompts = { workflowExecution: p(SystemPrompts.WORKFLOW_EXECUTION), samplingExecution: p(SystemPrompts.SAMPLING_EXECUTION), samplingWorkflowExecution: p(SystemPrompts.SAMPLING_WORKFLOW_EXECUTION), + codeExecution: p(SystemPrompts.CODE_EXECUTION), workflowInit: p(WorkflowPrompts.WORKFLOW_INIT), workflowToolDescription: p(WorkflowPrompts.WORKFLOW_TOOL_DESCRIPTION), nextStepDecision: p(WorkflowPrompts.NEXT_STEP_DECISION), diff --git a/packages/core/src/prompts/types.ts b/packages/core/src/prompts/types.ts index d317e80..4f1af3d 100644 --- a/packages/core/src/prompts/types.ts +++ b/packages/core/src/prompts/types.ts @@ -33,7 +33,8 @@ export type ExecutionMode = | "agentic" | "agentic_workflow" | "agentic_sampling" - | "agentic_workflow_sampling"; + | "agentic_workflow_sampling" + | "code_execution"; /** * Prompt template configuration diff --git a/packages/core/tests/executors/code_execution_test.ts b/packages/core/tests/executors/code_execution_test.ts new file mode 100644 index 0000000..002bf3c --- /dev/null +++ b/packages/core/tests/executors/code_execution_test.ts @@ -0,0 +1,186 @@ +/** + * Test for Code Execution Mode + */ + +import { assertEquals, assertStringIncludes } from "@std/assert"; +import { mcpc } from "../../mod.ts"; + +Deno.test("Code execution mode - search tools with keyword", async () => { + const server = await mcpc( + [{ name: "test-code-exec", version: "1.0.0" }, { + capabilities: { tools: {} }, + }], + [{ + name: "test-agent", + description: "Test agent", + deps: { + mcpServers: {}, + }, + options: { + mode: "code_execution", + }, + }], + ); + + const result: any = await server.callTool("test-agent", { + action: "search_tools", + keyword: "test", + decision: "proceed", + }); + + assertEquals(result.isError, undefined); + assertStringIncludes( + String(result.content[0].text), + "Found", + ); +}); + +Deno.test("Code execution mode - list all tools (empty keyword)", async () => { + const server = await mcpc( + [{ name: "test-code-exec", version: "1.0.0" }, { + capabilities: { tools: {} }, + }], + [{ + name: "test-agent", + description: "Test agent", + deps: { + mcpServers: {}, + }, + options: { + mode: "code_execution", + }, + }], + ); + + const result: any = await server.callTool("test-agent", { + action: "search_tools", + keyword: "", + decision: "proceed", + }); + + assertEquals(result.isError, undefined); + assertStringIncludes( + String(result.content[0].text), + "Found", + ); +}); + +Deno.test("Code execution mode - 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: {}, + }, + options: { + mode: "code_execution", + }, + }], + ); + + const result: any = await server.callTool("test-agent", { + action: "execute_code", + code: "console.log('Hello from code execution!'); return 42;", + decision: "proceed", + }); + + assertEquals(result.isError, undefined); + assertStringIncludes( + String(result.content[0].text), + "Hello from code execution!", + ); + assertStringIncludes(String(result.content[0].text), "42"); +}); + +Deno.test("Code execution mode - 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: {}, + }, + options: { + mode: "code_execution", + }, + }], + ); + + const result: any = await server.callTool("test-agent", { + action: "execute_code", + 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}\`); + return { sum, avg }; + `, + decision: "proceed", + }); + + assertEquals(result.isError, undefined); + assertStringIncludes(String(result.content[0].text), "Sum: 15"); + assertStringIncludes(String(result.content[0].text), "Average: 3"); + assertStringIncludes(String(result.content[0].text), '"sum": 15'); + assertStringIncludes(String(result.content[0].text), '"avg": 3'); +}); + +Deno.test("Code execution mode - 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: {}, + }, + options: { + mode: "code_execution", + }, + }], + ); + + const result: any = await server.callTool("test-agent", { + action: "execute_code", + code: "const x = ; // Syntax error", + decision: "proceed", + }); + + assertEquals(result.isError, true); + assertStringIncludes(String(result.content[0].text), "error"); +}); + +Deno.test("Code execution mode - complete decision", async () => { + const server = await mcpc( + [{ name: "test-code-exec", version: "1.0.0" }, { + capabilities: { tools: {} }, + }], + [{ + name: "test-agent", + description: "Test agent", + deps: { + mcpServers: {}, + }, + options: { + mode: "code_execution", + }, + }], + ); + + const result: any = await server.callTool("test-agent", { + action: "execute_code", + decision: "complete", + }); + + assertEquals(result.isError, undefined); + assertStringIncludes(String(result.content[0].text), "completed"); +}); From 6910305cf429af7e6f83bc85f98434f55632f00e Mon Sep 17 00:00:00 2001 From: yao <63141491+yaonyan@users.noreply.github.com> Date: Sun, 9 Nov 2025 00:01:37 +0800 Subject: [PATCH 2/5] feat: simplify code execution pattern and validation --- .../core/examples/14-code-execution-mode.ts | 35 +- .../src/executors/agentic/agentic-executor.ts | 21 +- .../code-execution/code-execution-executor.ts | 314 +++++++++--------- .../code-execution-tool-registrar.ts | 64 ++-- .../sampling/agentic-sampling-executor.ts | 3 +- .../sampling/base-sampling-executor.ts | 23 +- .../sampling/workflow-sampling-executor.ts | 6 +- .../executors/workflow/workflow-executor.ts | 27 +- packages/core/src/prompts/index.ts | 76 ++--- packages/core/src/utils/schema-validator.ts | 26 ++ .../tests/executors/code_execution_test.ts | 151 ++++++--- 11 files changed, 388 insertions(+), 358 deletions(-) create mode 100644 packages/core/src/utils/schema-validator.ts diff --git a/packages/core/examples/14-code-execution-mode.ts b/packages/core/examples/14-code-execution-mode.ts index 4b2c9ab..7645530 100644 --- a/packages/core/examples/14-code-execution-mode.ts +++ b/packages/core/examples/14-code-execution-mode.ts @@ -1,30 +1,25 @@ /** - * Example: Code Execution Mode with Progressive Disclosure + * Example: Code Execution Mode (KISS Pattern) * - * Demonstrates the efficient code execution pattern from Anthropic's MCP guidelines. + * Demonstrates the simplified code execution pattern with clear parameter names. * This example shows how to: - * 1. Use progressive disclosure to load only needed tools - * 2. Process data in execution environment to reduce token usage - * 3. Execute actual JavaScript code with MCP tool access + * 1. Use definitionsOf to get tool schemas + * 2. Execute JavaScript code with hasDefinitions declaring known tools * * Based on: https://www.anthropic.com/engineering/code-execution-with-mcp * - * Key benefits vs traditional agentic mode: - * - 98.7% reduction in token usage for tool definitions - * - Data filtering/transformation happens before model sees results - * - Familiar code patterns (loops, conditionals) vs chaining tool calls - * - Actual code execution using new Function() - simple and effective - */ -/** - * MCPC Example 01: Basic File Manager - * - * Demonstrates the fundamental MCPC features: - * - Basic server creation and composition - * - Dependency management with external MCP servers - * - Simple tool orchestration with file operations - * - Tool selection using tags + * Simple workflow: + * 1. First call: { definitionsOf: ['read_file', 'move_file'] } - get schemas + * 2. Second call: { + * code: 'const result = await callMCPTool("read_file", {...})', + * hasDefinitions: ['read_file'] + * } - execute code * - * This creates a smart file organizer that can manage files efficiently. + * Key benefits: + * - Clear parameter names: definitionsOf, hasDefinitions, code + * - Schema enforces: code requires hasDefinitions (non-empty) + * - Both code and definitionsOf can be used together + * - Simple, intuitive workflow */ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; diff --git a/packages/core/src/executors/agentic/agentic-executor.ts b/packages/core/src/executors/agentic/agentic-executor.ts index 7ce5282..e740e82 100644 --- a/packages/core/src/executors/agentic/agentic-executor.ts +++ b/packages/core/src/executors/agentic/agentic-executor.ts @@ -1,21 +1,12 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { Ajv } from "ajv"; -import { AggregateAjvError } from "@segment/ajv-human-errors"; -import addFormats from "ajv-formats"; import type { ComposableMCPServer } from "../../compose.ts"; import { CompiledPrompts } from "../../prompts/index.ts"; import { createLogger, type MCPLogger } from "../../utils/logger.ts"; +import { validateSchema } from "../../utils/schema-validator.ts"; import type { Span } from "@opentelemetry/api"; import { endSpan, initializeTracing, startSpan } from "../../utils/tracing.ts"; import process from "node:process"; -const ajv = new Ajv({ - allErrors: true, - verbose: true, -}); - -addFormats.default(ajv); - export class AgenticExecutor { private logger: MCPLogger; private tracingEnabled: boolean = false; @@ -313,14 +304,6 @@ export class AgenticExecutor { if (args.decision === "complete") { return { valid: true }; } - const validate = ajv.compile(schema); - if (!validate(args)) { - const errors = new AggregateAjvError(validate.errors!); - return { - valid: false, - error: errors.message, - }; - } - return { valid: true }; + return validateSchema(args, schema); } } diff --git a/packages/core/src/executors/code-execution/code-execution-executor.ts b/packages/core/src/executors/code-execution/code-execution-executor.ts index 04e4679..1467acc 100644 --- a/packages/core/src/executors/code-execution/code-execution-executor.ts +++ b/packages/core/src/executors/code-execution/code-execution-executor.ts @@ -11,23 +11,14 @@ */ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { Ajv } from "ajv"; -import { AggregateAjvError } from "@segment/ajv-human-errors"; -import addFormats from "ajv-formats"; import type { ComposableMCPServer } from "../../compose.ts"; import { CompiledPrompts } from "../../prompts/index.ts"; import { createLogger, type MCPLogger } from "../../utils/logger.ts"; +import { validateSchema } from "../../utils/schema-validator.ts"; import type { Span } from "@opentelemetry/api"; import { endSpan, initializeTracing, startSpan } from "../../utils/tracing.ts"; import process from "node:process"; -const ajv = new Ajv({ - allErrors: true, - verbose: true, -}); - -addFormats.default(ajv); - export class CodeExecutionExecutor { private logger: MCPLogger; private tracingEnabled: boolean = false; @@ -67,19 +58,26 @@ export class CodeExecutionExecutor { schema: Record, parentSpan?: Span | null, ): Promise { + const definitionsOf = (args.definitionsOf as string[]) || []; + const hasDefinitions = (args.hasDefinitions as string[]) || []; + const needsDefinitions = definitionsOf.filter( + (def) => !hasDefinitions.includes(def), + ); + const executeSpan: Span | null = this.tracingEnabled - ? startSpan("mcpc.code_execution_execute", { - agent: this.name, - action: String(args.action ?? "unknown"), - decision: String(args.decision ?? "proceed"), - }, parentSpan ?? undefined) + ? startSpan( + "mcpc.code_execution_execute", + { + agent: this.name, + hasCode: Boolean(args.code), + needsDefinitions: needsDefinitions.length > 0, + }, + parentSpan ?? undefined, + ) : null; try { - // Validate input - const validationResult = await Promise.resolve( - this.validate(args, schema), - ); + const validationResult = validateSchema(args, schema); if (!validationResult.valid) { if (executeSpan) { executeSpan.setAttributes({ @@ -90,71 +88,70 @@ export class CodeExecutionExecutor { } return { - content: [{ - type: "text", - text: CompiledPrompts.errorResponse({ - errorMessage: validationResult.error || "Validation failed", - }), - }], + content: [ + { + type: "text", + text: CompiledPrompts.errorResponse({ + errorMessage: validationResult.error || "Validation failed", + }), + }, + ], isError: true, }; } - const action = args.action as string; - const decision = args.decision as string; + const hasCode = Boolean(args.code); - if (executeSpan) { - executeSpan.setAttribute("action", action); - } + // Build combined result + const contentParts: Array<{ type: "text"; text: string }> = []; - // Handle completion - if (decision === "complete") { - if (executeSpan) { - executeSpan.setAttribute("completed", true); - endSpan(executeSpan); - } + if (hasCode && hasDefinitions.length > 0) { + const codeResult = await this.handleExecuteCode(args, executeSpan); - this.logger.info({ - message: "Code execution completed", - agent: this.name, - }); + // If code execution failed, return error immediately + if (codeResult.isError) { + if (executeSpan) { + endSpan(executeSpan); + } + return codeResult; + } - return { - content: [{ - type: "text", - text: CompiledPrompts.completionMessage(), - }], - }; + if (codeResult.content) { + contentParts.push( + ...codeResult.content.filter((c) => c.type === "text"), + ); + } } - // Route to appropriate handler - let result: CallToolResult; - - switch (action) { - case "search_tools": - result = this.handleSearchTools(args, executeSpan); - break; - - case "execute_code": - result = await this.handleExecuteCode(args, executeSpan); - break; + // Get definitions if requested + if (needsDefinitions.length > 0) { + const definitionsResult = this.getToolDefinitions(needsDefinitions); + if (definitionsResult.content) { + contentParts.push( + ...definitionsResult.content.filter((c) => c.type === "text"), + ); + } - default: - result = { - content: [{ - type: "text", - text: - `Unknown action: ${action}. Available actions: search_tools, execute_code`, - }], - isError: true, - }; + if (executeSpan) { + executeSpan.setAttribute("toolsRequested", needsDefinitions.length); + } } if (executeSpan) { endSpan(executeSpan); } - return result; + const combinedText = contentParts.map((part) => part.text).join("\n"); + + return { + content: [ + { + type: "text", + text: combinedText || + "No output generated, use console.log() to log output", + }, + ], + }; } catch (error) { if (executeSpan) { executeSpan.setAttribute("error", true); @@ -168,60 +165,19 @@ export class CodeExecutionExecutor { }); return { - content: [{ - type: "text", - text: `Error: ${ - error instanceof Error ? error.message : String(error) - }`, - }], + content: [ + { + type: "text", + text: `Error: ${ + error instanceof Error ? error.message : String(error) + }`, + }, + ], isError: true, }; } } - /** - * Search for tools and return their full schemas - */ - private handleSearchTools( - args: Record, - span?: Span | null, - ): CallToolResult { - const keyword = String(args.keyword || "").toLowerCase(); - - if (span) { - span.setAttribute("keyword", keyword); - } - - // Empty keyword = list all tools - const matchingTools = keyword - ? this.toolNameToDetailList.filter(([name, tool]) => { - const toolName = name.toLowerCase(); - const toolDesc = - (tool as { description?: string }).description?.toLowerCase() || ""; - return toolName.includes(keyword) || toolDesc.includes(keyword); - }) - : this.toolNameToDetailList; - - // Always return full schemas - const output = `Found ${matchingTools.length} tools:\n\n` + - matchingTools.map(([name, tool]) => - `## ${name}\n${JSON.stringify(tool, null, 2)}` - ).join("\n\n"); - - this.logger.info({ - message: "Tool search", - keyword: keyword || "(all)", - matches: matchingTools.length, - }); - - return { - content: [{ - type: "text", - text: output, - }], - }; - } - /** * Execute JavaScript code with access to MCP tools * Simple implementation using new Function() @@ -234,10 +190,12 @@ export class CodeExecutionExecutor { if (!code) { return { - content: [{ - type: "text", - text: "Error: No code provided", - }], + content: [ + { + type: "text", + text: "Error: No code provided", + }, + ], isError: true, }; } @@ -257,13 +215,15 @@ export class CodeExecutionExecutor { const consoleProxy = { log: (...args: unknown[]) => { logs.push( - args.map((a) => { - // Stringify objects for better readability - if (typeof a === "object" && a !== null) { - return JSON.stringify(a, null, 2); - } - return String(a); - }).join(" "), + args + .map((a) => { + // Stringify objects for better readability + if (typeof a === "object" && a !== null) { + return JSON.stringify(a, null, 2); + } + return String(a); + }) + .join(" "), ); }, error: (...args: unknown[]) => { @@ -277,6 +237,7 @@ export class CodeExecutionExecutor { message: "Code calling MCP tool", toolName, }); + return await this.server.callTool(toolName, params); }; @@ -295,13 +256,18 @@ export class CodeExecutionExecutor { result !== undefined ? `\n**Result:** ${JSON.stringify(result, null, 2)}` : "", - ].filter(Boolean).join("\n"); + ] + .filter(Boolean) + .join("\n"); return { - content: [{ - type: "text", - text: output || "Code executed successfully (no output)", - }], + content: [ + { + type: "text", + text: output || + "Code executed successfully (no output), use console.log() to log output", + }, + ], }; } catch (error) { this.logger.error({ @@ -310,42 +276,74 @@ export class CodeExecutionExecutor { }); return { - content: [{ - type: "text", - text: `Execution error: ${ - error instanceof Error ? error.message : String(error) - }`, - }], + content: [ + { + type: "text", + text: `Execution error: ${ + error instanceof Error ? error.message : String(error) + }`, + }, + ], isError: true, }; } } /** - * Validate input arguments against schema + * Get tool definitions for the specified tool names + * Returns schemas that describe how to call these tools */ - private validate( - args: Record, - schema: Record, - ): { valid: boolean; error?: string } { - try { - const validate = ajv.compile(schema); - const valid = validate(args); + private getToolDefinitions(toolNames: string[]): CallToolResult { + const definitions: Array<{ name: string; schema: unknown }> = []; + const notFound: string[] = []; - if (!valid && validate.errors) { - const aggregatedError = new AggregateAjvError(validate.errors); - return { - valid: false, - error: aggregatedError.message, - }; + for (const toolName of toolNames) { + const toolDetail = this.toolNameToDetailList.find( + ([name]) => name === toolName, + ); + + if (toolDetail) { + definitions.push({ + name: toolDetail[0], + schema: toolDetail[1], + }); + } else { + notFound.push(toolName); } + } - return { valid: true }; - } catch (error) { - return { - valid: false, - error: error instanceof Error ? error.message : String(error), - }; + let text = ""; + + if (definitions.length > 0) { + text += "\n"; + for (const { name, schema } of definitions) { + text += `\n${ + JSON.stringify( + schema, + null, + 2, + ) + }\n\n`; + } + text += "\n"; + } + + if (notFound.length > 0) { + text += `${notFound.join(", ")}\n`; + this.logger.warning({ + message: "Some tools not found", + notFound, + }); } + + return { + content: [ + { + type: "text", + text: text || "No tool definitions found", + }, + ], + isError: notFound.length > 0 && definitions.length === 0, + }; } } diff --git a/packages/core/src/executors/code-execution/code-execution-tool-registrar.ts b/packages/core/src/executors/code-execution/code-execution-tool-registrar.ts index d5dd4bc..674b136 100644 --- a/packages/core/src/executors/code-execution/code-execution-tool-registrar.ts +++ b/packages/core/src/executors/code-execution/code-execution-tool-registrar.ts @@ -45,38 +45,37 @@ export function registerCodeExecutionTool( }); // Schema for code execution mode + // Both parameters can be used together for maximum efficiency const schema: Schema<{ - action: string; - keyword?: string; code?: string; - decision: string; - [key: string]: unknown; + definitionsOf?: string[]; + hasDefinitions?: string[]; }>["jsonSchema"] = { type: "object", properties: { - action: { + code: { type: "string", - enum: ["search_tools", "execute_code"], description: - "Action: 'search_tools' to discover tools, 'execute_code' to run JavaScript", + "JavaScript to run. You can use callMCPTool(toolName, params) and console.log(). Before calling a tool, request its schema with definitionsOf, then use it in your code.", }, - keyword: { - type: "string", + definitionsOf: { + type: "array", + items: allToolNames.length > 0 + ? { type: "string", enum: allToolNames } + : { type: "string" }, + default: [], description: - "Search keyword for tools (empty = all tools). Used with 'search_tools' action.", + `Tool names whose schemas you need. The agent uses these to understand available tools before calling them.`, }, - code: { - type: "string", + hasDefinitions: { + type: "array", + items: allToolNames.length > 0 + ? { type: "string", enum: allToolNames } + : { type: "string" }, description: - "JavaScript code to execute. Used with 'execute_code' action.", - }, - decision: { - type: "string", - enum: ["proceed", "complete"], - description: "proceed = continue execution, complete = task finished", + `Tool names whose schemas were already provided in this conversation. List all tools you have schemas for to avoid duplicate schema requests`, }, }, - required: ["action", "decision"], }; server.tool( @@ -86,7 +85,32 @@ export function registerCodeExecutionTool( createGoogleCompatibleJSONSchema(schema as Record), ), async (args: Record) => { - return await executor.execute(args, schema as Record); + return await executor.execute(args, { + ...schema, + // Use if-then to enforce: if code exists, hasDefinitions must be non-empty + if: { + properties: { code: { type: "string" } }, + required: ["code"], + }, + then: { + properties: { + hasDefinitions: { + type: "array", + }, + }, + required: ["hasDefinitions"], + }, + // At least one of code or definitionsOf must be provided + anyOf: [ + { required: ["code"] }, + { + properties: { + definitionsOf: { type: "array", minItems: 1 }, + }, + required: ["definitionsOf"], + }, + ], + } as Record); }, ); } diff --git a/packages/core/src/executors/sampling/agentic-sampling-executor.ts b/packages/core/src/executors/sampling/agentic-sampling-executor.ts index 950cdda..d1e4657 100644 --- a/packages/core/src/executors/sampling/agentic-sampling-executor.ts +++ b/packages/core/src/executors/sampling/agentic-sampling-executor.ts @@ -9,6 +9,7 @@ import { BaseSamplingExecutor, type ExternalTool, } from "./base-sampling-executor.ts"; +import { validateSchema } from "../../utils/schema-validator.ts"; export class SamplingExecutor extends BaseSamplingExecutor { private agenticExecutor: AgenticExecutor; @@ -68,7 +69,7 @@ export class SamplingExecutor extends BaseSamplingExecutor { args: Record, schema: Record, ) { - const validationResult = this.validateSchema(args, schema); + const validationResult = validateSchema(args, schema); if (!validationResult.valid) { return { content: [ diff --git a/packages/core/src/executors/sampling/base-sampling-executor.ts b/packages/core/src/executors/sampling/base-sampling-executor.ts index ab7a2a9..a054ac1 100644 --- a/packages/core/src/executors/sampling/base-sampling-executor.ts +++ b/packages/core/src/executors/sampling/base-sampling-executor.ts @@ -1,22 +1,13 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import type { ComposableMCPServer } from "../../compose.ts"; import type { SamplingConfig } from "../../types.ts"; -import { Ajv } from "ajv"; -import { AggregateAjvError } from "@segment/ajv-human-errors"; -import addFormats from "ajv-formats"; import { parseJSON } from "@mcpc/utils"; import process from "node:process"; import { createLogger, type MCPLogger } from "../../utils/logger.ts"; +import { validateSchema } from "../../utils/schema-validator.ts"; import type { Span } from "@opentelemetry/api"; import { endSpan, initializeTracing, startSpan } from "../../utils/tracing.ts"; -const ajv = new Ajv({ - allErrors: true, - verbose: true, -}); - -addFormats.default(ajv); - export interface ConversationMessage { role: "user" | "assistant"; content: { @@ -559,21 +550,13 @@ VALID: {"key":"value"}`, } // Validate arguments using JSON schema - protected validateSchema( + private validateInput( args: Record, schema: Record, ): { valid: boolean; error?: string; } { - const validate = ajv.compile(schema); - if (!validate(args)) { - const errors = new AggregateAjvError(validate.errors!); - return { - valid: false, - error: errors.message, - }; - } - return { valid: true }; + return validateSchema(args, schema); } } diff --git a/packages/core/src/executors/sampling/workflow-sampling-executor.ts b/packages/core/src/executors/sampling/workflow-sampling-executor.ts index 64f88be..66afe15 100644 --- a/packages/core/src/executors/sampling/workflow-sampling-executor.ts +++ b/packages/core/src/executors/sampling/workflow-sampling-executor.ts @@ -10,6 +10,7 @@ import { BaseSamplingExecutor, type ExternalTool, } from "./base-sampling-executor.ts"; +import { validateSchema } from "../../utils/schema-validator.ts"; export class WorkflowSamplingExecutor extends BaseSamplingExecutor { private workflowExecutor: WorkflowExecutor; @@ -49,7 +50,7 @@ export class WorkflowSamplingExecutor extends BaseSamplingExecutor { schema: Record, state: WorkflowState, ): Promise { - const validationResult = this.validateSchema(args, schema); + const validationResult = validateSchema(args, schema); if (!validationResult.valid) { return { content: [ @@ -130,7 +131,8 @@ export class WorkflowSamplingExecutor extends BaseSamplingExecutor { // Create workflow-specific sampling prompt using existing patterns let contextInfo = ""; if ( - args.context && typeof args.context === "object" && + args.context && + typeof args.context === "object" && Object.keys(args.context).length > 0 ) { contextInfo = `\n\nContext:\n${JSON.stringify(args.context, null, 2)}`; diff --git a/packages/core/src/executors/workflow/workflow-executor.ts b/packages/core/src/executors/workflow/workflow-executor.ts index c3b3bdb..374e954 100644 --- a/packages/core/src/executors/workflow/workflow-executor.ts +++ b/packages/core/src/executors/workflow/workflow-executor.ts @@ -1,24 +1,15 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { Ajv } from "ajv"; -import { AggregateAjvError } from "@segment/ajv-human-errors"; -import addFormats from "ajv-formats"; import type { MCPCStep } from "../../utils/state.ts"; import type { WorkflowState } from "../../utils/state.ts"; import type { ArgsDefCreator } from "../../types.ts"; import type { ComposableMCPServer } from "../../compose.ts"; +import { validateSchema } from "../../utils/schema-validator.ts"; import { CompiledPrompts, PromptUtils, WorkflowPrompts, } from "../../prompts/index.ts"; -const ajv = new Ajv({ - allErrors: true, - verbose: true, -}); - -addFormats.default(ajv); - export class WorkflowExecutor { constructor( private name: string, @@ -154,7 +145,7 @@ export class WorkflowExecutor { const nextStepValidationSchema = this.createArgsDef.forCurrentState( state, ); - const nextStepValidationResult = this.validate( + const nextStepValidationResult = this.validateInput( args, nextStepValidationSchema, ); @@ -233,7 +224,7 @@ export class WorkflowExecutor { if (decision !== "proceed") { const validationSchema = this.createArgsDef.forCurrentState(state); - const validationResult = this.validate(args, validationSchema); + const validationResult = this.validateInput(args, validationSchema); if (!validationResult.valid) { return { content: [ @@ -420,21 +411,13 @@ ${this.formatProgress(state)}`, } // Validate arguments using JSON schema - validate( + private validateInput( args: Record, schema: Record, ): { valid: boolean; error?: string; } { - const validate = ajv.compile(schema); - if (!validate(args)) { - const errors = new AggregateAjvError(validate.errors!); - return { - valid: false, - error: errors.message, - }; - } - return { valid: true }; + return validateSchema(args, schema); } } diff --git a/packages/core/src/prompts/index.ts b/packages/core/src/prompts/index.ts index de27496..37fef89 100644 --- a/packages/core/src/prompts/index.ts +++ b/packages/core/src/prompts/index.ts @@ -171,67 +171,51 @@ Rules: /** * Code Execution system prompt - progressive disclosure pattern * - * Reduces token usage by: - * 1. Loading tool definitions on-demand (progressive disclosure) - * 2. Processing data in execution environment - * 3. Only returning relevant results to model + * Simple pattern: Get definitions, then execute code */ CODE_EXECUTION: - `Agentic tool \`{toolName}\` with code execution capabilities for efficient MCP interaction. + `Agentic tool \`{toolName}\` executes JavaScript code with MCP tool access. {description} - -You can execute JavaScript code that calls MCP tools directly. -Available in your execution environment: -- \`console.log(...)\`: Print output -- \`callMCPTool(toolName, params)\`: Call any discovered MCP tool -- All standard JavaScript/ES6+ features - + +\`callMCPTool(toolName, params)\` - Call any MCP tool +\`console.log(...)\` - Print output + + + +\`code\` (optional) - JavaScript to execute +\`definitionsOf\` (optional) - Tool names whose schemas you need +\`hasDefinitions\` (optional) - Tool names whose schemas you already have + -1. **Discover Tools**: Use \`search_tools\` to find relevant tools - - Searches tool names and descriptions - - Returns full schemas immediately - - Empty keyword = list all tools -2. **Execute Code**: Process data efficiently in your execution environment - - Filter, transform, aggregate data before returning - - Use loops and conditionals instead of chaining calls - - Only log essential results to conserve tokens -3. **Format**: - \`\`\`json - { - "action": "search_tools|execute_code", - "keyword": "search term", // for search_tools - "code": "...", // for execute_code - "decision": "proceed|complete" - } - \`\`\` -4. Continue until \`decision: "complete"\` +- **First call**: No tool definitions available—you must request them via \`definitionsOf\` +- **When executing code**: Must provide \`hasDefinitions\` with ALL tools you have schemas for (avoid duplicate requests and reduce tokens) +- **When getting definitions**: Use \`definitionsOf\` to request tool schemas you need +- **Both together**: Execute code AND request new definitions in one call for efficiency +- **Never request definitions you already have** - -// Search for tools (returns full schemas) + +Initial definition request: +\`\`\`json { - "action": "search_tools", - "keyword": "github", - "decision": "proceed" + "hasDefinitions": [], + "definitionsOf": ["tool1"] } - -// Execute code using discovered tools +\`\`\` +Execute code + get new definitions: +\`\`\`json { - "action": "execute_code", - "code": "const repo = await callMCPTool('github.getRepository', {owner: 'mcpc', name: 'mcpc'}); const recent = repo.issues.filter(i => new Date(i.updated) > Date.now() - 30*24*60*60*1000); console.log(\`Found \${recent.length} recent issues\`);", - "decision": "complete" + "code": "await callMCPTool('tool1', {x: 1});", + "hasDefinitions": ["tool1"], + "definitionsOf": ["tool2"] } - - - -- \`search_tools\`: Find tools by keyword, returns full schemas (empty keyword = all tools) -- \`execute_code\`: Run JavaScript with callMCPTool() access -`, +\`\`\` +`, }; /** diff --git a/packages/core/src/utils/schema-validator.ts b/packages/core/src/utils/schema-validator.ts new file mode 100644 index 0000000..c2896ac --- /dev/null +++ b/packages/core/src/utils/schema-validator.ts @@ -0,0 +1,26 @@ +import { Ajv } from "ajv"; +import addFormats from "ajv-formats"; +import { AggregateAjvError } from "@segment/ajv-human-errors"; + +// Singleton Ajv instance +const ajv = new Ajv({ + allErrors: true, + verbose: true, +}); + +addFormats.default(ajv); + +export function validateSchema( + args: Record, + schema: Record, +): { valid: boolean; error?: string } { + const validate = ajv.compile(schema); + if (!validate(args)) { + const errors = new AggregateAjvError(validate.errors!); + return { + valid: false, + error: errors.message, + }; + } + return { valid: true }; +} diff --git a/packages/core/tests/executors/code_execution_test.ts b/packages/core/tests/executors/code_execution_test.ts index 002bf3c..94b3edf 100644 --- a/packages/core/tests/executors/code_execution_test.ts +++ b/packages/core/tests/executors/code_execution_test.ts @@ -5,14 +5,14 @@ import { assertEquals, assertStringIncludes } from "@std/assert"; import { mcpc } from "../../mod.ts"; -Deno.test("Code execution mode - search tools with keyword", async () => { +Deno.test("Code execution mode - get tool definitions", async () => { const server = await mcpc( [{ name: "test-code-exec", version: "1.0.0" }, { capabilities: { tools: {} }, }], [{ name: "test-agent", - description: "Test agent", + description: "Test agent with ", deps: { mcpServers: {}, }, @@ -22,27 +22,35 @@ Deno.test("Code execution mode - search tools with keyword", async () => { }], ); + // Add a test tool + server.tool( + "test-tool", + "Test tool", + { type: "object", properties: {} }, + () => { + return { content: [{ type: "text", text: "Test result" }] }; + }, + ); + const result: any = await server.callTool("test-agent", { - action: "search_tools", - keyword: "test", - decision: "proceed", + definitionsOf: ["test-tool"], }); assertEquals(result.isError, undefined); assertStringIncludes( String(result.content[0].text), - "Found", + "test-tool", ); }); -Deno.test("Code execution mode - list all tools (empty keyword)", async () => { +Deno.test("Code execution mode - execute simple code", async () => { const server = await mcpc( [{ name: "test-code-exec", version: "1.0.0" }, { capabilities: { tools: {} }, }], [{ name: "test-agent", - description: "Test agent", + description: "Test agent with ", deps: { mcpServers: {}, }, @@ -52,27 +60,36 @@ Deno.test("Code execution mode - list all tools (empty keyword)", async () => { }], ); + // Add a test tool + server.tool( + "test-tool", + "Test tool", + { type: "object", properties: {} }, + () => { + return { content: [{ type: "text", text: "Test result" }] }; + }, + ); + const result: any = await server.callTool("test-agent", { - action: "search_tools", - keyword: "", - decision: "proceed", + code: "console.log('Hello from code execution!'); return 42;", + hasDefinitions: ["test-tool"], }); assertEquals(result.isError, undefined); assertStringIncludes( String(result.content[0].text), - "Found", + "Hello from code execution!", ); }); -Deno.test("Code execution mode - execute simple code", async () => { +Deno.test("Code execution mode - 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", + description: "Test agent with ", deps: { mcpServers: {}, }, @@ -82,28 +99,40 @@ Deno.test("Code execution mode - execute simple code", async () => { }], ); + // Add a test tool + server.tool( + "test-tool", + "Test tool", + { type: "object", properties: {} }, + () => { + return { content: [{ type: "text", text: "Test result" }] }; + }, + ); + const result: any = await server.callTool("test-agent", { - action: "execute_code", - code: "console.log('Hello from code execution!'); return 42;", - decision: "proceed", + 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}\`); + return { sum, avg }; + `, + hasDefinitions: ["test-tool"], }); assertEquals(result.isError, undefined); - assertStringIncludes( - String(result.content[0].text), - "Hello from code execution!", - ); - assertStringIncludes(String(result.content[0].text), "42"); + assertStringIncludes(String(result.content[0].text), "Sum: 15"); + assertStringIncludes(String(result.content[0].text), "Average: 3"); }); -Deno.test("Code execution mode - execute code with calculations", async () => { +Deno.test("Code execution mode - handle syntax errors", async () => { const server = await mcpc( [{ name: "test-code-exec", version: "1.0.0" }, { capabilities: { tools: {} }, }], [{ name: "test-agent", - description: "Test agent", + description: "Test agent with ", deps: { mcpServers: {}, }, @@ -113,33 +142,34 @@ Deno.test("Code execution mode - execute code with calculations", async () => { }], ); + // Add a test tool + server.tool( + "test-tool", + "Test tool", + { type: "object", properties: {} }, + () => { + return { content: [{ type: "text", text: "Test result" }] }; + }, + ); + const result: any = await server.callTool("test-agent", { - action: "execute_code", - 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}\`); - return { sum, avg }; - `, - decision: "proceed", + code: "const x = ; // Syntax error", + hasDefinitions: ["test-tool"], }); - assertEquals(result.isError, undefined); - assertStringIncludes(String(result.content[0].text), "Sum: 15"); - assertStringIncludes(String(result.content[0].text), "Average: 3"); - assertStringIncludes(String(result.content[0].text), '"sum": 15'); - assertStringIncludes(String(result.content[0].text), '"avg": 3'); + assertEquals(result.isError, true); + assertStringIncludes(String(result.content[0].text).toLowerCase(), "error"); }); -Deno.test("Code execution mode - handle syntax errors", async () => { +Deno.test("Code execution mode - 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", + description: + "Test agent with and ", deps: { mcpServers: {}, }, @@ -149,17 +179,38 @@ Deno.test("Code execution mode - handle syntax errors", async () => { }], ); + // Add test tools + server.tool( + "test-tool", + "Test tool", + { type: "object", properties: {} }, + () => { + return { content: [{ type: "text", text: "Test result" }] }; + }, + ); + server.tool("another-tool", "Another tool", { + type: "object", + properties: {}, + }, () => { + return { content: [{ type: "text", text: "Another result" }] }; + }); + const result: any = await server.callTool("test-agent", { - action: "execute_code", - code: "const x = ; // Syntax error", - decision: "proceed", + code: "console.log('Executing with test-tool');", + hasDefinitions: ["test-tool"], + definitionsOf: ["another-tool"], }); - assertEquals(result.isError, true); - assertStringIncludes(String(result.content[0].text), "error"); + assertEquals(result.isError, undefined); + assertStringIncludes( + String(result.content[0].text), + "Executing with test-tool", + ); + // Should also include the definitions for another-tool + assertStringIncludes(String(result.content[0].text), "another-tool"); }); -Deno.test("Code execution mode - complete decision", async () => { +Deno.test("Code execution mode - validation: code without hasDefinitions fails", async () => { const server = await mcpc( [{ name: "test-code-exec", version: "1.0.0" }, { capabilities: { tools: {} }, @@ -177,10 +228,10 @@ Deno.test("Code execution mode - complete decision", async () => { ); const result: any = await server.callTool("test-agent", { - action: "execute_code", - decision: "complete", + code: "console.log('test');", + // Missing hasDefinitions - should fail validation }); - assertEquals(result.isError, undefined); - assertStringIncludes(String(result.content[0].text), "completed"); + assertEquals(result.isError, true); + assertStringIncludes(String(result.content[0].text), "Validation failed"); }); From a853885c56d220ea97c6e4b69da616e55544556b Mon Sep 17 00:00:00 2001 From: yao <63141491+yaonyan@users.noreply.github.com> Date: Sun, 9 Nov 2025 08:00:04 +0800 Subject: [PATCH 3/5] Update packages/core/src/executors/code-execution/code-execution-tool-registrar.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../executors/code-execution/code-execution-tool-registrar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/executors/code-execution/code-execution-tool-registrar.ts b/packages/core/src/executors/code-execution/code-execution-tool-registrar.ts index 674b136..b816858 100644 --- a/packages/core/src/executors/code-execution/code-execution-tool-registrar.ts +++ b/packages/core/src/executors/code-execution/code-execution-tool-registrar.ts @@ -73,7 +73,7 @@ export function registerCodeExecutionTool( ? { type: "string", enum: allToolNames } : { type: "string" }, description: - `Tool names whose schemas were already provided in this conversation. List all tools you have schemas for to avoid duplicate schema requests`, + `Tool names whose schemas were already provided in this conversation. List all tools you have schemas for to avoid duplicate schema requests.`, }, }, }; From 312285b2b779ca7c7af7cb5e19009ef1d87b0187 Mon Sep 17 00:00:00 2001 From: yao <63141491+yaonyan@users.noreply.github.com> Date: Sun, 9 Nov 2025 10:01:48 +0800 Subject: [PATCH 4/5] chore: refactor import patterns and update to beta versions --- deno.lock | 24 +++++++++++++- packages/cli/deno.json | 4 +-- packages/core/deno.json | 2 +- packages/core/src/compose.ts | 10 +++--- packages/core/src/types.ts | 14 ++++---- packages/core/src/utils/compose-helpers.ts | 33 +++++-------------- .../examples/background_code_analysis.ts | 4 +-- 7 files changed, 49 insertions(+), 42 deletions(-) diff --git a/deno.lock b/deno.lock index baf5112..c6cdfec 100644 --- a/deno.lock +++ b/deno.lock @@ -2,6 +2,7 @@ "version": "5", "specifiers": { "jsr:@es-toolkit/es-toolkit@^1.37.2": "1.41.0", + "jsr:@mcpc/core@0.3": "0.3.1", "jsr:@std/assert@1": "1.0.14", "jsr:@std/assert@^1.0.14": "1.0.14", "jsr:@std/cli@^1.0.21": "1.0.21", @@ -45,6 +46,27 @@ "@es-toolkit/es-toolkit@1.41.0": { "integrity": "4df54a18e80b869880cee8a8a9ff7a5e1c424a9fd0916dccd38d34686f110071" }, + "@mcpc/core@0.3.1": { + "integrity": "440d96a5bc96742305c034dcaf15a381f7d599c8cdc94a9416b844294244e9ff", + "dependencies": [ + "jsr:@es-toolkit/es-toolkit", + "npm:@modelcontextprotocol/sdk", + "npm:@opentelemetry/api", + "npm:@opentelemetry/exporter-trace-otlp-http", + "npm:@opentelemetry/resources", + "npm:@opentelemetry/sdk-trace-base", + "npm:@opentelemetry/sdk-trace-node", + "npm:@opentelemetry/semantic-conventions", + "npm:@segment/ajv-human-errors", + "npm:ajv", + "npm:ajv-formats", + "npm:cheerio", + "npm:json-schema-to-zod", + "npm:json-schema-traverse", + "npm:jsonrepair", + "npm:zod" + ] + }, "@std/assert@1.0.14": { "integrity": "68d0d4a43b365abc927f45a9b85c639ea18a9fab96ad92281e493e4ed84abaa4", "dependencies": [ @@ -1312,7 +1334,7 @@ }, "packages/cli": { "dependencies": [ - "jsr:@mcpc/core@~0.3.1", + "jsr:@mcpc/core@~0.3.2-beta.1", "jsr:@mcpc/utils@~0.2.2", "jsr:@std/assert@^1.0.14", "jsr:@std/http@^1.0.14", diff --git a/packages/cli/deno.json b/packages/cli/deno.json index 64ee7a6..7121549 100644 --- a/packages/cli/deno.json +++ b/packages/cli/deno.json @@ -1,6 +1,6 @@ { "name": "@mcpc/cli", - "version": "0.1.11", + "version": "0.1.12-beta.1", "repository": { "type": "git", "url": "git+https://github.com/mcpc-tech/mcpc.git" @@ -12,7 +12,7 @@ "./app": "./src/app.ts" }, "imports": { - "@mcpc/core": "jsr:@mcpc/core@^0.3.1", + "@mcpc/core": "jsr:@mcpc/core@^0.3.2-beta.1", "@mcpc/utils": "jsr:@mcpc/utils@^0.2.2", "@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.8.0", "@mcpc-tech/ripgrep-napi": "npm:@mcpc-tech/ripgrep-napi@^0.0.4", diff --git a/packages/core/deno.json b/packages/core/deno.json index 02e000d..da1ba2f 100644 --- a/packages/core/deno.json +++ b/packages/core/deno.json @@ -1,6 +1,6 @@ { "name": "@mcpc/core", - "version": "0.3.1", + "version": "0.3.2-beta.1", "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 fe1e25d..9263e90 100644 --- a/packages/core/src/compose.ts +++ b/packages/core/src/compose.ts @@ -29,7 +29,10 @@ import { sortPluginsByOrder, validatePlugins } from "./plugin-utils.ts"; // Import new manager modules import { PluginManager } from "./utils/plugin-manager.ts"; import { ToolManager } from "./utils/tool-manager.ts"; -import { buildDependencyGroups } from "./utils/compose-helpers.ts"; +import { + buildDependencyGroups, + processToolsWithPlugins, +} from "./utils/compose-helpers.ts"; import { sanitizePropertyKey } from "./utils/common/provider.ts"; const ALL_TOOLS_PLACEHOLDER = "__ALL__"; @@ -344,10 +347,7 @@ export class ComposableMCPServer extends Server { externalTools: Record, mode: ExecutionMode, ): Promise { - const { processToolsWithPlugins: processTools } = await import( - "./utils/compose-helpers.ts" - ); - await processTools(this, externalTools, mode); + await processToolsWithPlugins(this, externalTools, mode); } /** diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 5f7c294..a1fc16a 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,3 +1,5 @@ +import type { MCPCStep, WorkflowState } from "./utils/state.ts"; + export type JSONSchema = Record; export type ToolCallback = (args: unknown, extra?: unknown) => unknown; @@ -20,7 +22,7 @@ export interface RegisterToolParams { } export interface RegisterWorkflowToolParams extends RegisterToolParams { - predefinedSteps?: import("./utils/state.ts").MCPCStep[]; + predefinedSteps?: MCPCStep[]; ensureStepActions?: string[]; toolNameToIdMapping?: Map; } @@ -36,9 +38,9 @@ export interface ArgsDefCreator { action: () => JSONSchema; forTool: () => JSONSchema; forCurrentState: ( - state: import("./utils/state.ts").WorkflowState, + state: WorkflowState, ) => JSONSchema; - forNextState: (state: import("./utils/state.ts").WorkflowState) => JSONSchema; + forNextState: (state: WorkflowState) => JSONSchema; forSampling: () => JSONSchema; forAgentic: ( toolNameToDetailList: [string, unknown][], @@ -48,11 +50,11 @@ export interface ArgsDefCreator { ) => JSONSchema; forToolDescription: ( description: string, - state: import("./utils/state.ts").WorkflowState, + state: WorkflowState, ) => string; forInitialStepDescription: ( - steps: import("./utils/state.ts").MCPCStep[], - state: import("./utils/state.ts").WorkflowState, + steps: MCPCStep[], + state: WorkflowState, ) => string; } diff --git a/packages/core/src/utils/compose-helpers.ts b/packages/core/src/utils/compose-helpers.ts index 7e0ffc4..022f58e 100644 --- a/packages/core/src/utils/compose-helpers.ts +++ b/packages/core/src/utils/compose-helpers.ts @@ -17,7 +17,7 @@ import { sanitizePropertyKey } from "./common/provider.ts"; */ export async function processToolsWithPlugins( server: ComposableMCPServer, - externalTools: Record, + _externalTools: Record, mode: ExecutionMode, ): Promise { const toolManager = (server as any).toolManager; @@ -54,25 +54,6 @@ export async function processToolsWithPlugins( processedTool.inputSchema as JSONSchema, processedTool.execute, ); - - if (externalTools[toolId]) { - // If a visibility processor is provided by built-in plugins, try to call it. - try { - const builtIn: any = await import("../plugins/built-in/index.ts"); - if (builtIn && typeof builtIn.processToolVisibility === "function") { - builtIn.processToolVisibility( - toolId, - processedTool, - server, - externalTools, - ); - } - } catch { - // ignore if not present - } - - externalTools[toolId] = processedTool; - } } } @@ -98,12 +79,12 @@ export function buildDependencyGroups( } if (!tool) { - const allToolNames = [ - ...toolNameToDetailList.map(([n]) => n), - ]; + const allToolNames = [...toolNameToDetailList.map(([n]) => n)]; throw new Error( `Action ${toolName} not found, available action list: ${ - allToolNames.join(", ") + allToolNames.join( + ", ", + ) }`, ); } @@ -153,7 +134,9 @@ export function registerGlobalTools( if (!tool) { throw new Error( `Global tool ${toolId} not found in registry, available: ${ - Object.keys(tools).join(", ") + Object.keys( + tools, + ).join(", ") }`, ); } diff --git a/packages/mcp-sampling-ai-provider/examples/background_code_analysis.ts b/packages/mcp-sampling-ai-provider/examples/background_code_analysis.ts index d08660e..5465f0f 100644 --- a/packages/mcp-sampling-ai-provider/examples/background_code_analysis.ts +++ b/packages/mcp-sampling-ai-provider/examples/background_code_analysis.ts @@ -21,6 +21,8 @@ import { createMCPSamplingProvider } from "../mod.ts"; import { generateText, jsonSchema, stepCountIs, tool } from "ai"; import process from "node:process"; import { convertToAISDKTools } from "../../core/src/ai-sdk-adapter.ts"; +import { promisify } from "node:util"; +import { execFile } from "node:child_process"; // Store analysis results const analysisResults = new Map< @@ -84,8 +86,6 @@ const server = await mcpc( // Get git diff for changed files async function getGitDiff(workDir: string, filePath?: string): Promise { - const { promisify } = await import("node:util"); - const { execFile } = await import("node:child_process"); const execFilePromise = promisify(execFile); const args = filePath ? ["diff", "HEAD", filePath] : ["diff", "HEAD"]; From 9662cbd0c37fe052ad1856be9f457c4c17bc19cf Mon Sep 17 00:00:00 2001 From: yao <63141491+yaonyan@users.noreply.github.com> Date: Sun, 9 Nov 2025 11:19:10 +0800 Subject: [PATCH 5/5] docs: add security warning comment in code-execution-executor.ts Add a TODO comment explaining that using new Function() with user-provided code introduces a code injection vulnerability. This clarifies the security risk for future reviewers and maintainers. --- .../src/executors/code-execution/code-execution-executor.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/executors/code-execution/code-execution-executor.ts b/packages/core/src/executors/code-execution/code-execution-executor.ts index 1467acc..94a7f55 100644 --- a/packages/core/src/executors/code-execution/code-execution-executor.ts +++ b/packages/core/src/executors/code-execution/code-execution-executor.ts @@ -241,7 +241,8 @@ export class CodeExecutionExecutor { return await this.server.callTool(toolName, params); }; - // Create and execute function with injected APIs + // Create and execute function with injected APIs, + // TODO: using new Function() with user-provided code creates a code injection vulnerability, using deno to sandbox would be safer. const fn = new Function( "console", "callMCPTool",