Skip to content

Bridging MCP Servers

hypnguyen1209 edited this page Aug 25, 2026 · 3 revisions

Bridging Other MCP Servers

Codex Free can act as an MCP aggregator: it connects to other MCP servers (local stdio or remote Streamable HTTP), discovers their tools at startup, and re-exposes them through its own /mcp endpoint — so the ChatGPT-side agent can call them too.

Each upstream tool is offered as <server>__<tool> (e.g. remote_exec__docker_ps), and calls are forwarded verbatim — text, images, structured content, and error flags all pass through.

⚠️ Bridged servers carry delegated authority. A stdio upstream launches a real process running as your OS user; an HTTP upstream receives model-directed calls plus its configured token. Only bridge servers you trust. See Security Model.


Automatic discovery from Codex

Codex Free reads $CODEX_HOME/config.toml (or ~/.codex/config.toml) — read-only, never rewritten — and imports user-configured MCP servers without requiring a codex executable.

For each [mcp_servers.<name>] entry it preserves the fields it can:

  • command, args, env, cwd for local stdio launch;
  • local env_vars, resolved from Codex Free's process environment;
  • url for Streamable HTTP;
  • bearer_token_env_var, http_headers, env_http_headers for HTTP auth and headers;
  • startup_timeout_sec (or legacy startup_timeout_ms) and tool_timeout_sec;
  • enabled = false as a disabled upstream;
  • enabled_tools as an allow-list and disabled_tools as a deny-list applied afterward.

By default it also runs codex mcp list --json and, for servers in Codex's effective catalogue but absent from config.toml, codex mcp get <name> --json — so plugin-provided enablement and tool allow/deny lists are preserved. Each invocation is bounded to 30 s and 4 MiB of stdout, parsed in memory without logging literal env values.

When the CLI is missing/fails/times out, startup continues with the direct config.toml result and prints a warning that plugin-provided servers may be missing. Pass --codex-cli to make CLI discovery mandatory (the same condition then becomes a startup error). Configure via the codexMcp block:

{ "codexMcp": { "enabled": false }, "mcpServers": {} }   // disable all auto-import, keep explicit
{ "codexMcp": { "enabled": true, "useCli": false } }      // keep direct import, never run the CLI

Explicit servers

The mcpServers map is always supported. A local (stdio) entry:

{
  "mcpServers": {
    "idasql": {
      "command": "idasql-mcp",
      "args": ["--stdio"],
      "env": { "IDA_PATH": "C:/Program Files/IDA" }
    }
  }
}

A remote (Streamable HTTP) entry — keep secrets in env vars, not the JSON:

{
  "mcpServers": {
    "remote-docs": {
      "url": "https://mcp.example.com/mcp",
      "bearerTokenEnvVar": "REMOTE_MCP_TOKEN",
      "httpHeaders": { "X-Client": "codex-free" },
      "envHttpHeaders": { "X-Tenant": "REMOTE_MCP_TENANT" },
      "startupTimeoutSec": 20,
      "toolTimeoutSec": 60
    }
  }
}

bearerTokenEnvVar must exist and be non-empty when configured. Env-backed headers override a same-named static header. Don't configure both bearerTokenEnvVar and an Authorization header entry.

Overriding an imported server

An explicit entry with the same name as an imported Codex server is a field-by-field overlay — reuse Codex's launch settings while adding bridge-only options without copying command/args/env:

{ "mcpServers": { "remote-exec": { "mode": "gateway", "tools": ["exec", "machine_list"] } } }

Set an empty array/object to replace an imported collection with an empty one. Explicit command/url replace the imported transport.

Per-entry options

  • disabled: true — keep config but skip it (shown -> disabled).
  • tools: [...] — allow-list of upstream tool names to bridge.
  • disabledTools: [...] — remove tools after the allow-list.
  • cwd — child process working directory.
  • startupTimeoutSec — bounds init + first tools/list (default 20 s).
  • toolTimeoutSec — bounds each forwarded call; sends MCP cancellation on timeout.
  • Bridged names are sanitized to [A-Za-z0-9_] (e.g. remote_exec__exec).
  • A bridged name colliding with a native tool is skipped with a warning.
  • type is inferred (command → stdio, url → HTTP); aliases "http", "streamable-http", "streamable_http" accepted. Legacy SSE and WebSocket are rejected.

OAuth login/credential persistence is not implemented — an OAuth-protected upstream must be given a usable bearer token via bearerTokenEnvVar or an env-backed Authorization header.

The startup banner tells you everything

Codex MCP config discovery: /home/user/.codex/config.toml
  idasql -> imported from Codex config
Codex CLI MCP discovery: codex
  idalib -> imported from Codex CLI (not present in config.toml)
bridged MCP server 'idasql': 12 tool(s)
Tools loaded (38): 26 native + 12 bridged from upstream MCP servers

Upstream MCP servers:
  remote-exec -> 84 tool(s)
  idasql      -> FAILED: could not launch 'D:/wrong/path.exe': The system cannot find the path specified. (os error 3)

An upstream that fails to launch/connect/authenticate is skipped, never blocking startup or the native tools. If a server doesn't show up, check the banner first — the most common cause is a wrong command path.

Gateway mode

Some clients (ChatGPT among them) won't reliably surface a large bridged tool set. mode: "gateway" collapses a whole server into a single dispatcher tool plus a generated skill:

{ "mcpServers": { "remote-exec": { "mode": "gateway" } } }

It registers one tool named remote_exec taking { "function": "<name>", "arguments": { ... } }, and auto-generates a skill (skills_read name="remote-exec") documenting every function and its schema. So an 84-tool server shows up as 1 tool + 1 skill instead of 84. disabled, type, tools, and disabledTools still apply. When the server was imported from Codex, the overlay alone is enough; include command and launch fields when it exists only in codex.config.json.


See also

Clone this wiki locally