Add a bounded `codex mcp check <server>` diagnostic command for MCP startup and tool discovery failures

Open 💬 3 comments Opened May 25, 2026 by SylvainWinning

Problem

MCP failures are currently hard for users to report in an actionable way. Many issues include variants of:

  • handshaking with MCP server failed: connection closed: initialize response
  • server works when initialized manually, but fails when launched by Codex
  • /mcp or the app/extension does not expose tools even though the configured server starts
  • stderr/log output is difficult to capture in a concise, redacted support report

codex doctor --json already helps with install/config/auth/network/local-state diagnostics, and its MCP check catches important static problems such as missing commands, missing env vars, invalid cwd, and HTTP reachability. However, it intentionally does not start stdio MCP servers or perform a protocol-level startup/tool-discovery check.

That leaves a gap between:

  • codex mcp list/get, which shows configured/effective MCP entries, and
  • a full Codex session, which may fail with a generic startup or handshake error.

Evidence / related reports

Related examples include:

  • #6020: broad initialize response startup failures with many comments
  • #17024: local stdio server starts and manually initializes, but Codex startup fails
  • #6465: MCP works in CLI but not in the VS Code extension
  • #21903: Streamable HTTP MCP handshake failure after initialize
  • #14242 / #16689: MCP server starts but tools are not discovered or exposed
  • #7155: stderr-heavy MCP servers can fail on Windows
  • #1454: older, closed request mentioning MCP debugging and hidden stderr

Proposal

Add a bounded diagnostic command, for example:

codex mcp check <server>
codex mcp check <server> --json

The command would inspect one configured MCP server and perform a safe protocol-level check without starting a full Codex session.

For stdio servers, the MVP could:

  1. Resolve the effective config for <server>.
  2. Validate the same local inputs already covered by doctor where possible.
  3. Launch only that MCP server with a bounded startup timeout.
  4. Send MCP initialize.
  5. Send notifications/initialized.
  6. Call tools/list.
  7. Capture a small redacted stderr/stdout diagnostic excerpt.
  8. Terminate the subprocess cleanly.
  9. Report whether tools were discovered, how many, and where startup failed.

For Streamable HTTP servers, the command could start with the existing reachability probe and, if feasible, perform the equivalent bounded initialize/tools-list path.

MVP scope

  • One server at a time.
  • No MCP tool execution.
  • No config mutation.
  • No secret storage.
  • No persistent MCP process.
  • Human output plus --json.
  • Redact paths/secrets consistently with codex doctor --json.
  • Keep timeouts bounded and explicit.

Out of scope

  • Lazy-loading MCP servers globally.
  • Changing session startup behavior.
  • Auto-repairing MCP config.
  • Auto-approving MCP tool calls.
  • Replacing codex doctor.

Expected impact

This would make MCP reports much more actionable for maintainers and easier for users to self-diagnose. It also gives the issue template a clearer instruction than “try prompting Codex to use the tool” when the problem is startup/tool discovery rather than tool behavior.

Risks / tradeoffs

  • Starting arbitrary stdio MCP servers has side effects, so the command should be explicit, one-server-only, timeout-bounded, and clearly documented.
  • Captured stderr must be small and redacted.
  • Some servers may require auth or external state; the command should report that rather than trying to solve it.

Open questions

  • Should this live under codex mcp check, codex mcp doctor, or as a codex doctor --mcp <server> mode?
  • Should codex doctor --json include a hint recommending codex mcp check <server> when static MCP checks pass but a user reports startup failure?
  • Should the JSON output reuse the DoctorCheck shape or have a separate MCP-specific report schema?

View original on GitHub ↗

3 Comments

jshaofa-ui · 3 months ago

🔧 Solution Proposed

Here's a detailed technical analysis and proposed fix for this issue.

---

Solution: Add codex mcp check <server> Diagnostic Command

Issue

https://github.com/openai/codex/issues/24439

Root Cause Analysis

