Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 64 additions & 4 deletions cli/src/codex/utils/buildHapiMcpBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { startHappyServer } from '@/claude/utils/startHappyServer';
import { getHappyCliCommand } from '@/utils/spawnHappyCLI';
import { spawnSync } from 'node:child_process';
import type { ApiSessionClient } from '@/api/apiSession';

/**
Expand Down Expand Up @@ -39,6 +40,61 @@ export interface HapiMcpBridgeOptions {
emitTitleSummary?: boolean;
}

// Codex app-server 0.130 can close a Bun-backed MCP stdio server while
// processing the initialize response. Keep a tiny Node stdio shim in front of
// the real hapi MCP bridge so Codex talks to a stable Node process. This is
// intentionally inline instead of a sidecar file so compiled/release HAPI can
// spawn it without needing extra packaged assets.
const HAPI_MCP_STDIO_PROXY_SCRIPT = String.raw`
const { spawn } = require('node:child_process');
const [command, ...args] = process.argv.slice(1);
if (!command) {
process.stderr.write('[hapi-mcp-proxy] Missing child command\n');
process.exit(2);
}
const child = spawn(command, args, {
stdio: ['pipe', 'pipe', 'pipe'],
env: process.env,
windowsHide: process.platform === 'win32'
});
let exiting = false;
function safeEnd(stream) { try { stream.end(); } catch {} }
function safeKill() {
if (child.killed || child.exitCode !== null) return;
try { child.kill(); } catch {}
}
process.stdin.on('data', (chunk) => {
if (!child.stdin.destroyed) child.stdin.write(chunk);
});
process.stdin.on('end', () => safeEnd(child.stdin));
process.stdin.on('error', () => safeEnd(child.stdin));
child.stdout.on('data', (chunk) => process.stdout.write(chunk));
child.stderr.on('data', (chunk) => process.stderr.write(chunk));
child.on('error', (error) => {
process.stderr.write('[hapi-mcp-proxy] Failed to start child: ' + (error instanceof Error ? error.message : String(error)) + '\n');
if (!exiting) { exiting = true; process.exit(1); }
});
child.on('exit', (code, signal) => {
if (exiting) return;
exiting = true;
if (signal) { process.kill(process.pid, signal); return; }
process.exit(code ?? 0);
});
process.on('SIGTERM', () => { safeKill(); process.exit(143); });
process.on('SIGINT', () => { safeKill(); process.exit(130); });
process.on('exit', safeKill);
`;

function resolveNodeExecutable(): string | null {
const override = process.env.HAPI_NODE_EXECUTABLE?.trim();
if (override) {
return override;
}

const result = spawnSync('node', ['--version'], { stdio: 'ignore' });
return result.status === 0 ? 'node' : null;
}

/**
* Start the hapi MCP bridge server and return the configuration
* needed to connect Codex to it.
Expand All @@ -54,17 +110,21 @@ export async function buildHapiMcpBridge(
emitTitleSummary: options.emitTitleSummary
});
const bridgeCommand = getHappyCliCommand(['mcp', '--url', happyServer.url]);
const nodeCommand = resolveNodeExecutable();
const hapiMcpServer = nodeCommand
? {
command: nodeCommand,
args: ['-e', HAPI_MCP_STDIO_PROXY_SCRIPT, bridgeCommand.command, ...bridgeCommand.args]
}
: bridgeCommand;

return {
server: {
url: happyServer.url,
stop: happyServer.stop
},
mcpServers: {
hapi: {
command: bridgeCommand.command,
args: bridgeCommand.args
}
hapi: hapiMcpServer
}
};
}
Loading
Loading