From 9a28e6e0a18b173ae1f0e17c48173f414632d34b Mon Sep 17 00:00:00 2001 From: weishu Date: Mon, 5 Jan 2026 18:15:18 +0800 Subject: [PATCH] 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. --- cli/src/codex/codexLocalLauncher.ts | 12 +--- cli/src/codex/codexRemoteLauncher.ts | 14 +---- cli/src/codex/utils/buildHapiMcpBridge.ts | 61 ++++++++++++++++++++ cli/src/codex/utils/codexStartConfig.test.ts | 6 +- cli/src/codex/utils/codexStartConfig.ts | 15 +++-- 5 files changed, 80 insertions(+), 28 deletions(-) create mode 100644 cli/src/codex/utils/buildHapiMcpBridge.ts diff --git a/cli/src/codex/codexLocalLauncher.ts b/cli/src/codex/codexLocalLauncher.ts index 04cdc980..f1e30772 100644 --- a/cli/src/codex/codexLocalLauncher.ts +++ b/cli/src/codex/codexLocalLauncher.ts @@ -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(); // 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) => { diff --git a/cli/src/codex/codexRemoteLauncher.ts b/cli/src/codex/codexRemoteLauncher.ts index 8464c663..f4cf39fc 100644 --- a/cli/src/codex/codexRemoteLauncher.ts +++ b/cli/src/codex/codexRemoteLauncher.ts @@ -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>; +type HappyServer = Awaited>['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(), diff --git a/cli/src/codex/utils/buildHapiMcpBridge.ts b/cli/src/codex/utils/buildHapiMcpBridge.ts new file mode 100644 index 00000000..8520ae22 --- /dev/null +++ b/cli/src/codex/utils/buildHapiMcpBridge.ts @@ -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; + +/** + * 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 { + 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 + } + } + }; +} diff --git a/cli/src/codex/utils/codexStartConfig.test.ts b/cli/src/codex/utils/codexStartConfig.test.ts index c09d8815..c6e547d7 100644 --- a/cli/src/codex/utils/codexStartConfig.test.ts +++ b/cli/src/codex/utils/codexStartConfig.test.ts @@ -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', () => { diff --git a/cli/src/codex/utils/codexStartConfig.ts b/cli/src/codex/utils/codexStartConfig.ts index dd0acaa6..45560afc 100644 --- a/cli/src/codex/utils/codexStartConfig.ts +++ b/cli/src/codex/utils/codexStartConfig.ts @@ -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 = { mcp_servers: args.mcpServers }; - if (args.developerInstructions) { - config.developer_instructions = args.developerInstructions; - } + const prompt = args.message; + const baseInstructions = codexSystemPrompt; + const config: Record = { + mcp_servers: args.mcpServers, + developer_instructions: args.developerInstructions + ? `${baseInstructions}\n\n${args.developerInstructions}` + : baseInstructions + }; const startConfig: CodexSessionConfig = { prompt, sandbox: resolvedSandbox,