fix(agents): expose configured MCP tools in Pi profiles

This commit is contained in:
Peter Steinberger
2026-04-23 00:46:27 +01:00
parent bba63d4e78
commit c4e5ca8625
17 changed files with 301 additions and 25 deletions
+5
View File
@@ -2,6 +2,7 @@ import { spawn, type ChildProcess } from "node:child_process";
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { logDebug, logWarn } from "../logger.js";
import { setPluginToolMeta } from "../plugins/tools.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import { loadEmbeddedPiLspConfig } from "./embedded-pi-lsp.js";
import {
@@ -368,6 +369,10 @@ export async function createBundleLspToolRuntime(params: {
continue;
}
reservedNames.add(normalizedName);
setPluginToolMeta(tool, {
pluginId: "bundle-lsp",
optional: false,
});
tools.push(tool);
}
+8 -1
View File
@@ -3,6 +3,7 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { logWarn } from "../logger.js";
import { setPluginToolMeta } from "../plugins/tools.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import {
buildSafeToolName,
@@ -10,6 +11,7 @@ import {
TOOL_NAME_SEPARATOR,
} from "./pi-bundle-mcp-names.js";
import type { BundleMcpToolRuntime, SessionMcpRuntime } from "./pi-bundle-mcp-types.js";
import type { AnyAgentTool } from "./tools/common.js";
function toAgentToolResult(params: {
serverName: string;
@@ -96,7 +98,7 @@ export async function materializeBundleMcpToolsForRun(params: {
);
}
reservedNames.add(normalizeLowercaseStringOrEmpty(safeToolName));
tools.push({
const agentTool: AnyAgentTool = {
name: safeToolName,
label: tool.title ?? tool.toolName,
description: tool.description || tool.fallbackDescription,
@@ -109,7 +111,12 @@ export async function materializeBundleMcpToolsForRun(params: {
result,
});
},
};
setPluginToolMeta(agentTool, {
pluginId: "bundle-mcp",
optional: false,
});
tools.push(agentTool);
}
// Sort tools deterministically by name so the tools block in API requests is stable across
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { getPluginToolMeta } from "../plugins/tools.js";
import {
createBundleMcpToolRuntime,
materializeBundleMcpToolsForRun,
@@ -58,6 +59,7 @@ describe("createBundleMcpToolRuntime", () => {
});
expect(runtime.tools.map((tool) => tool.name)).toEqual(["bundleProbe__bundle_probe"]);
expect(getPluginToolMeta(runtime.tools[0])?.pluginId).toBe("bundle-mcp");
const result = await runtime.tools[0].execute("call-bundle-probe", {}, undefined, undefined);
expect(result.content[0]).toMatchObject({
type: "text",
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
import path from "node:path";
import "./test-helpers/fast-coding-tools.js";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { setPluginToolMeta } from "../plugins/tools.js";
import {
cleanupEmbeddedPiRunnerTestWorkspace,
createEmbeddedPiRunnerOpenAiConfig,
@@ -53,24 +54,26 @@ vi.mock("./pi-bundle-mcp-tools.js", () => ({
}),
dispose: async () => {},
}),
materializeBundleMcpToolsForRun: async () => ({
tools: [
{
name: "bundleProbe__bundle_probe",
label: "bundle_probe",
description: "Bundle MCP probe",
parameters: { type: "object", properties: {} },
execute: async () => ({
content: [{ type: "text", text: "FROM-BUNDLE" }],
details: {
mcpServer: "bundleProbe",
mcpTool: "bundle_probe",
},
}),
},
],
dispose: async () => {},
}),
materializeBundleMcpToolsForRun: async () => {
const tool = {
name: "bundleProbe__bundle_probe",
label: "bundle_probe",
description: "Bundle MCP probe",
parameters: { type: "object", properties: {} },
execute: async () => ({
content: [{ type: "text", text: "FROM-BUNDLE" }],
details: {
mcpServer: "bundleProbe",
mcpTool: "bundle_probe",
},
}),
};
setPluginToolMeta(tool as any, { pluginId: "bundle-mcp", optional: false });
return {
tools: [tool],
dispose: async () => {},
};
},
}));
vi.mock("@mariozechner/pi-ai", async () => {
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { setPluginToolMeta } from "../../plugins/tools.js";
import type { AnyAgentTool } from "../tools/common.js";
import { applyFinalEffectiveToolPolicy } from "./effective-tool-policy.js";
@@ -117,4 +118,30 @@ describe("applyFinalEffectiveToolPolicy", () => {
expect(warnings.some((w) => w.includes("totally-made-up-tool"))).toBe(true);
});
it("keeps bundle MCP tools in the coding profile via plugin metadata", () => {
const mcpTool = makeTool("bundleProbe__bundle_probe");
setPluginToolMeta(mcpTool, { pluginId: "bundle-mcp", optional: false });
const filtered = applyFinalEffectiveToolPolicy({
bundledTools: [mcpTool],
config: { tools: { profile: "coding" } },
warn: () => {},
});
expect(filtered.map((tool) => tool.name)).toEqual(["bundleProbe__bundle_probe"]);
});
it("lets explicit deny entries override the profile bundle MCP allowlist", () => {
const mcpTool = makeTool("bundleProbe__bundle_probe");
setPluginToolMeta(mcpTool, { pluginId: "bundle-mcp", optional: false });
const filtered = applyFinalEffectiveToolPolicy({
bundledTools: [mcpTool],
config: { tools: { profile: "coding", deny: ["bundle-mcp"] } },
warn: () => {},
});
expect(filtered).toEqual([]);
});
});
+6
View File
@@ -14,4 +14,10 @@ describe("tool-catalog", () => {
expect(policy!.allow).toContain("video_generate");
expect(policy!.allow).toContain("update_plan");
});
it("includes bundle MCP tools in coding and messaging profile policies", () => {
expect(resolveCoreToolProfilePolicy("coding")?.allow).toContain("bundle-mcp");
expect(resolveCoreToolProfilePolicy("messaging")?.allow).toContain("bundle-mcp");
expect(resolveCoreToolProfilePolicy("minimal")?.allow).not.toContain("bundle-mcp");
});
});
+2 -2
View File
@@ -318,10 +318,10 @@ const CORE_TOOL_PROFILES: Record<ToolProfileId, ToolProfilePolicy> = {
allow: listCoreToolIdsForProfile("minimal"),
},
coding: {
allow: listCoreToolIdsForProfile("coding"),
allow: [...listCoreToolIdsForProfile("coding"), "bundle-mcp"],
},
messaging: {
allow: listCoreToolIdsForProfile("messaging"),
allow: [...listCoreToolIdsForProfile("messaging"), "bundle-mcp"],
},
full: {},
};
+3 -1
View File
@@ -128,7 +128,9 @@ export function applyToolPolicyPipeline(params: {
const warnableGatedCoreEntries = step.suppressUnavailableCoreToolWarning
? []
: gatedCoreEntries.filter((entry) => !unavailableCoreWarningAllowlist.has(entry));
const otherEntries = resolved.unknownAllowlist.filter((entry) => !isKnownCoreToolId(entry));
const otherEntries = resolved.unknownAllowlist.filter(
(entry) => !isKnownCoreToolId(entry) && !unavailableCoreWarningAllowlist.has(entry),
);
const warningEntries = [...warnableGatedCoreEntries, ...otherEntries];
if (
shouldWarnAboutUnknownAllowlist({
+5 -1
View File
@@ -13,13 +13,17 @@ import {
} from "./runtime/load-context.js";
import type { OpenClawPluginToolContext } from "./types.js";
type PluginToolMeta = {
export type PluginToolMeta = {
pluginId: string;
optional: boolean;
};
const pluginToolMeta = new WeakMap<AnyAgentTool, PluginToolMeta>();
export function setPluginToolMeta(tool: AnyAgentTool, meta: PluginToolMeta): void {
pluginToolMeta.set(tool, meta);
}
export function getPluginToolMeta(tool: AnyAgentTool): PluginToolMeta | undefined {
return pluginToolMeta.get(tool);
}