Skip to content

Commit 3ceef1f

Browse files
committed
fix(opencode): hide app mcp child processes on windows
1 parent 58ff926 commit 3ceef1f

9 files changed

Lines changed: 854 additions & 37 deletions

File tree

mcp-server/src/index.ts

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,24 @@ import { FastMCP } from 'fastmcp';
55

66
import { registerTools } from './tools';
77

8+
const HTTP_TRANSPORT = 'httpStream';
9+
const STDIO_TRANSPORT = 'stdio';
10+
const DEFAULT_HTTP_HOST = '127.0.0.1';
11+
const DEFAULT_HTTP_ENDPOINT = '/mcp';
12+
13+
export type AgentTeamsMcpStartOptions =
14+
| {
15+
transportType: typeof STDIO_TRANSPORT;
16+
}
17+
| {
18+
transportType: typeof HTTP_TRANSPORT;
19+
httpStream: {
20+
host: string;
21+
port: number;
22+
endpoint: `/${string}`;
23+
};
24+
};
25+
826
export function createServer() {
927
const server = new FastMCP({
1028
name: 'agent-teams-mcp',
@@ -16,9 +34,64 @@ export function createServer() {
1634
return server;
1735
}
1836

37+
function getArgValue(argv: string[], name: string): string | null {
38+
const directPrefix = `${name}=`;
39+
for (let index = 2; index < argv.length; index += 1) {
40+
const value = argv[index];
41+
if (value === name) {
42+
return argv[index + 1] ?? null;
43+
}
44+
if (value.startsWith(directPrefix)) {
45+
return value.slice(directPrefix.length);
46+
}
47+
}
48+
return null;
49+
}
50+
51+
function normalizeEndpoint(value: string | null | undefined): `/${string}` {
52+
const trimmed = value?.trim();
53+
if (!trimmed) {
54+
return DEFAULT_HTTP_ENDPOINT;
55+
}
56+
return (trimmed.startsWith('/') ? trimmed : `/${trimmed}`) as `/${string}`;
57+
}
58+
59+
function parsePort(value: string | null | undefined): number {
60+
const parsed = Number(value);
61+
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
62+
throw new Error(`Invalid agent-teams MCP HTTP port: ${value ?? '<empty>'}`);
63+
}
64+
return parsed;
65+
}
66+
67+
export function resolveStartOptions(
68+
argv: string[] = process.argv,
69+
env: NodeJS.ProcessEnv = process.env
70+
): AgentTeamsMcpStartOptions {
71+
const transport =
72+
getArgValue(argv, '--transport') ??
73+
getArgValue(argv, '--transportType') ??
74+
env.AGENT_TEAMS_MCP_TRANSPORT ??
75+
STDIO_TRANSPORT;
76+
77+
if (transport !== HTTP_TRANSPORT) {
78+
return { transportType: STDIO_TRANSPORT };
79+
}
80+
81+
return {
82+
transportType: HTTP_TRANSPORT,
83+
httpStream: {
84+
host:
85+
getArgValue(argv, '--host')?.trim() ||
86+
env.AGENT_TEAMS_MCP_HTTP_HOST?.trim() ||
87+
DEFAULT_HTTP_HOST,
88+
port: parsePort(getArgValue(argv, '--port') ?? env.AGENT_TEAMS_MCP_HTTP_PORT),
89+
endpoint: normalizeEndpoint(getArgValue(argv, '--endpoint') ?? env.AGENT_TEAMS_MCP_HTTP_ENDPOINT),
90+
},
91+
};
92+
}
93+
1994
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
2095
const server = createServer();
21-
void server.start({
22-
transportType: 'stdio',
23-
});
96+
void server.start(resolveStartOptions());
2497
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { resolveStartOptions } from '../src/index';
4+
5+
describe('agent-teams MCP start options', () => {
6+
it('defaults to stdio transport', () => {
7+
expect(resolveStartOptions(['node', 'index.js'], {})).toEqual({
8+
transportType: 'stdio',
9+
});
10+
});
11+
12+
it('resolves HTTP stream transport from CLI args', () => {
13+
expect(
14+
resolveStartOptions(
15+
[
16+
'node',
17+
'index.js',
18+
'--transport',
19+
'httpStream',
20+
'--host',
21+
'127.0.0.1',
22+
'--port',
23+
'43123',
24+
'--endpoint',
25+
'mcp',
26+
],
27+
{}
28+
)
29+
).toEqual({
30+
transportType: 'httpStream',
31+
httpStream: {
32+
host: '127.0.0.1',
33+
port: 43123,
34+
endpoint: '/mcp',
35+
},
36+
});
37+
});
38+
39+
it('resolves HTTP stream transport from environment', () => {
40+
expect(
41+
resolveStartOptions(['node', 'index.js'], {
42+
AGENT_TEAMS_MCP_TRANSPORT: 'httpStream',
43+
AGENT_TEAMS_MCP_HTTP_PORT: '43124',
44+
})
45+
).toEqual({
46+
transportType: 'httpStream',
47+
httpStream: {
48+
host: '127.0.0.1',
49+
port: 43124,
50+
endpoint: '/mcp',
51+
},
52+
});
53+
});
54+
});

src/main/index.ts

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ import {
133133
import { startEventLoopLagMonitor } from './services/infrastructure/EventLoopLagMonitor';
134134
import { HttpServer } from './services/infrastructure/HttpServer';
135135
import { clearAutoResumeService } from './services/team/AutoResumeService';
136+
import { agentTeamsMcpHttpServer } from './services/team/AgentTeamsMcpHttpServer';
136137
import { LaunchIoGovernor } from './services/team/LaunchIoGovernor';
137138
import { OpenCodeBridgeCommandClient } from './services/team/opencode/bridge/OpenCodeBridgeCommandClient';
138139
import {
@@ -381,23 +382,37 @@ async function createOpenCodeRuntimeAdapterRegistry(
381382
);
382383
}
383384
try {
384-
reportProgress('runtime-mcp', 'Resolving Agent Teams MCP server...');
385-
const mcpLaunchSpec = await resolveAgentTeamsMcpLaunchSpec({
386-
onProgress: ({ phase, message }) => reportProgress(`mcp-${phase}`, message),
387-
});
388-
const mcpEntry = mcpLaunchSpec.args[0];
389-
if (mcpEntry) {
390-
bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND = mcpLaunchSpec.command;
391-
bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY = mcpEntry;
392-
bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON = JSON.stringify(mcpLaunchSpec.args);
393-
}
385+
reportProgress('runtime-mcp-http', 'Starting Agent Teams MCP server...');
386+
const mcpHttpServer = await agentTeamsMcpHttpServer.ensureStarted();
387+
bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL = mcpHttpServer.url;
388+
reportProgress('runtime-mcp-http-ready', 'Agent Teams MCP server is ready...');
394389
} catch (error) {
395390
logger.warn(
396-
`[OpenCode] Runtime adapter bridge MCP entrypoint unresolved: ${
391+
`[OpenCode] Runtime adapter bridge MCP HTTP server unavailable: ${
397392
error instanceof Error ? error.message : String(error)
398393
}`
399394
);
400395
}
396+
if (!bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL) {
397+
try {
398+
reportProgress('runtime-mcp', 'Resolving Agent Teams MCP server...');
399+
const mcpLaunchSpec = await resolveAgentTeamsMcpLaunchSpec({
400+
onProgress: ({ phase, message }) => reportProgress(`mcp-${phase}`, message),
401+
});
402+
const mcpEntry = mcpLaunchSpec.args[0];
403+
if (mcpEntry) {
404+
bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND = mcpLaunchSpec.command;
405+
bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY = mcpEntry;
406+
bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON = JSON.stringify(mcpLaunchSpec.args);
407+
}
408+
} catch (error) {
409+
logger.warn(
410+
`[OpenCode] Runtime adapter bridge MCP entrypoint unresolved: ${
411+
error instanceof Error ? error.message : String(error)
412+
}`
413+
);
414+
}
415+
}
401416

402417
reportProgress('runtime-bridge', 'Preparing OpenCode bridge...');
403418
const bridgeClient = new OpenCodeBridgeCommandClient({
@@ -2081,6 +2096,9 @@ async function shutdownServices(): Promise<void> {
20812096
() => cleanupOpenCodeHostsForLifecycle('shutdown'),
20822097
10_000
20832098
);
2099+
await runShutdownStep('Agent Teams MCP HTTP server cleanup', () =>
2100+
agentTeamsMcpHttpServer.stop()
2101+
);
20842102
await runShutdownStep('tracked CLI subprocess cleanup', () =>
20852103
killTrackedCliProcesses('SIGKILL')
20862104
);

0 commit comments

Comments
 (0)