feat: mount MCP servers as local CLIs after gateway starts#25928
feat: mount MCP servers as local CLIs after gateway starts#25928
Conversation
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/f6e50b8d-7b6d-4cef-a64b-c7b0f9f8878d Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
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: true` flag (default `false`). When enabled, all user-facing MCP servers are exposed as standalone shell commands and excluded from the agent's final MCP config so the agent uses the CLI wrappers exclusively. Reconstructs the feature from PR #24503. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/f6e50b8d-7b6d-4cef-a64b-c7b0f9f8878d Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
|
@copilot ignore the github MCP server, it is handled differently. |
The github MCP server is handled differently and should not be mounted as a CLI tool. Added it to the exclusion lists in both Go and JavaScript code, updated prompt template examples, and restored smoke test instructions to use github as a normal MCP tool. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/35a490ef-753b-4f4e-8ab3-0d10a2d44b21 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Excluded the 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 enable flag for smoke-copilot |
|
@copilot always enabled for safeoutputs, mcpscripts. Update the built prompt generator to reflect this change too. |
The |
Removed safeoutputs, mcp-scripts, and mcpscripts from the internalMCPServerNames exclusion list in both Go and JavaScript. Added explicit logic to always include safeoutputs and mcpscripts in getMCPCLIServerNames() when they are enabled. Updated the prompt template with safeoutputs/mcpscripts examples and updated smoke-copilot test instructions accordingly. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/20bf245a-f76f-48bb-9b17-8597b84ee941 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Done in 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:
|
|
✅ All tools validated successfully! Agent Container Smoke Test confirms agent container is ready. |
|
|
|
✨ The prophecy is fulfilled... Smoke Codex has completed its mystical journey. The stars align. 🌟 |
|
📰 BREAKING: Smoke Copilot is now investigating this pull request. Sources say the story is developing... |
|
🎬 THE END — Smoke Claude MISSION: ACCOMPLISHED! The hero saves the day! ✨ |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/3a98acd1-b34f-4013-9f93-d0687d6ae8c4 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Done in
Converted to 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 do a deep review of the changes in this PR
|
…n and logging Agent-Logs-Url: https://github.com/github/gh-aw/sessions/f0e52bd1-8746-4739-880a-05eeeae4d91b Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/f0e52bd1-8746-4739-880a-05eeeae4d91b Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Done in Shell injection prevention:
Server name validation in both Logging improvements:
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 always use actions core.* for logging. In node processes, require the shim.cjs to install those types. |
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/129a2970-da0b-4c29-96bc-ec046a84142e Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/129a2970-da0b-4c29-96bc-ec046a84142e Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Done in 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 Contribution Guidelines Check
Verdict: 🟢 Aligned · Quality: Two housekeeping items before merge:
|
|
@copilot ensure that this feature is NOT enabled by default.
|
…eature-24503 # Conflicts: # .github/workflows/api-consumption-report.lock.yml # .github/workflows/approach-validator.lock.yml # .github/workflows/audit-workflows.lock.yml # .github/workflows/blog-auditor.lock.yml # .github/workflows/ci-doctor.lock.yml # .github/workflows/claude-code-user-docs-review.lock.yml # .github/workflows/cli-version-checker.lock.yml # .github/workflows/cloclo.lock.yml # .github/workflows/commit-changes-analyzer.lock.yml # .github/workflows/contribution-check.lock.yml # .github/workflows/copilot-agent-analysis.lock.yml # .github/workflows/copilot-pr-nlp-analysis.lock.yml # .github/workflows/copilot-session-insights.lock.yml # .github/workflows/copilot-token-audit.lock.yml # .github/workflows/daily-choice-test.lock.yml # .github/workflows/daily-code-metrics.lock.yml # .github/workflows/daily-doc-healer.lock.yml # .github/workflows/daily-doc-updater.lock.yml # .github/workflows/daily-function-namer.lock.yml # .github/workflows/daily-integrity-analysis.lock.yml # .github/workflows/daily-issues-report.lock.yml # .github/workflows/daily-multi-device-docs-tester.lock.yml # .github/workflows/daily-otel-instrumentation-advisor.lock.yml # .github/workflows/daily-rendering-scripts-verifier.lock.yml # .github/workflows/daily-safe-output-optimizer.lock.yml # .github/workflows/daily-safe-outputs-conformance.lock.yml # .github/workflows/daily-security-red-team.lock.yml # .github/workflows/daily-team-evolution-insights.lock.yml # .github/workflows/deep-report.lock.yml # .github/workflows/design-decision-gate.lock.yml # .github/workflows/developer-docs-consolidator.lock.yml # .github/workflows/example-workflow-analyzer.lock.yml # .github/workflows/github-mcp-structural-analysis.lock.yml # .github/workflows/github-mcp-tools-report.lock.yml # .github/workflows/go-fan.lock.yml # .github/workflows/go-logger.lock.yml # .github/workflows/go-pattern-detector.lock.yml # .github/workflows/hourly-ci-cleaner.lock.yml # .github/workflows/instructions-janitor.lock.yml # .github/workflows/lockfile-stats.lock.yml # .github/workflows/org-health-report.lock.yml # .github/workflows/prompt-clustering-analysis.lock.yml # .github/workflows/safe-output-health.lock.yml # .github/workflows/schema-consistency-checker.lock.yml # .github/workflows/scout.lock.yml # .github/workflows/semantic-function-refactor.lock.yml # .github/workflows/sergo.lock.yml # .github/workflows/smoke-agent-all-merged.lock.yml # .github/workflows/smoke-agent-all-none.lock.yml # .github/workflows/smoke-agent-public-approved.lock.yml # .github/workflows/smoke-agent-public-none.lock.yml # .github/workflows/smoke-agent-scoped-approved.lock.yml # .github/workflows/smoke-claude.lock.yml # .github/workflows/smoke-copilot.lock.yml # .github/workflows/stale-repo-identifier.lock.yml # .github/workflows/static-analysis-report.lock.yml # .github/workflows/step-name-alignment.lock.yml # .github/workflows/test-create-pr-error-handling.lock.yml # .github/workflows/typist.lock.yml # .github/workflows/unbloat-docs.lock.yml Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…0.2.19 Agent-Logs-Url: https://github.com/github/gh-aw/sessions/f4bbdc76-d016-4c7a-b2c6-2e4c53cf778f Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Verified and done in 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 merge main and recompile |
There was a problem hiding this comment.
Pull request overview
Adds an opt-in feature to mount eligible MCP servers as local CLI wrappers after the gateway starts, shifting agent access from MCP protocol tools to shell commands and hardening where configs/manifests are stored.
Changes:
- Introduces
tools.mount-as-clis(defaultfalse) and wiring to generate a post-gateway “mount as CLIs” step plus a new prompt section. - Moves MCP config/manifest storage from
/tmp/gh-awto${RUNNER_TEMP}/gh-aw(with corresponding engine/script updates) and switches gateway startup tostart_mcp_gateway.cjs. - Updates placeholder validation and regenerates workflow lockfiles/goldens to reflect the new behavior and paths.
Show a summary per file
| File | Description |
|---|---|
| pkg/workflow/mcp_cli_mount.go | Determines which MCP servers are mounted as CLIs and emits the mount step + prompt section. |
| pkg/workflow/mcp_setup_generator.go | Exports GH_AW_MCP_CLI_SERVERS for downstream converter filtering and updates MCP config dir to ${RUNNER_TEMP}. |
| pkg/workflow/unified_prompt_step.go | Injects the new <mcp-clis> prompt section when CLI mounting is enabled. |
| actions/setup/js/mount_mcp_as_cli.cjs | Implements manifest reading, MCP handshake/tool discovery, and wrapper generation under ${RUNNER_TEMP}/gh-aw/mcp-cli/bin. |
| actions/setup/sh/convert_gateway_config_gemini.sh | Filters out CLI-mounted servers from the agent-facing config (Gemini path). |
| actions/setup/sh/validate_prompt_placeholders.sh | Adjusts placeholder validation to ignore placeholder names inside inline backticks. |
| pkg/parser/schemas/main_workflow_schema.json | Adds schema for tools.mount-as-clis. |
| pkg/workflow/* (engine + rendering files) | Updates gateway invocation to start_mcp_gateway.cjs and path usage to ${RUNNER_TEMP} / ${{ runner.temp }} where appropriate. |
| pkg/workflow/testdata/**.golden | Updates golden outputs for new gateway invocation and ${RUNNER_TEMP} paths. |
| .github/workflows/*.lock.yml | Large-scale recompilation of locked workflows to reflect updated setup/gateway steps and paths. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 229/233 changed files
- Comments generated: 4
| "description": "When true, each user-facing MCP server is mounted as a standalone CLI tool on PATH. The agent can then call MCP servers via shell commands (e.g. 'github issue_read --method get ...'). CLI-mounted servers remain in the MCP gateway config so their containers can start, and are removed only from the agent's final config during convert_gateway_config_*.sh processing. Default: false.", | ||
| "examples": [true] |
There was a problem hiding this comment.
Schema description for tools.mount-as-clis still says CLI-mounted servers are removed during convert_gateway_config_*.sh processing, but this PR migrates converters to convert_gateway_config_*.cjs. Updating this text will avoid confusing users about which scripts implement the filtering logic.
| // Collect user-facing standard MCP tools from the raw Tools map | ||
| for toolName, toolValue := range data.Tools { | ||
| if toolValue == false { | ||
| continue | ||
| } | ||
| // Only include tools that have MCP servers (skip bash, web-fetch, web-search, edit, cache-memory, etc.) | ||
| // Note: "github" is excluded — it is handled differently and should not be CLI-mounted. | ||
| switch toolName { | ||
| case "playwright", "qmd": | ||
| servers = append(servers, toolName) | ||
| case "agentic-workflows": | ||
| // The gateway and manifest use "agenticworkflows" (no hyphen) as the server ID. | ||
| // Using the gateway ID here ensures GH_AW_MCP_CLI_SERVERS matches the manifest entries. | ||
| servers = append(servers, constants.AgenticWorkflowsMCPServerID.String()) | ||
| default: | ||
| // Include custom MCP servers (not in the internal list) | ||
| if !internalMCPServerNames[toolName] { | ||
| if mcpConfig, ok := toolValue.(map[string]any); ok { | ||
| if hasMcp, _ := hasMCPConfig(mcpConfig); hasMcp { | ||
| servers = append(servers, toolName) | ||
| } | ||
| } |
There was a problem hiding this comment.
getMCPCLIServerNames() can include arbitrary custom server names (from data.Tools / ParsedTools.Custom) without validating them against the same constraints used by mount_mcp_as_cli.cjs (e.g., /^[a-zA-Z0-9_-]{1,64}$/). If a name is invalid, the JS mounting step will skip it, but GH_AW_MCP_CLI_SERVERS will still cause converter scripts to filter it out of the agent MCP config—leaving the agent with neither an MCP tool nor a CLI wrapper. Consider validating/sanitizing names here (or only excluding from agent config when the mount step will actually create a wrapper), and log/skip invalid entries consistently with the JS script.
| if cliServers := getMCPCLIServerNames(workflowData); len(cliServers) > 0 { | ||
| cliServersJSON, err := json.Marshal(cliServers) | ||
| if err == nil { | ||
| yaml.WriteString(" export GH_AW_MCP_CLI_SERVERS='" + string(cliServersJSON) + "'\n") | ||
| yaml.WriteString(" echo 'GH_AW_MCP_CLI_SERVERS=" + string(cliServersJSON) + "' >> \"$GITHUB_ENV\"\n") | ||
| } |
There was a problem hiding this comment.
GH_AW_MCP_CLI_SERVERS is written into a bash export and into $GITHUB_ENV using raw JSON string concatenation. If any server name contains shell-significant characters (quotes/newlines), this can break the step or become an injection vector. Either ensure getMCPCLIServerNames() only returns a strictly validated character set (and reject/escape anything else), or write the env var in a way that is robust to arbitrary JSON (e.g., printf with proper escaping / heredoc).
| jq --arg urlPrefix "$URL_PREFIX" --argjson cliServers "${GH_AW_MCP_CLI_SERVERS:-[]}" ' | ||
| .mcpServers |= with_entries( | ||
| select(.key | IN($cliServers[]) | not) | | ||
| .value |= ( |
There was a problem hiding this comment.
The jq filter select(.key | IN($cliServers[]) | not) can drop all entries when cliServers is an empty array, because $cliServers[] produces no values and the predicate yields no output for select(...). This would result in an empty .mcpServers even when no CLI servers are configured. Use an emptiness-safe membership check instead (e.g., select(($cliServers | index(.key)) == null) or similar).
Reconstructs the feature from #24503. Adds opt-in mounting of MCP servers as standalone CLI tools on
PATHviatools.mount-as-clis: true(defaultfalse). When enabled, user-facing MCP servers are exposed as shell commands and excluded from the agent's final MCP config, forcing the agent to use CLI wrappers exclusively. ThegithubMCP server is excluded from CLI mounting as it is handled differently and remains available as a normal MCP tool. Thesafeoutputsandmcpscriptsservers are always included in CLI mounting when enabled — they are not treated as internal infrastructure.How it works
The gateway starts containers for all configured servers (so they're reachable), then filtering happens in two stages:
${RUNNER_TEMP}/gh-aw/mcp-cli/manifest.jsonGH_AW_MCP_CLI_SERVERSenv var in eachconvert_gateway_config_*.cjsscriptAfter the gateway starts,
mount_mcp_as_cli.cjsreads the manifest, queries each server'stools/listvia the full MCP session protocol (initialize→notifications/initialized→tools/list), and generates locked-down CLI wrappers under${RUNNER_TEMP}/gh-aw/mcp-cli/bin/.Note: The
githubMCP server is not CLI-mounted — it is handled differently and remains available as a normal MCP tool even whenmount-as-clis: trueis set.Changes
New files
pkg/workflow/mcp_cli_mount.go—getMCPCLIServerNames(),generateMCPCLIMountStep(),buildMCPCLIPromptSection()actions/setup/js/mount_mcp_as_cli.cjs— manifest reader, MCP handshake, CLI wrapper generator; scripts placed at${RUNNER_TEMP}/gh-aw/mcp-cli/bin/(inside AWF sandbox mount), directory lockedchmod 555, API key baked in at generation time (not available in sandbox env)actions/setup/md/mcp_cli_tools_prompt.md—<mcp-clis>prompt section templateactions/setup/js/convert_gateway_config_copilot.cjs— JavaScript gateway config converter for Copilot (filters CLI servers, addstools:["*"], rewrites URLs)actions/setup/js/convert_gateway_config_claude.cjs— JavaScript gateway config converter for Claude (filters CLI servers, setstype:"http", rewrites URLs)actions/setup/js/convert_gateway_config_codex.cjs— JavaScript gateway config converter for Codex (filters CLI servers, resolveshost.docker.internalto172.30.0.1, generates TOML)actions/setup/js/convert_gateway_config_gemini.cjs— JavaScript gateway config converter for Gemini (filters CLI servers, removestypefield for transport auto-detection, rewrites URLs, addscontext.includeDirectories)actions/setup/js/start_mcp_gateway.cjs— JavaScript gateway startup script (reads MCP config from stdin, launches Docker container, health check polling, engine-specific converter dispatch, CLI manifest creation, GitHub Actions outputs)Go pipeline integration
ToolsConfig.MountAsCLIsfield + parser + schema + validationcompiler_yaml_main_job.gocallsgenerateMCPCLIMountStep()after gateway setupmcp_setup_generator.goexportsGH_AW_MCP_CLI_SERVERSJSON array to$GITHUB_ENVunified_prompt_step.goinjects CLI tools prompt section (8a, after safe outputs)mcp_rendering.godocuments why CLI servers stay in gateway configmcp_renderer.goinvokesnode start_mcp_gateway.cjs(converted frombash start_mcp_gateway.sh)getMCPCLIServerNames()always includessafeoutputsandmcpscriptswhen they are enabled (HasSafeOutputsEnabled/IsMCPScriptsEnabled)Gateway scripts (bash → JavaScript)
start_mcp_gateway.cjs— full JavaScript port of the gateway startup script usingchild_process.spawnfor Docker container management, Node.jshttpmodule for health checks, andchild_process.execSyncfor converter/checker dispatch. Maintains all security controls (umask, symlink attack prevention, 0o600 file permissions)convert_gateway_config_{copilot,claude,codex,gemini}.cjs— all four engine converters are JavaScript for CLI server filtering, URL rewriting, and engine-specific config generationvalidate_prompt_placeholders.sh— strips backtick code spans before checking for unreplaced__GH_AW_*__placeholdersConfig storage hardening (
/tmp→RUNNER_TEMP)mcp-config/) and CLI manifest (mcp-cli/) moved from/tmp/gh-aw/to${RUNNER_TEMP}/gh-aw/to prevent tampering —/tmpis world-writable,RUNNER_TEMPis per-runner and not world-accessiblestart_mcp_gateway.cjs, converter scripts,mount_mcp_as_cli.cjs,redact_secrets.cjs) and Go engine code (claude_engine.go,codex_engine.go,codex_mcp.go,claude_mcp.go,gemini_mcp.go,mcp_setup_generator.go,threat_detection.go,step_order_validation.go)/tmp/gh-aw/mcp-logs/since the Docker gateway container writes there via-v /tmp:/tmp:rwSecurity hardening (shell injection, input validation)
shellEscapeDoubleQuoted()inmount_mcp_as_cli.cjs— escapes$ `` \ " !and strips\r\nfor all values embedded in generated CLI wrapper bash scripts (server name, URL, tools file path, API key), preventing shell injection and line injection attacksisValidServerName()inmount_mcp_as_cli.cjs— validates server names against[a-zA-Z0-9_-]with 64-char max length, preventing path traversal and shell metacharacter injection via malicious server names; servers failing validation are skipped with a warningnew URL()before processinggatewayPortverified to be strictly numeric before interpolation intoexecSyncshell commands instart_mcp_gateway.cjscheck_mcp_servers.shcall uses$MCP_GATEWAY_API_KEYenv var reference instead of direct JS string interpolation to avoid shell metacharacter risksstart_mcp_gateway.cjsfilters invalid server names when generating the CLI manifestObservability improvements
core.*logging viashim.cjs(core.info,core.error,core.warning) instead ofconsole.*, ensuring consistent GitHub Actions annotation outputstart_mcp_gateway.cjslogs startup banner (engine, port, domain, runner temp), server names in CLI manifest; top-level error handler preserves stack traces viacore.error(err.stack)beforecore.setFailed()mount_mcp_as_cli.cjslogs manifest server count after internal filtering, skipped servers with validation error reasonsServer exclusions
githubMCP server is excluded from CLI mounting in both Go (internalMCPServerNames) and JavaScript (INTERNAL_SERVERS) — it is handled differently and remains a normal MCP toolsafeoutputsandmcpscriptsare not excluded — they are always CLI-mounted when enabled, with usage examples in the prompt templateSmoke test
smoke-copilot.mdenablesmount-as-clis: trueand updates test instructions to use CLI syntax for playwright/serena/safeoutputs/mcpscripts while keeping github as a normal MCP tool