mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-27 17:11:46 +00:00
fix: restore claude cli loopback mcp bridge (#35676) (thanks @mylukin)
This commit is contained in:
@@ -85,6 +85,84 @@ describe("prepareCliBundleMcpConfig", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("merges loopback overlay config with bundle MCP servers", async () => {
|
||||
const env = captureEnv(["HOME"]);
|
||||
try {
|
||||
const homeDir = await tempHarness.createTempDir("openclaw-cli-bundle-mcp-home-");
|
||||
const workspaceDir = await tempHarness.createTempDir("openclaw-cli-bundle-mcp-workspace-");
|
||||
process.env.HOME = homeDir;
|
||||
|
||||
await createBundleProbePlugin(homeDir);
|
||||
|
||||
const config: OpenClawConfig = {
|
||||
plugins: {
|
||||
entries: {
|
||||
"bundle-probe": { enabled: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const prepared = await prepareCliBundleMcpConfig({
|
||||
enabled: true,
|
||||
backend: {
|
||||
command: "node",
|
||||
args: ["./fake-claude.mjs"],
|
||||
},
|
||||
workspaceDir,
|
||||
config,
|
||||
additionalConfig: {
|
||||
mcpServers: {
|
||||
openclaw: {
|
||||
type: "http",
|
||||
url: "http://127.0.0.1:23119/mcp",
|
||||
headers: {
|
||||
Authorization: "Bearer ${OPENCLAW_MCP_TOKEN}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const configFlagIndex = prepared.backend.args?.indexOf("--mcp-config") ?? -1;
|
||||
const generatedConfigPath = prepared.backend.args?.[configFlagIndex + 1];
|
||||
const raw = JSON.parse(await fs.readFile(generatedConfigPath as string, "utf-8")) as {
|
||||
mcpServers?: Record<string, { url?: string; headers?: Record<string, string> }>;
|
||||
};
|
||||
expect(Object.keys(raw.mcpServers ?? {}).toSorted()).toEqual(["bundleProbe", "openclaw"]);
|
||||
expect(raw.mcpServers?.openclaw?.url).toBe("http://127.0.0.1:23119/mcp");
|
||||
expect(raw.mcpServers?.openclaw?.headers?.Authorization).toBe("Bearer ${OPENCLAW_MCP_TOKEN}");
|
||||
|
||||
await prepared.cleanup?.();
|
||||
} finally {
|
||||
env.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves extra env values alongside generated MCP config", async () => {
|
||||
const workspaceDir = await tempHarness.createTempDir("openclaw-cli-bundle-mcp-env-");
|
||||
|
||||
const prepared = await prepareCliBundleMcpConfig({
|
||||
enabled: true,
|
||||
backend: {
|
||||
command: "node",
|
||||
args: ["./fake-claude.mjs"],
|
||||
},
|
||||
workspaceDir,
|
||||
config: {},
|
||||
env: {
|
||||
OPENCLAW_MCP_TOKEN: "loopback-token-123",
|
||||
OPENCLAW_MCP_SESSION_KEY: "agent:main:telegram:group:chat123",
|
||||
},
|
||||
});
|
||||
|
||||
expect(prepared.env).toEqual({
|
||||
OPENCLAW_MCP_TOKEN: "loopback-token-123",
|
||||
OPENCLAW_MCP_SESSION_KEY: "agent:main:telegram:group:chat123",
|
||||
});
|
||||
|
||||
await prepared.cleanup?.();
|
||||
});
|
||||
|
||||
it("leaves args untouched when bundle MCP is disabled", async () => {
|
||||
const prepared = await prepareCliBundleMcpConfig({
|
||||
enabled: false,
|
||||
|
||||
@@ -15,6 +15,7 @@ type PreparedCliBundleMcpConfig = {
|
||||
backend: CliBackendConfig;
|
||||
cleanup?: () => Promise<void>;
|
||||
mcpConfigHash?: string;
|
||||
env?: Record<string, string>;
|
||||
};
|
||||
|
||||
async function readExternalMcpConfig(configPath: string): Promise<BundleMcpConfig> {
|
||||
@@ -69,10 +70,12 @@ export async function prepareCliBundleMcpConfig(params: {
|
||||
backend: CliBackendConfig;
|
||||
workspaceDir: string;
|
||||
config?: OpenClawConfig;
|
||||
additionalConfig?: BundleMcpConfig;
|
||||
env?: Record<string, string>;
|
||||
warn?: (message: string) => void;
|
||||
}): Promise<PreparedCliBundleMcpConfig> {
|
||||
if (!params.enabled) {
|
||||
return { backend: params.backend };
|
||||
return { backend: params.backend, env: params.env };
|
||||
}
|
||||
|
||||
const existingMcpConfigPath =
|
||||
@@ -97,6 +100,9 @@ export async function prepareCliBundleMcpConfig(params: {
|
||||
params.warn?.(`bundle MCP skipped for ${diagnostic.pluginId}: ${diagnostic.message}`);
|
||||
}
|
||||
mergedConfig = applyMergePatch(mergedConfig, bundleConfig.config) as BundleMcpConfig;
|
||||
if (params.additionalConfig) {
|
||||
mergedConfig = applyMergePatch(mergedConfig, params.additionalConfig) as BundleMcpConfig;
|
||||
}
|
||||
|
||||
// Always pass an explicit strict MCP config for background claude-cli runs.
|
||||
// Otherwise Claude may inherit ambient user/global MCP servers (for example
|
||||
@@ -116,6 +122,7 @@ export async function prepareCliBundleMcpConfig(params: {
|
||||
),
|
||||
},
|
||||
mcpConfigHash: crypto.createHash("sha256").update(serializedConfig).digest("hex"),
|
||||
env: params.env,
|
||||
cleanup: async () => {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
},
|
||||
|
||||
@@ -176,6 +176,7 @@ export async function executePreparedCliRun(
|
||||
for (const key of backend.clearEnv ?? []) {
|
||||
delete next[key];
|
||||
}
|
||||
Object.assign(next, context.preparedBackend.env);
|
||||
return next;
|
||||
})();
|
||||
const noOutputTimeoutMs = resolveCliNoOutputTimeoutMs({
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { resolveHeartbeatPrompt } from "../../auto-reply/heartbeat.js";
|
||||
import {
|
||||
createMcpLoopbackServerConfig,
|
||||
getActiveMcpLoopbackRuntime,
|
||||
} from "../../gateway/mcp-http.js";
|
||||
import { resolveSessionAgentIds } from "../agent-scope.js";
|
||||
import {
|
||||
buildBootstrapInjectionStats,
|
||||
@@ -28,6 +32,8 @@ import type { PreparedCliRunContext, RunCliAgentParams } from "./types.js";
|
||||
const prepareDeps = {
|
||||
makeBootstrapWarn: makeBootstrapWarnImpl,
|
||||
resolveBootstrapContextForRun: resolveBootstrapContextForRunImpl,
|
||||
getActiveMcpLoopbackRuntime,
|
||||
createMcpLoopbackServerConfig,
|
||||
};
|
||||
|
||||
export function setCliRunnerPrepareTestDeps(overrides: Partial<typeof prepareDeps>): void {
|
||||
@@ -59,30 +65,10 @@ export async function prepareCliRunContext(
|
||||
if (!backendResolved) {
|
||||
throw new Error(`Unknown CLI backend: ${params.provider}`);
|
||||
}
|
||||
const preparedBackend = await prepareCliBundleMcpConfig({
|
||||
enabled: backendResolved.bundleMcp,
|
||||
backend: backendResolved.config,
|
||||
workspaceDir,
|
||||
config: params.config,
|
||||
warn: (message) => cliBackendLog.warn(message),
|
||||
});
|
||||
const extraSystemPrompt = params.extraSystemPrompt?.trim() ?? "";
|
||||
const extraSystemPromptHash = hashCliSessionText(extraSystemPrompt);
|
||||
const reusableCliSession = resolveCliSessionReuse({
|
||||
binding:
|
||||
params.cliSessionBinding ??
|
||||
(params.cliSessionId ? { sessionId: params.cliSessionId } : undefined),
|
||||
authProfileId: params.authProfileId,
|
||||
extraSystemPromptHash,
|
||||
mcpConfigHash: preparedBackend.mcpConfigHash,
|
||||
});
|
||||
if (reusableCliSession.invalidatedReason) {
|
||||
cliBackendLog.info(
|
||||
`cli session reset: provider=${params.provider} reason=${reusableCliSession.invalidatedReason}`,
|
||||
);
|
||||
}
|
||||
const modelId = (params.model ?? "default").trim() || "default";
|
||||
const normalizedModel = normalizeCliModel(modelId, preparedBackend.backend);
|
||||
const normalizedModel = normalizeCliModel(modelId, backendResolved.config);
|
||||
const modelDisplay = `${params.provider}/${modelId}`;
|
||||
|
||||
const sessionLabel = params.sessionKey ?? params.sessionId;
|
||||
@@ -118,6 +104,40 @@ export async function prepareCliRunContext(
|
||||
config: params.config,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
const mcpLoopbackRuntime =
|
||||
backendResolved.id === "claude-cli" ? prepareDeps.getActiveMcpLoopbackRuntime() : undefined;
|
||||
const preparedBackend = await prepareCliBundleMcpConfig({
|
||||
enabled: backendResolved.bundleMcp,
|
||||
backend: backendResolved.config,
|
||||
workspaceDir,
|
||||
config: params.config,
|
||||
additionalConfig: mcpLoopbackRuntime
|
||||
? prepareDeps.createMcpLoopbackServerConfig(mcpLoopbackRuntime.port)
|
||||
: undefined,
|
||||
env: mcpLoopbackRuntime
|
||||
? {
|
||||
OPENCLAW_MCP_TOKEN: mcpLoopbackRuntime.token,
|
||||
OPENCLAW_MCP_AGENT_ID: sessionAgentId ?? "",
|
||||
OPENCLAW_MCP_ACCOUNT_ID: params.agentAccountId ?? "",
|
||||
OPENCLAW_MCP_SESSION_KEY: params.sessionKey ?? "",
|
||||
OPENCLAW_MCP_MESSAGE_CHANNEL: params.messageProvider ?? "",
|
||||
}
|
||||
: undefined,
|
||||
warn: (message) => cliBackendLog.warn(message),
|
||||
});
|
||||
const reusableCliSession = resolveCliSessionReuse({
|
||||
binding:
|
||||
params.cliSessionBinding ??
|
||||
(params.cliSessionId ? { sessionId: params.cliSessionId } : undefined),
|
||||
authProfileId: params.authProfileId,
|
||||
extraSystemPromptHash,
|
||||
mcpConfigHash: preparedBackend.mcpConfigHash,
|
||||
});
|
||||
if (reusableCliSession.invalidatedReason) {
|
||||
cliBackendLog.info(
|
||||
`cli session reset: provider=${params.provider} reason=${reusableCliSession.invalidatedReason}`,
|
||||
);
|
||||
}
|
||||
const heartbeatPrompt =
|
||||
sessionAgentId === defaultAgentId
|
||||
? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
|
||||
|
||||
@@ -30,12 +30,15 @@ export type RunCliAgentParams = {
|
||||
bootstrapPromptWarningSignature?: string;
|
||||
images?: ImageContent[];
|
||||
imageOrder?: PromptImageOrderEntry[];
|
||||
messageProvider?: string;
|
||||
agentAccountId?: string;
|
||||
};
|
||||
|
||||
export type CliPreparedBackend = {
|
||||
backend: CliBackendConfig;
|
||||
cleanup?: () => Promise<void>;
|
||||
mcpConfigHash?: string;
|
||||
env?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type CliReusableSession = {
|
||||
|
||||
Reference in New Issue
Block a user