Download src/agents/cli-runner/bundle-mcp.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 19.9 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/agents/cli-runner/bundle-mcp.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/agents/cli-runner/bundle-mcp.ts
-
curl -L -o bundle-mcp.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/agents/cli-runner/bundle-mcp.ts
19.9 kB
| /** | |
| * Prepares bundled MCP configuration for CLI runner backends. | |
| */ | |
| import crypto from "node:crypto"; | |
| import path from "node:path"; | |
| import { applyMergePatch } from "../../config/merge-patch.js"; | |
| import type { SessionToolOverrides } from "../../config/sessions/types.js"; | |
| import type { OpenClawConfig } from "../../config/types.openclaw.js"; | |
| import { formatErrorMessage } from "../../infra/errors.js"; | |
| import { tryReadJson } from "../../infra/json-files.js"; | |
| import { | |
| OPENCLAW_TOOLS_MCP_SYSTEM_AGENT_APPROVAL_ARMED_ENV, | |
| OPENCLAW_TOOLS_MCP_SYSTEM_AGENT_PROPOSAL_ENV, | |
| OPENCLAW_TOOLS_MCP_TOOLS_ENV, | |
| } from "../../mcp/openclaw-tools-serve-config.js"; | |
| import { | |
| extractMcpServerMap, | |
| type BundleMcpConfig, | |
| type BundleMcpServerConfig, | |
| } from "../../plugins/bundle-mcp.js"; | |
| import type { CliBackendConfig } from "../../plugins/cli-backend.types.js"; | |
| import type { CliBundleMcpMode } from "../../plugins/types.js"; | |
| import { | |
| acquireSessionMcpRuntime, | |
| releaseSessionMcpRuntime, | |
| } from "../agent-bundle-mcp-manager-api.js"; | |
| import { isRecord } from "../bundle-mcp-adapter.js"; | |
| import { | |
| loadMergedBundleMcpConfig, | |
| prepareOwnedBundleMcpDataDirs, | |
| toCliBundleMcpServerConfig, | |
| } from "../bundle-mcp-config.js"; | |
| import type { ResolvedConversationCapabilityProfile } from "../conversation-capability-profile.js"; | |
| import { resolveMcpBearerBundleConfig } from "../mcp-auth-profile.js"; | |
| import { | |
| applyPreparedNativeMcpPolicy, | |
| prepareNativeMcpPolicy, | |
| preparedNativeMcpDenials, | |
| } from "../native-mcp-policy.js"; | |
| import { | |
| findClaudeMcpConfigPaths, | |
| injectClaudeMcpConfigArgs, | |
| injectClaudeWebSearchDisabledArgs, | |
| writeClaudeMcpCaptureConfig, | |
| } from "./bundle-mcp-claude.js"; | |
| import { injectCodexMcpConfigArgs } from "./bundle-mcp-codex.js"; | |
| import { | |
| writeGeminiMcpCaptureSettings, | |
| writeGeminiSystemSettings, | |
| writeGeminiWebSearchDisabledSettings, | |
| } from "./bundle-mcp-gemini.js"; | |
| import { injectBundleMcpBackendArgs, writeTemporaryBundleMcpJson } from "./bundle-mcp-runtime.js"; | |
| type PreparedCliBundleMcpConfig = { | |
| backend: CliBackendConfig; | |
| beforeExecution?: () => Promise<void>; | |
| cleanup?: () => Promise<void>; | |
| mcpConfigHash?: string; | |
| mcpResumeHash?: string; | |
| env?: Record<string, string>; | |
| }; | |
| async function readExternalMcpConfig(configPath: string): Promise<BundleMcpConfig> { | |
| return { mcpServers: extractMcpServerMap(await tryReadJson<unknown>(configPath)) }; | |
| } | |
| function sortJsonValue(value: unknown): unknown { | |
| if (Array.isArray(value)) { | |
| return value.map((entry) => sortJsonValue(entry)); | |
| } | |
| if (!isRecord(value)) { | |
| return value; | |
| } | |
| return Object.fromEntries( | |
| Object.keys(value) | |
| .toSorted() | |
| .map((key) => [key, sortJsonValue(value[key])]), | |
| ); | |
| } | |
| function normalizeOpenClawLoopbackUrl(value: string): string { | |
| const match = | |
| /^(http:\/\/(?:127\.0\.0\.1|localhost|\[::1\])):\d+(\/mcp)$/.exec(value.trim()) ?? undefined; | |
| if (!match) { | |
| return value; | |
| } | |
| return `${match[1]}:<openclaw-loopback>${match[2]}`; | |
| } | |
| function canonicalizeSystemAgentTurnStateForResume( | |
| server: BundleMcpConfig["mcpServers"][string], | |
| ): BundleMcpConfig["mcpServers"][string] { | |
| if (!isRecord(server.env) || server.env[OPENCLAW_TOOLS_MCP_TOOLS_ENV] !== "openclaw") { | |
| return server; | |
| } | |
| // The host reissues approval authority through a fresh stdio server each turn. | |
| // Its values may change while tool topology and the native transcript stay safe to resume. | |
| return { | |
| ...server, | |
| env: { | |
| ...server.env, | |
| [OPENCLAW_TOOLS_MCP_SYSTEM_AGENT_APPROVAL_ARMED_ENV]: "<openclaw-turn-state>", | |
| [OPENCLAW_TOOLS_MCP_SYSTEM_AGENT_PROPOSAL_ENV]: "<openclaw-turn-state>", | |
| }, | |
| }; | |
| } | |
| function canonicalizeBundleMcpConfigForResume(config: BundleMcpConfig): BundleMcpConfig { | |
| // The OpenClaw loopback MCP port changes across runs. Replace it before | |
| // hashing so resume compatibility tracks config shape, not ephemeral ports. | |
| const canonicalServers = Object.fromEntries( | |
| Object.entries(config.mcpServers).map(([name, server]) => { | |
| const canonicalServer = canonicalizeSystemAgentTurnStateForResume(server); | |
| if (name !== "openclaw" || typeof canonicalServer.url !== "string") { | |
| return [name, sortJsonValue(canonicalServer)]; | |
| } | |
| return [ | |
| name, | |
| sortJsonValue({ | |
| ...canonicalServer, | |
| url: normalizeOpenClawLoopbackUrl(canonicalServer.url), | |
| }), | |
| ]; | |
| }), | |
| ) as BundleMcpConfig["mcpServers"]; | |
| return { | |
| mcpServers: sortJsonValue(canonicalServers) as BundleMcpConfig["mcpServers"], | |
| }; | |
| } | |
| const OPENCLAW_MCP_ENV_TEMPLATE_PATTERN = /\$\{(OPENCLAW_MCP_[A-Z0-9_]+)\}/g; | |
| function normalizeMcpToolDenials( | |
| value?: Record<string, string[]>, | |
| ): Record<string, string[]> | undefined { | |
| const entries = Object.entries(value ?? {}) | |
| .map(([serverName, toolNames]) => [serverName, [...new Set(toolNames)].toSorted()] as const) | |
| .filter(([, toolNames]) => toolNames.length > 0) | |
| .toSorted(([left], [right]) => left.localeCompare(right)); | |
| return entries.length > 0 ? Object.fromEntries(entries) : undefined; | |
| } | |
| function applyCodexMcpToolDenials( | |
| config: BundleMcpConfig, | |
| denials: Record<string, string[]> | undefined, | |
| ): BundleMcpConfig { | |
| if (!denials) { | |
| return config; | |
| } | |
| return { | |
| mcpServers: Object.fromEntries( | |
| Object.entries(config.mcpServers).map(([serverName, server]) => { | |
| const denied = Object.hasOwn(denials, serverName) ? denials[serverName] : undefined; | |
| if (!denied?.length) { | |
| return [serverName, server]; | |
| } | |
| const toolFilter = isRecord(server.toolFilter) ? server.toolFilter : {}; | |
| const existing = Array.isArray(toolFilter.exclude) | |
| ? toolFilter.exclude.filter((name): name is string => typeof name === "string") | |
| : []; | |
| return [ | |
| serverName, | |
| { | |
| ...server, | |
| toolFilter: { | |
| ...toolFilter, | |
| exclude: [...new Set([...existing, ...denied])].toSorted(), | |
| }, | |
| } satisfies BundleMcpServerConfig, | |
| ]; | |
| }), | |
| ), | |
| }; | |
| } | |
| function applyMcpServerOverrides( | |
| config: BundleMcpConfig, | |
| overrides: Record<string, boolean> | undefined, | |
| ): BundleMcpConfig { | |
| return overrides | |
| ? { | |
| mcpServers: Object.fromEntries( | |
| Object.entries(config.mcpServers).filter( | |
| ([serverName]) => | |
| !Object.hasOwn(overrides, serverName) || overrides[serverName] !== false, | |
| ), | |
| ), | |
| } | |
| : config; | |
| } | |
| function selectBundleMcpServers( | |
| config: BundleMcpConfig, | |
| selected: BundleMcpConfig, | |
| ): BundleMcpConfig { | |
| return { | |
| mcpServers: Object.fromEntries( | |
| Object.entries(config.mcpServers).filter(([name]) => | |
| Object.hasOwn(selected.mcpServers, name), | |
| ), | |
| ), | |
| }; | |
| } | |
| function resolveOpenClawMcpEnvTemplates(value: unknown, env?: Record<string, string>): unknown { | |
| if (!env) { | |
| return value; | |
| } | |
| if (typeof value === "string") { | |
| return value.replace(OPENCLAW_MCP_ENV_TEMPLATE_PATTERN, (match, name: string) => { | |
| const replacement = env[name]; | |
| return Object.hasOwn(env, name) && replacement !== undefined ? replacement : match; | |
| }); | |
| } | |
| if (Array.isArray(value)) { | |
| return value.map((entry) => resolveOpenClawMcpEnvTemplates(entry, env)); | |
| } | |
| if (!isRecord(value)) { | |
| return value; | |
| } | |
| return Object.fromEntries( | |
| Object.entries(value).map(([key, entry]) => [key, resolveOpenClawMcpEnvTemplates(entry, env)]), | |
| ); | |
| } | |
| async function prepareModeSpecificBundleMcpConfig(params: { | |
| mode: CliBundleMcpMode; | |
| backend: CliBackendConfig; | |
| mergedConfig: BundleMcpConfig; | |
| env?: Record<string, string>; | |
| mcpToolsDeny?: Record<string, string[]>; | |
| webSearchEnabled?: boolean; | |
| }): Promise<PreparedCliBundleMcpConfig> { | |
| const mcpToolsDeny = normalizeMcpToolDenials(params.mcpToolsDeny); | |
| const webSearchDisabled = params.webSearchEnabled === false; | |
| const configHashInput = | |
| mcpToolsDeny || webSearchDisabled | |
| ? { config: params.mergedConfig, mcpToolsDeny, webSearchDisabled } | |
| : params.mergedConfig; | |
| const serializedConfig = `${JSON.stringify(configHashInput, null, 2)}\n`; | |
| const mcpConfigHash = crypto.createHash("sha256").update(serializedConfig).digest("hex"); | |
| const serializedResumeConfig = `${JSON.stringify( | |
| mcpToolsDeny || webSearchDisabled | |
| ? { | |
| config: canonicalizeBundleMcpConfigForResume(params.mergedConfig), | |
| mcpToolsDeny, | |
| webSearchDisabled, | |
| } | |
| : canonicalizeBundleMcpConfigForResume(params.mergedConfig), | |
| null, | |
| 2, | |
| )}\n`; | |
| const mcpResumeHash = crypto.createHash("sha256").update(serializedResumeConfig).digest("hex"); | |
| if (params.mode === "codex-config-overrides") { | |
| const codexConfig = applyCodexMcpToolDenials(params.mergedConfig, mcpToolsDeny); | |
| return { | |
| backend: injectBundleMcpBackendArgs(params.backend, (args) => | |
| webSearchDisabled | |
| ? [...injectCodexMcpConfigArgs(args, codexConfig), "-c", 'web_search="disabled"'] | |
| : injectCodexMcpConfigArgs(args, codexConfig), | |
| ), | |
| mcpConfigHash, | |
| mcpResumeHash, | |
| env: params.env, | |
| }; | |
| } | |
| if (params.mode === "gemini-system-settings") { | |
| const settings = await writeGeminiSystemSettings( | |
| params.mergedConfig, | |
| params.env, | |
| mcpToolsDeny, | |
| params.webSearchEnabled, | |
| ); | |
| return { | |
| backend: params.backend, | |
| mcpConfigHash, | |
| mcpResumeHash, | |
| env: settings.env, | |
| cleanup: settings.cleanup, | |
| }; | |
| } | |
| const runtimeConfig = resolveOpenClawMcpEnvTemplates( | |
| params.mergedConfig, | |
| params.env, | |
| ) as BundleMcpConfig; | |
| const claudeConfig: BundleMcpConfig = { | |
| mcpServers: Object.fromEntries( | |
| Object.entries(runtimeConfig.mcpServers).map(([name, server]) => { | |
| const { toolFilter: _toolFilter, ...nativeServer } = server; | |
| return [name, nativeServer]; | |
| }), | |
| ), | |
| }; | |
| const temporary = await writeTemporaryBundleMcpJson( | |
| "openclaw-cli-mcp-", | |
| claudeConfig, | |
| "mcp.json", | |
| false, | |
| ); | |
| return { | |
| backend: injectBundleMcpBackendArgs(params.backend, (args) => | |
| injectClaudeMcpConfigArgs(args, temporary.filePath, mcpToolsDeny, params.webSearchEnabled), | |
| ), | |
| mcpConfigHash, | |
| mcpResumeHash, | |
| env: params.env, | |
| cleanup: temporary.cleanup, | |
| }; | |
| } | |
| async function prepareCliWebSearchDisabled(params: { | |
| mode: CliBundleMcpMode; | |
| backend: CliBackendConfig; | |
| env?: Record<string, string>; | |
| }): Promise<PreparedCliBundleMcpConfig> { | |
| const fingerprint = crypto.createHash("sha256").update("web-search-disabled-v1").digest("hex"); | |
| if (params.mode === "gemini-system-settings") { | |
| const settings = await writeGeminiWebSearchDisabledSettings(params.env); | |
| return { | |
| backend: params.backend, | |
| env: settings.env, | |
| cleanup: settings.cleanup, | |
| mcpConfigHash: fingerprint, | |
| mcpResumeHash: fingerprint, | |
| }; | |
| } | |
| const backend = injectBundleMcpBackendArgs(params.backend, (args) => | |
| params.mode === "codex-config-overrides" | |
| ? [...(args ?? []), "-c", 'web_search="disabled"'] | |
| : injectClaudeWebSearchDisabledArgs(args), | |
| ); | |
| return { backend, env: params.env, mcpConfigHash: fingerprint, mcpResumeHash: fingerprint }; | |
| } | |
| /** Prepare backend args/env/cleanup for bundle MCP injection into a CLI run. */ | |
| export async function prepareCliBundleMcpConfig(params: { | |
| enabled: boolean; | |
| mode?: CliBundleMcpMode; | |
| backend: CliBackendConfig; | |
| workspaceDir: string; | |
| config?: OpenClawConfig; | |
| toolOverrides?: SessionToolOverrides; | |
| agentDir?: string; | |
| additionalConfig?: BundleMcpConfig; | |
| /** | |
| * Serve exactly these servers, skipping user/plugin/additional merges. | |
| * Ring-zero OpenClaw runs use this so the CLI harness sees only the | |
| * openclaw MCP server instead of the normal openclaw tool surface. | |
| */ | |
| exclusiveConfig?: BundleMcpConfig; | |
| env?: Record<string, string>; | |
| warn?: (message: string) => void; | |
| nativeMcpPolicy?: { | |
| sessionId: string; | |
| sessionKey?: string; | |
| capabilityProfile: ResolvedConversationCapabilityProfile; | |
| runtimeToolsAllow?: string[]; | |
| }; | |
| }): Promise<PreparedCliBundleMcpConfig> { | |
| if (!params.enabled) { | |
| return params.toolOverrides?.webSearch === false | |
| ? await prepareCliWebSearchDisabled({ | |
| mode: params.mode ?? "claude-config-file", | |
| backend: params.backend, | |
| env: params.env, | |
| }) | |
| : { backend: params.backend, env: params.env }; | |
| } | |
| const mode = params.mode ?? "claude-config-file"; | |
| if (params.exclusiveConfig) { | |
| return await prepareModeSpecificBundleMcpConfig({ | |
| mode, | |
| backend: params.backend, | |
| mergedConfig: applyMcpServerOverrides( | |
| params.exclusiveConfig, | |
| params.toolOverrides?.mcpServers, | |
| ), | |
| env: params.env, | |
| mcpToolsDeny: params.toolOverrides?.mcpToolsDeny, | |
| webSearchEnabled: params.toolOverrides?.webSearch, | |
| }); | |
| } | |
| 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: {} }; | |
| 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) | |
| ? existingMcpConfigPath | |
| : path.resolve(params.workspaceDir, existingMcpConfigPath); | |
| mergedConfig = applyMergePatch( | |
| mergedConfig, | |
| await readExternalMcpConfig(resolvedExistingPath), | |
| ) as BundleMcpConfig; | |
| } | |
| const bundleConfig = loadMergedBundleMcpConfig({ | |
| workspaceDir: params.workspaceDir, | |
| cfg: params.config, | |
| mapConfiguredServer: toCliBundleMcpServerConfig, | |
| toolOverrides: params.toolOverrides, | |
| }); | |
| for (const diagnostic of bundleConfig.diagnostics) { | |
| params.warn?.(`bundle MCP skipped for ${diagnostic.pluginId}: ${diagnostic.message}`); | |
| } | |
| mergedConfig = applyMergePatch(mergedConfig, bundleConfig.config) as BundleMcpConfig; | |
| const prepareDataDirsByServer = { ...bundleConfig.prepareDataDirsByServer }; | |
| const additionalServerNames = new Set(Object.keys(params.additionalConfig?.mcpServers ?? {})); | |
| if (params.additionalConfig) { | |
| mergedConfig = applyMergePatch(mergedConfig, params.additionalConfig) as BundleMcpConfig; | |
| for (const serverName of Object.keys(params.additionalConfig.mcpServers)) { | |
| delete prepareDataDirsByServer[serverName]; | |
| } | |
| } | |
| const warnUnavailableOAuthServer = (serverName: string, error: unknown) => | |
| params.warn?.( | |
| `bundle MCP skipped unavailable OAuth server ${serverName}: ${formatErrorMessage(error)}`, | |
| ); | |
| const resolvedBearerConfig = await resolveMcpBearerBundleConfig({ | |
| config: mergedConfig, | |
| cfg: params.config, | |
| agentDir: params.agentDir, | |
| env: params.env, | |
| omitUnavailableOAuthServers: true, | |
| onServerUnavailable: warnUnavailableOAuthServer, | |
| }); | |
| const preparedDataDirs = prepareOwnedBundleMcpDataDirs({ | |
| config: applyMcpServerOverrides(resolvedBearerConfig.config, params.toolOverrides?.mcpServers), | |
| prepareDataDirsByServer, | |
| }); | |
| for (const diagnostic of preparedDataDirs.diagnostics) { | |
| params.warn?.(`bundle MCP skipped for ${diagnostic.pluginId}: ${diagnostic.message}`); | |
| } | |
| let effectiveConfig = preparedDataDirs.config; | |
| let effectiveEnv = resolvedBearerConfig.env; | |
| let effectiveDenials = params.toolOverrides?.mcpToolsDeny; | |
| const policyConfig: BundleMcpConfig = { | |
| mcpServers: Object.fromEntries( | |
| Object.entries(effectiveConfig.mcpServers).filter( | |
| ([serverName]) => !additionalServerNames.has(serverName), | |
| ), | |
| ), | |
| }; | |
| if (params.nativeMcpPolicy && Object.keys(policyConfig.mcpServers).length > 0) { | |
| // Native policy discovery runs in-process and owns OAuth refresh. Restrict it | |
| // to projected survivors without passing external-runtime bearer placeholders. | |
| const nativePolicyConfig = selectBundleMcpServers(mergedConfig, policyConfig); | |
| const runtimeConfig: OpenClawConfig = { | |
| ...params.config, | |
| mcp: { ...params.config?.mcp, servers: nativePolicyConfig.mcpServers }, | |
| }; | |
| const acquisition = await acquireSessionMcpRuntime({ | |
| sessionId: params.nativeMcpPolicy.sessionId, | |
| sessionKey: params.nativeMcpPolicy.sessionKey, | |
| workspaceDir: params.workspaceDir, | |
| agentDir: params.agentDir, | |
| cfg: runtimeConfig, | |
| toolOverrides: params.toolOverrides, | |
| }); | |
| let policy: Awaited<ReturnType<typeof prepareNativeMcpPolicy>>; | |
| try { | |
| policy = await prepareNativeMcpPolicy({ | |
| runtime: acquisition.runtime, | |
| config: params.config, | |
| workspaceDir: params.workspaceDir, | |
| capabilityProfile: params.nativeMcpPolicy.capabilityProfile, | |
| runtimeToolsAllow: params.nativeMcpPolicy.runtimeToolsAllow, | |
| warn: params.warn ?? (() => {}), | |
| }); | |
| } finally { | |
| await releaseSessionMcpRuntime(acquisition); | |
| } | |
| effectiveConfig = { | |
| mcpServers: { | |
| ...applyPreparedNativeMcpPolicy(policyConfig, policy).mcpServers, | |
| ...Object.fromEntries( | |
| Object.entries(effectiveConfig.mcpServers).filter(([serverName]) => | |
| additionalServerNames.has(serverName), | |
| ), | |
| ), | |
| }, | |
| }; | |
| const preservedAdditionalDenials = Object.fromEntries( | |
| Object.entries(params.toolOverrides?.mcpToolsDeny ?? {}).filter(([serverName]) => | |
| additionalServerNames.has(serverName), | |
| ), | |
| ); | |
| const combinedDenials = { | |
| ...preparedNativeMcpDenials(policy), | |
| ...preservedAdditionalDenials, | |
| }; | |
| effectiveDenials = Object.keys(combinedDenials).length > 0 ? combinedDenials : undefined; | |
| // Policy discovery can refresh OAuth. Reproject the final survivors afterward | |
| // so the external runtime receives the same current credential. | |
| const refreshedBearerConfig = await resolveMcpBearerBundleConfig({ | |
| config: selectBundleMcpServers(mergedConfig, effectiveConfig), | |
| cfg: params.config, | |
| agentDir: params.agentDir, | |
| env: params.env, | |
| omitUnavailableOAuthServers: true, | |
| onServerUnavailable: warnUnavailableOAuthServer, | |
| }); | |
| effectiveConfig = selectBundleMcpServers(effectiveConfig, refreshedBearerConfig.config); | |
| effectiveEnv = refreshedBearerConfig.env; | |
| } | |
| return await prepareModeSpecificBundleMcpConfig({ | |
| mode, | |
| backend: params.backend, | |
| mergedConfig: effectiveConfig, | |
| env: effectiveEnv, | |
| mcpToolsDeny: effectiveDenials, | |
| webSearchEnabled: params.toolOverrides?.webSearch, | |
| }); | |
| } | |
| /** Prepares a per-attempt capture token without changing resume compatibility hashes. */ | |
| export async function prepareCliBundleMcpCaptureAttempt(params: { | |
| mode?: CliBundleMcpMode; | |
| backend?: CliBackendConfig; | |
| env?: Record<string, string>; | |
| captureKey?: string; | |
| }): Promise<{ env?: Record<string, string>; cleanup?: () => Promise<void> }> { | |
| if (!params.captureKey) { | |
| return { env: params.env }; | |
| } | |
| if ((params.mode ?? "claude-config-file") === "gemini-system-settings") { | |
| return await writeGeminiMcpCaptureSettings({ | |
| inheritedEnv: params.env, | |
| captureKey: params.captureKey, | |
| }); | |
| } | |
| if ((params.mode ?? "claude-config-file") === "claude-config-file") { | |
| const mcpConfigPath = | |
| findClaudeMcpConfigPaths(params.backend?.args)[0] ?? | |
| findClaudeMcpConfigPaths(params.backend?.resumeArgs)[0]; | |
| if (mcpConfigPath) { | |
| await writeClaudeMcpCaptureConfig({ | |
| mcpConfigPath, | |
| captureKey: params.captureKey, | |
| }); | |
| } | |
| } | |
| return { | |
| env: { | |
| ...params.env, | |
| OPENCLAW_MCP_CLI_CAPTURE_KEY: params.captureKey, | |
| }, | |
| }; | |
| } | |