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/examples/14-code-execution-mode.ts b/packages/core/examples/14-code-execution-mode.ts new file mode 100644 index 0000000..7645530 --- /dev/null +++ b/packages/core/examples/14-code-execution-mode.ts @@ -0,0 +1,83 @@ +/** + * Example: Code Execution Mode (KISS Pattern) + * + * Demonstrates the simplified code execution pattern with clear parameter names. + * This example shows how to: + * 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 + * + * 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 + * + * 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"; +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/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/executors/agentic/agentic-executor.ts b/packages/core/src/executors/agentic/agentic-executor.ts index 53c7666..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, -}); -// @ts-ignore - -addFormats(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 new file mode 100644 index 0000000..94a7f55 --- /dev/null +++ b/packages/core/src/executors/code-execution/code-execution-executor.ts @@ -0,0 +1,350 @@ +/** + * 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 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"; + +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 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, + hasCode: Boolean(args.code), + needsDefinitions: needsDefinitions.length > 0, + }, + parentSpan ?? undefined, + ) + : null; + + try { + const validationResult = validateSchema(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 hasCode = Boolean(args.code); + + // Build combined result + const contentParts: Array<{ type: "text"; text: string }> = []; + + if (hasCode && hasDefinitions.length > 0) { + const codeResult = await this.handleExecuteCode(args, executeSpan); + + // If code execution failed, return error immediately + if (codeResult.isError) { + if (executeSpan) { + endSpan(executeSpan); + } + return codeResult; + } + + if (codeResult.content) { + contentParts.push( + ...codeResult.content.filter((c) => c.type === "text"), + ); + } + } + + // 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"), + ); + } + + if (executeSpan) { + executeSpan.setAttribute("toolsRequested", needsDefinitions.length); + } + } + + if (executeSpan) { + endSpan(executeSpan); + } + + 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); + 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, + }; + } + } + + /** + * 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, + // 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", + `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), use console.log() to log 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, + }; + } + } + + /** + * Get tool definitions for the specified tool names + * Returns schemas that describe how to call these tools + */ + private getToolDefinitions(toolNames: string[]): CallToolResult { + const definitions: Array<{ name: string; schema: unknown }> = []; + const notFound: string[] = []; + + 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); + } + } + + 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 new file mode 100644 index 0000000..b816858 --- /dev/null +++ b/packages/core/src/executors/code-execution/code-execution-tool-registrar.ts @@ -0,0 +1,116 @@ +/** + * 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 + // Both parameters can be used together for maximum efficiency + const schema: Schema<{ + code?: string; + definitionsOf?: string[]; + hasDefinitions?: string[]; + }>["jsonSchema"] = { + type: "object", + properties: { + code: { + type: "string", + description: + "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.", + }, + definitionsOf: { + type: "array", + items: allToolNames.length > 0 + ? { type: "string", enum: allToolNames } + : { type: "string" }, + default: [], + description: + `Tool names whose schemas you need. The agent uses these to understand available tools before calling them.`, + }, + hasDefinitions: { + type: "array", + items: allToolNames.length > 0 + ? { 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.`, + }, + }, + }; + + server.tool( + name, + description, + jsonSchema>( + createGoogleCompatibleJSONSchema(schema as Record), + ), + async (args: 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 2243193..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, -}); -// @ts-ignore - -addFormats(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 f236e58..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, -}); -// @ts-ignore - -addFormats(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/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..37fef89 100644 --- a/packages/core/src/prompts/index.ts +++ b/packages/core/src/prompts/index.ts @@ -167,6 +167,55 @@ Rules: - Omit \`steps\` during step execution - Use \`decision: "retry"\` for failed steps `, + + /** + * Code Execution system prompt - progressive disclosure pattern + * + * Simple pattern: Get definitions, then execute code + */ + CODE_EXECUTION: + `Agentic tool \`{toolName}\` executes JavaScript code with MCP tool access. + + +{description} + + + +\`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 + + + +- **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** + + + +Initial definition request: +\`\`\`json +{ + "hasDefinitions": [], + "definitionsOf": ["tool1"] +} +\`\`\` +Execute code + get new definitions: +\`\`\`json +{ + "code": "await callMCPTool('tool1', {x: 1});", + "hasDefinitions": ["tool1"], + "definitionsOf": ["tool2"] +} +\`\`\` +`, }; /** @@ -328,6 +377,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/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/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 new file mode 100644 index 0000000..94b3edf --- /dev/null +++ b/packages/core/tests/executors/code_execution_test.ts @@ -0,0 +1,237 @@ +/** + * Test for Code Execution Mode + */ + +import { assertEquals, assertStringIncludes } from "@std/assert"; +import { mcpc } from "../../mod.ts"; + +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 with ", + deps: { + mcpServers: {}, + }, + options: { + mode: "code_execution", + }, + }], + ); + + // 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", { + definitionsOf: ["test-tool"], + }); + + assertEquals(result.isError, undefined); + assertStringIncludes( + String(result.content[0].text), + "test-tool", + ); +}); + +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 with ", + deps: { + mcpServers: {}, + }, + options: { + mode: "code_execution", + }, + }], + ); + + // 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", { + code: "console.log('Hello from code execution!'); return 42;", + hasDefinitions: ["test-tool"], + }); + + assertEquals(result.isError, undefined); + assertStringIncludes( + String(result.content[0].text), + "Hello from code execution!", + ); +}); + +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 with ", + deps: { + mcpServers: {}, + }, + options: { + mode: "code_execution", + }, + }], + ); + + // 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", { + 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), "Sum: 15"); + assertStringIncludes(String(result.content[0].text), "Average: 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 with ", + deps: { + mcpServers: {}, + }, + options: { + mode: "code_execution", + }, + }], + ); + + // 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", { + code: "const x = ; // Syntax error", + hasDefinitions: ["test-tool"], + }); + + assertEquals(result.isError, true); + assertStringIncludes(String(result.content[0].text).toLowerCase(), "error"); +}); + +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 with and ", + deps: { + mcpServers: {}, + }, + options: { + mode: "code_execution", + }, + }], + ); + + // 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", { + code: "console.log('Executing with test-tool');", + hasDefinitions: ["test-tool"], + definitionsOf: ["another-tool"], + }); + + 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 - 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: {}, + }, + options: { + mode: "code_execution", + }, + }], + ); + + const result: any = await server.callTool("test-agent", { + code: "console.log('test');", + // Missing hasDefinitions - should fail validation + }); + + assertEquals(result.isError, true); + assertStringIncludes(String(result.content[0].text), "Validation failed"); +}); 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"];