openclaw/src/agents/cli-runner/bundle-mcp.ts

121 lines
3.6 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import type { OpenClawConfig } from "../../config/config.js";
import { applyMergePatch } from "../../config/merge-patch.js";
import type { CliBackendConfig } from "../../config/types.js";
import {
extractMcpServerMap,
loadEnabledBundleMcpConfig,
type BundleMcpConfig,
} from "../../plugins/bundle-mcp.js";
type PreparedCliBundleMcpConfig = {
backend: CliBackendConfig;
cleanup?: () => Promise<void>;
};
async function readExternalMcpConfig(configPath: string): Promise<BundleMcpConfig> {
try {
const raw = JSON.parse(await fs.readFile(configPath, "utf-8")) as unknown;
return { mcpServers: extractMcpServerMap(raw) };
} catch {
return { mcpServers: {} };
}
}
function findMcpConfigPath(args?: string[]): string | undefined {
if (!args?.length) {
return undefined;
}
for (let i = 0; i < args.length; i += 1) {
const arg = args[i] ?? "";
if (arg === "--mcp-config") {
const next = args[i + 1];
return typeof next === "string" && next.trim() ? next.trim() : undefined;
}
if (arg.startsWith("--mcp-config=")) {
const inline = arg.slice("--mcp-config=".length).trim();
return inline || undefined;
}
}
return undefined;
}
function injectMcpConfigArgs(args: string[] | undefined, mcpConfigPath: string): string[] {
const next: string[] = [];
for (let i = 0; i < (args?.length ?? 0); i += 1) {
const arg = args?.[i] ?? "";
if (arg === "--strict-mcp-config") {
continue;
}
if (arg === "--mcp-config") {
i += 1;
continue;
}
if (arg.startsWith("--mcp-config=")) {
continue;
}
next.push(arg);
}
next.push("--strict-mcp-config", "--mcp-config", mcpConfigPath);
return next;
}
export async function prepareCliBundleMcpConfig(params: {
backendId: string;
backend: CliBackendConfig;
workspaceDir: string;
config?: OpenClawConfig;
warn?: (message: string) => void;
}): Promise<PreparedCliBundleMcpConfig> {
if (params.backendId !== "claude-cli") {
return { backend: params.backend };
}
const existingMcpConfigPath =
findMcpConfigPath(params.backend.resumeArgs) ?? findMcpConfigPath(params.backend.args);
let mergedConfig: BundleMcpConfig = { mcpServers: {} };
if (existingMcpConfigPath) {
const resolvedExistingPath = path.isAbsolute(existingMcpConfigPath)
? existingMcpConfigPath
: path.resolve(params.workspaceDir, existingMcpConfigPath);
mergedConfig = applyMergePatch(
mergedConfig,
await readExternalMcpConfig(resolvedExistingPath),
) as BundleMcpConfig;
}
const bundleConfig = loadEnabledBundleMcpConfig({
workspaceDir: params.workspaceDir,
cfg: params.config,
});
for (const diagnostic of bundleConfig.diagnostics) {
params.warn?.(`bundle MCP skipped for ${diagnostic.pluginId}: ${diagnostic.message}`);
}
mergedConfig = applyMergePatch(mergedConfig, bundleConfig.config) as BundleMcpConfig;
if (Object.keys(mergedConfig.mcpServers).length === 0) {
return { backend: params.backend };
}
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cli-mcp-"));
const mcpConfigPath = path.join(tempDir, "mcp.json");
await fs.writeFile(mcpConfigPath, `${JSON.stringify(mergedConfig, null, 2)}\n`, "utf-8");
return {
backend: {
...params.backend,
args: injectMcpConfigArgs(params.backend.args, mcpConfigPath),
resumeArgs: injectMcpConfigArgs(
params.backend.resumeArgs ?? params.backend.args ?? [],
mcpConfigPath,
),
},
cleanup: async () => {
await fs.rm(tempDir, { recursive: true, force: true });
},
};
}