Skip to content

Bridging MCP Servers

hypnguyen1209 edited this page Aug 26, 2026 · 2 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 server is re-exposed through one of three exposure modes, and calls are always forwarded verbatim — text, images, structured content, and error flags all pass through:

  • direct — every upstream tool is offered individually as <server>__<tool> (e.g. remote_exec__docker_ps). The default for explicit mcpServers entries.
  • gateway — the whole server collapses to one dispatcher tool plus a generated skill. See Gateway mode below.
  • catalog — the server's tools are indexed privately and reached through four fixed discovery tools instead of being advertised individually. The default for servers imported from Codex (config.toml and CLI, including plugins). See Catalog mode below.

Set mode per server to override its default. So an explicit entry stays direct unless you ask otherwise, while a large imported tool set stays out of the advertised surface until searched.

⚠️ 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).
  • mode: "direct" | "gateway" | "catalog" — exposure mode (see above). Defaults to direct for explicit entries, catalog for Codex-imported ones.
  • 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)
Tools loaded (114): 26 native + 88 upstream-facing MCP tools
Upstream MCP servers:
  idasql      -> catalog (12 private tool(s))
  idalib      -> catalog (5 private tool(s))
  remote-exec -> direct (84 tool(s))
  brokensrv   -> FAILED: could not launch 'D:/wrong/path.exe': The system cannot find the path specified. (os error 3)

Imported servers (idasql, idalib) default to catalog, so their tools are private and together add just the four mcp_* discovery tools; the explicit remote-exec is direct, contributing its 84 tools individually. That's why upstream-facing (88) is 4 + 84, not 12 + 5 + 84.

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.

Catalog mode

mode: "catalog" keeps a server's tools out of the advertised surface entirely and makes them reachable through progressive disclosure. This is the default for imported Codex servers (config.toml and CLI, including plugins), so a large transitive tool set doesn't flood the connector; set mode explicitly to opt an explicit server in, or to force an imported one to direct/gateway.

{ "mcpServers": { "idasql": { "mode": "catalog" } } }

However many catalog-mode servers you bridge, they collectively add just four fixed tools:

  • mcp_list_sources — list the indexed servers (id, name, provenance, transport category, tool count, and the server's own self-description).
  • mcp_search_tools — weighted (BM25) search across the private tool index; returns opaque source/tool ids and summaries.
  • mcp_get_tool — fetch one tool's full definition (description + input schema) by id.
  • mcp_call_tool — invoke a tool by its source/tool id with arguments; the call is forwarded verbatim to the upstream server.

The full tool definitions live only in Codex Free's process — the model sees them one lookup at a time. disabled, type, tools, and disabledTools still apply first, so a filtered-out tool never enters the index and can't be searched or called. mcp_call_tool resolves the model-supplied ids to a stored upstream tool name (never one reconstructed from model input) and fails closed on any unknown id. Only the server name, provenance, transport category (stdio/streamable-http), the upstream's self-reported implementation info, and per-tool definitions are ever exposed — never url, command, env, or auth headers. See Security Model.


See also

Clone this wiki locally