Skip to content
Open
Show file tree
Hide file tree
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 Apr 12, 2026
afeaeed
feat: mount MCP servers as local CLIs after gateway starts
Copilot Apr 12, 2026
4951367
fix: exclude github MCP server from CLI mounting
Copilot Apr 12, 2026
fb83906
feat: always include safeoutputs and mcpscripts in CLI mounting
Copilot Apr 12, 2026
7da162a
Add changeset
github-actions[bot] Apr 12, 2026
91198ae
refactor: convert gateway config scripts from bash to JavaScript
Copilot Apr 13, 2026
d639caf
fix: add type validation for mcpServers in gateway config converters
Copilot Apr 13, 2026
4424bbd
fix: quote shell variables and fix comment references in scripts
Copilot Apr 13, 2026
36a30db
refactor: convert Gemini gateway config converter from bash to JavaSc…
Copilot Apr 13, 2026
e8bc926
refactor: convert start_mcp_gateway.sh to JavaScript
Copilot Apr 13, 2026
b9c7d1e
security: store MCP config under RUNNER_TEMP to prevent tampering
Copilot Apr 13, 2026
e7501b3
security: harden scripts against shell injection, add input validatio…
Copilot Apr 13, 2026
1b18ebd
security: strip newlines in shell escape to prevent line injection
Copilot Apr 13, 2026
e997fe0
refactor: migrate JS logging from console.* to core.* via shim.cjs
Copilot Apr 13, 2026
a30140e
fix: preserve stack trace in start_mcp_gateway.cjs error handler
Copilot Apr 13, 2026
130f98d
Merge remote-tracking branch 'origin/main' into copilot/reconstruct-f…
Copilot Apr 13, 2026
de8393b
fix: merge main, update golden files and test expectations for MCPG v…
Copilot Apr 13, 2026
6767feb
Merge remote-tracking branch 'origin/main' into copilot/reconstruct-f…
Copilot Apr 13, 2026
05db9fe
Merge remote-tracking branch 'origin/main' into copilot/reconstruct-f…
Copilot Apr 13, 2026
5ab0f5f
Merge remote-tracking branch 'origin/main' into copilot/reconstruct-f…
Copilot Apr 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/patch-mount-mcp-servers-as-clis.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/agentics-maintenance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ jobs:
validate_workflows:
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.operation == 'validate' && !github.event.repository.fork }}
runs-on: ubuntu-slim
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
Expand Down
67 changes: 44 additions & 23 deletions .github/workflows/smoke-copilot.lock.yml

Large diffs are not rendered by default.

25 changes: 21 additions & 4 deletions .github/workflows/smoke-copilot.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ tools:
- pelikhan
playwright:
web-fetch:
mount-as-clis: true
runtimes:
go:
version: "1.25"
Expand Down Expand Up @@ -128,14 +129,30 @@ strict: false

**IMPORTANT: Keep all outputs extremely short and concise. Use single-line responses where possible. No verbose explanations.**

## Tool Access Overview

This workflow uses `mount-as-clis: true`. The following MCP servers are **NOT available as MCP tools** — they are mounted exclusively as **shell CLI commands** (see `<mcp-clis>` section above). You **must** use them via the `bash` tool:

- **`playwright`** — use `playwright <tool> [--param value...]` in bash (e.g. `playwright browser_navigate --url ...`)
- **`serena`** — use `serena <tool> [--param value...]` in bash (e.g. `serena activate_project --path ...`)
- **`agenticworkflows`** — use `agenticworkflows <tool> [--param value...]` in bash
- **`safeoutputs`** — use `safeoutputs <tool> [--param value...]` in bash (e.g. `safeoutputs add_comment --body "..."`)
- **`mcpscripts`** — use `mcpscripts <tool> [--param value...]` in bash (e.g. `mcpscripts mcpscripts-gh --args "..."`)

The `github` MCP server is **NOT** CLI-mounted — it remains available as a normal MCP tool.

Run `<server> --help` to list all available tools for a server, or `<server> <tool> --help` for detailed parameter info.

These are **not** MCP protocol tools — they are bash executables. Call them with the `bash` tool only.

## Test Requirements

1. **GitHub MCP Testing**: Review the last 2 merged pull requests in ${{ github.repository }}
2. **MCP Scripts GH CLI Testing**: Use the `mcpscripts-gh` tool to query 2 pull requests from ${{ github.repository }} (use args: "pr list --repo ${{ github.repository }} --limit 2 --json number,title,author")
3. **Serena MCP Testing**:
- Use the Serena MCP server tool `activate_project` to initialize the workspace at `${{ github.workspace }}` and verify it succeeds (do NOT use bash to run go commands - use Serena's MCP tools)
- After initialization, use the `find_symbol` tool to search for symbols (find which tool to call) and verify that at least 3 symbols are found in the results
4. **Playwright Testing**: Use the playwright tools to navigate to <https://github.com> and verify the page title contains "GitHub" (do NOT try to install playwright - use the provided MCP tools)
3. **Serena CLI Testing**:
- Use bash to run `serena activate_project --path ${{ github.workspace }}` to initialize the workspace and verify it succeeds (do NOT use bash to run go commands - use the serena CLI only)
- After initialization, use bash to run `serena find_symbol --name_path <symbol>` to search for symbols and verify that at least 3 symbols are found in the results
4. **Playwright CLI Testing**: Use bash to run `playwright browser_navigate --url https://github.com` to navigate to <https://github.com>, then `playwright browser_snapshot` to capture the page and verify the title contains "GitHub" (do NOT try to install playwright - use the `playwright` CLI command via bash only)
5. **Web Fetch Testing**: Use the web-fetch tool to fetch https://github.com and verify the response contains "GitHub" (do NOT use bash or playwright for this test - use the web-fetch tool directly)
6. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-copilot-${{ github.run_id }}.txt` with content "Smoke test passed for Copilot at $(date)" (create the directory if it doesn't exist)
7. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back)
Expand Down
108 changes: 108 additions & 0 deletions actions/setup/js/convert_gateway_config_claude.cjs
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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cliServers filtering: Good use of Set for O(1) lookup when filtering out CLI-mounted servers. The fallback to [] when GH_AW_MCP_CLI_SERVERS is not set means no servers are filtered, which is the correct default behavior when mount-as-clis is disabled.

}
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 };
103 changes: 103 additions & 0 deletions actions/setup/js/convert_gateway_config_codex.cjs
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 = {};
110 changes: 110 additions & 0 deletions actions/setup/js/convert_gateway_config_copilot.cjs
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 };
Loading
Loading