Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
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