Fix Codex MCP agent-control access - pass MCP servers to Codex tool call
WHAT: - Fixed `buildCodexMcpConfig()` to include MCP servers in the Codex tool call - Added `CodexMcpServerConfig` and `CodexConfigPayload` types for proper typing - Added `managedAgentId` parameter to append caller agent ID to agent-control URL - Built MCP servers config including: 1. `agent-control` HTTP MCP with URL and `http_headers` 2. `playwright` STDIO MCP server 3. User-provided MCP servers from `config.mcpServers` - Added `managedAgentId` property to `CodexMcpAgentSession` class - Updated `setManagedAgentId()` to store the ID - Updated all call sites of `buildCodexMcpConfig()` to pass managed agent ID ROOT CAUSE: Claude provider builds MCP servers config and passes to Claude SDK. Codex MCP provider only passed `config.extra.codex` - completely ignoring `config.agentControlMcp` and `config.mcpServers`. Codex CLI expects MCP servers in `config.mcp_servers` field with `http_headers` (not `headers`) for HTTP servers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
b6f0cc5c9c
commit
007b41e455
@@ -0,0 +1,75 @@
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
import { z } from "zod";
|
||||
|
||||
async function main() {
|
||||
// The bearer token is the base64-encoded credentials
|
||||
const bearerToken = Buffer.from("mo:bo").toString("base64");
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
command: "codex",
|
||||
args: ["mcp-server"],
|
||||
env: {
|
||||
...process.env,
|
||||
// Set the bearer token env var
|
||||
PASEO_AGENT_CONTROL_TOKEN: bearerToken
|
||||
},
|
||||
});
|
||||
|
||||
const client = new Client(
|
||||
{ name: "test-client", version: "1.0.0" },
|
||||
{ capabilities: { elicitation: {} } }
|
||||
);
|
||||
|
||||
// Listen for events
|
||||
client.setNotificationHandler(
|
||||
z.object({
|
||||
method: z.literal("codex/event"),
|
||||
params: z.object({ msg: z.any() }),
|
||||
}).passthrough(),
|
||||
(data) => {
|
||||
const event = (data.params as { msg: unknown }).msg as { type?: string };
|
||||
if (event.type === "mcp_startup_update" || event.type === "mcp_startup_complete") {
|
||||
console.log("MCP Event:", JSON.stringify(event, null, 2));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await client.connect(transport);
|
||||
|
||||
// Use correct route (/mcp/agents) and bearer token env var
|
||||
console.log("\n=== Testing HTTP MCP server with correct route and bearer token ===\n");
|
||||
|
||||
try {
|
||||
const result = await client.callTool({
|
||||
name: "codex",
|
||||
arguments: {
|
||||
prompt: "List all the MCP tools you have available. Just list them, don't use any.",
|
||||
sandbox: "danger-full-access",
|
||||
"approval-policy": "never",
|
||||
config: {
|
||||
mcp_servers: {
|
||||
"agent-control": {
|
||||
url: "http://localhost:6767/mcp/agents",
|
||||
bearer_token_env_var: "PASEO_AGENT_CONTROL_TOKEN"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, undefined, { timeout: 60000 });
|
||||
|
||||
console.log("\n=== RESULT ===");
|
||||
const content = (result as { content: { text?: string }[] }).content;
|
||||
for (const item of content) {
|
||||
if (item.text) {
|
||||
console.log(item.text);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
}
|
||||
|
||||
await client.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
import { z } from "zod";
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioClientTransport({
|
||||
command: "codex",
|
||||
args: ["mcp-server"],
|
||||
env: {
|
||||
...process.env,
|
||||
// Try passing credentials via env var that Codex might read
|
||||
AGENT_CONTROL_AUTH: "mo:bo"
|
||||
},
|
||||
});
|
||||
|
||||
const client = new Client(
|
||||
{ name: "test-client", version: "1.0.0" },
|
||||
{ capabilities: { elicitation: {} } }
|
||||
);
|
||||
|
||||
// Listen for events
|
||||
client.setNotificationHandler(
|
||||
z.object({
|
||||
method: z.literal("codex/event"),
|
||||
params: z.object({ msg: z.any() }),
|
||||
}).passthrough(),
|
||||
(data) => {
|
||||
const event = (data.params as { msg: unknown }).msg as { type?: string };
|
||||
if (event.type === "mcp_startup_update" || event.type === "mcp_startup_complete") {
|
||||
console.log("MCP Event:", JSON.stringify(event, null, 2));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await client.connect(transport);
|
||||
|
||||
// Try passing MCP server config via the config parameter with headers
|
||||
console.log("\n=== Testing HTTP MCP server with basic auth in config ===\n");
|
||||
|
||||
try {
|
||||
// Create base64 encoded credentials
|
||||
const credentials = Buffer.from("mo:bo").toString("base64");
|
||||
|
||||
const result = await client.callTool({
|
||||
name: "codex",
|
||||
arguments: {
|
||||
prompt: "List all the MCP tools you have available. Just list them, don't use any.",
|
||||
sandbox: "danger-full-access",
|
||||
"approval-policy": "never",
|
||||
config: {
|
||||
mcp_servers: {
|
||||
"agent-control": {
|
||||
url: "http://localhost:6767/mcp/agent-control",
|
||||
// Try various ways to pass auth
|
||||
headers: {
|
||||
"Authorization": `Basic ${credentials}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, undefined, { timeout: 60000 });
|
||||
|
||||
console.log("\n=== RESULT ===");
|
||||
const content = (result as { content: { text?: string }[] }).content;
|
||||
for (const item of content) {
|
||||
if (item.text) {
|
||||
console.log(item.text);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
}
|
||||
|
||||
await client.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
import { z } from "zod";
|
||||
|
||||
async function main() {
|
||||
// Create base64 encoded credentials
|
||||
const credentials = Buffer.from("mo:bo").toString("base64");
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
command: "codex",
|
||||
args: ["mcp-server"],
|
||||
env: {
|
||||
...process.env,
|
||||
// Set the bearer token env var that we will reference
|
||||
AGENT_CONTROL_TOKEN: `Basic ${credentials}`
|
||||
},
|
||||
});
|
||||
|
||||
const client = new Client(
|
||||
{ name: "test-client", version: "1.0.0" },
|
||||
{ capabilities: { elicitation: {} } }
|
||||
);
|
||||
|
||||
// Listen for events
|
||||
client.setNotificationHandler(
|
||||
z.object({
|
||||
method: z.literal("codex/event"),
|
||||
params: z.object({ msg: z.any() }),
|
||||
}).passthrough(),
|
||||
(data) => {
|
||||
const event = (data.params as { msg: unknown }).msg as { type?: string };
|
||||
if (event.type === "mcp_startup_update" || event.type === "mcp_startup_complete") {
|
||||
console.log("MCP Event:", JSON.stringify(event, null, 2));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await client.connect(transport);
|
||||
|
||||
// Try passing MCP server config with bearer_token_env_var
|
||||
console.log("\n=== Testing HTTP MCP server with bearer_token_env_var ===\n");
|
||||
|
||||
try {
|
||||
const result = await client.callTool({
|
||||
name: "codex",
|
||||
arguments: {
|
||||
prompt: "List all the MCP tools you have available. Just list them, don't use any.",
|
||||
sandbox: "danger-full-access",
|
||||
"approval-policy": "never",
|
||||
config: {
|
||||
mcp_servers: {
|
||||
"agent-control": {
|
||||
url: "http://localhost:6767/mcp/agent-control",
|
||||
bearer_token_env_var: "AGENT_CONTROL_TOKEN"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, undefined, { timeout: 60000 });
|
||||
|
||||
console.log("\n=== RESULT ===");
|
||||
const content = (result as { content: { text?: string }[] }).content;
|
||||
for (const item of content) {
|
||||
if (item.text) {
|
||||
console.log(item.text);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
}
|
||||
|
||||
await client.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
import { z } from "zod";
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioClientTransport({
|
||||
command: "codex",
|
||||
args: ["mcp-server"],
|
||||
env: { ...process.env },
|
||||
});
|
||||
|
||||
const client = new Client(
|
||||
{ name: "test-client", version: "1.0.0" },
|
||||
{ capabilities: { elicitation: {} } }
|
||||
);
|
||||
|
||||
// Listen for events
|
||||
client.setNotificationHandler(
|
||||
z.object({
|
||||
method: z.literal("codex/event"),
|
||||
params: z.object({ msg: z.any() }),
|
||||
}).passthrough(),
|
||||
(data) => {
|
||||
const event = (data.params as { msg: unknown }).msg as { type?: string };
|
||||
if (event.type === "mcp_startup_update" || event.type === "mcp_startup_complete") {
|
||||
console.log("MCP Event:", JSON.stringify(event, null, 2));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await client.connect(transport);
|
||||
|
||||
// Try passing MCP server config via the config parameter with HTTP URL
|
||||
console.log("\n=== Testing HTTP MCP server config (agent-control style) ===\n");
|
||||
|
||||
try {
|
||||
const result = await client.callTool({
|
||||
name: "codex",
|
||||
arguments: {
|
||||
prompt: "List all the MCP tools you have available. Just list them, don't use any.",
|
||||
sandbox: "danger-full-access",
|
||||
"approval-policy": "never",
|
||||
config: {
|
||||
mcp_servers: {
|
||||
"agent-control": {
|
||||
url: "http://localhost:6767/mcp/agent-control",
|
||||
type: "http"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, undefined, { timeout: 60000 });
|
||||
|
||||
console.log("\n=== RESULT ===");
|
||||
const content = (result as { content: { text?: string }[] }).content;
|
||||
for (const item of content) {
|
||||
if (item.text) {
|
||||
console.log(item.text);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
}
|
||||
|
||||
await client.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
import { z } from "zod";
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioClientTransport({
|
||||
command: "codex",
|
||||
args: ["mcp-server"],
|
||||
env: { ...process.env },
|
||||
});
|
||||
|
||||
const client = new Client(
|
||||
{ name: "test-client", version: "1.0.0" },
|
||||
{ capabilities: { elicitation: {} } }
|
||||
);
|
||||
|
||||
// Listen for events
|
||||
client.setNotificationHandler(
|
||||
z.object({
|
||||
method: z.literal("codex/event"),
|
||||
params: z.object({ msg: z.any() }),
|
||||
}).passthrough(),
|
||||
(data) => {
|
||||
const event = (data.params as { msg: unknown }).msg as {
|
||||
type?: string;
|
||||
data?: { text?: string; item?: { type?: string } };
|
||||
text?: string;
|
||||
item?: { type?: string };
|
||||
};
|
||||
if (event.type === "turn.started") {
|
||||
console.log("\n=== TURN STARTED ===");
|
||||
} else if (event.type === "agent_message") {
|
||||
console.log("Agent:", event.data?.text || event.text);
|
||||
} else if (event.type === "mcp_tool_call") {
|
||||
console.log("MCP Tool Call:", JSON.stringify(event.data));
|
||||
} else if (event.type === "thread.item") {
|
||||
const item = event.data?.item || event.item;
|
||||
if (item?.type === "mcp_tool_call") {
|
||||
console.log("MCP Tool from thread:", JSON.stringify(item));
|
||||
}
|
||||
} else {
|
||||
console.log("Event:", event.type);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await client.connect(transport);
|
||||
|
||||
// Try passing MCP server config via the config parameter
|
||||
console.log("\n=== Testing dynamic MCP server config ===\n");
|
||||
|
||||
try {
|
||||
const result = await client.callTool({
|
||||
name: "codex",
|
||||
arguments: {
|
||||
prompt: "List all the MCP tools you have available. Just list them, don't use any.",
|
||||
sandbox: "danger-full-access",
|
||||
"approval-policy": "never",
|
||||
config: {
|
||||
mcp_servers: {
|
||||
"test-server": {
|
||||
command: "npx",
|
||||
args: ["-y", "mcp-server-time"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, undefined, { timeout: 60000 });
|
||||
|
||||
console.log("\n=== RESULT ===");
|
||||
const content = (result as { content: { text?: string }[] }).content;
|
||||
for (const item of content) {
|
||||
if (item.text) {
|
||||
console.log(item.text);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
}
|
||||
|
||||
await client.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -2321,16 +2321,30 @@ function getCodexMcpCommand(): string {
|
||||
}
|
||||
}
|
||||
|
||||
type CodexMcpServerConfig = {
|
||||
url?: string;
|
||||
http_headers?: Record<string, string>; // Static HTTP headers for HTTP servers
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
};
|
||||
|
||||
type CodexConfigPayload = {
|
||||
mcp_servers?: Record<string, CodexMcpServerConfig>;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
function buildCodexMcpConfig(
|
||||
config: AgentSessionConfig,
|
||||
prompt: string,
|
||||
modeId: string
|
||||
modeId: string,
|
||||
managedAgentId?: string
|
||||
): {
|
||||
prompt: string;
|
||||
cwd?: string;
|
||||
"approval-policy": string;
|
||||
sandbox: string;
|
||||
config?: unknown;
|
||||
config?: CodexConfigPayload;
|
||||
model?: string;
|
||||
} {
|
||||
const preset =
|
||||
@@ -2343,22 +2357,71 @@ function buildCodexMcpConfig(
|
||||
: preset.approvalPolicy;
|
||||
const sandbox =
|
||||
config.sandboxMode !== undefined ? config.sandboxMode : preset.sandbox;
|
||||
const extra = config.extra ? config.extra.codex : undefined;
|
||||
|
||||
// Build the config payload with MCP servers
|
||||
const innerConfig: CodexConfigPayload = {};
|
||||
|
||||
// Add extra codex config if provided
|
||||
if (config.extra?.codex) {
|
||||
Object.assign(innerConfig, config.extra.codex);
|
||||
}
|
||||
|
||||
// Build MCP servers configuration
|
||||
const mcpServers: Record<string, CodexMcpServerConfig> = {};
|
||||
|
||||
// Add agent-control MCP server (HTTP-based) if configured
|
||||
if (config.agentControlMcp) {
|
||||
let agentControlUrl = config.agentControlMcp.url;
|
||||
// Append caller agent ID to URL if this is a managed agent
|
||||
if (managedAgentId) {
|
||||
const separator = agentControlUrl.includes("?") ? "&" : "?";
|
||||
agentControlUrl = `${agentControlUrl}${separator}callerAgentId=${encodeURIComponent(managedAgentId)}`;
|
||||
}
|
||||
mcpServers["agent-control"] = {
|
||||
url: agentControlUrl,
|
||||
...(config.agentControlMcp.headers ? { http_headers: config.agentControlMcp.headers } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
// Add playwright MCP server (same as Claude provider)
|
||||
mcpServers["playwright"] = {
|
||||
command: "npx",
|
||||
args: ["@playwright/mcp", "--headless", "--isolated"],
|
||||
};
|
||||
|
||||
// Merge user-provided MCP servers (they take precedence)
|
||||
if (config.mcpServers) {
|
||||
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
||||
if (typeof serverConfig === "object" && serverConfig !== null) {
|
||||
mcpServers[name] = serverConfig as CodexMcpServerConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only add mcp_servers to config if there are any
|
||||
if (Object.keys(mcpServers).length > 0) {
|
||||
innerConfig.mcp_servers = mcpServers;
|
||||
}
|
||||
|
||||
const configPayload: {
|
||||
prompt: string;
|
||||
cwd?: string;
|
||||
"approval-policy": string;
|
||||
sandbox: string;
|
||||
config?: unknown;
|
||||
config?: CodexConfigPayload;
|
||||
model?: string;
|
||||
} = {
|
||||
prompt,
|
||||
cwd: config.cwd,
|
||||
"approval-policy": approvalPolicy,
|
||||
sandbox,
|
||||
config: extra,
|
||||
};
|
||||
|
||||
// Only include config if it has content
|
||||
if (Object.keys(innerConfig).length > 0) {
|
||||
configPayload.config = innerConfig;
|
||||
}
|
||||
|
||||
if (typeof config.model === "string" && config.model.length > 0) {
|
||||
configPayload.model = config.model;
|
||||
}
|
||||
@@ -2450,6 +2513,7 @@ class CodexMcpAgentSession implements AgentSession {
|
||||
private turnState: TurnState | null = null;
|
||||
private pendingPatchChanges = new Map<string, PatchFileChange[]>();
|
||||
private patchChangesByCallId = new Map<string, PatchFileChange[]>();
|
||||
private managedAgentId: string | null = null;
|
||||
|
||||
constructor(config: CodexMcpAgentConfig, resumeHandle?: AgentPersistenceHandle) {
|
||||
this.config = config;
|
||||
@@ -2840,9 +2904,8 @@ class CodexMcpAgentSession implements AgentSession {
|
||||
this.conversationId = null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
setManagedAgentId(_agentId: string): void {
|
||||
// Codex MCP sessions do not currently use the agent-control MCP channel.
|
||||
setManagedAgentId(agentId: string): void {
|
||||
this.managedAgentId = agentId;
|
||||
}
|
||||
|
||||
private async forwardPrompt(
|
||||
@@ -2858,7 +2921,7 @@ class CodexMcpAgentSession implements AgentSession {
|
||||
let response: unknown;
|
||||
try {
|
||||
if (!this.sessionId) {
|
||||
const config = buildCodexMcpConfig(this.config, prompt, this.currentMode);
|
||||
const config = buildCodexMcpConfig(this.config, prompt, this.currentMode, this.managedAgentId ?? undefined);
|
||||
const attempt = async (arguments_: CodexToolArguments) =>
|
||||
this.client.callTool(
|
||||
{ name: "codex", arguments: arguments_ },
|
||||
@@ -2896,7 +2959,7 @@ class CodexMcpAgentSession implements AgentSession {
|
||||
);
|
||||
if (isMissingConversationIdResponse(response)) {
|
||||
const replayPrompt = this.buildResumePrompt(prompt);
|
||||
const config = buildCodexMcpConfig(this.config, replayPrompt, this.currentMode);
|
||||
const config = buildCodexMcpConfig(this.config, replayPrompt, this.currentMode, this.managedAgentId ?? undefined);
|
||||
const attempt = async (arguments_: CodexToolArguments) =>
|
||||
this.client.callTool(
|
||||
{ name: "codex", arguments: arguments_ },
|
||||
@@ -2919,7 +2982,7 @@ class CodexMcpAgentSession implements AgentSession {
|
||||
} catch (error) {
|
||||
if (isMissingConversationIdError(error)) {
|
||||
const replayPrompt = this.buildResumePrompt(prompt);
|
||||
const config = buildCodexMcpConfig(this.config, replayPrompt, this.currentMode);
|
||||
const config = buildCodexMcpConfig(this.config, replayPrompt, this.currentMode, this.managedAgentId ?? undefined);
|
||||
const attempt = async (arguments_: CodexToolArguments) =>
|
||||
this.client.callTool(
|
||||
{ name: "codex", arguments: arguments_ },
|
||||
|
||||
Reference in New Issue
Block a user