-
Notifications
You must be signed in to change notification settings - Fork 351
feat: mount MCP servers as local CLIs after gateway starts #25928
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Copilot
wants to merge
20
commits into
main
Choose a base branch
from
copilot/reconstruct-feature-24503
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 7 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
fd64ca2
initial plan for MCP CLI mount feature
Copilot afeaeed
feat: mount MCP servers as local CLIs after gateway starts
Copilot 4951367
fix: exclude github MCP server from CLI mounting
Copilot fb83906
feat: always include safeoutputs and mcpscripts in CLI mounting
Copilot 7da162a
Add changeset
github-actions[bot] 91198ae
refactor: convert gateway config scripts from bash to JavaScript
Copilot d639caf
fix: add type validation for mcpServers in gateway config converters
Copilot 4424bbd
fix: quote shell variables and fix comment references in scripts
Copilot 36a30db
refactor: convert Gemini gateway config converter from bash to JavaSc…
Copilot e8bc926
refactor: convert start_mcp_gateway.sh to JavaScript
Copilot b9c7d1e
security: store MCP config under RUNNER_TEMP to prevent tampering
Copilot e7501b3
security: harden scripts against shell injection, add input validatio…
Copilot 1b18ebd
security: strip newlines in shell escape to prevent line injection
Copilot e997fe0
refactor: migrate JS logging from console.* to core.* via shim.cjs
Copilot a30140e
fix: preserve stack trace in start_mcp_gateway.cjs error handler
Copilot 130f98d
Merge remote-tracking branch 'origin/main' into copilot/reconstruct-f…
Copilot de8393b
fix: merge main, update golden files and test expectations for MCPG v…
Copilot 6767feb
Merge remote-tracking branch 'origin/main' into copilot/reconstruct-f…
Copilot 05db9fe
Merge remote-tracking branch 'origin/main' into copilot/reconstruct-f…
Copilot 5ab0f5f
Merge remote-tracking branch 'origin/main' into copilot/reconstruct-f…
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| // @ts-check | ||
| "use strict"; | ||
|
|
||
| /** | ||
| * convert_gateway_config_claude.cjs | ||
| * | ||
| * Converts the MCP gateway's standard HTTP-based configuration to the JSON | ||
| * format expected by Claude. Reads the gateway output JSON, filters out | ||
| * CLI-mounted servers, sets type:"http", rewrites URLs to use the correct | ||
| * domain, and writes the result to /tmp/gh-aw/mcp-config/mcp-servers.json. | ||
| * | ||
| * Required environment variables: | ||
| * - MCP_GATEWAY_OUTPUT: Path to gateway output configuration file | ||
| * - MCP_GATEWAY_DOMAIN: Domain for MCP server URLs (e.g., host.docker.internal) | ||
| * - MCP_GATEWAY_PORT: Port for MCP gateway (e.g., 80) | ||
| * | ||
| * Optional: | ||
| * - GH_AW_MCP_CLI_SERVERS: JSON array of server names to exclude from agent config | ||
| */ | ||
|
|
||
| const fs = require("fs"); | ||
| const path = require("path"); | ||
|
|
||
| const OUTPUT_PATH = "/tmp/gh-aw/mcp-config/mcp-servers.json"; | ||
|
|
||
| /** | ||
| * Rewrite a gateway URL to use the configured domain and port. | ||
| * Replaces http://<anything>/mcp/ with http://<domain>:<port>/mcp/. | ||
| * | ||
| * @param {string} url - Original URL from gateway output | ||
| * @param {string} urlPrefix - Target URL prefix (e.g., http://host.docker.internal:80) | ||
| * @returns {string} Rewritten URL | ||
| */ | ||
| function rewriteUrl(url, urlPrefix) { | ||
| return url.replace(/^http:\/\/[^/]+\/mcp\//, `${urlPrefix}/mcp/`); | ||
| } | ||
|
|
||
| function main() { | ||
| const gatewayOutput = process.env.MCP_GATEWAY_OUTPUT; | ||
| const domain = process.env.MCP_GATEWAY_DOMAIN; | ||
| const port = process.env.MCP_GATEWAY_PORT; | ||
|
|
||
| if (!gatewayOutput) { | ||
| console.error("ERROR: MCP_GATEWAY_OUTPUT environment variable is required"); | ||
| process.exit(1); | ||
| } | ||
| if (!fs.existsSync(gatewayOutput)) { | ||
| console.error(`ERROR: Gateway output file not found: ${gatewayOutput}`); | ||
| process.exit(1); | ||
| } | ||
| if (!domain) { | ||
| console.error("ERROR: MCP_GATEWAY_DOMAIN environment variable is required"); | ||
| process.exit(1); | ||
| } | ||
| if (!port) { | ||
| console.error("ERROR: MCP_GATEWAY_PORT environment variable is required"); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| console.log("Converting gateway configuration to Claude format..."); | ||
| console.log(`Input: ${gatewayOutput}`); | ||
| console.log(`Target domain: ${domain}:${port}`); | ||
|
|
||
| const urlPrefix = `http://${domain}:${port}`; | ||
|
|
||
| /** @type {Set<string>} */ | ||
| const cliServers = new Set(JSON.parse(process.env.GH_AW_MCP_CLI_SERVERS || "[]")); | ||
|
|
||
| /** @type {Record<string, unknown>} */ | ||
| const config = JSON.parse(fs.readFileSync(gatewayOutput, "utf8")); | ||
| const rawServers = config.mcpServers; | ||
| const servers = | ||
| /** @type {Record<string, Record<string, unknown>>} */ | ||
| rawServers && typeof rawServers === "object" && !Array.isArray(rawServers) ? rawServers : {}; | ||
|
|
||
| /** @type {Record<string, Record<string, unknown>>} */ | ||
| const result = {}; | ||
| for (const [name, value] of Object.entries(servers)) { | ||
| if (cliServers.has(name)) continue; | ||
| const entry = { ...value }; | ||
| // Claude uses "type": "http" for HTTP-based MCP servers | ||
| entry.type = "http"; | ||
| // Fix the URL to use the correct domain | ||
| if (typeof entry.url === "string") { | ||
| entry.url = rewriteUrl(entry.url, urlPrefix); | ||
| } | ||
| result[name] = entry; | ||
| } | ||
|
|
||
| const output = JSON.stringify({ mcpServers: result }, null, 2); | ||
|
|
||
| // Ensure output directory exists | ||
| fs.mkdirSync(path.dirname(OUTPUT_PATH), { recursive: true }); | ||
|
|
||
| // Write with owner-only permissions (0o600) to protect the gateway bearer token. | ||
| // An attacker who reads mcp-servers.json could bypass --allowed-tools by issuing | ||
| // raw JSON-RPC calls directly to the gateway. | ||
| fs.writeFileSync(OUTPUT_PATH, output, { mode: 0o600 }); | ||
|
|
||
| console.log(`Claude configuration written to ${OUTPUT_PATH}`); | ||
| console.log(""); | ||
| console.log("Converted configuration:"); | ||
| console.log(output); | ||
| } | ||
|
|
||
| main(); | ||
|
|
||
| module.exports = { rewriteUrl }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| // @ts-check | ||
| "use strict"; | ||
|
|
||
| /** | ||
| * convert_gateway_config_codex.cjs | ||
| * | ||
| * Converts the MCP gateway's standard HTTP-based configuration to the TOML | ||
| * format expected by Codex. Reads the gateway output JSON, filters out | ||
| * CLI-mounted servers, resolves host.docker.internal to 172.30.0.1 for Rust | ||
| * DNS compatibility, and writes the result to /tmp/gh-aw/mcp-config/config.toml. | ||
| * | ||
| * Required environment variables: | ||
| * - MCP_GATEWAY_OUTPUT: Path to gateway output configuration file | ||
| * - MCP_GATEWAY_DOMAIN: Domain for MCP server URLs (e.g., host.docker.internal) | ||
| * - MCP_GATEWAY_PORT: Port for MCP gateway (e.g., 80) | ||
| * | ||
| * Optional: | ||
| * - GH_AW_MCP_CLI_SERVERS: JSON array of server names to exclude from agent config | ||
| */ | ||
|
|
||
| const fs = require("fs"); | ||
| const path = require("path"); | ||
|
|
||
| const OUTPUT_PATH = "/tmp/gh-aw/mcp-config/config.toml"; | ||
|
|
||
| function main() { | ||
| const gatewayOutput = process.env.MCP_GATEWAY_OUTPUT; | ||
| const domain = process.env.MCP_GATEWAY_DOMAIN; | ||
| const port = process.env.MCP_GATEWAY_PORT; | ||
|
|
||
| if (!gatewayOutput) { | ||
| console.error("ERROR: MCP_GATEWAY_OUTPUT environment variable is required"); | ||
| process.exit(1); | ||
| } | ||
| if (!fs.existsSync(gatewayOutput)) { | ||
| console.error(`ERROR: Gateway output file not found: ${gatewayOutput}`); | ||
| process.exit(1); | ||
| } | ||
| if (!domain) { | ||
| console.error("ERROR: MCP_GATEWAY_DOMAIN environment variable is required"); | ||
| process.exit(1); | ||
| } | ||
| if (!port) { | ||
| console.error("ERROR: MCP_GATEWAY_PORT environment variable is required"); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| console.log("Converting gateway configuration to Codex TOML format..."); | ||
| console.log(`Input: ${gatewayOutput}`); | ||
| console.log(`Target domain: ${domain}:${port}`); | ||
|
|
||
| // For host.docker.internal, resolve to the gateway IP to avoid DNS resolution | ||
| // issues in Rust | ||
| let resolvedDomain = domain; | ||
| if (domain === "host.docker.internal") { | ||
| // AWF network gateway IP is always 172.30.0.1 | ||
| resolvedDomain = "172.30.0.1"; | ||
| console.log(`Resolving host.docker.internal to gateway IP: ${resolvedDomain}`); | ||
| } | ||
|
|
||
| const urlPrefix = `http://${resolvedDomain}:${port}`; | ||
|
|
||
| /** @type {Set<string>} */ | ||
| const cliServers = new Set(JSON.parse(process.env.GH_AW_MCP_CLI_SERVERS || "[]")); | ||
|
|
||
| /** @type {Record<string, unknown>} */ | ||
| const config = JSON.parse(fs.readFileSync(gatewayOutput, "utf8")); | ||
| const rawServers = config.mcpServers; | ||
| const servers = | ||
| /** @type {Record<string, Record<string, unknown>>} */ | ||
| rawServers && typeof rawServers === "object" && !Array.isArray(rawServers) ? rawServers : {}; | ||
|
|
||
| // Build the TOML output | ||
| let toml = '[history]\npersistence = "none"\n\n'; | ||
|
|
||
| for (const [name, value] of Object.entries(servers)) { | ||
| if (cliServers.has(name)) continue; | ||
| const url = `${urlPrefix}/mcp/${name}`; | ||
| const headers = /** @type {Record<string, string>} */ value.headers || {}; | ||
| const authKey = headers.Authorization || ""; | ||
| toml += `[mcp_servers.${name}]\n`; | ||
| toml += `url = "${url}"\n`; | ||
| toml += `http_headers = { Authorization = "${authKey}" }\n`; | ||
| toml += "\n"; | ||
| } | ||
|
|
||
| // Ensure output directory exists | ||
| fs.mkdirSync(path.dirname(OUTPUT_PATH), { recursive: true }); | ||
|
|
||
| // Write with owner-only permissions (0o600) to protect the gateway bearer token. | ||
| // An attacker who reads config.toml could issue raw JSON-RPC calls directly | ||
| // to the gateway. | ||
| fs.writeFileSync(OUTPUT_PATH, toml, { mode: 0o600 }); | ||
|
|
||
| console.log(`Codex configuration written to ${OUTPUT_PATH}`); | ||
| console.log(""); | ||
| console.log("Converted configuration:"); | ||
| console.log(toml); | ||
| } | ||
|
|
||
| main(); | ||
|
|
||
| module.exports = {}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| // @ts-check | ||
| "use strict"; | ||
|
|
||
| /** | ||
| * convert_gateway_config_copilot.cjs | ||
| * | ||
| * Converts the MCP gateway's standard HTTP-based configuration to the format | ||
| * expected by GitHub Copilot CLI. Reads the gateway output JSON, filters out | ||
| * CLI-mounted servers, adds tools:["*"] if missing, rewrites URLs to use the | ||
| * correct domain, and writes the result to /home/runner/.copilot/mcp-config.json. | ||
| * | ||
| * Required environment variables: | ||
| * - MCP_GATEWAY_OUTPUT: Path to gateway output configuration file | ||
| * - MCP_GATEWAY_DOMAIN: Domain for MCP server URLs (e.g., host.docker.internal) | ||
| * - MCP_GATEWAY_PORT: Port for MCP gateway (e.g., 80) | ||
| * | ||
| * Optional: | ||
| * - GH_AW_MCP_CLI_SERVERS: JSON array of server names to exclude from agent config | ||
| */ | ||
|
|
||
| const fs = require("fs"); | ||
| const path = require("path"); | ||
|
|
||
| const OUTPUT_PATH = "/home/runner/.copilot/mcp-config.json"; | ||
|
|
||
| /** | ||
| * Rewrite a gateway URL to use the configured domain and port. | ||
| * Replaces http://<anything>/mcp/ with http://<domain>:<port>/mcp/. | ||
| * | ||
| * @param {string} url - Original URL from gateway output | ||
| * @param {string} urlPrefix - Target URL prefix (e.g., http://host.docker.internal:80) | ||
| * @returns {string} Rewritten URL | ||
| */ | ||
| function rewriteUrl(url, urlPrefix) { | ||
| return url.replace(/^http:\/\/[^/]+\/mcp\//, `${urlPrefix}/mcp/`); | ||
| } | ||
|
|
||
| function main() { | ||
| const gatewayOutput = process.env.MCP_GATEWAY_OUTPUT; | ||
| const domain = process.env.MCP_GATEWAY_DOMAIN; | ||
| const port = process.env.MCP_GATEWAY_PORT; | ||
|
|
||
| if (!gatewayOutput) { | ||
| console.error("ERROR: MCP_GATEWAY_OUTPUT environment variable is required"); | ||
| process.exit(1); | ||
| } | ||
| if (!fs.existsSync(gatewayOutput)) { | ||
| console.error(`ERROR: Gateway output file not found: ${gatewayOutput}`); | ||
| process.exit(1); | ||
| } | ||
| if (!domain) { | ||
| console.error("ERROR: MCP_GATEWAY_DOMAIN environment variable is required"); | ||
| process.exit(1); | ||
| } | ||
| if (!port) { | ||
| console.error("ERROR: MCP_GATEWAY_PORT environment variable is required"); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| console.log("Converting gateway configuration to Copilot format..."); | ||
| console.log(`Input: ${gatewayOutput}`); | ||
| console.log(`Target domain: ${domain}:${port}`); | ||
|
|
||
| const urlPrefix = `http://${domain}:${port}`; | ||
|
|
||
| /** @type {Set<string>} */ | ||
| const cliServers = new Set(JSON.parse(process.env.GH_AW_MCP_CLI_SERVERS || "[]")); | ||
|
|
||
| /** @type {Record<string, unknown>} */ | ||
| const config = JSON.parse(fs.readFileSync(gatewayOutput, "utf8")); | ||
| const rawServers = config.mcpServers; | ||
| const servers = | ||
| /** @type {Record<string, Record<string, unknown>>} */ | ||
| rawServers && typeof rawServers === "object" && !Array.isArray(rawServers) ? rawServers : {}; | ||
|
|
||
| /** @type {Record<string, Record<string, unknown>>} */ | ||
| const result = {}; | ||
| for (const [name, value] of Object.entries(servers)) { | ||
| if (cliServers.has(name)) continue; | ||
| const entry = { ...value }; | ||
| // Add tools field if not present | ||
| if (!entry.tools) { | ||
| entry.tools = ["*"]; | ||
| } | ||
| // Fix the URL to use the correct domain | ||
| if (typeof entry.url === "string") { | ||
| entry.url = rewriteUrl(entry.url, urlPrefix); | ||
| } | ||
| result[name] = entry; | ||
| } | ||
|
|
||
| const output = JSON.stringify({ mcpServers: result }, null, 2); | ||
|
|
||
| // Ensure output directory exists | ||
| fs.mkdirSync(path.dirname(OUTPUT_PATH), { recursive: true }); | ||
|
|
||
| // Write with owner-only permissions (0o600) to protect the gateway bearer token. | ||
| // An attacker who reads mcp-config.json could bypass --allowed-tools by issuing | ||
| // raw JSON-RPC calls directly to the gateway. | ||
| fs.writeFileSync(OUTPUT_PATH, output, { mode: 0o600 }); | ||
|
|
||
| console.log(`Copilot configuration written to ${OUTPUT_PATH}`); | ||
| console.log(""); | ||
| console.log("Converted configuration:"); | ||
| console.log(output); | ||
| } | ||
|
|
||
| main(); | ||
|
|
||
| module.exports = { rewriteUrl }; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cliServersfiltering: Good use ofSetfor O(1) lookup when filtering out CLI-mounted servers. The fallback to[]whenGH_AW_MCP_CLI_SERVERSis not set means no servers are filtered, which is the correct default behavior whenmount-as-clisis disabled.