Plugin MCP servers need a supported user secret/env configuration path

Open 💬 2 comments Opened May 25, 2026 by gcaaa31928

Summary

I maintain a Codex plugin that bundles an MCP server via .codex-plugin/plugin.json:

{
  "mcpServers": "./.mcp.codex.json"
}

The plugin MCP config includes defaults and declares a user-provided secret through env_vars:

{
  "mcpServers": {
    "poka": {
      "command": "node",
      "args": ["mcp-server/dist/index.js"],
      "cwd": ".",
      "env": {
        "POKA_BROKER_URL": "wss://...",
        "WORKPLUS_API_ENDPOINT": "https://...",
        "WORKPLUS_READ_ONLY": "false"
      },
      "env_vars": [
        "WORKPLUS_ACCESS_TOKEN"
      ]
    }
  }
}

The unclear part is: what is the intended UX for a plugin user to provide their own WORKPLUS_ACCESS_TOKEN?

Problem

For normal MCP servers, users can write a complete block in ~/.codex/config.toml:

[mcp_servers.my_server]
command = "..."
args = ["..."]
env_vars = ["MY_TOKEN"]

or:

[mcp_servers.my_server.env]
MY_TOKEN = "..."

But for a plugin-provided MCP server, the server definition already comes from the plugin bundle. If the user adds only:

[mcp_servers.poka.env]
WORKPLUS_ACCESS_TOKEN = "..."

Codex appears to treat this as an incomplete user-defined MCP server config instead of a partial override/merge for the plugin-provided server. The MCP config then breaks because command / url is missing.

So there does not appear to be a stable way for users to provide a secret env var for a plugin-provided MCP server without one of the following workarounds:

  1. Launching Codex with a parent environment variable already set.
  2. Duplicating the entire plugin MCP server block in config.toml, including plugin cache paths.
  3. Having the plugin implement its own credential storage outside Codex config.

All three are awkward for end users, especially in Codex Desktop.

Expected Behavior

Codex should provide a documented, supported way for users to configure secret values for plugin-provided MCP servers.

Possible designs:

Option A: Partial plugin MCP env override

Allow this to merge into the plugin-provided server named poka:

[mcp_servers.poka.env]
WORKPLUS_ACCESS_TOKEN = "..."

without requiring users to repeat command, args, or cwd.

Option B: Plugin user config / secrets

Allow plugin manifests to declare configurable secret fields, similar to:

{
  "user_config": {
    "workplus_access_token": {
      "type": "string",
      "sensitive": true,
      "required": false
    }
  }
}

and allow .mcp.codex.json to reference them:

{
  "env": {
    "WORKPLUS_ACCESS_TOKEN": "${user_config.workplus_access_token}"
  }
}

Ideally Codex Desktop would expose this in plugin settings and store sensitive values securely.

Option C: Documented env_vars UX for Desktop

If env_vars is the intended solution, please document how a Codex Desktop user is supposed to set those values persistently without relying on shell-specific launch behavior.

Actual Behavior

Plugin MCP config loads and works. However, user-level env customization for that plugin server is unclear.

Adding only:

[mcp_servers.poka.env]
WORKPLUS_ACCESS_TOKEN = "..."

breaks the server config because Codex appears to treat it as a separate incomplete server definition rather than an override for the plugin-provided server.

Why This Matters

Many MCP plugins need per-user credentials: API keys, access tokens, internal service tokens, etc.

As a plugin maintainer, I currently do not know what to tell users. Asking users to copy the full MCP server config is brittle because plugin cache paths and versions can change. Asking Desktop users to set parent process environment variables is not a reasonable product UX.

Environment

  • Codex version: 0.133.0-alpha.1
  • Platform: macOS
  • Plugin MCP server via .codex-plugin/plugin.json mcpServers
  • MCP transport: stdio

Related Observations

  • env_vars is documented/used for allowing selected parent environment variables into MCP subprocesses.
  • config.toml does not appear to support shell-style env expansion like TOKEN = "$TOKEN".
  • Plugin-provided MCP config and user config.toml MCP server config do not appear to support partial deep merge for env overrides.

View original on GitHub ↗

2 Comments

jshaofa-ui · 3 months ago

Solution: codex #24401 — Plugin MCP servers need a supported user secret/env configuration path

Issue Summary

Plugin MCP servers bundled via .codex-plugin/plugin.json have no standard way to accept user-provided secrets through env_vars. Adding only [mcp_servers.poka.env] in config.toml is treated as a separate incomplete server definition rather than a partial override for the plugin-provided server.

Root Cause Analysis

The MCP server configuration loading has two separate code paths:

  1. User-defined servers in config.toml: Full server definition required (command, args, env)
  2. Plugin-provided servers from .codex-plugin/plugin.json: Complete definition from plugin bundle

