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"];