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

View File

@@ -33,6 +33,9 @@ Docs: https://docs.openclaw.ai
- Providers/Amazon Bedrock Mantle: refresh IAM-backed bearer tokens at runtime instead of baking discovery-time tokens into provider config, so long-lived Mantle sessions keep working after the initial token ages out. Thanks @wirjo.
- Config/includes: write through single-file top-level includes for isolated OpenClaw-owned mutations, so `plugins install` and `plugins update` update an included `plugins.json5` file instead of flattening modular `$include` configs. Fixes #41050 and #66048.
- Config/reload: plan gateway reloads from source-authored config instead of runtime-materialized snapshots, so plugin update writes no longer trigger false restarts from derived provider/plugin config paths. Fixes #68732.
- Agents/MCP: keep `mcp.servers` and bundle MCP tools available in Pi embedded
`coding` and `messaging` sessions while preserving `minimal` profile and
`tools.deny: ["bundle-mcp"]` opt-out behavior. Fixes #68875 and #68818.
- Codex harness: rotate the shared app-server websocket client when the configured bearer token changes, so auth-token refreshes reconnect with the new `Authorization` header instead of reusing a stale socket. (#70328) Thanks @Lucenx9.
- Telegram/sandbox: keep Telegram bot DMs on per-account sender session keys even when `session.dmScope=main`, so sandbox/tool policy can distinguish Telegram-originated direct chats from the agent main session.
- Config/models: merge provider-scoped model allowlist updates and protect model/provider map writes from accidental full replacement, adding `config set --merge` for additive updates and `--replace` for intentional clobbers. Fixes #65920, #68392, and #68653.

View File

@@ -369,6 +369,9 @@ Important behavior:
reachable right now
- runtime adapters decide which transport shapes they actually support at
execution time
- embedded Pi exposes configured MCP tools in normal `coding` and `messaging`
tool profiles; `minimal` still hides them, and `tools.deny: ["bundle-mcp"]`
disables them explicitly
## Saved MCP server definitions

View File

@@ -882,7 +882,7 @@ These Docker runners split into two buckets:
`OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=90000`. Override those env vars when you
explicitly want the larger exhaustive scan.
- `test:docker:all` builds the live Docker image once via `test:docker:live-build`, then reuses it for the two live Docker lanes.
- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:gateway-network`, `test:docker:mcp-channels`, and `test:docker:plugins` boot one or more real containers and verify higher-level integration paths.
- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:gateway-network`, `test:docker:mcp-channels`, `test:docker:pi-bundle-mcp-tools`, and `test:docker:plugins` boot one or more real containers and verify higher-level integration paths.
The live-model Docker runners also bind-mount only the needed CLI auth homes (or all supported ones when the run is not narrowed), then copy them into the container home before the run so external-CLI OAuth can refresh tokens without mutating the host auth store:
@@ -895,6 +895,7 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or
- Onboarding wizard (TTY, full scaffolding): `pnpm test:docker:onboard` (script: `scripts/e2e/onboard-docker.sh`)
- Gateway networking (two containers, WS auth + health): `pnpm test:docker:gateway-network` (script: `scripts/e2e/gateway-network-docker.sh`)
- MCP channel bridge (seeded Gateway + stdio bridge + raw Claude notification-frame smoke): `pnpm test:docker:mcp-channels` (script: `scripts/e2e/mcp-channels-docker.sh`)
- Pi bundle MCP tools (real stdio MCP server + embedded Pi profile allow/deny smoke): `pnpm test:docker:pi-bundle-mcp-tools` (script: `scripts/e2e/pi-bundle-mcp-tools-docker.sh`)
- Plugins (install smoke + `/plugin` alias + Claude-bundle restart semantics): `pnpm test:docker:plugins` (script: `scripts/e2e/plugins-docker.sh`)
- Bundled plugin runtime deps: `pnpm test:docker:bundled-channel-deps` builds a small Docker runner image by default, builds and packs OpenClaw once on the host, then mounts that tarball into each Linux install scenario. Reuse the image with `OPENCLAW_SKIP_DOCKER_BUILD=1`, skip the host rebuild after a fresh local build with `OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD=0`, or point at an existing tarball with `OPENCLAW_BUNDLED_CHANNEL_PACKAGE_TGZ=/path/to/openclaw-*.tgz`.
- Narrow bundled plugin runtime deps while iterating by disabling unrelated scenarios, for example:
@@ -931,6 +932,11 @@ live event queue behavior, outbound send routing, and Claude-style channel +
permission notifications over the real stdio MCP bridge. The notification check
inspects the raw stdio MCP frames directly so the smoke validates what the
bridge actually emits, not just what a specific client SDK happens to surface.
`test:docker:pi-bundle-mcp-tools` is deterministic and does not need a live
model key. It builds the repo Docker image, starts a real stdio MCP probe server
inside the container, materializes that server through the embedded Pi bundle
MCP runtime, executes the tool, then verifies `coding` and `messaging` keep
`bundle-mcp` tools while `minimal` and `tools.deny: ["bundle-mcp"]` filter them.
Manual ACP plain-language thread smoke (not CI):

View File

@@ -104,6 +104,8 @@ loader. Cursor command markdown works through the same path.
`mcpServers`
- OpenClaw exposes supported bundle MCP tools during embedded Pi agent turns by
launching stdio servers or connecting to HTTP servers
- the `coding` and `messaging` tool profiles include bundle MCP tools by
default; use `tools.deny: ["bundle-mcp"]` to opt out for an agent or gateway
- project-local Pi settings still apply after bundle defaults, so workspace
settings can override bundle MCP entries when needed
- bundle MCP tool catalogs are sorted deterministically before registration, so
@@ -170,6 +172,9 @@ OpenClaw registers bundle MCP tools with provider-safe names in the form
- colliding sanitized names are disambiguated with numeric suffixes
- final exposed tool order is deterministic by safe name to keep repeated Pi
turns cache-stable
- profile filtering treats all tools from one bundle MCP server as plugin-owned
by `bundle-mcp`, so profile allowlists and deny lists can include either
individual exposed tool names or the `bundle-mcp` plugin key
#### Embedded Pi settings

View File

@@ -139,6 +139,11 @@ Per-agent override: `agents.list[].tools.profile`.
| `messaging` | `group:messaging`, `sessions_list`, `sessions_history`, `sessions_send`, `session_status` |
| `minimal` | `session_status` only |
The `coding` and `messaging` profiles also allow configured bundle MCP tools
under the plugin key `bundle-mcp`. Add `tools.deny: ["bundle-mcp"]` when you
want a profile to keep its normal built-ins but hide all configured MCP tools.
The `minimal` profile does not include bundle MCP tools.
### Tool groups
Use `group:*` shorthands in allow/deny lists:

View File

@@ -1412,7 +1412,7 @@
"test:contracts:plugins": "node scripts/run-vitest.mjs run --config test/vitest/vitest.contracts-plugin.config.ts --maxWorkers=1",
"test:coverage": "node scripts/run-vitest.mjs run --config test/vitest/vitest.unit.config.ts --coverage",
"test:coverage:changed": "node scripts/run-vitest.mjs run --config test/vitest/vitest.unit.config.ts --coverage --changed origin/main",
"test:docker:all": "pnpm test:docker:live-build && OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-models && OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-gateway && pnpm test:docker:openwebui && pnpm test:docker:onboard && pnpm test:docker:gateway-network && pnpm test:docker:mcp-channels && pnpm test:docker:cron-mcp-cleanup && pnpm test:docker:qr && pnpm test:docker:doctor-switch && pnpm test:docker:plugins && pnpm test:docker:bundled-channel-deps && pnpm test:docker:cleanup",
"test:docker:all": "pnpm test:docker:live-build && OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-models && OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-gateway && pnpm test:docker:openwebui && pnpm test:docker:onboard && pnpm test:docker:gateway-network && pnpm test:docker:mcp-channels && pnpm test:docker:pi-bundle-mcp-tools && pnpm test:docker:cron-mcp-cleanup && pnpm test:docker:qr && pnpm test:docker:doctor-switch && pnpm test:docker:plugins && pnpm test:docker:bundled-channel-deps && pnpm test:docker:cleanup",
"test:docker:bundled-channel-deps": "bash scripts/e2e/bundled-channel-runtime-deps-docker.sh",
"test:docker:cleanup": "bash scripts/test-cleanup-docker.sh",
"test:docker:cron-mcp-cleanup": "bash scripts/e2e/cron-mcp-cleanup-docker.sh",
@@ -1440,6 +1440,7 @@
"test:docker:mcp-channels": "bash scripts/e2e/mcp-channels-docker.sh",
"test:docker:onboard": "bash scripts/e2e/onboard-docker.sh",
"test:docker:openwebui": "bash scripts/e2e/openwebui-docker.sh",
"test:docker:pi-bundle-mcp-tools": "bash scripts/e2e/pi-bundle-mcp-tools-docker.sh",
"test:docker:plugins": "bash scripts/e2e/plugins-docker.sh",
"test:docker:qr": "bash scripts/e2e/qr-import-docker.sh",
"test:e2e": "node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts",

View File

@@ -0,0 +1,157 @@
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 "../../src/agents/pi-bundle-mcp-materialize.ts";
import {
disposeAllSessionMcpRuntimes,
getOrCreateSessionMcpRuntime,
} from "../../src/agents/pi-bundle-mcp-runtime.ts";
import { applyFinalEffectiveToolPolicy } from "../../src/agents/pi-embedded-runner/effective-tool-policy.ts";
import type { OpenClawConfig } from "../../src/config/types.openclaw.ts";
import { getPluginToolMeta } from "../../src/plugins/tools.ts";
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<ReturnType<typeof materializeBundleMcpToolsForRun>>["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");
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,
},
},
null,
2,
) + "\n",
);
} finally {
await disposeAllSessionMcpRuntimes();
}
}
await main();

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh"
IMAGE_NAME="${OPENCLAW_IMAGE:-openclaw-pi-bundle-mcp-tools-e2e}"
CONTAINER_NAME="openclaw-pi-bundle-mcp-tools-e2e-$$"
RUN_LOG="$(mktemp -t openclaw-pi-bundle-mcp-tools-log.XXXXXX)"
cleanup() {
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
rm -f "$RUN_LOG"
}
trap cleanup EXIT
if [ "${OPENCLAW_SKIP_DOCKER_BUILD:-0}" != "1" ]; then
echo "Building Docker image..."
run_logged pi-bundle-mcp-tools-build docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR"
fi
echo "Running in-container Pi bundle MCP tool availability smoke..."
set +e
docker run --rm \
--name "$CONTAINER_NAME" \
-e "OPENCLAW_STATE_DIR=/tmp/openclaw-state" \
"$IMAGE_NAME" \
bash -lc "set -euo pipefail
node --import tsx scripts/e2e/pi-bundle-mcp-tools-docker-client.ts
" >"$RUN_LOG" 2>&1
status=${PIPESTATUS[0]}
set -e
if [ "$status" -ne 0 ]; then
echo "Docker Pi bundle MCP tool availability smoke failed"
cat "$RUN_LOG"
exit "$status"
fi
cat "$RUN_LOG"
echo "OK"

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);
}

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

View File

@@ -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",

View File

@@ -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 () => {

View File

@@ -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([]);
});
});

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");
});
});

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: {},
};

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({

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);
}