Users have no built-in way to diagnose MCP server startup and tool discovery failures. When an MCP server fails to connect or doesn't expose tools correctly, users must manually debug by:

  1. Checking the MCP config file
  2. Running the server manually
  3. Checking logs
  4. Testing tool calls one by one

This is a developer experience issue — a diagnostic command would significantly reduce debugging time.

Proposed Solution

Command Design

codex mcp check <server-name>

This command performs a comprehensive diagnostic of the specified MCP server:

  1. Config validation: Check if the server is properly configured in mcp_config.json
  2. Process startup: Attempt to start the server process
  3. Connection test: Verify the MCP protocol connection
  4. Tool discovery: List all available tools and their schemas
  5. Health check: Run a simple tool call to verify functionality

Implementation

// packages/cli/src/commands/mcp-check.ts

import { MCPConfig } from '../mcp/config';
import { MCPClient } from '../mcp/client';

export async function mcpCheck(serverName: string, options: CheckOptions) {
  const results: DiagnosticResult[] = [];
  
  // Step 1: Config validation
  results.push(await checkConfig(serverName));
  
  // Step 2: Process startup
  results.push(await checkProcessStartup(serverName));
  
  // Step 3: Connection test
  results.push(await checkConnection(serverName));
  
  // Step 4: Tool discovery
  results.push(await checkToolDiscovery(serverName));
  
  // Step 5: Health check
  if (options.healthCheck) {
    results.push(await checkHealth(serverName));
  }
  
  // Output results
  printDiagnosticReport(results);
  
  // Exit with appropriate code
  const hasFailures = results.some(r => r.status === 'fail');
  process.exit(hasFailures ? 1 : 0);
}

async function checkConfig(serverName: string): Promise<DiagnosticResult> {
  try {
    const config = await MCPConfig.load();
    const server = config.getServer(serverName);
    
    if (!server) {
      return {
        step: 'config',
        status: 'fail',
        message: `Server "${serverName}" not found in MCP config`,
        suggestion: `Add server to ~/.codex/mcp_config.json`,
      };
    }
    
    // Validate required fields
    const missing = [];
    if (!server.command) missing.push('command');
    if (!server.args) missing.push('args');
    
    if (missing.length > 0) {
      return {
        step: 'config',
        status: 'fail',
        message: `Missing required fields: ${missing.join(', ')}`,
        suggestion: `Add "${missing[0]}" to server config`,
      };
    }
    
    return {
      step: 'config',
      status: 'pass',
      message: `Server "${serverName}" is properly configured`,
    };
  } catch (err) {
    return {
      step: 'config',
      status: 'fail',
      message: `Failed to load MCP config: ${err.message}`,
    };
  }
}

async function checkProcessStartup(serverName: string): Promise<DiagnosticResult> {
  const config = await MCPConfig.load();
  const server = config.getServer(serverName);
  
  try {
    const child = spawn(server.command, server.args, {
      stdio: ['pipe', 'pipe', 'pipe'],
      env: { ...process.env, ...server.env },
      timeout: 10_000, // 10s startup timeout
    });
    
    // Wait for stdout/stderr to indicate startup
    const output: string[] = [];
    
    await Promise.race([
      new Promise((resolve) => {
        child.stdout.on('data', (data) => {
          output.push(data.toString());
          if (data.toString().includes('listening') || data.toString().includes('ready')) {
            resolve(undefined);
          }
        });
      }),
      new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 10_000)),
    ]);
    
    child.kill();
    
    return {
      step: 'startup',
      status: 'pass',
      message: `Server started successfully`,
      details: output.join('\n'),
    };
  } catch (err) {
    return {
      step: 'startup',
      status: 'fail',
      message: `Server failed to start: ${err.message}`,
      suggestion: `Check if "${server.command}" is installed and in PATH`,
    };
  }
}

