Skip to content

Commit 6c75c0a

Browse files
committed
feat(codex): project MCP servers into runtime config
1 parent 7c72a93 commit 6c75c0a

3 files changed

Lines changed: 126 additions & 3 deletions

File tree

electron/main/codexAppServerManager.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ interface CodexStreamStartParams {
6767
instructions?: string
6868
effort?: string
6969
dynamicTools?: CodexDynamicToolSpec[]
70+
config?: Record<string, any>
7071
}
7172

7273
interface ActiveCodexStream {
@@ -793,6 +794,7 @@ class CodexAppServerManager {
793794
serviceName: 'alice_electron',
794795
ephemeral: true,
795796
environments: [],
797+
config: params.config || undefined,
796798
dynamicTools:
797799
stream.dynamicTools.size > 0
798800
? Array.from(stream.dynamicTools.values())

src/services/llmProviders/__tests__/codex.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
22
import { createPinia, setActivePinia } from 'pinia'
33
import { useSettingsStore } from '../../../stores/settingsStore'
44
import {
5+
convertToolsToCodexRuntimeConfig,
56
convertToolsToCodexDynamicTools,
67
convertResponsesInputToCodexInput,
78
createCodexResponse,
@@ -123,12 +124,45 @@ describe('listCodexModels', () => {
123124
])
124125
})
125126

127+
it('projects OpenAI MCP tools into Codex runtime mcp_servers config', () => {
128+
expect(
129+
convertToolsToCodexRuntimeConfig([
130+
{
131+
type: 'mcp',
132+
server_label: 'deepwiki',
133+
server_url: 'https://mcp.deepwiki.com/mcp',
134+
require_approval: 'never',
135+
headers: { Authorization: 'Bearer test' },
136+
},
137+
])
138+
).toEqual({
139+
mcp_servers: {
140+
deepwiki: {
141+
url: 'https://mcp.deepwiki.com/mcp',
142+
default_tools_approval_mode: 'approve',
143+
http_headers: { Authorization: 'Bearer test' },
144+
},
145+
},
146+
})
147+
})
148+
126149
it('uses live app-server models instead of a stale configured Codex model', async () => {
127150
setActivePinia(createPinia())
128151
const settingsStore = useSettingsStore()
129152
settingsStore.updateSetting('aiProvider', 'codex')
130153
settingsStore.updateSetting('assistantModel', 'gpt-5.5')
131154
settingsStore.updateSetting('assistantTools', ['get_current_datetime'])
155+
settingsStore.updateSetting(
156+
'mcpServersConfig',
157+
JSON.stringify([
158+
{
159+
type: 'mcp',
160+
server_label: 'deepwiki',
161+
server_url: 'https://mcp.deepwiki.com/mcp',
162+
require_approval: 'never',
163+
},
164+
])
165+
)
132166

133167
const listeners = new Map<string, (event: any, payload: any) => void>()
134168
const startArgs: any[] = []
@@ -204,6 +238,14 @@ describe('listCodexModels', () => {
204238
}),
205239
])
206240
)
241+
expect(startArgs[0]?.config).toEqual({
242+
mcp_servers: {
243+
deepwiki: {
244+
url: 'https://mcp.deepwiki.com/mcp',
245+
default_tools_approval_mode: 'approve',
246+
},
247+
},
248+
})
207249
})
208250

