Skip to content

User-provided MCP config file paths are silently discarded when action prepends inline JSON #754

@nbaju1

Description

@nbaju1

Summary

When providing a --mcp-config flag with a file path in claude_args, the user's MCP servers are silently discarded because the action prepends its own inline JSON config. The mergeMcpConfigs function in base-action/src/parse-sdk-options.ts does not read file paths when inline JSON configs exist.

Reproduction

Workflow configuration

- name: Setup GitHub MCP Server Config
  run: |
    mkdir -p /tmp/mcp-config
    cat > /tmp/mcp-config/mcp-servers.json << EOF
    {
      "mcpServers": {
        "my_custom_server": {
          "command": "docker",
          "args": ["run", "-i", "--rm", "my-mcp-server"],
          "env": {}
        }
      }
    }
    EOF

- name: Run Claude
  uses: anthropics/claude-code-action@v1
  with:
    anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
    prompt: "Do something"
    claude_args: |
      --mcp-config /tmp/mcp-config/mcp-servers.json
      --allowedTools "mcp__my_custom_server__some_tool"

Expected behavior

The final MCP config should include both:

  • The action's built-in servers (github_comment, github_ci, etc.)
  • The user's custom server (my_custom_server)

Actual behavior

Only the action's built-in servers are included. The user's file path is silently discarded, and my_custom_server is never available.

From the action logs:

"extraArgs": {
  "mcp-config": "{\"mcpServers\":{\"github_comment\":{...},\"github_ci\":{...}}}",
}

The user's my_custom_server is missing entirely.

Root cause

In base-action/src/parse-sdk-options.ts, the mergeMcpConfigs function handles file paths incorrectly:

function mergeMcpConfigs(configValues: string[]): string {
  const merged: McpConfig = { mcpServers: {} };
  let lastFilePath: string | null = null;

  for (const config of configValues) {
    const trimmed = config.trim();
    if (!trimmed) continue;

    if (trimmed.startsWith("{")) {
      // Inline JSON - gets merged ✓
      const parsed = JSON.parse(trimmed) as McpConfig;
      Object.assign(merged.mcpServers!, parsed.mcpServers);
    } else {
      // File path - just stored, never read ✗
      lastFilePath = trimmed;
    }
  }

  // File path only returned if NO inline JSON exists
  if (Object.keys(merged.mcpServers!).length === 0 && lastFilePath) {
    return lastFilePath;
  }

  // File path is discarded here!
  return JSON.stringify(merged);
}

The issue is that:

  1. The action prepends inline JSON config for built-in servers
  2. User provides a file path
  3. File path is stored in lastFilePath but never read
  4. Since inline JSON exists, the condition Object.keys(merged.mcpServers!).length === 0 is false
  5. The function returns only the merged inline JSON, discarding the file path

The code comments acknowledge this limitation but describe it as expected behavior:

"If user passes a file path, they should ensure it includes all needed servers"

However, this is problematic because:

  1. Users have no way to include the action's built-in servers in their file (they don't know the paths)
  2. The file is silently discarded with no warning
  3. The documentation doesn't mention this limitation

Suggested fix

Read and merge file paths at parse time:

import { readFileSync } from "fs";

function mergeMcpConfigs(configValues: string[]): string {
  const merged: McpConfig = { mcpServers: {} };

  for (const config of configValues) {
    const trimmed = config.trim();
    if (!trimmed) continue;

    let configToMerge: McpConfig;
    
    if (trimmed.startsWith("{")) {
      // Inline JSON
      configToMerge = JSON.parse(trimmed);
    } else {
      // File path - read and parse
      try {
        const fileContent = readFileSync(trimmed, "utf-8");
        configToMerge = JSON.parse(fileContent);
      } catch (error) {
        console.warn(`Failed to read MCP config file: ${trimmed}`, error);
        continue;
      }
    }
    
    if (configToMerge.mcpServers) {
      Object.assign(merged.mcpServers!, configToMerge.mcpServers);
    }
  }

  return JSON.stringify(merged);
}

Workaround

Until this is fixed, users must provide their MCP config as inline JSON instead of a file path:

claude_args: |
  --mcp-config '{"mcpServers":{"my_custom_server":{"command":"docker","args":["run","-i","--rm","my-mcp-server"],"env":{}}}}'

This is cumbersome for complex configurations and doesn't match the documented behavior.

Environment

  • anthropics/claude-code-action@v1
  • Using claude_args with --mcp-config pointing to a file path

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingmcpp2Non-showstopper bug or popular feature request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions