// Pi bundle MCP tools Docker harness. // Imports packaged dist modules so tool materialization is verified against the // npm tarball installed in the functional image. import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import { createRequire } from "node:module"; import os from "node:os"; import path from "node:path"; import { materializeBundleMcpToolsForRun } from "../../dist/agents/pi-bundle-mcp-materialize.js"; import { disposeAllSessionMcpRuntimes, getOrCreateSessionMcpRuntime, } from "../../dist/agents/pi-bundle-mcp-runtime.js"; import { applyFinalEffectiveToolPolicy } from "../../dist/agents/pi-embedded-runner/effective-tool-policy.js"; import { splitSdkTools } from "../../dist/agents/pi-embedded-runner/tool-split.js"; import type { OpenClawConfig } from "../../dist/config/types.openclaw.js"; import { getPluginToolMeta } from "../../dist/plugins/tools.js"; const require = createRequire(import.meta.url); function assert(condition: unknown, message: string): asserts condition { if (!condition) { throw new Error(message); } } async function writeProbeServer(serverPath: string) { const sdkMcpServerPath = require.resolve("@modelcontextprotocol/sdk/server/mcp.js"); const sdkStdioServerPath = require.resolve("@modelcontextprotocol/sdk/server/stdio.js"); await fs.writeFile( serverPath, `#!/usr/bin/env node import { McpServer } from ${JSON.stringify(sdkMcpServerPath)}; import { StdioServerTransport } from ${JSON.stringify(sdkStdioServerPath)}; const server = new McpServer({ name: "pi-bundle-mcp-tools-probe", version: "1.0.0" }); server.tool("docker_probe", "Docker Pi MCP tool availability probe", async () => ({ content: [{ type: "text", text: "pi-bundle-mcp-tools-ok" }], })); await server.connect(new StdioServerTransport()); `, { encoding: "utf-8", mode: 0o755 }, ); } function applyPolicy(params: { tools: Awaited>["tools"]; config: OpenClawConfig; }) { const warnings: string[] = []; return { tools: applyFinalEffectiveToolPolicy({ bundledTools: params.tools, config: params.config, sessionKey: "agent:main:docker-pi-bundle-mcp", agentId: "main", senderIsOwner: true, warn: (message) => { warnings.push(message); }, }), warnings, }; } async function main() { const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || path.join(os.tmpdir(), `openclaw-pi-bundle-mcp-${process.pid}`); const probeDir = path.join(stateDir, "pi-bundle-mcp-tools"); const serverPath = path.join(probeDir, "probe-server.mjs"); await fs.mkdir(probeDir, { recursive: true }); await writeProbeServer(serverPath); const cfg: OpenClawConfig = { tools: { profile: "coding", }, mcp: { servers: { dockerProbe: { command: "node", args: [serverPath], cwd: probeDir, connectionTimeoutMs: 5000, }, }, }, }; try { const runtime = await getOrCreateSessionMcpRuntime({ sessionId: `docker-pi-bundle-mcp-${randomUUID()}`, sessionKey: "agent:main:docker-pi-bundle-mcp", workspaceDir: probeDir, cfg, }); const materialized = await materializeBundleMcpToolsForRun({ runtime }); const probeTool = materialized.tools.find((tool) => tool.name === "dockerProbe__docker_probe"); assert(probeTool, "expected dockerProbe__docker_probe to materialize"); assert( getPluginToolMeta(probeTool)?.pluginId === "bundle-mcp", "expected materialized MCP tool to be tagged as bundle-mcp", ); const result = await probeTool.execute("docker-mcp-probe", {}, undefined, undefined); assert( result.content.some((item) => item.type === "text" && item.text === "pi-bundle-mcp-tools-ok"), "expected materialized MCP tool execution result", ); const coding = applyPolicy({ tools: materialized.tools, config: cfg }); assert( coding.tools.some((tool) => tool.name === probeTool.name), "expected coding profile to keep bundle MCP tools", ); const messaging = applyPolicy({ tools: materialized.tools, config: { ...cfg, tools: { profile: "messaging" } }, }); assert( messaging.tools.some((tool) => tool.name === probeTool.name), "expected messaging profile to keep bundle MCP tools", ); const minimal = applyPolicy({ tools: materialized.tools, config: { ...cfg, tools: { profile: "minimal" } }, }); assert(minimal.tools.length === 0, "expected minimal profile to filter bundle MCP tools"); const denied = applyPolicy({ tools: materialized.tools, config: { ...cfg, tools: { profile: "coding", deny: ["bundle-mcp"] } }, }); assert(denied.tools.length === 0, "expected tools.deny bundle-mcp to filter MCP tools"); // The disputed boundary on #76063 is what reaches the SDK as `customTools`, // since that is the exact value serialized to the outbound provider request. // Prove the live stdio probe survives the materialize -> filter -> split chain // through `splitSdkTools` for the same four profiles already asserted above. const codingCustom = splitSdkTools({ tools: coding.tools, sandboxEnabled: false }).customTools; const messagingCustom = splitSdkTools({ tools: messaging.tools, sandboxEnabled: false, }).customTools; const minimalCustom = splitSdkTools({ tools: minimal.tools, sandboxEnabled: false, }).customTools; const deniedCustom = splitSdkTools({ tools: denied.tools, sandboxEnabled: false }).customTools; assert( codingCustom.some((tool) => tool.name === probeTool.name), "expected coding profile customTools to include bundle MCP tools", ); assert( messagingCustom.some((tool) => tool.name === probeTool.name), "expected messaging profile customTools to include bundle MCP tools", ); assert( minimalCustom.length === 0, "expected minimal profile customTools to exclude bundle MCP tools", ); assert( deniedCustom.length === 0, "expected tools.deny bundle-mcp customTools to exclude bundle MCP tools", ); process.stdout.write( JSON.stringify( { ok: true, tool: probeTool.name, profileCounts: { coding: coding.tools.length, messaging: messaging.tools.length, minimal: minimal.tools.length, denied: denied.tools.length, }, customToolsCounts: { coding: codingCustom.length, messaging: messagingCustom.length, minimal: minimalCustom.length, denied: deniedCustom.length, }, customToolNames: { coding: codingCustom.map((tool) => tool.name), messaging: messagingCustom.map((tool) => tool.name), minimal: minimalCustom.map((tool) => tool.name), denied: deniedCustom.map((tool) => tool.name), }, }, null, 2, ) + "\n", ); } finally { await disposeAllSessionMcpRuntimes(); } } await main();