209251
it('aggregates Codex app-server text for background responses', async () => {

src/services/llmProviders/codex.ts

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export interface CodexDynamicToolSpec {
1010
inputSchema: Record<string, any>
1111
}
1212

13+
export interface CodexRuntimeConfig {
14+
mcp_servers?: Record<string, Record<string, any>>
15+
}
16+
1317
interface CodexModelListResult {
1418
success?: boolean
1519
models?: Array<{
@@ -128,9 +132,79 @@ export function convertToolsToCodexDynamicTools(
128132
}))
129133
}
130134

131-
async function buildCodexDynamicTools(): Promise<CodexDynamicToolSpec[]> {
135+
function normalizeCodexMcpApprovalMode(value: unknown): string | undefined {
136+
if (value === 'never' || value === false) {
137+
return 'approve'
138+
}
139+
if (value === 'always' || value === true) {
140+
return 'prompt'
141+
}
142+
return undefined
143+
}
144+
145+
function normalizeStringRecord(value: unknown): Record<string, string> | null {
146+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
147+
return null
148+
}
149+
150+
const entries = Object.entries(value as Record<string, unknown>).filter(
151+
(entry): entry is [string, string] =>
152+
typeof entry[0] === 'string' && typeof entry[1] === 'string'
153+
)
154+
155+
return entries.length > 0 ? Object.fromEntries(entries) : null
156+
}
157+
158+
export function convertToolsToCodexRuntimeConfig(
159+
tools: any[]
160+
): CodexRuntimeConfig | undefined {
161+
const mcpServers: Record<string, Record<string, any>> = {}
162+
163+
for (const tool of tools) {
164+
if (
165+
tool?.type !== 'mcp' ||
166+
typeof tool.server_label !== 'string' ||
167+
typeof tool.server_url !== 'string'
168+
) {
169+
continue
170+
}
171+
172+
const serverName = tool.server_label.trim()
173+
const serverUrl = tool.server_url.trim()
174+
if (!serverName || !serverUrl) {
175+
continue
176+
}
177+
178+
const serverConfig: Record<string, any> = { url: serverUrl }
179+
const approvalMode = normalizeCodexMcpApprovalMode(tool.require_approval)
180+
if (approvalMode) {
181+
serverConfig.default_tools_approval_mode = approvalMode
182+
}
183+
184+
const headers = normalizeStringRecord(tool.headers)
185+
if (headers) {
186+
serverConfig.http_headers = headers
187+
}
188+
189+
mcpServers[serverName] = serverConfig
190+
}
191+
192+
if (Object.keys(mcpServers).length === 0) {
193+
return undefined
194+
}
195+
196+
return { mcp_servers: mcpServers }
197+
}
198+
199+
async function buildCodexTooling(): Promise<{
200+
dynamicTools: CodexDynamicToolSpec[]
201+
config?: CodexRuntimeConfig
202+
}> {
132203
const tools = await buildToolsForProvider()
133-
return convertToolsToCodexDynamicTools(tools)
204+
return {
205+
dynamicTools: convertToolsToCodexDynamicTools(tools),
206+
config: convertToolsToCodexRuntimeConfig(tools),
207+
}
134208
}
135209

136210
function isLikelyCodexModelCompatibilityError(error: unknown): boolean {
@@ -150,6 +224,7 @@ async function* streamViaCodexAppServerWithFallback(
150224
instructions: string
151225
effort?: string
152226
dynamicTools?: CodexDynamicToolSpec[]
227+
config?: CodexRuntimeConfig
153228
},
154229
signal?: AbortSignal
155230
): AsyncGenerator<any> {
@@ -275,13 +350,15 @@ export const createCodexResponse = async (
275350
const instructions =
276351
customInstructions ||
277352
buildAssistantSystemPrompt(settings.assistantSystemPrompt)
353+
const tooling = await buildCodexTooling()
278354

279355
const request = {
280356
input: convertResponsesInputToCodexInput(input),
281357
model,
282358
instructions,
283359
effort: normalizeCodexEffort(settings.assistantReasoningEffort),
284-
dynamicTools: await buildCodexDynamicTools(),
360+
dynamicTools: tooling.dynamicTools,
361+
config: tooling.config,
285362
}
286363

287364
return streamViaCodexAppServerWithFallback(request, signal)
@@ -320,6 +397,7 @@ async function* streamViaCodexAppServer(
320397
instructions: string
321398
effort?: string
322399
dynamicTools?: CodexDynamicToolSpec[]
400+
config?: CodexRuntimeConfig
323401
},
324402
signal?: AbortSignal
325403
): AsyncGenerator<any> {
@@ -380,6 +458,7 @@ async function* streamViaCodexAppServer(
380458
instructions: request.instructions,
381459
effort: request.effort,
382460
dynamicTools: request.dynamicTools,
461+
config: request.config,
383462
}
384463
)
385464
if (!startResult?.success) {

0 commit comments

Comments
 (0)