From 429dfe552831c6b47504c40757ff2d69df7d4ebe Mon Sep 17 00:00:00 2001 From: iliya Date: Sat, 16 May 2026 21:11:23 +0300 Subject: [PATCH 1/3] fix(opencode): default to http mcp bridge --- src/main/index.ts | 41 ++--- .../bridge/OpenCodeBridgeCommandClient.ts | 21 ++- .../opencode/bridge/OpenCodeMcpBridgeEnv.ts | 24 +++ .../team/OpenCodeBridgeCommandClient.test.ts | 143 ++++++++++++++---- .../team/OpenCodeMcpBridgeEnv.test.ts | 38 +++++ 5 files changed, 211 insertions(+), 56 deletions(-) create mode 100644 src/main/services/team/opencode/bridge/OpenCodeMcpBridgeEnv.ts create mode 100644 test/main/services/team/OpenCodeMcpBridgeEnv.test.ts diff --git a/src/main/index.ts b/src/main/index.ts index 4e8069e0..928a1edf 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -145,6 +145,10 @@ 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 { @@ -353,10 +357,11 @@ async function createOpenCodeRuntimeAdapterRegistry( const bridgeEnv = applyOpenCodeAutoUpdatePolicy({ ...process.env }); bridgeEnv.CLAUDE_TEAM_APP_INSTANCE_ID = openCodeManagedHostInstanceId; bridgeEnv.AGENT_TEAMS_MCP_CLAUDE_DIR = getClaudeBasePath(); - const useHttpMcpBridge = bridgeEnv.CLAUDE_TEAM_OPENCODE_MCP_HTTP === '1'; + const useHttpMcpBridge = isOpenCodeMcpHttpBridgeEnabled(bridgeEnv); if (!useHttpMcpBridge) { - // The OpenCode bridge direct tools/list proof currently requires a local MCP command. delete bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL; + } else { + clearOpenCodeLocalMcpLaunchEnv(bridgeEnv); } const applyMcpLaunchSpecEnv = async ( targetEnv: NodeJS.ProcessEnv, @@ -418,6 +423,7 @@ async function createOpenCodeRuntimeAdapterRegistry( reportProgress('runtime-mcp-http', 'Starting Agent Teams MCP server...'); const mcpHttpServer = await agentTeamsMcpHttpServer.ensureStarted(); bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL = mcpHttpServer.url; + clearOpenCodeLocalMcpLaunchEnv(bridgeEnv); reportProgress('runtime-mcp-http-ready', 'Agent Teams MCP server is ready...'); } catch (error) { logger.warn( @@ -427,45 +433,26 @@ async function createOpenCodeRuntimeAdapterRegistry( ); } } - if (!bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL) { + if (!useHttpMcpBridge && !bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL) { await applyMcpLaunchSpecEnv(bridgeEnv, { emitProgress: true }); } reportProgress('runtime-bridge', 'Preparing OpenCode bridge...'); const resolveBridgeCommandEnv = async (): Promise => { const nextEnv = { ...bridgeEnv }; - if (!useHttpMcpBridge || !bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL) { + if (!useHttpMcpBridge) { return nextEnv; } try { const mcpHttpServer = await agentTeamsMcpHttpServer.ensureStarted(); bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL = mcpHttpServer.url; nextEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL = mcpHttpServer.url; - delete nextEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND; - delete nextEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY; - delete nextEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON; + clearOpenCodeLocalMcpLaunchEnv(bridgeEnv); + clearOpenCodeLocalMcpLaunchEnv(nextEnv); } catch (error) { + delete bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL; delete nextEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL; - if ( - bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND && - bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY && - bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON - ) { - nextEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND = - bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND; - nextEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY = - bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY; - nextEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON = - bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON; - } else { - await applyMcpLaunchSpecEnv(nextEnv); - bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND = - nextEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND; - bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY = - nextEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY; - bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON = - nextEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON; - } + clearOpenCodeLocalMcpLaunchEnv(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/OpenCodeBridgeCommandClient.ts b/src/main/services/team/opencode/bridge/OpenCodeBridgeCommandClient.ts index f5602d64..87778ee2 100644 --- a/src/main/services/team/opencode/bridge/OpenCodeBridgeCommandClient.ts +++ b/src/main/services/team/opencode/bridge/OpenCodeBridgeCommandClient.ts @@ -57,6 +57,25 @@ export interface OpenCodeBridgeCommandClientOptions { const DEFAULT_STDOUT_LIMIT_BYTES = 1_000_000; const DEFAULT_STDERR_LIMIT_BYTES = 256_000; +const WINDOWS_BATCH_EXTENSIONS = new Set(['.cmd', '.bat']); + +export function resolveOpenCodeBridgeProcessCwd( + binaryPath: string, + requestedCwd: string, + platform: NodeJS.Platform = process.platform +): string { + if (platform !== 'win32') { + return requestedCwd; + } + + const extension = path.win32.extname(binaryPath).toLowerCase(); + if (!WINDOWS_BATCH_EXTENSIONS.has(extension)) { + return requestedCwd; + } + + const launcherDirectory = path.win32.dirname(binaryPath); + return launcherDirectory && launcherDirectory !== '.' ? launcherDirectory : requestedCwd; +} export class ExecCliOpenCodeBridgeProcessRunner implements OpenCodeBridgeProcessRunner { async run(input: OpenCodeBridgeProcessRunInput): Promise { @@ -146,7 +165,7 @@ export class OpenCodeBridgeCommandClient { const processResult = await this.processRunner.run({ binaryPath: this.binaryPath, args: ['runtime', 'opencode-command', '--json', '--input', inputPath], - cwd: options.cwd, + cwd: resolveOpenCodeBridgeProcessCwd(this.binaryPath, options.cwd), timeoutMs: options.timeoutMs, stdoutLimitBytes: options.stdoutLimitBytes ?? DEFAULT_STDOUT_LIMIT_BYTES, stderrLimitBytes: options.stderrLimitBytes ?? DEFAULT_STDERR_LIMIT_BYTES, diff --git a/src/main/services/team/opencode/bridge/OpenCodeMcpBridgeEnv.ts b/src/main/services/team/opencode/bridge/OpenCodeMcpBridgeEnv.ts new file mode 100644 index 00000000..c6f516ea --- /dev/null +++ b/src/main/services/team/opencode/bridge/OpenCodeMcpBridgeEnv.ts @@ -0,0 +1,24 @@ +const DISABLED_HTTP_MCP_VALUES = new Set(['0', 'false', 'no', 'off']); + +const LOCAL_MCP_LAUNCH_ENV_KEYS = [ + 'CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND', + 'CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY', + 'CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON', +] as const; + +export interface OpenCodeMcpHttpBridgeEnv { + CLAUDE_TEAM_OPENCODE_MCP_HTTP?: string; +} + +export function isOpenCodeMcpHttpBridgeEnabled( + env: OpenCodeMcpHttpBridgeEnv = 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 { + for (const key of LOCAL_MCP_LAUNCH_ENV_KEYS) { + delete env[key]; + } +} diff --git a/test/main/services/team/OpenCodeBridgeCommandClient.test.ts b/test/main/services/team/OpenCodeBridgeCommandClient.test.ts index ef0c7479..e5700370 100644 --- a/test/main/services/team/OpenCodeBridgeCommandClient.test.ts +++ b/test/main/services/team/OpenCodeBridgeCommandClient.test.ts @@ -7,6 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { OpenCodeBridgeCommandClient, redactBridgeDiagnosticText, + resolveOpenCodeBridgeProcessCwd, type OpenCodeBridgeDiagnosticsSink, type OpenCodeBridgeProcessRunInput, type OpenCodeBridgeProcessRunResult, @@ -41,10 +42,14 @@ describe('OpenCodeBridgeCommandClient', () => { }; const client = createClient(); - const result = await client.execute('opencode.launchTeam', { runId: 'run-1' }, { - cwd: '/tmp/project', - timeoutMs: 10_000, - }); + const result = await client.execute( + 'opencode.launchTeam', + { runId: 'run-1' }, + { + cwd: '/tmp/project', + timeoutMs: 10_000, + } + ); expect(result).toMatchObject({ ok: true, @@ -83,10 +88,14 @@ describe('OpenCodeBridgeCommandClient', () => { }; const client = createClient(); - const result = await client.execute('opencode.launchTeam', { runId: 'run-1' }, { - cwd: '/tmp/project', - timeoutMs: 10_000, - }); + const result = await client.execute( + 'opencode.launchTeam', + { runId: 'run-1' }, + { + cwd: '/tmp/project', + timeoutMs: 10_000, + } + ); expect(result).toMatchObject({ ok: false, @@ -116,10 +125,14 @@ describe('OpenCodeBridgeCommandClient', () => { }; const client = createClient(); - const result = await client.execute('opencode.launchTeam', { runId: 'run-1' }, { - cwd: '/tmp/project', - timeoutMs: 10_000, - }); + const result = await client.execute( + 'opencode.launchTeam', + { runId: 'run-1' }, + { + cwd: '/tmp/project', + timeoutMs: 10_000, + } + ); expect(result).toMatchObject({ ok: false, @@ -148,10 +161,14 @@ describe('OpenCodeBridgeCommandClient', () => { }; const client = createClient(); - const result = await client.execute('opencode.launchTeam', { runId: 'run-1' }, { - cwd: '/tmp/project', - timeoutMs: 10_000, - }); + const result = await client.execute( + 'opencode.launchTeam', + { runId: 'run-1' }, + { + cwd: '/tmp/project', + timeoutMs: 10_000, + } + ); expect(result).toMatchObject({ ok: false, @@ -175,10 +192,14 @@ describe('OpenCodeBridgeCommandClient', () => { }; const client = createClient(); - const result = await client.execute('opencode.launchTeam', { runId: 'run-1' }, { - cwd: '/tmp/project', - timeoutMs: 10_000, - }); + const result = await client.execute( + 'opencode.launchTeam', + { runId: 'run-1' }, + { + cwd: '/tmp/project', + timeoutMs: 10_000, + } + ); expect(result).toMatchObject({ ok: false, @@ -208,14 +229,22 @@ describe('OpenCodeBridgeCommandClient', () => { }, }); - await client.execute('opencode.launchTeam', { runId: 'run-1' }, { - cwd: '/tmp/project', - timeoutMs: 10_000, - }); - await client.execute('opencode.launchTeam', { runId: 'run-2' }, { - cwd: '/tmp/project', - timeoutMs: 10_000, - }); + await client.execute( + 'opencode.launchTeam', + { runId: 'run-1' }, + { + cwd: '/tmp/project', + timeoutMs: 10_000, + } + ); + await client.execute( + 'opencode.launchTeam', + { runId: 'run-2' }, + { + cwd: '/tmp/project', + timeoutMs: 10_000, + } + ); expect(runner.calls[0].env).toMatchObject({ CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL: 'http://127.0.0.1:5001/mcp', @@ -226,6 +255,36 @@ describe('OpenCodeBridgeCommandClient', () => { OPENCODE_DISABLE_AUTOUPDATE: '1', }); }); + + it('runs Windows batch launchers from their launcher directory while preserving envelope cwd', async () => { + runner.nextResult = { + stdout: `${JSON.stringify(bridgeSuccess({ data: { runId: 'run-1' } }))}\n`, + stderr: '', + exitCode: 0, + timedOut: false, + }; + const client = createClient({ + binaryPath: 'C:\\runtime\\agent_teams_orchestrator\\cli-dev.cmd', + }); + + await client.execute( + 'opencode.launchTeam', + { runId: 'run-1' }, + { + cwd: 'C:\\projects\\team workspace', + timeoutMs: 10_000, + } + ); + + expect(runner.calls[0].cwd).toBe( + process.platform === 'win32' + ? 'C:\\runtime\\agent_teams_orchestrator' + : 'C:\\projects\\team workspace' + ); + expect(JSON.parse(await runner.readInputEnvelope(0))).toMatchObject({ + cwd: 'C:\\projects\\team workspace', + }); + }); }); describe('redactBridgeDiagnosticText', () => { @@ -242,6 +301,34 @@ describe('redactBridgeDiagnosticText', () => { }); }); +describe('resolveOpenCodeBridgeProcessCwd', () => { + it('keeps non-Windows launchers on the requested project cwd', () => { + expect( + resolveOpenCodeBridgeProcessCwd('/usr/local/bin/claude-multimodel', '/repo', 'linux') + ).toBe('/repo'); + }); + + it('uses the launcher directory for Windows batch launchers', () => { + expect( + resolveOpenCodeBridgeProcessCwd( + 'C:\\runtime\\agent_teams_orchestrator\\cli-dev.cmd', + 'C:\\projects\\team workspace', + 'win32' + ) + ).toBe('C:\\runtime\\agent_teams_orchestrator'); + }); + + it('keeps Windows exe launchers on the requested project cwd', () => { + expect( + resolveOpenCodeBridgeProcessCwd( + 'C:\\runtime-cache\\claude-multimodel.exe', + 'C:\\projects\\team workspace', + 'win32' + ) + ).toBe('C:\\projects\\team workspace'); + }); +}); + function createClient( overrides: Partial[0]> = {} ): OpenCodeBridgeCommandClient { diff --git a/test/main/services/team/OpenCodeMcpBridgeEnv.test.ts b/test/main/services/team/OpenCodeMcpBridgeEnv.test.ts new file mode 100644 index 00000000..4622061e --- /dev/null +++ b/test/main/services/team/OpenCodeMcpBridgeEnv.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest'; + +import { + clearOpenCodeLocalMcpLaunchEnv, + isOpenCodeMcpHttpBridgeEnabled, +} from '@main/services/team/opencode/bridge/OpenCodeMcpBridgeEnv'; + +describe('OpenCodeMcpBridgeEnv', () => { + it('uses the app-owned HTTP MCP bridge by default', () => { + expect(isOpenCodeMcpHttpBridgeEnabled({})).toBe(true); + expect(isOpenCodeMcpHttpBridgeEnabled({ CLAUDE_TEAM_OPENCODE_MCP_HTTP: '1' })).toBe(true); + expect(isOpenCodeMcpHttpBridgeEnabled({ CLAUDE_TEAM_OPENCODE_MCP_HTTP: 'true' })).toBe(true); + }); + + it('keeps the legacy local MCP command path behind an explicit opt-out', () => { + expect(isOpenCodeMcpHttpBridgeEnabled({ CLAUDE_TEAM_OPENCODE_MCP_HTTP: '0' })).toBe(false); + expect(isOpenCodeMcpHttpBridgeEnabled({ CLAUDE_TEAM_OPENCODE_MCP_HTTP: ' false ' })).toBe( + false + ); + expect(isOpenCodeMcpHttpBridgeEnabled({ CLAUDE_TEAM_OPENCODE_MCP_HTTP: 'off' })).toBe(false); + }); + + it('removes local MCP launch env when HTTP MCP is active', () => { + const env: NodeJS.ProcessEnv = { + 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"]', + CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL: 'http://127.0.0.1:41001/mcp', + }; + + clearOpenCodeLocalMcpLaunchEnv(env); + + expect(env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND).toBeUndefined(); + expect(env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY).toBeUndefined(); + expect(env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON).toBeUndefined(); + expect(env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL).toBe('http://127.0.0.1:41001/mcp'); + }); +}); From 2c393bc78f39b444a7e16a07ab15932f72109ca2 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sat, 16 May 2026 22:30:11 +0300 Subject: [PATCH 2/3] 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', From bfadfc95ee7f583cb2aa480944a104a2d31deff6 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sat, 16 May 2026 22:49:43 +0300 Subject: [PATCH 3/3] fix(opencode): preserve explicit mcp fallback env --- src/main/index.ts | 7 +++++++ .../opencode/bridge/OpenCodeMcpBridgeEnv.ts | 12 +++++++++++ .../team/OpenCodeMcpBridgeEnv.test.ts | 21 +++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/src/main/index.ts b/src/main/index.ts index 7dee47f0..1de646af 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -73,6 +73,7 @@ import { copyOpenCodeLocalMcpLaunchEnv, hasOpenCodeLocalMcpLaunchEnv, isOpenCodeMcpHttpBridgeEnabled, + snapshotOpenCodeLocalMcpLaunchEnv, } from '@main/services/team/opencode/bridge/OpenCodeMcpBridgeEnv'; import { ReviewApplierService } from '@main/services/team/ReviewApplierService'; import { TeamBackupService } from '@main/services/team/TeamBackupService'; @@ -360,6 +361,7 @@ async function createOpenCodeRuntimeAdapterRegistry( bridgeEnv.CLAUDE_TEAM_APP_INSTANCE_ID = openCodeManagedHostInstanceId; bridgeEnv.AGENT_TEAMS_MCP_CLAUDE_DIR = getClaudeBasePath(); const useHttpMcpBridge = isOpenCodeMcpHttpBridgeEnabled(bridgeEnv); + const explicitLocalMcpLaunchEnv = snapshotOpenCodeLocalMcpLaunchEnv(bridgeEnv); if (!useHttpMcpBridge) { delete bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL; } else { @@ -401,6 +403,11 @@ async function createOpenCodeRuntimeAdapterRegistry( copyOpenCodeLocalMcpLaunchEnv(bridgeEnv, targetEnv); return; } + if (explicitLocalMcpLaunchEnv) { + copyOpenCodeLocalMcpLaunchEnv(explicitLocalMcpLaunchEnv, targetEnv); + copyOpenCodeLocalMcpLaunchEnv(explicitLocalMcpLaunchEnv, bridgeEnv); + return; + } await applyMcpLaunchSpecEnv(targetEnv, options); if (hasOpenCodeLocalMcpLaunchEnv(targetEnv)) { diff --git a/src/main/services/team/opencode/bridge/OpenCodeMcpBridgeEnv.ts b/src/main/services/team/opencode/bridge/OpenCodeMcpBridgeEnv.ts index 6bf354cb..965c68ea 100644 --- a/src/main/services/team/opencode/bridge/OpenCodeMcpBridgeEnv.ts +++ b/src/main/services/team/opencode/bridge/OpenCodeMcpBridgeEnv.ts @@ -31,6 +31,18 @@ export function copyOpenCodeLocalMcpLaunchEnv( } } +export function snapshotOpenCodeLocalMcpLaunchEnv( + env: OpenCodeMcpBridgeEnv +): OpenCodeMcpBridgeEnv | null { + if (!hasOpenCodeLocalMcpLaunchEnv(env)) { + return null; + } + + const snapshot: OpenCodeMcpBridgeEnv = {}; + copyOpenCodeLocalMcpLaunchEnv(env, snapshot); + return snapshot; +} + 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 e9e8f533..9b905630 100644 --- a/test/main/services/team/OpenCodeMcpBridgeEnv.test.ts +++ b/test/main/services/team/OpenCodeMcpBridgeEnv.test.ts @@ -5,6 +5,7 @@ import { copyOpenCodeLocalMcpLaunchEnv, hasOpenCodeLocalMcpLaunchEnv, isOpenCodeMcpHttpBridgeEnabled, + snapshotOpenCodeLocalMcpLaunchEnv, } from '@main/services/team/opencode/bridge/OpenCodeMcpBridgeEnv'; describe('OpenCodeMcpBridgeEnv', () => { @@ -69,6 +70,26 @@ describe('OpenCodeMcpBridgeEnv', () => { expect(target.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL).toBe('http://127.0.0.1:41001/mcp'); }); + it('snapshots explicit local MCP launch env before HTTP mode clears it', () => { + const env: NodeJS.ProcessEnv = { + 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"] ', + CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL: 'http://127.0.0.1:41001/mcp', + }; + + const snapshot = snapshotOpenCodeLocalMcpLaunchEnv(env); + clearOpenCodeLocalMcpLaunchEnv(env); + + expect(snapshot).toEqual({ + 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"]', + }); + expect(hasOpenCodeLocalMcpLaunchEnv(snapshot ?? {})).toBe(true); + expect(env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND).toBeUndefined(); + }); + it('removes local MCP launch env when HTTP MCP is active', () => { const env: NodeJS.ProcessEnv = { CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: 'node',