From 2c393bc78f39b444a7e16a07ab15932f72109ca2 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sat, 16 May 2026 22:30:11 +0300 Subject: [PATCH] fix(opencode): preserve local mcp fallback --- src/main/index.ts | 30 +++++++++--- .../opencode/bridge/OpenCodeMcpBridgeEnv.ts | 28 ++++++++--- .../team/OpenCodeMcpBridgeEnv.test.ts | 49 +++++++++++++++++++ 3 files changed, 94 insertions(+), 13 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 928a1edf..7dee47f0 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -68,6 +68,12 @@ import { ChangeExtractorService } from '@main/services/team/ChangeExtractorServi import { CrossTeamService } from '@main/services/team/CrossTeamService'; import { FileContentResolver } from '@main/services/team/FileContentResolver'; import { GitDiffFallback } from '@main/services/team/GitDiffFallback'; +import { + clearOpenCodeLocalMcpLaunchEnv, + copyOpenCodeLocalMcpLaunchEnv, + hasOpenCodeLocalMcpLaunchEnv, + isOpenCodeMcpHttpBridgeEnabled, +} from '@main/services/team/opencode/bridge/OpenCodeMcpBridgeEnv'; import { ReviewApplierService } from '@main/services/team/ReviewApplierService'; import { TeamBackupService } from '@main/services/team/TeamBackupService'; import { TeamConfigReader } from '@main/services/team/TeamConfigReader'; @@ -145,10 +151,6 @@ import { OpenCodeBridgeCommandHandshakePort, } from './services/team/opencode/bridge/OpenCodeBridgeHandshakeClient'; import { cleanupManagedOpenCodeServeProcesses } from './services/team/opencode/bridge/OpenCodeManagedHostProcessCleanup'; -import { - clearOpenCodeLocalMcpLaunchEnv, - isOpenCodeMcpHttpBridgeEnabled, -} from './services/team/opencode/bridge/OpenCodeMcpBridgeEnv'; import { OpenCodeStateChangingBridgeCommandService } from './services/team/opencode/bridge/OpenCodeStateChangingBridgeCommandService'; import { OpenCodeRuntimeManifestEvidenceReader } from './services/team/opencode/store/OpenCodeRuntimeManifestEvidenceReader'; import { @@ -361,6 +363,7 @@ async function createOpenCodeRuntimeAdapterRegistry( if (!useHttpMcpBridge) { delete bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL; } else { + delete bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL; clearOpenCodeLocalMcpLaunchEnv(bridgeEnv); } const applyMcpLaunchSpecEnv = async ( @@ -390,6 +393,20 @@ async function createOpenCodeRuntimeAdapterRegistry( ); } }; + const ensureOpenCodeLocalMcpLaunchEnv = async ( + targetEnv: NodeJS.ProcessEnv, + options: { emitProgress?: boolean } = {} + ): Promise => { + if (hasOpenCodeLocalMcpLaunchEnv(bridgeEnv)) { + copyOpenCodeLocalMcpLaunchEnv(bridgeEnv, targetEnv); + return; + } + + await applyMcpLaunchSpecEnv(targetEnv, options); + if (hasOpenCodeLocalMcpLaunchEnv(targetEnv)) { + copyOpenCodeLocalMcpLaunchEnv(targetEnv, bridgeEnv); + } + }; try { const appManagedOpenCodeBinary = await resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath(); if (appManagedOpenCodeBinary && !bridgeEnv.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH) { @@ -433,8 +450,8 @@ async function createOpenCodeRuntimeAdapterRegistry( ); } } - if (!useHttpMcpBridge && !bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL) { - await applyMcpLaunchSpecEnv(bridgeEnv, { emitProgress: true }); + if (!bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL) { + await ensureOpenCodeLocalMcpLaunchEnv(bridgeEnv, { emitProgress: true }); } reportProgress('runtime-bridge', 'Preparing OpenCode bridge...'); @@ -453,6 +470,7 @@ async function createOpenCodeRuntimeAdapterRegistry( delete bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL; delete nextEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL; clearOpenCodeLocalMcpLaunchEnv(nextEnv); + await ensureOpenCodeLocalMcpLaunchEnv(nextEnv); logger.warn( `[OpenCode] Runtime adapter bridge MCP HTTP server refresh failed: ${ error instanceof Error ? error.message : String(error) diff --git a/src/main/services/team/opencode/bridge/OpenCodeMcpBridgeEnv.ts b/src/main/services/team/opencode/bridge/OpenCodeMcpBridgeEnv.ts index c6f516ea..6bf354cb 100644 --- a/src/main/services/team/opencode/bridge/OpenCodeMcpBridgeEnv.ts +++ b/src/main/services/team/opencode/bridge/OpenCodeMcpBridgeEnv.ts @@ -6,18 +6,32 @@ const LOCAL_MCP_LAUNCH_ENV_KEYS = [ 'CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON', ] as const; -export interface OpenCodeMcpHttpBridgeEnv { - CLAUDE_TEAM_OPENCODE_MCP_HTTP?: string; -} +export type OpenCodeMcpBridgeEnv = Record; -export function isOpenCodeMcpHttpBridgeEnabled( - env: OpenCodeMcpHttpBridgeEnv = process.env -): boolean { +export function isOpenCodeMcpHttpBridgeEnabled(env: OpenCodeMcpBridgeEnv = process.env): boolean { const rawValue = env.CLAUDE_TEAM_OPENCODE_MCP_HTTP?.trim().toLowerCase(); return rawValue ? !DISABLED_HTTP_MCP_VALUES.has(rawValue) : true; } -export function clearOpenCodeLocalMcpLaunchEnv(env: NodeJS.ProcessEnv): void { +export function hasOpenCodeLocalMcpLaunchEnv(env: OpenCodeMcpBridgeEnv): boolean { + return LOCAL_MCP_LAUNCH_ENV_KEYS.every((key) => Boolean(env[key]?.trim())); +} + +export function copyOpenCodeLocalMcpLaunchEnv( + sourceEnv: OpenCodeMcpBridgeEnv, + targetEnv: OpenCodeMcpBridgeEnv +): void { + for (const key of LOCAL_MCP_LAUNCH_ENV_KEYS) { + const value = sourceEnv[key]?.trim(); + if (value) { + targetEnv[key] = value; + } else { + delete targetEnv[key]; + } + } +} + +export function clearOpenCodeLocalMcpLaunchEnv(env: OpenCodeMcpBridgeEnv): void { for (const key of LOCAL_MCP_LAUNCH_ENV_KEYS) { delete env[key]; } diff --git a/test/main/services/team/OpenCodeMcpBridgeEnv.test.ts b/test/main/services/team/OpenCodeMcpBridgeEnv.test.ts index 4622061e..e9e8f533 100644 --- a/test/main/services/team/OpenCodeMcpBridgeEnv.test.ts +++ b/test/main/services/team/OpenCodeMcpBridgeEnv.test.ts @@ -2,6 +2,8 @@ import { describe, expect, it } from 'vitest'; import { clearOpenCodeLocalMcpLaunchEnv, + copyOpenCodeLocalMcpLaunchEnv, + hasOpenCodeLocalMcpLaunchEnv, isOpenCodeMcpHttpBridgeEnabled, } from '@main/services/team/opencode/bridge/OpenCodeMcpBridgeEnv'; @@ -20,6 +22,53 @@ describe('OpenCodeMcpBridgeEnv', () => { expect(isOpenCodeMcpHttpBridgeEnabled({ CLAUDE_TEAM_OPENCODE_MCP_HTTP: 'off' })).toBe(false); }); + it('accepts process-style env objects', () => { + const env: NodeJS.ProcessEnv = { + PATH: '/usr/bin', + CLAUDE_TEAM_OPENCODE_MCP_HTTP: 'no', + }; + + expect(isOpenCodeMcpHttpBridgeEnabled(env)).toBe(false); + }); + + it('detects complete local MCP launch env', () => { + expect( + hasOpenCodeLocalMcpLaunchEnv({ + CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: 'node', + CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: 'mcp-server/dist/index.js', + CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: '["mcp-server/dist/index.js"]', + }) + ).toBe(true); + + expect( + hasOpenCodeLocalMcpLaunchEnv({ + CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: 'node', + CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: '', + CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: '["mcp-server/dist/index.js"]', + }) + ).toBe(false); + }); + + it('copies local MCP launch env for HTTP fallback without copying the HTTP URL', () => { + const target: NodeJS.ProcessEnv = { + CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL: 'http://127.0.0.1:41001/mcp', + }; + + copyOpenCodeLocalMcpLaunchEnv( + { + CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: 'node', + CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: 'mcp-server/dist/index.js', + CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: '["mcp-server/dist/index.js"]', + }, + target + ); + + expect(target.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND).toBe('node'); + expect(target.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY).toBe('mcp-server/dist/index.js'); + expect(target.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON).toBe('["mcp-server/dist/index.js"]'); + expect(target.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL).toBe('http://127.0.0.1:41001/mcp'); + }); + it('removes local MCP launch env when HTTP MCP is active', () => { const env: NodeJS.ProcessEnv = { CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: 'node',