anthropics / anthropics/claude-code-action
--mcp-config file path in claude_args is silently dropped when action's inline JSON config is present
- Dominant language
- TypeScript
- Stars
- 8.9k
- Forks
- 2.1k
- Avg merge
- 3d 9h
- Merged PRs (30d)
- 10
Description
## 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:
```typescript
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:
```json
"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
1. Write a custom MCP config to a temp file in a workflow step:
```yaml
- name: Write MCP config
run: |
cat > /tmp/my-mcp-config.json <<'EOF'
{"mcpServers":{"my_server":{"command":"uvx","args":["..."]}}}
EOF
```
2. Pass it via `claude_args`:
```yaml
- uses: anthropics/claude-code-action@v1
with:
claude_args: --mcp-config /tmp/my-mcp-config.json --allowedTools "mcp__my_server__*"
```
3. 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:
```yaml
- 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.
Contributor guide
Assessment
This issue has not been assessed yet.