refactor: unify MCP bridge setup and system prompt delivery

Extract duplicate MCP bridge initialization code from codexLocalLauncher and
codexRemoteLauncher into a new buildHapiMcpBridge utility. This ensures both
modes use identical server setup logic and provides a single source of truth
for MCP configuration.

Update codexStartConfig to deliver system prompt via developer_instructions
instead of appending to the user message. This ensures consistent behavior
between local and remote modes and properly separates system guidance from
user intent.
This commit is contained in:
weishu
2026-01-05 18:38:58 +08:00
parent c50621e85a
commit 9a28e6e0a1
5 changed files with 80 additions and 28 deletions
+2 -10
View File
@@ -5,8 +5,7 @@ import { Future } from '@/utils/future';
import { createCodexSessionScanner } from './utils/codexSessionScanner';
import { convertCodexEvent } from './utils/codexEventConverter';
import { getLocalLaunchExitReason } from '@/agent/localLaunchPolicy';
import { startHappyServer } from '@/claude/utils/startHappyServer';
import { getHappyCliCommand } from '@/utils/spawnHappyCLI';
import { buildHapiMcpBridge } from './utils/buildHapiMcpBridge';
export async function codexLocalLauncher(session: CodexSession): Promise<'switch' | 'exit'> {
let exitReason: 'switch' | 'exit' | null = null;
@@ -14,14 +13,7 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
const exitFuture = new Future<void>();
// Start hapi server for MCP bridge (same as remote mode)
const happyServer = await startHappyServer(session.client);
const bridgeCommand = getHappyCliCommand(['mcp', '--url', happyServer.url]);
const mcpServers = {
hapi: {
command: bridgeCommand.command,
args: bridgeCommand.args
}
};
const { server: happyServer, mcpServers } = await buildHapiMcpBridge(session.client);
logger.debug(`[codex-local]: Started hapi MCP bridge server at ${happyServer.url}`);
const handleSessionMatchFailed = (message: string) => {
+3 -11
View File
@@ -11,8 +11,7 @@ import { DiffProcessor } from './utils/diffProcessor';
import { logger } from '@/ui/logger';
import { CodexDisplay } from '@/ui/ink/CodexDisplay';
import type { CodexSessionConfig } from './types';
import { getHappyCliCommand } from '@/utils/spawnHappyCLI';
import { startHappyServer } from '@/claude/utils/startHappyServer';
import { buildHapiMcpBridge } from './utils/buildHapiMcpBridge';
import { emitReadyIfIdle } from './utils/emitReadyIfIdle';
import type { CodexSession } from './session';
import type { EnhancedMode } from './loop';
@@ -25,7 +24,7 @@ import {
type RemoteLauncherExitReason
} from '@/modules/common/remote/RemoteLauncherBase';
type HappyServer = Awaited<ReturnType<typeof startHappyServer>>;
type HappyServer = Awaited<ReturnType<typeof buildHapiMcpBridge>>['server'];
class CodexRemoteLauncher extends RemoteLauncherBase {
private readonly session: CodexSession;
@@ -420,15 +419,8 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
}
});
const happyServer = await startHappyServer(session.client);
const { server: happyServer, mcpServers } = await buildHapiMcpBridge(session.client);
this.happyServer = happyServer;
const bridgeCommand = getHappyCliCommand(['mcp', '--url', happyServer.url]);
const mcpServers = {
hapi: {
command: bridgeCommand.command,
args: bridgeCommand.args
}
} as const;
this.setupAbortHandlers(session.client.rpcHandlerManager, {
onAbort: () => this.handleAbort(),
+61
View File
@@ -0,0 +1,61 @@
/**
* Unified MCP bridge setup for Codex local and remote modes.
*
* This module provides a single source of truth for starting the hapi MCP
* bridge server and generating the MCP server configuration that Codex needs.
*/
import { startHappyServer } from '@/claude/utils/startHappyServer';
import { getHappyCliCommand } from '@/utils/spawnHappyCLI';
import type { ApiSessionClient } from '@/api/apiSession';
/**
* MCP server entry configuration.
*/
export interface McpServerEntry {
command: string;
args: string[];
}
/**
* Map of MCP server names to their configurations.
*/
export type McpServersConfig = Record<string, McpServerEntry>;
/**
* Result of starting the hapi MCP bridge.
*/
export interface HapiMcpBridge {
/** The running server instance */
server: {
url: string;
stop: () => void;
};
/** MCP server config to pass to Codex (works for both CLI and SDK) */
mcpServers: McpServersConfig;
}
/**
* Start the hapi MCP bridge server and return the configuration
* needed to connect Codex to it.
*
* This is the single source of truth for MCP bridge setup,
* used by both local and remote launchers.
*/
export async function buildHapiMcpBridge(client: ApiSessionClient): Promise<HapiMcpBridge> {
const happyServer = await startHappyServer(client);
const bridgeCommand = getHappyCliCommand(['mcp', '--url', happyServer.url]);
return {
server: {
url: happyServer.url,
stop: happyServer.stop
},
mcpServers: {
hapi: {
command: bridgeCommand.command,
args: bridgeCommand.args
}
}
};
}
+5 -1
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest';
import { buildCodexStartConfig } from './codexStartConfig';
import { codexSystemPrompt } from './systemPrompt';
describe('buildCodexStartConfig', () => {
const mcpServers = { hapi: { command: 'node', args: ['mcp'] } };
@@ -15,7 +16,10 @@ describe('buildCodexStartConfig', () => {
expect(config.sandbox).toBe('danger-full-access');
expect(config['approval-policy']).toBe('never');
expect(config.config).toEqual({ mcp_servers: mcpServers });
expect(config.config).toEqual({
mcp_servers: mcpServers,
developer_instructions: codexSystemPrompt
});
});
it('ignores CLI overrides when permission mode is not default', () => {
+9 -6
View File
@@ -1,7 +1,7 @@
import type { CodexSessionConfig } from '../types';
import type { EnhancedMode } from '../loop';
import type { CodexCliOverrides } from './codexCliOverrides';
import { TITLE_INSTRUCTION } from './systemPrompt';
import { codexSystemPrompt } from './systemPrompt';
function resolveApprovalPolicy(mode: EnhancedMode): CodexSessionConfig['approval-policy'] {
switch (mode.permissionMode) {
@@ -42,11 +42,14 @@ export function buildCodexStartConfig(args: {
const resolvedApprovalPolicy = cliOverrides?.approvalPolicy ?? approvalPolicy;
const resolvedSandbox = cliOverrides?.sandbox ?? sandbox;
const prompt = args.first ? `${args.message}\n\n${TITLE_INSTRUCTION}` : args.message;
const config: Record<string, unknown> = { mcp_servers: args.mcpServers };
if (args.developerInstructions) {
config.developer_instructions = args.developerInstructions;
}
const prompt = args.message;
const baseInstructions = codexSystemPrompt;
const config: Record<string, unknown> = {
mcp_servers: args.mcpServers,
developer_instructions: args.developerInstructions
? `${baseInstructions}\n\n${args.developerInstructions}`
: baseInstructions
};
const startConfig: CodexSessionConfig = {
prompt,
sandbox: resolvedSandbox,