Summary
When a user passes --mcp-config /path/to/file.json inside claude_args, the file path is silently dropped and the custom MCP servers never start. The agent appears to have the tools available (they show up in allowedTools) but the MCP server is absent from mcp_servers entirely — so the tools are uncallable.
Root Cause (traced to source)
In base-action/src/parse-sdk-options.ts, mergeMcpConfigs has two branches:
- Inline JSON (starts with
{) → parsed and merged into merged.mcpServers
- File path → stored as
lastFilePath, returned only if merged.mcpServers is empty
The action always prepends its own built-in servers (e.g. github_comment) as inline JSON before user claude_args are processed. This means merged.mcpServers is never empty by the time user's file path is evaluated. The condition on line 69:
if (Object.keys(merged.mcpServers!).length === 0 && lastFilePath) {
return lastFilePath;
}
is never true, so the function falls through to return JSON.stringify(merged) — returning only the action's own inline-JSON servers, with the user's file path silently lost.
The code comment acknowledges this but proposes an impossible workaround:
"If user passes a file path, they should ensure it includes all needed servers."
This is not feasible since users cannot know the action's internal server configuration at workflow-write time, and it changes between action versions.
Note also that the function-level docstring contradicts the implementation:
"For file paths, they are kept as-is (user's file takes precedence and is used last)."
Evidence From CI Logs
Session init (extraArgs["mcp-config"]) shows only the action's built-in server — the user's file path is absent:
"mcp-config": "{\"mcpServers\":{\"github_comment\":{...}}}"
mcp_servers at connection time: custom server is not listed as FAILED or needs-auth — it is completely absent, confirming the config referencing it was never passed to the SDK.
The allowedTools array does include the custom tools (because --allowedTools is a separate flag that passes through verbatim), creating a misleading state: tools appear allowed but the server never started.
Steps to Reproduce
- Write a custom MCP config to a temp file in a workflow step:
- name: Write MCP config
run: |
cat > /tmp/my-mcp-config.json <<'EOF'
{"mcpServers":{"my_server":{"command":"uvx","args":["..."]}}}
EOF
- Pass it via
claude_args:
- uses: anthropics/claude-code-action@v1
with:
claude_args: --mcp-config /tmp/my-mcp-config.json --allowedTools "mcp__my_server__*"
- Inspect session init logs —
my_server will be absent from mcp_servers.
Expected Behavior
The function docstring says:
"For file paths, they are kept as-is (user's file takes precedence and is used last)."
Both the action's built-in inline JSON servers and the user's file-path server should be active in the session.
Workaround
Pass the custom MCP config as inline JSON instead of a file path. Inline JSON is merged correctly:
- uses: anthropics/claude-code-action@v1
with:
claude_args: >-
--mcp-config '{"mcpServers":{"my_server":{"command":"uvx","args":["..."]}}}'
--allowedTools "mcp__my_server__*"
The "Write MCP config" step can be removed entirely when using this approach.
Suggested Fix
Read the file at merge time — existsSync and readFileSync are already imported in run.ts and available in the runner environment. When a file path is encountered in mergeMcpConfigs, read and parse the file content and merge its mcpServers with the rest, rather than storing it as lastFilePath and conditionally returning it.
Summary
When a user passes
--mcp-config /path/to/file.jsoninsideclaude_args, the file path is silently dropped and the custom MCP servers never start. The agent appears to have the tools available (they show up inallowedTools) but the MCP server is absent frommcp_serversentirely — so the tools are uncallable.Root Cause (traced to source)
In
base-action/src/parse-sdk-options.ts,mergeMcpConfigshas two branches:{) → parsed and merged intomerged.mcpServerslastFilePath, returned only ifmerged.mcpServersis emptyThe action always prepends its own built-in servers (e.g.
github_comment) as inline JSON before userclaude_argsare processed. This meansmerged.mcpServersis never empty by the time user's file path is evaluated. The condition on line 69:is never true, so the function falls through to
return JSON.stringify(merged)— returning only the action's own inline-JSON servers, with the user's file path silently lost.The code comment acknowledges this but proposes an impossible workaround:
This is not feasible since users cannot know the action's internal server configuration at workflow-write time, and it changes between action versions.
Note also that the function-level docstring contradicts the implementation:
Evidence From CI Logs
Session init (
extraArgs["mcp-config"]) shows only the action's built-in server — the user's file path is absent:mcp_serversat connection time: custom server is not listed asFAILEDorneeds-auth— it is completely absent, confirming the config referencing it was never passed to the SDK.The
allowedToolsarray does include the custom tools (because--allowedToolsis a separate flag that passes through verbatim), creating a misleading state: tools appear allowed but the server never started.Steps to Reproduce
claude_args:my_serverwill be absent frommcp_servers.Expected Behavior
The function docstring says:
Both the action's built-in inline JSON servers and the user's file-path server should be active in the session.
Workaround
Pass the custom MCP config as inline JSON instead of a file path. Inline JSON is merged correctly:
The "Write MCP config" step can be removed entirely when using this approach.
Suggested Fix
Read the file at merge time —
existsSyncandreadFileSyncare already imported inrun.tsand available in the runner environment. When a file path is encountered inmergeMcpConfigs, read and parse the file content and merge itsmcpServerswith the rest, rather than storing it aslastFilePathand conditionally returning it.