These paths don't support partial deep merge. When a user adds [mcp_servers.poka.env] to override just the env section, Codex treats it as a new incomplete server definition (missing command/args/cwd) rather than merging it with the plugin-provided definition.

The env_vars field is documented for allowing parent environment variables into MCP subprocesses, but there's no documented UX for Desktop users to persistently set those values.

Proposed Fix

Option A: Partial plugin MCP env override (recommended, minimal change)

Allow user config.toml to provide partial overrides that merge with plugin-provided server definitions:

# User adds only env override — merges with plugin's command/args/cwd
[mcp_servers.poka.env]
WORKPLUS_ACCESS_TOKEN = "user-provided-token"

Implementation:

// In MCP config loading (config.ts or similar)
function resolveMcpServerConfig(
  pluginServer: McpServerConfig,  // from plugin.json
  userOverride: Partial<McpServerConfig> | undefined  // from config.toml
): McpServerConfig {
  if (!userOverride) return pluginServer
  
  return {
    // Plugin provides the base config
    command: pluginServer.command,
    args: pluginServer.args,
    cwd: pluginServer.cwd,
    url: pluginServer.url,
    
    // Deep merge env: user overrides take precedence
    env: {
      ...pluginServer.env,
      ...userOverride.env,
    },
    
    // Merge env_vars lists
    env_vars: [
      ...(pluginServer.env_vars || []),
      ...(userOverride.env_vars || []),
    ],
  }
}

// During config loading
for (const [name, pluginServer] of pluginMcpServers) {
  const userOverride = userConfig.mcp_servers?.[name]
  if (userOverride && !userOverride.command && !userOverride.url) {
    // User provided partial override (env only) — merge with plugin config
    resolved[name] = resolveMcpServerConfig(pluginServer, userOverride)
  } else if (userOverride) {
    // User provided full override — use as-is
    resolved[name] = userOverride
  } else {
    resolved[name] = pluginServer
  }
}

Option B: Plugin user config / secrets manifest (long-term, more comprehensive)

Allow plugin manifests to declare configurable secret fields:

// .codex-plugin/plugin.json
{
  "mcpServers": "./.mcp.codex.json",
  "userConfig": {
    "workplus_access_token": {
      "type": "string",
      "sensitive": true,
      "required": false,
      "description": "WorkPlus API access token"
    }
  }
}
// .mcp.codex.json — reference user config via template
{
  "mcpServers": {
    "poka": {
      "env": {
        "WORKPLUS_ACCESS_TOKEN": "${userConfig.workplus_access_token}"
      }
    }
  }
}

User provides values via:

  • ~/.codex/user-secrets.json (encrypted)
  • Codex Desktop plugin settings UI
  • Environment variables: CODEX_USER_CONFIG_WORKPLUS_ACCESS_TOKEN=xxx
// Template resolution
function resolveEnvTemplate(
  template: string,
  userConfig: Record<string, string>
): string {
  return template.replace(
    /\$\{userConfig\.(\w+)\}/g,
    (_, key) => userConfig[key] || ''
  )
}

Option C: Documented env_vars UX for Desktop

If env_vars is the intended solution, document and implement a clear path:

# ~/.codex/config.toml — documented pattern
[mcp_servers.poka]
# Partial env override for plugin-provided server
env.WORKPLUS_ACCESS_TOKEN = "user-token-here"

Or via environment:

# Documented: set plugin MCP secrets via environment
export CODEX_MCP_SECRETS_POKA_WORKPLUS_ACCESS_TOKEN="token"

Recommended Implementation Order

  1. Immediate: Option A — partial env merge (2-3 lines of merge logic, fixes the reported issue)
  2. Short-term: Option C — document the env_vars UX for Desktop users
  3. Long-term: Option B — plugin user config manifest with secure storage in Desktop

Test Plan

  1. Partial override merge: Plugin provides server with command/args/env. User adds [mcp_servers.poka.env] with just a token → server should start with merged config
  2. Full override: User provides complete server definition → should override plugin entirely
  3. No override: User provides nothing → plugin config should work as-is
  4. Desktop UI: Plugin settings UI should show configurable fields and securely store values
  5. Template resolution: ${userConfig.key} should resolve correctly in env templates

Impact Assessment

  • Severity: P2 — plugin MCP servers with user secrets are awkward to configure
  • Affected users: Plugin maintainers and users who need per-user credentials for MCP servers
  • Fix complexity: Low for Option A (merge logic), Medium for Option B (manifest + UI)
  • Risk: Minimal — additive change, backward compatible

Submission

Post as comment on: https://github.com/openai/codex/issues/24401

bendavis78 · 20 days ago

Ideally the solution should follow Anthropic's own standard for userConfig values w/ sensitive: true. This should support the system's keychain for secret storage.

See here for the full spec: https://code.claude.com/docs/en/plugins-reference#user-configuration