mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
@@ -5,21 +5,40 @@ import fs from "node:fs/promises";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
|
||||
/** Find an existing Claude `--mcp-config` argument value. */
|
||||
export function findClaudeMcpConfigPath(args?: string[]): string | undefined {
|
||||
/** Find existing Claude `--mcp-config` argument values. */
|
||||
export function findClaudeMcpConfigPaths(args?: string[]): string[] {
|
||||
const paths: string[] = [];
|
||||
if (!args?.length) {
|
||||
return undefined;
|
||||
return paths;
|
||||
}
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const arg = args[i] ?? "";
|
||||
if (arg === "--mcp-config") {
|
||||
return normalizeOptionalString(args[i + 1]);
|
||||
// Claude treats --mcp-config as variadic. Keep this scan aligned with
|
||||
// extensions/anthropic/cli-shared.ts so user config files are not leaked
|
||||
// as positional prompts after OpenClaw injects its strict overlay.
|
||||
while (typeof args[i + 1] === "string" && !args[i + 1]?.startsWith("-")) {
|
||||
i += 1;
|
||||
const path = normalizeOptionalString(args[i]);
|
||||
if (path) {
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--mcp-config=")) {
|
||||
return normalizeOptionalString(arg.slice("--mcp-config=".length));
|
||||
const path = normalizeOptionalString(arg.slice("--mcp-config=".length));
|
||||
if (path) {
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
return paths;
|
||||
}
|
||||
|
||||
/** Find an existing Claude `--mcp-config` argument value. */
|
||||
export function findClaudeMcpConfigPath(args?: string[]): string | undefined {
|
||||
return findClaudeMcpConfigPaths(args)[0];
|
||||
}
|
||||
|
||||
/** Return Claude args with OpenClaw's strict MCP config path injected. */
|
||||
@@ -34,7 +53,9 @@ export function injectClaudeMcpConfigArgs(
|
||||
continue;
|
||||
}
|
||||
if (arg === "--mcp-config") {
|
||||
i += 1;
|
||||
while (typeof args?.[i + 1] === "string" && !args[i + 1]?.startsWith("-")) {
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--mcp-config=")) {
|
||||
|
||||
@@ -58,6 +58,127 @@ describe("prepareCliBundleMcpConfig", () => {
|
||||
await prepared.cleanup?.();
|
||||
});
|
||||
|
||||
it("strips variadic Claude --mcp-config values and merges every listed config", async () => {
|
||||
const workspaceDir = await cliBundleMcpHarness.tempHarness.createTempDir(
|
||||
"openclaw-cli-bundle-mcp-variadic-",
|
||||
);
|
||||
const firstConfig = path.join(workspaceDir, "first-mcp.json");
|
||||
const secondConfig = path.join(workspaceDir, "second-mcp.json");
|
||||
await fs.writeFile(
|
||||
firstConfig,
|
||||
`${JSON.stringify({
|
||||
mcpServers: {
|
||||
first: { command: "node", args: ["first.mjs"] },
|
||||
shared: { command: "node", args: ["old.mjs"] },
|
||||
},
|
||||
})}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
secondConfig,
|
||||
`${JSON.stringify({
|
||||
mcpServers: {
|
||||
second: { command: "node", args: ["second.mjs"] },
|
||||
shared: { command: "node", args: ["new.mjs"] },
|
||||
},
|
||||
})}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const prepared = await prepareCliBundleMcpConfig({
|
||||
enabled: true,
|
||||
mode: "claude-config-file",
|
||||
backend: {
|
||||
command: "node",
|
||||
args: [
|
||||
"./fake-claude.mjs",
|
||||
"--mcp-config",
|
||||
"first-mcp.json",
|
||||
"second-mcp.json",
|
||||
"--verbose",
|
||||
],
|
||||
},
|
||||
workspaceDir,
|
||||
config: { plugins: { enabled: false } },
|
||||
});
|
||||
|
||||
expect(prepared.backend.args).not.toContain("first-mcp.json");
|
||||
expect(prepared.backend.args).not.toContain("second-mcp.json");
|
||||
expect(prepared.backend.args).toContain("--verbose");
|
||||
const generatedConfigPath = requireMcpConfigPath(prepared.backend.args);
|
||||
const raw = JSON.parse(await fs.readFile(generatedConfigPath, "utf-8")) as {
|
||||
mcpServers?: Record<string, { args?: string[] }>;
|
||||
};
|
||||
expect(raw.mcpServers?.first?.args).toEqual(["first.mjs"]);
|
||||
expect(raw.mcpServers?.second?.args).toEqual(["second.mjs"]);
|
||||
expect(raw.mcpServers?.shared?.args).toEqual(["new.mjs"]);
|
||||
|
||||
await prepared.cleanup?.();
|
||||
});
|
||||
|
||||
it("merges and strips Claude --mcp-config equals form", async () => {
|
||||
const workspaceDir = await cliBundleMcpHarness.tempHarness.createTempDir(
|
||||
"openclaw-cli-bundle-mcp-equals-",
|
||||
);
|
||||
const configPath = path.join(workspaceDir, "equals-mcp.json");
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
`${JSON.stringify({
|
||||
mcpServers: {
|
||||
equals: { command: "node", args: ["equals.mjs"] },
|
||||
},
|
||||
})}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const prepared = await prepareCliBundleMcpConfig({
|
||||
enabled: true,
|
||||
mode: "claude-config-file",
|
||||
backend: {
|
||||
command: "node",
|
||||
args: ["./fake-claude.mjs", "--mcp-config=equals-mcp.json"],
|
||||
},
|
||||
workspaceDir,
|
||||
config: { plugins: { enabled: false } },
|
||||
});
|
||||
|
||||
expect(prepared.backend.args).not.toContain("--mcp-config=equals-mcp.json");
|
||||
const generatedConfigPath = requireMcpConfigPath(prepared.backend.args);
|
||||
const raw = JSON.parse(await fs.readFile(generatedConfigPath, "utf-8")) as {
|
||||
mcpServers?: Record<string, { args?: string[] }>;
|
||||
};
|
||||
expect(raw.mcpServers?.equals?.args).toEqual(["equals.mjs"]);
|
||||
|
||||
await prepared.cleanup?.();
|
||||
});
|
||||
|
||||
it("keeps dash-prefixed args after Claude --mcp-config because they terminate variadic values", async () => {
|
||||
const workspaceDir = await cliBundleMcpHarness.tempHarness.createTempDir(
|
||||
"openclaw-cli-bundle-mcp-dash-",
|
||||
);
|
||||
|
||||
const prepared = await prepareCliBundleMcpConfig({
|
||||
enabled: true,
|
||||
mode: "claude-config-file",
|
||||
backend: {
|
||||
command: "node",
|
||||
args: ["./fake-claude.mjs", "--mcp-config", "--verbose", "prompt"],
|
||||
},
|
||||
workspaceDir,
|
||||
config: { plugins: { enabled: false } },
|
||||
});
|
||||
|
||||
expect(prepared.backend.args).toContain("--verbose");
|
||||
expect(prepared.backend.args).toContain("prompt");
|
||||
const generatedConfigPath = requireMcpConfigPath(prepared.backend.args);
|
||||
const raw = JSON.parse(await fs.readFile(generatedConfigPath, "utf-8")) as {
|
||||
mcpServers?: Record<string, unknown>;
|
||||
};
|
||||
expect(raw.mcpServers).toStrictEqual({});
|
||||
|
||||
await prepared.cleanup?.();
|
||||
});
|
||||
|
||||
it("loads workspace bundle MCP plugins from the configured workspace root", async () => {
|
||||
const workspaceDir = await cliBundleMcpHarness.tempHarness.createTempDir(
|
||||
"openclaw-cli-bundle-mcp-workspace-root-",
|
||||
|
||||
@@ -15,6 +15,7 @@ import { loadMergedBundleMcpConfig, toCliBundleMcpServerConfig } from "../bundle
|
||||
import { isRecord } from "./bundle-mcp-adapter-shared.js";
|
||||
import {
|
||||
findClaudeMcpConfigPath,
|
||||
findClaudeMcpConfigPaths,
|
||||
injectClaudeMcpConfigArgs,
|
||||
writeClaudeMcpCaptureConfig,
|
||||
} from "./bundle-mcp-claude.js";
|
||||
@@ -188,14 +189,17 @@ export async function prepareCliBundleMcpConfig(params: {
|
||||
}
|
||||
|
||||
const mode = resolveBundleMcpMode(params.mode);
|
||||
const existingMcpConfigPath =
|
||||
mode === "claude-config-file"
|
||||
? (findClaudeMcpConfigPath(params.backend.resumeArgs) ??
|
||||
findClaudeMcpConfigPath(params.backend.args))
|
||||
: undefined;
|
||||
const resumeMcpConfigPaths =
|
||||
mode === "claude-config-file" ? findClaudeMcpConfigPaths(params.backend.resumeArgs) : [];
|
||||
const existingMcpConfigPaths =
|
||||
mode === "claude-config-file" && resumeMcpConfigPaths.length > 0
|
||||
? resumeMcpConfigPaths
|
||||
: mode === "claude-config-file"
|
||||
? findClaudeMcpConfigPaths(params.backend.args)
|
||||
: [];
|
||||
let mergedConfig: BundleMcpConfig = { mcpServers: {} };
|
||||
|
||||
if (existingMcpConfigPath) {
|
||||
for (const existingMcpConfigPath of existingMcpConfigPaths) {
|
||||
// Merge any user-provided Claude MCP config first so bundle/plugin config can
|
||||
// override intentionally managed server entries.
|
||||
const resolvedExistingPath = path.isAbsolute(existingMcpConfigPath)
|
||||
|
||||
Reference in New Issue
Block a user