Problem
The Gemini engine compiler generates MCP gateway URLs using host.docker.internal, but Gemini CLI runs directly on the host runner (not inside a Docker container). On Linux GitHub Actions runners, host.docker.internal does not resolve from the host — it only resolves inside Docker containers that are started with --add-host host.docker.internal:host-gateway.
This causes all MCP tools (github, safeoutputs) to be unreachable, which means:
- Gemini cannot call
list_pull_requests, search_issues, etc.
- Gemini cannot call
noop, add_comment, add_labels (safe outputs)
- The "Validate safe outputs were invoked" step fails every time
Root Cause
The start_mcp_gateway.cjs script correctly writes Gemini-format config to .gemini/settings.json with MCP server entries. However, it uses the MCP_GATEWAY_DOMAIN value (host.docker.internal) for the URLs:
{
"mcpServers": {
"github": {
"url": "http://host.docker.internal:8080/mcp/github",
"headers": { "Authorization": "Bearer <key>" }
},
"safeoutputs": {
"url": "http://host.docker.internal:8080/mcp/safeoutputs",
"headers": { "Authorization": "Bearer <key>" }
}
}
}
This works for Claude because Claude runs inside the AWF sandbox container (where host.docker.internal resolves). But Gemini runs directly on the host where that hostname does not resolve.
Evidence
Failed run (before fix)
https://github.com/github/gh-aw-firewall/actions/runs/25259025540
Gateway logs show it wrote the config correctly:
[info] Gemini configuration written to .gemini/settings.json
[info] "github": { "url": "http://host.docker.internal:8080/mcp/github" }
[info] "safeoutputs": { "url": "http://host.docker.internal:8080/mcp/safeoutputs" }
But Gemini CLI reported:
MCP issues detected. Run /mcp list for status.
And tool calls failed:
Tool "noop" not found. Did you mean one of: "glob", "cli_help", "read_file"?
Tool "github:list_pull_requests" not found.
Successful run (after manual lock file patch)
https://github.com/github/gh-aw-firewall/actions/runs/25259435566
After replacing host.docker.internal with localhost in the lock file, all MCP tools work:
tool_name: "mcp_github_list_pull_requests" → success
tool_name: "mcp_safeoutputs_add_comment" → success
tool_name: "mcp_safeoutputs_add_labels" → success
Workaround PR
github/gh-aw-firewall#2405 — manual .lock.yml patch that adds a post-processing step to replace host.docker.internal → localhost in the Gemini settings.
Suggested Fix
In start_mcp_gateway.cjs (or wherever Gemini-format config is generated), detect when the engine runs on the host (not in a container) and use localhost instead of host.docker.internal for the MCP server URLs.
Possible approaches:
- Engine-aware domain selection: For engines that run on the host (Gemini, Codex), use
localhost. For engines that run in containers (Claude via AWF), use host.docker.internal.
- Always use
localhost for the Gemini settings.json since Gemini always runs on the host, and let the gateway bind to 0.0.0.0 (it already does — the health endpoint responds on localhost:8080).
- Add a
MCP_GATEWAY_HOST_DOMAIN variable that defaults to localhost and is used for host-side config generation, separate from MCP_GATEWAY_DOMAIN which is used for container-side config.
Additional Context
- The
MCP_GATEWAY_DOMAIN is set to host.docker.internal in the compiled lock file
- The gateway health endpoint responds on
http://localhost:8080/health confirming it binds to all interfaces
- The "Write Gemini Config" step does a jq deep merge (
$existing * $base) which correctly preserves the mcpServers block — the merge logic is fine
- This likely affects any engine that runs directly on the host (not inside AWF/Docker)
Problem
The Gemini engine compiler generates MCP gateway URLs using
host.docker.internal, but Gemini CLI runs directly on the host runner (not inside a Docker container). On Linux GitHub Actions runners,host.docker.internaldoes not resolve from the host — it only resolves inside Docker containers that are started with--add-host host.docker.internal:host-gateway.This causes all MCP tools (github, safeoutputs) to be unreachable, which means:
list_pull_requests,search_issues, etc.noop,add_comment,add_labels(safe outputs)Root Cause
The
start_mcp_gateway.cjsscript correctly writes Gemini-format config to.gemini/settings.jsonwith MCP server entries. However, it uses theMCP_GATEWAY_DOMAINvalue (host.docker.internal) for the URLs:{ "mcpServers": { "github": { "url": "http://host.docker.internal:8080/mcp/github", "headers": { "Authorization": "Bearer <key>" } }, "safeoutputs": { "url": "http://host.docker.internal:8080/mcp/safeoutputs", "headers": { "Authorization": "Bearer <key>" } } } }This works for Claude because Claude runs inside the AWF sandbox container (where
host.docker.internalresolves). But Gemini runs directly on the host where that hostname does not resolve.Evidence
Failed run (before fix)
https://github.com/github/gh-aw-firewall/actions/runs/25259025540
Gateway logs show it wrote the config correctly:
But Gemini CLI reported:
And tool calls failed:
Successful run (after manual lock file patch)
https://github.com/github/gh-aw-firewall/actions/runs/25259435566
After replacing
host.docker.internalwithlocalhostin the lock file, all MCP tools work:Workaround PR
github/gh-aw-firewall#2405 — manual
.lock.ymlpatch that adds a post-processing step to replacehost.docker.internal→localhostin the Gemini settings.Suggested Fix
In
start_mcp_gateway.cjs(or wherever Gemini-format config is generated), detect when the engine runs on the host (not in a container) and uselocalhostinstead ofhost.docker.internalfor the MCP server URLs.Possible approaches:
localhost. For engines that run in containers (Claude via AWF), usehost.docker.internal.localhostfor the Gemini settings.json since Gemini always runs on the host, and let the gateway bind to0.0.0.0(it already does — the health endpoint responds onlocalhost:8080).MCP_GATEWAY_HOST_DOMAINvariable that defaults tolocalhostand is used for host-side config generation, separate fromMCP_GATEWAY_DOMAINwhich is used for container-side config.Additional Context
MCP_GATEWAY_DOMAINis set tohost.docker.internalin the compiled lock filehttp://localhost:8080/healthconfirming it binds to all interfaces$existing * $base) which correctly preserves themcpServersblock — the merge logic is fine