feat: mount MCP servers as local CLIs after gateway starts#24503
feat: mount MCP servers as local CLIs after gateway starts#24503
Conversation
- Add `start_mcp_gateway.sh`: save CLI manifest (server names + local URLs) to /tmp/gh-aw/mcp-cli/manifest.json before cleanup - Add `mount_mcp_as_cli.sh`: new shell script that reads the manifest, queries each server's tools/list via JSON-RPC, and generates a standalone CLI wrapper script in /tmp/gh-aw/mcp-cli/bin/ (chmod 555) - Add `mcp_cli_mount.go`: Go helpers getMCPCLIServerNames() and generateMCPCLIMountStep() + buildMCPCLIPromptSection() - Add "Mount MCP servers as CLIs" workflow step in compiler_yaml_main_job.go - Inject MCP CLI tools prompt section in unified_prompt_step.go - Update golden test fixtures Agent-Logs-Url: https://github.com/github/gh-aw/sessions/f83b0114-9c57-4701-ac7d-bfa80f07a971 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
| <safe-output-tools> | ||
| Tools: add_comment(max:10), create_issue(max:5), create_discussion(max:2), missing_tool, missing_data, noop | ||
| </safe-output-tools> | ||
| ## MCP Tools Available as CLI Commands |
actions/setup/sh/mount_mcp_as_cli.sh
Outdated
| @@ -0,0 +1,309 @@ | |||
| #!/usr/bin/env bash | |||
There was a problem hiding this comment.
- convert this a hell script to actions/github-script (and delete after conversion)
- add logging
There was a problem hiding this comment.
Done in f3d7205. Converted to actions/setup/js/mount_mcp_as_cli.cjs with core.info() logging throughout (and core.warning() for non-fatal errors). The shell script has been deleted. The workflow step now uses actions/github-script.
There was a problem hiding this comment.
Great work converting to github-script! The added core.info() logging makes it much easier to debug MCP mounting issues in the Actions log. 🎉
📰 BREAKING: Report filed by Smoke Copilot · ● 860.4K
that enables this behavior. Default is false.
|
…ml tags - Delete mount_mcp_as_cli.sh shell script - Add mount_mcp_as_cli.cjs using actions/github-script with core.info() logging - Update generateMCPCLIMountStep to use actions/github-script - Wrap MCP CLI prompt section in <mcp-clis> XML tags - Update golden fixtures and recompile lock files Agent-Logs-Url: https://github.com/github/gh-aw/sessions/1bf60ced-fb56-48d7-a2ce-b870a185cc27 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
|
Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot add a field tools.mount-as-clis: true/false that enables this behavior. Default is false. When false, do not emit custom prompt or mount any MCPs. |
|
✅ All tools validated successfully! Agent Container Smoke Test confirms agent container is ready. |
|
🎬 THE END — Smoke Claude MISSION: ACCOMPLISHED! The hero saves the day! ✨ |
|
🌑 The shadows whisper... Smoke Codex failed. The oracle requires further meditation... |
|
📰 BREAKING: Smoke Copilot is now investigating this pull request. Sources say the story is developing... |
|
@copilot the cli wrappers must implement a proper mcp client protocol and maintain a session |
This comment has been minimized.
This comment has been minimized.
|
Smoke Test #23981224949 |
Overall:
|
There was a problem hiding this comment.
This PR adds MCP server CLI mounting to all workflow lock files — a useful improvement for discoverability. The pattern of continue-on-error: true is appropriate for non-critical tooling. Two minor suggestions: add explicit success logging in the script, and consider documenting the mount_mcp_as_cli.cjs function signature for future maintainers.
📰 BREAKING: Report filed by Smoke Copilot · ● 860.4K
| "apiKey": "${MCP_GATEWAY_API_KEY}", | ||
| "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" | ||
| } | ||
| } |
There was a problem hiding this comment.
Consider adding a debug output after await main() (e.g., core.info('MCP CLI mounts completed')) to make it easier to diagnose issues in the Actions log when continue-on-error silently eats failures.
| "apiKey": "${MCP_GATEWAY_API_KEY}", | ||
| "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" | ||
| } | ||
| } |
There was a problem hiding this comment.
The continue-on-error: true flag is appropriate here since mounting MCP CLIs is non-critical — failing silently is better than blocking the workflow. Good defensive pattern. 👍
|
📰 VERDICT: Smoke Copilot has concluded. All systems operational. This is a developing story. 🎤 |
This comment has been minimized.
This comment has been minimized.
|
🤖 Smoke test agent 23992551991 checking in!
Overall: ✅ PASS PR author: Run: https://github.com/github/gh-aw/actions/runs/23992551991
|
There was a problem hiding this comment.
🧪 Smoke test results for run §23992551991
The mount-as-clis feature was exercised by this smoke run. Two bugs were confirmed in testing:
-
jq double-quote bug (line 265 of
mount_mcp_as_cli.cjs): the jq filter string uses double quotes, causing$k/$vto be expanded by bash as shell variables (which are undefined in scope), producing invalid jq expressions like. + {(): }. -
localhost URL inaccessibility: the generated wrapper scripts use
(127.0.0.1/redacted) from the manifest, but inside the AWF sandbox container,127.0.0.1is the container's own loopback. The gateway is only reachable viahost.docker.internal` from within the sandbox.
Both issues prevent the CLI wrappers from working as generated. See inline comments for details.
📰 BREAKING: Report filed by Smoke Copilot · ● 6.5M
| if [ -f /tmp/gh-aw/mcp-config/gateway-output.json ] && \ | ||
| jq -e '.mcpServers' /tmp/gh-aw/mcp-config/gateway-output.json >/dev/null 2>&1; then | ||
| jq '{servers: [.mcpServers | to_entries[] | select(.value.url != null) | {name: .key, url: .value.url}]}' \ | ||
| /tmp/gh-aw/mcp-config/gateway-output.json > /tmp/gh-aw/mcp-cli/manifest.json |
There was a problem hiding this comment.
localhost URLs that aren't accessible inside AWF sandbox
The manifest is built from gateway-output.json which contains (127.0.0.1/redacted) URLs. These are valid on the *host* but inside the AWF agent container, 127.0.0.1` resolves to the container's own loopback, not the host.
The generated CLI wrappers use these manifest URLs, so every MCP call fails with "connection refused" inside the sandbox. The gateway is reachable as `(host.docker.internal/redacted) from within the container.
Consider rewriting the URL in the manifest (or in mount_mcp_as_cli.cjs) to replace 127.0.0.1 / localhost with host.docker.internal when generating the CLI wrappers.
| local key="\${1#--}" | ||
| if [[ \$# -ge 2 && "\$2" != --* ]]; then | ||
| local val="\$2" | ||
| args=\$(echo "\$args" | jq --arg k "\$key" --arg v "\$val" ". + {(\$k): \$v}") |
There was a problem hiding this comment.
🐛 Shell quoting bug causes jq argument parsing to fail
The jq filter uses double quotes:
args=$(echo "$args" | jq --arg k "$key" --arg v "$val" ". + {($k): $v}")Since the filter is double-quoted, bash expands $k and $v as shell variables before passing the expression to jq. The local variables are named key and val (not k and v), so $k and $v expand to empty strings, producing the invalid jq expression . + {(): }.
Fix: use single quotes for the jq filter so jq receives $k/$v as jq variables:
args=$(echo "$args" | jq --arg k "$key" --arg v "$val" '. + {($k): $v}')Same fix needed on line 268 for the boolean case.
|
📰 DEVELOPING STORY: Smoke Copilot reports failed. Our correspondents are investigating the incident... |
The manifest saved by start_mcp_gateway.sh stores raw gateway-output URLs (e.g., http://0.0.0.0:80/mcp/server) that work from the host. CLI wrapper scripts generated by mount_mcp_as_cli.cjs baked these URLs in directly, so when the agent inside the AWF sandbox ran e.g. `github pull_request_read ...`, the wrapper's curl call to 0.0.0.0:80 failed because that address is not routable inside the container. Fix: - start_mcp_gateway.sh: output gateway-domain alongside gateway-port - mcp_cli_mount.go: mount step now receives MCP_GATEWAY_DOMAIN and MCP_GATEWAY_PORT from gateway step outputs - mount_mcp_as_cli.cjs: added toContainerUrl() that rewrites raw manifest URLs to use MCP_GATEWAY_DOMAIN:MCP_GATEWAY_PORT; tool fetching still uses the host-accessible URL, but generateCLI WrapperScript receives the container-accessible rewritten URL; warns if env vars are absent so misconfiguration is diagnosable Recompiled all 180 workflows. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/6e5597e6-b5e4-4bc7-914a-9d913b80dba9 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Fixed in acd44a6. Root cause: Fix:
Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
🌑 The shadows whisper... Smoke Codex failed. The oracle requires further meditation... |
|
✅ All tools validated successfully! Agent Container Smoke Test confirms agent container is ready. |
|
🎬 THE END — Smoke Claude MISSION: ACCOMPLISHED! The hero saves the day! ✨ |
|
📰 BREAKING: Smoke Copilot is now investigating this pull request. Sources say the story is developing... |
Agent Container Tool Check
Result: 12/12 tools available ✅ Overall Status: PASS
|
|
Smoke Test 23993205511 — PR #24503
Status: Author:
|
There was a problem hiding this comment.
Smoke test review of PR #24503 (feat: mount MCP servers as local CLIs). The overall architecture is clean — separating gateway config (all servers) from agent MCP config (CLI-mounted servers excluded) is a sound approach. Two minor observations on mcp_cli_mount.go around the mutable package-level map and the RUNNER_TEMP vs /tmp path clarification. No blocking issues found.
📰 BREAKING: Report filed by Smoke Copilot · ● 1.2M
| ) | ||
|
|
||
| // mcp_cli_mount.go generates a workflow step that mounts MCP servers as local CLI tools | ||
| // and produces the prompt section that informs the agent about these tools. |
There was a problem hiding this comment.
The file-level comment says CLI scripts are written to \$\{RUNNER_TEMP}/gh-aw/mcp-cli/bin/, but in a few places the PR description refers to /tmp/gh-aw/mcp-cli/bin/. It would help to add a note here clarifying that RUNNER_TEMP ≠ /tmp so the AWF sandbox (which mounts \$\{RUNNER_TEMP}/gh-aw read-only) can access the generated scripts.
| "safeoutputs": true, | ||
| "mcp-scripts": true, | ||
| "mcpscripts": true, | ||
| } |
There was a problem hiding this comment.
The internalMCPServerNames variable is a package-level map[string]bool, which could be accidentally modified at runtime. Consider using a function or a constant set pattern to make it immutable, or at minimum add a comment that it should not be modified after initialization.
|
📰 VERDICT: Smoke Copilot has concluded. All systems operational. This is a developing story. 🎤 |
Smoke Test Results — Run §23993205568
Overall: PARTIAL (17 ✅, 2
|
There was a problem hiding this comment.
💥 Automated smoke test review - all systems nominal!
💥 [THE END] — Illustrated by Smoke Claude · ● 377K
| // Use RUNNER_TEMP so the bin and tools directories are inside the AWF sandbox mount | ||
| // (AWF mounts ${RUNNER_TEMP}/gh-aw read-only; /tmp/gh-aw is not accessible inside AWF) | ||
| const RUNNER_TEMP = process.env.RUNNER_TEMP || "/home/runner/work/_temp"; | ||
| const CLI_BIN_DIR = `${RUNNER_TEMP}/gh-aw/mcp-cli/bin`; |
There was a problem hiding this comment.
The MANIFEST_FILE path is hardcoded to /tmp/gh-aw/mcp-cli/manifest.json. Consider making this configurable via an environment variable to improve testability and flexibility across different runner configurations.
| * that work from the host. Inside the AWF sandbox the gateway is reachable via | ||
| * MCP_GATEWAY_DOMAIN:MCP_GATEWAY_PORT (typically host.docker.internal:80). | ||
| * | ||
| * @param {string} rawUrl - URL from the manifest (host-accessible) |
There was a problem hiding this comment.
The INTERNAL_SERVERS set is a good pattern for excluding infrastructure servers. Consider adding a comment explaining the rationale for each excluded server to help future maintainers understand why cache-memory is not in this list but also excluded (filtered elsewhere).
This comment has been minimized.
This comment has been minimized.
|
@copilot the agent is still very co fused about how to call the clis https://github.com/github/gh-aw/actions/runs/23993205511/job/69976454416 |
…s can filter CLIs Agent-Logs-Url: https://github.com/github/gh-aw/sessions/df695c39-da3e-46c3-af36-1af81c84fa29 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Fixed in 3214e78. Root cause: Fix: added Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
Hey One thing that would help get this across the finish line:
If you'd like a hand, you can assign this prompt to your coding agent:
|
There was a problem hiding this comment.
💥 Automated smoke test review - all systems nominal! Run 24001448881
💥 [THE END] — Illustrated by Smoke Claude · ● 246.1K
|
|
||
| const fs = require("fs"); | ||
| const http = require("http"); | ||
| const path = require("path"); |
There was a problem hiding this comment.
The MANIFEST_FILE path is hardcoded to /tmp/gh-aw/mcp-cli/manifest.json. Consider using a constant or env var shared with start_mcp_gateway.sh to avoid drift if the path changes. Smoke test review comment — Run 24001448881.
| @@ -0,0 +1,45 @@ | |||
| <mcp-clis> | |||
| ## MCP Servers Mounted as Shell CLI Commands | |||
|
|
|||
There was a problem hiding this comment.
Clear and actionable heading. The IMPORTANT callout is effective for preventing agent confusion. Smoke test review comment — Run 24001448881.
|
@copilot merge main and recompile |
…-mounting-mcp-servers # Conflicts: # .github/workflows/smoke-copilot.lock.yml Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/1d35fa17-1b02-4f55-85f0-9c17d491b2bd Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Done in 81db7fa. Merged main and recompiled all 181 workflows successfully. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
Summary
Adds automatic mounting of MCP servers as local CLI tools in agentic workflows. This behavior is opt-in via a new
tools.mount-as-clis: trueflag (defaultfalse). When enabled, all user-facing MCP servers — including built-in ones likegithubandplaywrightas well as custom servers — are exposed as standalone shell commands and excluded from the agent's final MCP config so the agent uses the CLI wrappers exclusively. Whenfalse, no CLI mounting step is emitted and no prompt section is injected.How It Works
After the MCP gateway starts, a new "Mount MCP servers as CLIs" workflow step runs
mount_mcp_as_cli.cjsviaactions/github-script. This script:start_mcp_gateway.shbefore cleanup)initialize→notifications/initialized→tools/list) to discover available tools${RUNNER_TEMP}/gh-aw/mcp-cli/bin/<server-name>chmod 555) so the agent cannot modify or add scriptsPATHviacore.addPath()for subsequent stepsThe agent is also informed about these tools via an injected prompt section (wrapped in
<mcp-clis>XML tags) listing the available CLI commands.Architecture: Gateway vs. Agent Config
The MCP gateway must start Docker containers for all configured servers — including CLI-mounted ones — so they have reachable endpoints for the CLI manifest. The filtering is therefore applied in two stages:
start_mcp_gateway.sh): includes all servers so the gateway starts their containers and populates/tmp/gh-aw/mcp-cli/manifest.json.convert_gateway_config_*.sh): CLI-mounted servers are excluded via theGH_AW_MCP_CLI_SERVERSenvironment variable (a JSON array exported in the gateway step), so the agent cannot reach them directly and must use the CLI wrappers.AWF Sandbox Path
CLI wrapper scripts and tools JSON are generated under
${RUNNER_TEMP}/gh-aw/mcp-cli/(not/tmp/gh-aw/) so they are accessible inside the AWF sandbox, which mounts${RUNNER_TEMP}/gh-awread-only. The manifest file written bystart_mcp_gateway.shremains at/tmp/gh-aw/mcp-cli/manifest.jsonsince it is only read during the setup step, which runs on the host before AWF starts.Changes
New:
actions/setup/js/mount_mcp_as_cli.cjs/tmp/gh-aw/mcp-cli/manifest.jsoninitialize— establishes session, capturesMcp-Session-Idnotifications/initialized— required handshake notification (server responds 204 No Content)tools/list/tools/call— executed within the established session<server> --help— list all available tools<server> <tool> --help— show tool description and parameters<server> <tool> [--param value ...]— call a tool (follows the same 3-step MCP session protocol per invocation)safeoutputs,mcp-scripts,mcpscripts) andcache-memoryare excluded${RUNNER_TEMP}/gh-aw/mcp-cli/bin/in achmod 555directory the agent cannot write to${RUNNER_TEMP}/gh-aw/mcp-cli/tools/so scripts can access it inside AWFMCP_GATEWAY_API_KEYis excluded from the AWF sandboxcore.info()/core.warning()logging throughoutNew:
actions/setup/md/mcp_cli_tools_prompt.md<mcp-clis>prompt section, following the same pattern as other prompt templates__GH_AW_MCP_CLI_SERVERS_LIST__placeholder substituted at runtime by thesubstitute_placeholdersstepgithubandplaywrightModified:
actions/setup/sh/start_mcp_gateway.sh/tmp/gh-aw/mcp-cli/manifest.jsonwith server names and local URLs beforegateway-output.jsonis deletedGH_AW_MCP_CLI_SERVERSModified:
actions/setup/sh/convert_gateway_config_*.sh(Copilot, Claude, Codex, Gemini)GH_AW_MCP_CLI_SERVERS(JSON array) and excludes those servers from the agent's final MCP config using--argjson cliServers "${GH_AW_MCP_CLI_SERVERS:-[]}"in thejqcallNew:
pkg/workflow/mcp_cli_mount.gogetMCPCLIServerNames()— returns sorted list of user-facing MCP servers to mount (built-in and custom); returns nil whentools.mount-as-clisis nottruegenerateMCPCLIMountStep()— generates the "Mount MCP servers as CLIs"actions/github-scriptstep in compiled workflows; no-op when flag is falsebuildMCPCLIPromptSection()— returnsIsFile: truereferencingmcp_cli_tools_prompt.mdwith the server list passed asGH_AW_MCP_CLI_SERVERS_LISTenv var; no-op when flag is falseModified:
pkg/workflow/tools_types.goandpkg/workflow/tools_parser.goMountAsCLIs boolfield toToolsConfig(parsed fromtools.mount-as-clis, defaultfalse)Modified:
pkg/parser/schemas/main_workflow_schema.jsonmount-as-clisboolean property to the tools schemaModified:
pkg/workflow/mcp_rendering.gocomposeCLIMountFilter— CLI-mounted servers are no longer excluded from the gateway config (they must be started by the gateway). Filtering is now delegated to the conversion scripts.Modified:
pkg/workflow/mcp_setup_generator.goGH_AW_MCP_CLI_SERVERS(JSON array built withjson.Marshal) in the gateway step script, afterGH_AW_ENGINE, so conversion scripts can exclude those servers from the agent configModified:
pkg/workflow/compiler_yaml_main_job.gogenerateMCPCLIMountStep()aftergenerateMCPSetup()completesModified:
pkg/workflow/unified_prompt_step.go<mcp-clis>XML tags) after safe outputs (section 8a)Modified:
actions/setup/sh/validate_prompt_placeholders.sh__GH_AW_*__placeholders, so documented placeholder names in PR descriptions or code examples are not flagged as errorsModified:
.github/workflows/smoke-copilot.mdmount-as-clis: trueto exercise the feature in smoke testinggithub pull_request_read --method list ...,serena activate_project --path ...,playwright browser_navigate --url ...) with "via bash" guidance to eliminate agent ambiguityUpdated
.lock.ymlfiles recompiledExample
For a workflow configured with
mount-as-clis: true,playwright,github, and a customtavilyMCP server, the agent will have:Those servers are excluded from the agent's MCP config, so the agent interacts with them exclusively through the CLI wrappers. The gateway still starts all Docker containers so the CLI wrappers can communicate with them.
The prompt injected to the agent (loaded from
mcp_cli_tools_prompt.md):✨ PR Review Safe Output Test - Run 23992552020
✨ PR Review Safe Output Test - Run 23993205568
✨ PR Review Safe Output Test - Run 24001448881