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