Skip to content

feat: add _meta.codex.strictMcpConfig for client-only MCP servers - #548

Open
ccarollo wants to merge 1 commit into
agentclientprotocol:mainfrom
ccarollo:strict-mcp-config
Open

ccarollo wants to merge 1 commit into
agentclientprotocol:mainfrom
ccarollo:strict-mcp-config

Conversation

@ccarollo

Copy link
Copy Markdown

Why

A client sometimes needs a session to have exactly the MCP servers it passed. One case is a client that routes every tool approval through its own UI and audit log, and must not let a session reach tools it never granted. Today a session gets the MCP servers in the user's Codex configuration plus the client's mcpServers, and Codex can add more through plugins and the ChatGPT apps connector. The client can't opt out:

  • Codex deep-merges the session config over the configuration layers, so sending an empty mcp_servers table removes nothing.
  • DISABLE_MCP_CONFIG_FILTERING only changes how name conflicts are handled, and it is process-wide.

claude-agent-acp offers the same guarantee as _meta.claudeCode.options.strictMcpConfig, which maps to Claude Code's --strict-mcp-config. This adds the Codex equivalent under the same key name.

What

When session/new, session/load, session/resume or session/fork carries _meta: {"codex": {"strictMcpConfig": true}}, createSessionConfig builds the session config differently:

  • It reads the MCP servers in the effective configuration for cwd (config/read), and sets enabled = false for each one in the session config.
  • It adds the client's servers as usual.
  • It turns off apps, plugins and skill_mcp_dependency_install, the features that add MCP servers of their own.
  • It rejects a client server whose name matches a configured server with invalid_params. Codex would deep-merge the two entries, so fields of the configured server would end up in the client's. The existing conflict filter handles this by dropping the client's server; in strict mode that would silently remove a server the client asked for.

Only the effective configuration's servers are disabled, not every layer's. Servers in a disabled layer (an untrusted project's .codex/config.toml, for example) never load. And trusting the project in the session config doesn't enable that layer. Naming such a server in the session config creates an entry with no transport, which Codex rejects ("invalid transport in mcp_servers.<name>"). The existing getConfigMcpServerNames still reads all layers for the non-strict conflict filter.

Without the key, or with false, nothing changes. A non-boolean value is rejected with invalid_params.

Tests

src/__tests__/CodexACPAgent/strict-mcp-config.test.ts checks the config sent to thread/start, thread/resume and thread/fork against a mocked app-server:

  • configured servers are disabled and the client's are added;
  • the three features are turned off, and cwd_relative_turn_diffs is still set;
  • configured servers are disabled even when the client passes none;
  • servers from disabled layers are not named;
  • a name conflict and a non-boolean value are rejected before thread/start;
  • absent or false changes nothing;
  • load, resume and fork apply it.

mcp-config-merge.test.ts runs a real codex app-server and reads each thread's servers back from mcpServerStatus/list, counting those whose runtimeStatus isn't disabled:

  • with a user-level server and an untrusted project, a normal session has client-mcp and shared-mcp, and a strict session has only client-mcp;
  • with the project trusted, a normal session also has project-mcp, and a strict session still has only client-mcp;
  • a strict session whose client server is named shared-mcp is rejected.

npm run typecheck is clean and npm test passes (784 passed, 32 skipped).

Docs

The README has a "Client-only MCP servers" section.

🤖 Generated with Claude Code

When a session request carries _meta.codex.strictMcpConfig = true, the
session's MCP servers are exactly the client's: every server in the
effective Codex configuration for cwd is disabled by name, and the apps,
plugins and skill_mcp_dependency_install features are turned off. A client
server whose name matches a configured one is rejected with
invalid_params. Applies to session/new, load, resume and fork.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant