From deb05bb7832eed95d6624e74374e1fa1b48ae361 Mon Sep 17 00:00:00 2001 From: weishu Date: Mon, 8 Jun 2026 13:41:57 +0800 Subject: [PATCH] Auto-approve Codex title MCP tool --- cli/src/codex/codexLocal.test.ts | 8 +++++- cli/src/codex/codexLocal.ts | 3 +- cli/src/codex/utils/appServerConfig.test.ts | 31 +++++++++++++++++++++ cli/src/codex/utils/appServerConfig.ts | 3 +- cli/src/codex/utils/buildHapiMcpBridge.ts | 14 +++++++++- cli/src/codex/utils/codexMcpConfig.test.ts | 18 ++++++++++++ cli/src/codex/utils/codexMcpConfig.ts | 12 +++++++- 7 files changed, 84 insertions(+), 5 deletions(-) diff --git a/cli/src/codex/codexLocal.test.ts b/cli/src/codex/codexLocal.test.ts index 0bd649bd..a51d01cc 100644 --- a/cli/src/codex/codexLocal.test.ts +++ b/cli/src/codex/codexLocal.test.ts @@ -80,7 +80,12 @@ describe('codexLocal', () => { mcpServers: { hapi: { command: hapiCommandPath, - args: ['mcp', '--url', 'http://127.0.0.1:63995/'] + args: ['mcp', '--url', 'http://127.0.0.1:63995/'], + tools: { + change_title: { + approval_mode: 'approve' + } + } } }, sessionHook: { @@ -108,6 +113,7 @@ describe('codexLocal', () => { expect(hookArg).toBeDefined(); expect(hookArg).toContain('{ hooks = [{ type = "command", command = "'); expect(args).toContain("mcp_servers.hapi.args=['mcp','--url','http://127.0.0.1:63995/']"); + expect(args).toContain('mcp_servers.hapi.tools.change_title.approval_mode="approve"'); }); it('passes reasoning effort through Codex config instead of an unsupported CLI flag', async () => { diff --git a/cli/src/codex/codexLocal.ts b/cli/src/codex/codexLocal.ts index a72ee38a..e60e3a87 100644 --- a/cli/src/codex/codexLocal.ts +++ b/cli/src/codex/codexLocal.ts @@ -9,6 +9,7 @@ import { import { codexSystemPrompt } from './utils/systemPrompt'; import type { ReasoningEffort } from './appServerTypes'; import { resolveCodexCommand } from './utils/codexExecutable'; +import type { McpServersConfig } from './utils/buildHapiMcpBridge'; /** * Filter out 'resume' subcommand which is managed internally by hapi. @@ -38,7 +39,7 @@ export async function codexLocal(opts: { sandbox?: 'read-only' | 'workspace-write' | 'danger-full-access'; onSessionFound: (id: string) => void; codexArgs?: string[]; - mcpServers?: Record; + mcpServers?: McpServersConfig; sessionHook?: { port: number; token: string; diff --git a/cli/src/codex/utils/appServerConfig.test.ts b/cli/src/codex/utils/appServerConfig.test.ts index 26e1cc5c..3a08c554 100644 --- a/cli/src/codex/utils/appServerConfig.test.ts +++ b/cli/src/codex/utils/appServerConfig.test.ts @@ -47,6 +47,37 @@ describe('appServerConfig', () => { expect(params.approvalPolicy).toBe('on-request'); }); + it('passes MCP per-tool approval config through thread config', () => { + const params = buildThreadStartParams({ + cwd: '/workspace/project', + mode: { permissionMode: 'default', collaborationMode: 'default' }, + mcpServers: { + hapi: { + command: 'node', + args: ['mcp'], + tools: { + change_title: { + approval_mode: 'approve' + } + } + } + } + }); + + expect(params.config).toEqual({ + 'mcp_servers.hapi': { + command: 'node', + args: ['mcp'], + tools: { + change_title: { + approval_mode: 'approve' + } + } + }, + developer_instructions: codexSystemPrompt + }); + }); + it('ignores CLI overrides when permission mode is not default', () => { const params = buildThreadStartParams({ cwd: '/workspace/project', diff --git a/cli/src/codex/utils/appServerConfig.ts b/cli/src/codex/utils/appServerConfig.ts index 4a2e4f7e..84f8d813 100644 --- a/cli/src/codex/utils/appServerConfig.ts +++ b/cli/src/codex/utils/appServerConfig.ts @@ -61,7 +61,8 @@ function buildMcpServerConfig(mcpServers: McpServersConfig): Record; } /** @@ -63,7 +70,12 @@ export async function buildHapiMcpBridge( mcpServers: { hapi: { command: bridgeCommand.command, - args: bridgeCommand.args + args: bridgeCommand.args, + tools: { + change_title: { + approval_mode: 'approve' + } + } } } }; diff --git a/cli/src/codex/utils/codexMcpConfig.test.ts b/cli/src/codex/utils/codexMcpConfig.test.ts index 6f99ac37..298f2dbd 100644 --- a/cli/src/codex/utils/codexMcpConfig.test.ts +++ b/cli/src/codex/utils/codexMcpConfig.test.ts @@ -23,6 +23,24 @@ describe('codexMcpConfig', () => { ]); }); + it('builds per-tool approval mode config', () => { + const mcpServers = { + hapi: { + command: 'hapi', + args: ['mcp'], + tools: { + change_title: { + approval_mode: 'approve' as const + } + } + } + }; + + const args = buildMcpServerConfigArgs(mcpServers); + + expect(args).toContain('mcp_servers.hapi.tools.change_title.approval_mode="approve"'); + }); + it('builds config args for multiple MCP servers', () => { const mcpServers = { hapi: { command: 'hapi', args: ['mcp'] }, diff --git a/cli/src/codex/utils/codexMcpConfig.ts b/cli/src/codex/utils/codexMcpConfig.ts index 1cb04531..367bee1a 100644 --- a/cli/src/codex/utils/codexMcpConfig.ts +++ b/cli/src/codex/utils/codexMcpConfig.ts @@ -9,6 +9,7 @@ import { createHash } from 'node:crypto'; import { getHappyCliCommand } from '@/utils/spawnHappyCLI'; +import type { McpServersConfig } from './buildHapiMcpBridge'; /** * Escape a string value for use in a TOML string literal. @@ -121,7 +122,7 @@ export function buildSessionStartHookConfigArgs(port: number, token: string): st * @returns Array of CLI arguments to pass to codex */ export function buildMcpServerConfigArgs( - mcpServers: Record + mcpServers: McpServersConfig ): string[] { const configArgs: string[] = []; @@ -133,6 +134,15 @@ export function buildMcpServerConfigArgs( // Use TOML literal strings to avoid shell-quote mangling on Windows. const argsToml = buildTomlLiteralArray(server.args); configArgs.push('-c', `mcp_servers.${name}.args=${argsToml}`); + + for (const [toolName, tool] of Object.entries(server.tools ?? {})) { + if (tool.approval_mode) { + configArgs.push( + '-c', + `mcp_servers.${name}.tools.${toolName}.approval_mode="${escapeTomlString(tool.approval_mode)}"` + ); + } + } } return configArgs;