fix(opencode): default to http mcp bridge
This commit is contained in:
parent
279439de10
commit
429dfe5528
5 changed files with 211 additions and 56 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
38
test/main/services/team/OpenCodeMcpBridgeEnv.test.ts
Normal file
38
test/main/services/team/OpenCodeMcpBridgeEnv.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue