File size: 5,057 Bytes
5c2a829 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | /**
* Standalone MCP server that exposes OpenClaw plugin-registered tools
* (e.g. memory-lancedb's memory_recall, memory_store, memory_forget)
* so ACP sessions running Claude Code can use them.
*
* Run via: node --import tsx src/mcp/plugin-tools-serve.ts
* Or: bun src/mcp/plugin-tools-serve.ts
*/
import { pathToFileURL } from "node:url";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { resolveEffectiveToolPolicy } from "../agents/agent-tools.policy.js";
import { pickSandboxToolPolicy } from "../agents/sandbox-tool-policy.js";
import {
applyToolPolicyPipeline,
buildDefaultToolPolicyPipelineSteps,
type ToolPolicyPipelineStep,
} from "../agents/tool-policy-pipeline.js";
import {
collectExplicitAllowlist,
collectExplicitDenylist,
mergeAlsoAllowPolicy,
resolveToolProfilePolicy,
} from "../agents/tool-policy.js";
import type { AnyAgentTool } from "../agents/tools/common.js";
import { getRuntimeConfig } from "../config/config.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { formatErrorMessage } from "../infra/errors.js";
import { logWarn } from "../logger.js";
import { routeLogsToStderr } from "../logging/console.js";
import type { LegacyPluginSdkResourceHost } from "../plugins/legacy-sdk-resource-host.js";
import { getPluginToolMeta } from "../plugins/tool-metadata.js";
import {
acquireStandalonePluginToolRegistry,
type PluginToolRegistryAcquisition,
} from "../plugins/tools.js";
import { resolveToolsMcpAgentId, resolveToolsMcpSessionContext } from "./agent-session-env.js";
import { createToolsMcpServer, serveRegisteredToolsMcpServer } from "./tools-stdio-server.js";
function resolvePluginToolPolicy(
config: OpenClawConfig,
context: ReturnType<typeof resolveToolsMcpSessionContext>,
): {
toolAllowlist?: string[];
toolDenylist?: string[];
steps?: ToolPolicyPipelineStep[];
} {
const effective = context.agentId
? resolveEffectiveToolPolicy({
config,
agentId: context.agentId,
sessionKey: context.sessionKey,
})
: undefined;
const profilePolicy = mergeAlsoAllowPolicy(
resolveToolProfilePolicy(effective?.profile ?? config.tools?.profile),
effective?.profileAlsoAllow ?? config.tools?.alsoAllow,
);
const globalPolicy = effective?.globalPolicy ?? pickSandboxToolPolicy(config.tools);
const steps = effective
? buildDefaultToolPolicyPipelineSteps({
profilePolicy,
profile: effective.profile,
globalPolicy,
agentPolicy: effective.agentPolicy,
agentId: effective.agentId,
}).map((step) =>
Object.assign({}, step, {
// This bridge exposes only plugin tools, so core-tool entries are absent by design.
suppressUnavailableCoreToolWarning: true,
}),
)
: undefined;
const policies = steps?.map((step) => step.policy) ?? [profilePolicy, globalPolicy];
const toolAllowlist = collectExplicitAllowlist(policies);
const toolDenylist = collectExplicitDenylist(policies);
return {
...(toolAllowlist.length > 0 ? { toolAllowlist } : {}),
...(toolDenylist.length > 0 ? { toolDenylist } : {}),
steps,
};
}
export async function acquirePluginToolsForMcp(params: {
config: OpenClawConfig;
agentSessionKey?: string;
agentId?: string;
}): Promise<PluginToolRegistryAcquisition> {
const sessionContext = resolveToolsMcpSessionContext(params);
const context = { config: params.config, ...sessionContext };
const { steps, ...pluginToolPolicy } = resolvePluginToolPolicy(params.config, sessionContext);
const acquisition = await acquireStandalonePluginToolRegistry({
context,
...pluginToolPolicy,
suppressNameConflicts: true,
});
return {
...acquisition,
resolveTools: () => {
const tools = acquisition.resolveTools();
return steps
? applyToolPolicyPipeline({ tools, toolMeta: getPluginToolMeta, warn: logWarn, steps })
: tools;
},
};
}
export function createPluginToolsMcpServer(params: {
tools: AnyAgentTool[];
sdkResourceHost?: LegacyPluginSdkResourceHost;
}): Server {
return createToolsMcpServer({ name: "openclaw-plugin-tools", ...params });
}
export async function servePluginToolsMcp(): Promise<void> {
// MCP stdio requires stdout to stay protocol-only, including during plugin
// tool discovery before the transport is connected.
routeLogsToStderr();
await serveRegisteredToolsMcpServer({
acquireRegistry: () =>
acquirePluginToolsForMcp({ config: getRuntimeConfig(), agentId: resolveToolsMcpAgentId() }),
createServer: (tools, sdkResourceHost) => {
if (tools.length === 0) {
process.stderr.write("plugin-tools-serve: no plugin tools found\n");
}
return createPluginToolsMcpServer({ tools, sdkResourceHost });
},
});
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
servePluginToolsMcp().catch((err: unknown) => {
process.stderr.write(`plugin-tools-serve: ${formatErrorMessage(err)}\n`);
process.exit(1);
});
}
|