fix(opencode): default to http mcp bridge

This commit is contained in:
iliya 2026-05-16 21:11:23 +03:00
parent 279439de10
commit 429dfe5528
5 changed files with 211 additions and 56 deletions

View file

@ -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<NodeJS.ProcessEnv> => {
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)

View file

@ -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<OpenCodeBridgeProcessRunResult> {
@ -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,

View file

@ -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];
}
}

View file

@ -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<ConstructorParameters<typeof OpenCodeBridgeCommandClient>[0]> = {}
): OpenCodeBridgeCommandClient {

View file

@ -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');
});
});