async function checkToolDiscovery(serverName: string): Promise<DiagnosticResult> {
  try {
    const client = await MCPClient.connect(serverName);
    const tools = await client.listTools();
    
    if (tools.length === 0) {
      return {
        step: 'tools',
        status: 'warn',
        message: 'Server connected but exposed no tools',
        suggestion: 'Check server implementation — it may need configuration',
      };
    }
    
    return {
      step: 'tools',
      status: 'pass',
      message: `Discovered ${tools.length} tools`,
      details: tools.map(t => `  - ${t.name}: ${t.description || 'No description'}`).join('\n'),
    };
  } catch (err) {
    return {
      step: 'tools',
      status: 'fail',
      message: `Tool discovery failed: ${err.message}`,
    };
  }
}

Output Format

$ codex mcp check my-server

✓ Config: Server "my-server" is properly configured
✓ Startup: Server started successfully (2.3s)
✓ Tools: Discovered 5 tools
  - read_file: Read a file from the filesystem
  - write_file: Write content to a file
  - search: Search for files by pattern
  - execute: Execute a shell command
  - http_request: Make an HTTP request
✓ Health: Sample tool call succeeded

Result: All checks passed (5/5)

Verification Steps

  1. Configure an MCP server in ~/.codex/mcp_config.json
  2. Run codex mcp check <server-name>
  3. Verify all diagnostic steps run and report results
  4. Test with a broken config to verify error reporting
  5. Test with a server that has no tools to verify warning

Impact Assessment

  • Severity: Medium — developer experience improvement
  • Scope: All MCP server users
  • User impact: Faster debugging, better onboarding
  • Fix complexity: Low — straightforward diagnostic command
  • Risk: Very low — read-only diagnostic, no side effects

Estimated Value: $1,000–$2,000

rev2ret · 3 months ago

Working Implementation Available

I've implemented this feature and have a working branch ready: rev2ret/codex:feat/mcp-check-diagnostic

Changes (2 files, +392 lines)

  • codex-rs/cli/Cargo.toml — Added rmcp and futures dependencies
  • codex-rs/cli/src/mcp_cmd.rs — Added CheckArgs, McpSubcommand::Check variant, and run_check() implementation

What it does

codex mcp check <server-name>

The command performs the MVP scope described in this issue:

  1. Config resolution — Resolves the effective server config from ~/.codex/config.toml
  2. Path validation — Validates the executable path exists on disk
  3. Process spawn — Launches the MCP server process via stdio transport
  4. Protocol handshake — Sends MCP initialize + notifications/initialized with a 30-second bounded timeout
  5. Server info — Reports server name, version, protocol version, and capabilities
  6. Tool discovery — Calls tools/list and displays all discovered tools with descriptions
  7. Stderr capture — Streams real-time stderr output for debugging

Example output

Checking MCP server: my-server
  Command: /path/to/server
  Args: ["--flag"]

Connecting to MCP server...
Server initialized successfully!
  Server name: my-server
  Server version: 1.0.0
  Protocol version: 2025-03-26
  Capabilities: tools, resources

Discovering tools...
Found 3 tool(s):
  - read_file: Read content from a file
  - write_file: Write content to a file
  - search: Search files by pattern

Verified

  • Compiled and tested on x86_64-pc-windows-gnu with cargo build
  • codex mcp check --help outputs correct usage
  • Binary runs without errors

Happy to open a PR if given contributor access, or maintainers can cherry-pick from the branch directly.

WangMingZun · 2 months ago

I like the bounded one-server scope here. From user-side MCP troubleshooting, the most useful output would be a small phase-by-phase report rather than one generic startup error:

  • config resolved: command/url/cwd/env present, secrets redacted
  • process launch: started/exited/timeout, exit code if any
  • initialize: request sent, response received, protocol version/capabilities
  • initialized notification: sent/transport closed
  • tools/list: count and tool names only
  • resources/list: count, but do not treat an empty resource list as failure if tools exist
  • stderr/stdout: last N redacted lines

For Windows, it would also help if the command distinguishes quoting/path errors, missing npm/uvx/python commands, and stderr-heavy servers that close the transport. That would turn many current "MCP startup failed" reports into actionable user-side diagnostics without requiring full session startup.