diff --git a/.github/workflows/test-claude-add-issue-comment.lock.yml b/.github/workflows/test-claude-add-issue-comment.lock.yml index dd848e6b608..5ccd79f90c0 100644 --- a/.github/workflows/test-claude-add-issue-comment.lock.yml +++ b/.github/workflows/test-claude-add-issue-comment.lock.yml @@ -491,17 +491,11 @@ jobs: { "mcpServers": { "github": { - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "-e", - "GITHUB_PERSONAL_ACCESS_TOKEN", - "ghcr.io/github/github-mcp-server:sha-09deac4" - ], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}" + "type": "http", + "url": "https://api.github.com/mcp", + "headers": { + "Authorization": "Bearer ${{ secrets.GITHUB_TOKEN }}", + "Content-Type": "application/json" } } } diff --git a/.github/workflows/test-claude-add-issue-labels.lock.yml b/.github/workflows/test-claude-add-issue-labels.lock.yml index f7b41d1f612..47e05c35cf8 100644 --- a/.github/workflows/test-claude-add-issue-labels.lock.yml +++ b/.github/workflows/test-claude-add-issue-labels.lock.yml @@ -491,17 +491,11 @@ jobs: { "mcpServers": { "github": { - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "-e", - "GITHUB_PERSONAL_ACCESS_TOKEN", - "ghcr.io/github/github-mcp-server:sha-09deac4" - ], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}" + "type": "http", + "url": "https://api.github.com/mcp", + "headers": { + "Authorization": "Bearer ${{ secrets.GITHUB_TOKEN }}", + "Content-Type": "application/json" } } } diff --git a/.github/workflows/test-claude-command.lock.yml b/.github/workflows/test-claude-command.lock.yml index 724f7d22fb3..12c413bf282 100644 --- a/.github/workflows/test-claude-command.lock.yml +++ b/.github/workflows/test-claude-command.lock.yml @@ -651,17 +651,11 @@ jobs: { "mcpServers": { "github": { - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "-e", - "GITHUB_PERSONAL_ACCESS_TOKEN", - "ghcr.io/github/github-mcp-server:sha-09deac4" - ], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}" + "type": "http", + "url": "https://api.github.com/mcp", + "headers": { + "Authorization": "Bearer ${{ secrets.GITHUB_TOKEN }}", + "Content-Type": "application/json" } } } diff --git a/.github/workflows/test-claude-create-issue.lock.yml b/.github/workflows/test-claude-create-issue.lock.yml index 58d2682339f..adb21762bfb 100644 --- a/.github/workflows/test-claude-create-issue.lock.yml +++ b/.github/workflows/test-claude-create-issue.lock.yml @@ -162,17 +162,11 @@ jobs: { "mcpServers": { "github": { - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "-e", - "GITHUB_PERSONAL_ACCESS_TOKEN", - "ghcr.io/github/github-mcp-server:sha-09deac4" - ], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}" + "type": "http", + "url": "https://api.github.com/mcp", + "headers": { + "Authorization": "Bearer ${{ secrets.GITHUB_TOKEN }}", + "Content-Type": "application/json" } } } diff --git a/.github/workflows/test-claude-create-pull-request-review-comment.lock.yml b/.github/workflows/test-claude-create-pull-request-review-comment.lock.yml index 312a5c8842f..ff1e8a7e24b 100644 --- a/.github/workflows/test-claude-create-pull-request-review-comment.lock.yml +++ b/.github/workflows/test-claude-create-pull-request-review-comment.lock.yml @@ -434,17 +434,11 @@ jobs: { "mcpServers": { "github": { - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "-e", - "GITHUB_PERSONAL_ACCESS_TOKEN", - "ghcr.io/github/github-mcp-server:sha-09deac4" - ], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}" + "type": "http", + "url": "https://api.github.com/mcp", + "headers": { + "Authorization": "Bearer ${{ secrets.GITHUB_TOKEN }}", + "Content-Type": "application/json" } } } diff --git a/.github/workflows/test-claude-create-pull-request.lock.yml b/.github/workflows/test-claude-create-pull-request.lock.yml index 2f0c90325a5..4ea8dd08766 100644 --- a/.github/workflows/test-claude-create-pull-request.lock.yml +++ b/.github/workflows/test-claude-create-pull-request.lock.yml @@ -167,17 +167,11 @@ jobs: { "mcpServers": { "github": { - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "-e", - "GITHUB_PERSONAL_ACCESS_TOKEN", - "ghcr.io/github/github-mcp-server:sha-09deac4" - ], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}" + "type": "http", + "url": "https://api.github.com/mcp", + "headers": { + "Authorization": "Bearer ${{ secrets.GITHUB_TOKEN }}", + "Content-Type": "application/json" } } } diff --git a/.github/workflows/test-claude-create-security-report.lock.yml b/.github/workflows/test-claude-create-security-report.lock.yml index a9d233cabcb..e24b575aa6d 100644 --- a/.github/workflows/test-claude-create-security-report.lock.yml +++ b/.github/workflows/test-claude-create-security-report.lock.yml @@ -421,17 +421,11 @@ jobs: { "mcpServers": { "github": { - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "-e", - "GITHUB_PERSONAL_ACCESS_TOKEN", - "ghcr.io/github/github-mcp-server:sha-09deac4" - ], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}" + "type": "http", + "url": "https://api.github.com/mcp", + "headers": { + "Authorization": "Bearer ${{ secrets.GITHUB_TOKEN }}", + "Content-Type": "application/json" } } } diff --git a/.github/workflows/test-claude-mcp.lock.yml b/.github/workflows/test-claude-mcp.lock.yml index f4cd90b6f53..21d4dd3ec6d 100644 --- a/.github/workflows/test-claude-mcp.lock.yml +++ b/.github/workflows/test-claude-mcp.lock.yml @@ -421,17 +421,11 @@ jobs: { "mcpServers": { "github": { - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "-e", - "GITHUB_PERSONAL_ACCESS_TOKEN", - "ghcr.io/github/github-mcp-server:sha-09deac4" - ], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}" + "type": "http", + "url": "https://api.github.com/mcp", + "headers": { + "Authorization": "Bearer ${{ secrets.GITHUB_TOKEN }}", + "Content-Type": "application/json" } }, "time": { diff --git a/.github/workflows/test-claude-push-to-branch.lock.yml b/.github/workflows/test-claude-push-to-branch.lock.yml index 837ffdbf278..78e4dc43f31 100644 --- a/.github/workflows/test-claude-push-to-branch.lock.yml +++ b/.github/workflows/test-claude-push-to-branch.lock.yml @@ -252,17 +252,11 @@ jobs: { "mcpServers": { "github": { - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "-e", - "GITHUB_PERSONAL_ACCESS_TOKEN", - "ghcr.io/github/github-mcp-server:sha-09deac4" - ], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}" + "type": "http", + "url": "https://api.github.com/mcp", + "headers": { + "Authorization": "Bearer ${{ secrets.GITHUB_TOKEN }}", + "Content-Type": "application/json" } } } diff --git a/.github/workflows/test-claude-update-issue.lock.yml b/.github/workflows/test-claude-update-issue.lock.yml index 136662338af..7179877f5c9 100644 --- a/.github/workflows/test-claude-update-issue.lock.yml +++ b/.github/workflows/test-claude-update-issue.lock.yml @@ -491,17 +491,11 @@ jobs: { "mcpServers": { "github": { - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "-e", - "GITHUB_PERSONAL_ACCESS_TOKEN", - "ghcr.io/github/github-mcp-server:sha-09deac4" - ], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}" + "type": "http", + "url": "https://api.github.com/mcp", + "headers": { + "Authorization": "Bearer ${{ secrets.GITHUB_TOKEN }}", + "Content-Type": "application/json" } } } diff --git a/.github/workflows/test-codex-command.lock.yml b/.github/workflows/test-codex-command.lock.yml index f98efef5b79..de788de8a04 100644 --- a/.github/workflows/test-codex-command.lock.yml +++ b/.github/workflows/test-codex-command.lock.yml @@ -651,17 +651,11 @@ jobs: { "mcpServers": { "github": { - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "-e", - "GITHUB_PERSONAL_ACCESS_TOKEN", - "ghcr.io/github/github-mcp-server:sha-09deac4" - ], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}" + "type": "http", + "url": "https://api.github.com/mcp", + "headers": { + "Authorization": "Bearer ${{ secrets.GITHUB_TOKEN }}", + "Content-Type": "application/json" } } } diff --git a/.github/workflows/test-codex-github-mcp.lock.yml b/.github/workflows/test-codex-github-mcp.lock.yml new file mode 100644 index 00000000000..52858e979e4 --- /dev/null +++ b/.github/workflows/test-codex-github-mcp.lock.yml @@ -0,0 +1,469 @@ +# This file was automatically generated by gh-aw. DO NOT EDIT. +# To update this file, edit the corresponding .md file and run: +# gh aw compile + +name: "Test GitHub MCP Docker Transport with Codex" +on: + # Start either every 10 minutes, or when some kind of human event occurs. + # Because of the implicit "concurrency" section, only one instance of this + # workflow will run at a time. + schedule: + - cron: "0/10 * * * *" + issues: + types: [opened, edited, closed] + issue_comment: + types: [created, edited] + pull_request: + types: [opened, edited, closed] + push: + branches: + - main + workflow_dispatch: + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}" + cancel-in-progress: true + +run-name: "Test GitHub MCP Docker Transport with Codex" + +jobs: + test-github-mcp-docker-transport-with-codex: + runs-on: ubuntu-latest + permissions: read-all + steps: + - name: Check team membership for workflow + id: check-team-member + uses: actions/github-script@v7 + env: + GITHUB_AW_REQUIRED_ROLES: admin,maintainer + with: + script: | + async function main() { + const { eventName } = context; + // skip check for safe events + const safeEvents = ["workflow_dispatch", "workflow_run", "schedule"]; + if (safeEvents.includes(eventName)) { + console.log(`✅ Event ${eventName} does not require validation`); + return; + } + const actor = context.actor; + const { owner, repo } = context.repo; + const requiredPermissionsEnv = process.env.GITHUB_AW_REQUIRED_ROLES; + const requiredPermissions = requiredPermissionsEnv + ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") + : []; + if (!requiredPermissions || requiredPermissions.length === 0) { + core.error( + "❌ Configuration error: Required permissions not specified. Contact repository administrator." + ); + process.exit(1); + } + // Check if the actor has the required repository permissions + try { + console.log( + `Checking if user '${actor}' has required permissions for ${owner}/${repo}` + ); + console.log(`Required permissions: ${requiredPermissions.join(", ")}`); + const repoPermission = + await github.rest.repos.getCollaboratorPermissionLevel({ + owner: owner, + repo: repo, + username: actor, + }); + const permission = repoPermission.data.permission; + console.log(`Repository permission level: ${permission}`); + // Check if user has one of the required permission levels + for (const requiredPerm of requiredPermissions) { + if ( + permission === requiredPerm || + (requiredPerm === "maintainer" && permission === "maintain") + ) { + console.log(`✅ User has ${permission} access to repository`); + return; + } + } + console.log( + `User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}` + ); + } catch (repoError) { + const errorMessage = + repoError instanceof Error ? repoError.message : String(repoError); + core.error(`Repository permission check failed: ${errorMessage}`); + process.exit(1); + } + // Fail the job when permission check fails + core.warning( + `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` + ); + process.exit(78); + } + await main(); + - name: Checkout repository + uses: actions/checkout@v5 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + - name: Install Codex + run: npm install -g @openai/codex + - name: Setup MCPs + run: | + mkdir -p /tmp/mcp-config + cat > /tmp/mcp-config/config.toml << EOF + [history] + persistence = "none" + + [mcp_servers.github] + command = "docker" + args = [ + "run", + "-i", + "--rm", + "-e", + "GITHUB_PERSONAL_ACCESS_TOKEN", + "ghcr.io/github/github-mcp-server:sha-09deac4" + ] + env = { "GITHUB_PERSONAL_ACCESS_TOKEN" = "${{ secrets.GITHUB_TOKEN }}" } + EOF + - name: Create prompt + env: + GITHUB_AW_PROMPT: /tmp/aw-prompts/prompt.txt + run: | + mkdir -p /tmp/aw-prompts + cat > $GITHUB_AW_PROMPT << 'EOF' + # Test GitHub MCP Docker Transport with Codex + + This is a test workflow that uses Codex engine, which should still use Docker transport. + + EOF + - name: Print prompt to step summary + run: | + echo "## Generated Prompt" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '``````markdown' >> $GITHUB_STEP_SUMMARY + cat $GITHUB_AW_PROMPT >> $GITHUB_STEP_SUMMARY + echo '``````' >> $GITHUB_STEP_SUMMARY + env: + GITHUB_AW_PROMPT: /tmp/aw-prompts/prompt.txt + - name: Generate agentic run info + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + const awInfo = { + engine_id: "codex", + engine_name: "Codex", + model: "", + version: "", + workflow_name: "Test GitHub MCP Docker Transport with Codex", + experimental: true, + supports_tools_whitelist: true, + supports_http_transport: false, + run_id: context.runId, + run_number: context.runNumber, + run_attempt: process.env.GITHUB_RUN_ATTEMPT, + repository: context.repo.owner + '/' + context.repo.repo, + ref: context.ref, + sha: context.sha, + actor: context.actor, + event_name: context.eventName, + created_at: new Date().toISOString() + }; + + // Write to /tmp directory to avoid inclusion in PR + const tmpPath = '/tmp/aw_info.json'; + fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); + console.log('Generated aw_info.json at:', tmpPath); + console.log(JSON.stringify(awInfo, null, 2)); + - name: Upload agentic run info + if: always() + uses: actions/upload-artifact@v4 + with: + name: aw_info.json + path: /tmp/aw_info.json + if-no-files-found: warn + - name: Run Codex + run: | + set -o pipefail + INSTRUCTION=$(cat /tmp/aw-prompts/prompt.txt) + export CODEX_HOME=/tmp/mcp-config + + # Create log directory outside git repo + mkdir -p /tmp/aw-logs + + # Run codex with log capture - pipefail ensures codex exit code is preserved + codex exec \ + -c model=o4-mini \ + --full-auto "$INSTRUCTION" 2>&1 | tee /tmp/test-github-mcp-docker-transport-with-codex.log + env: + GITHUB_AW_PROMPT: /tmp/aw-prompts/prompt.txt + GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@v7 + env: + AGENT_LOG_FILE: /tmp/test-github-mcp-docker-transport-with-codex.log + with: + script: | + function main() { + const fs = require("fs"); + try { + const logFile = process.env.AGENT_LOG_FILE; + if (!logFile) { + console.log("No agent log file specified"); + return; + } + if (!fs.existsSync(logFile)) { + console.log(`Log file not found: ${logFile}`); + return; + } + const content = fs.readFileSync(logFile, "utf8"); + const parsedLog = parseCodexLog(content); + if (parsedLog) { + core.summary.addRaw(parsedLog).write(); + console.log("Codex log parsed successfully"); + } else { + core.error("Failed to parse Codex log"); + } + } catch (error) { + core.setFailed(error.message); + } + } + function parseCodexLog(logContent) { + try { + const lines = logContent.split("\n"); + let markdown = "## 🤖 Commands and Tools\n\n"; + const commandSummary = []; + // First pass: collect commands for summary + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + // Detect tool usage and exec commands + if (line.includes("] tool ") && line.includes("(")) { + // Extract tool name + const toolMatch = line.match(/\] tool ([^(]+)\(/); + if (toolMatch) { + const toolName = toolMatch[1]; + // Look ahead to find the result status + let statusIcon = "❓"; // Unknown by default + for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) { + const nextLine = lines[j]; + if (nextLine.includes("success in")) { + statusIcon = "✅"; + break; + } else if ( + nextLine.includes("failure in") || + nextLine.includes("error in") || + nextLine.includes("failed in") + ) { + statusIcon = "❌"; + break; + } + } + if (toolName.includes(".")) { + // Format as provider::method + const parts = toolName.split("."); + const provider = parts[0]; + const method = parts.slice(1).join("_"); + commandSummary.push( + `* ${statusIcon} \`${provider}::${method}(...)\`` + ); + } else { + commandSummary.push(`* ${statusIcon} \`${toolName}(...)\``); + } + } + } else if (line.includes("] exec ")) { + // Extract exec command + const execMatch = line.match(/exec (.+?) in/); + if (execMatch) { + const formattedCommand = formatBashCommand(execMatch[1]); + // Look ahead to find the result status + let statusIcon = "❓"; // Unknown by default + for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) { + const nextLine = lines[j]; + if (nextLine.includes("succeeded in")) { + statusIcon = "✅"; + break; + } else if ( + nextLine.includes("failed in") || + nextLine.includes("error") + ) { + statusIcon = "❌"; + break; + } + } + commandSummary.push(`* ${statusIcon} \`${formattedCommand}\``); + } + } + } + // Add command summary + if (commandSummary.length > 0) { + for (const cmd of commandSummary) { + markdown += `${cmd}\n`; + } + } else { + markdown += "No commands or tools used.\n"; + } + // Add Information section + markdown += "\n## 📊 Information\n\n"; + // Extract metadata from Codex logs + let totalTokens = 0; + const tokenMatches = logContent.match(/tokens used: (\d+)/g); + if (tokenMatches) { + for (const match of tokenMatches) { + const tokens = parseInt(match.match(/(\d+)/)[1]); + totalTokens += tokens; + } + } + if (totalTokens > 0) { + markdown += `**Total Tokens Used:** ${totalTokens.toLocaleString()}\n\n`; + } + // Count tool calls and exec commands + const toolCalls = (logContent.match(/\] tool /g) || []).length; + const execCommands = (logContent.match(/\] exec /g) || []).length; + if (toolCalls > 0) { + markdown += `**Tool Calls:** ${toolCalls}\n\n`; + } + if (execCommands > 0) { + markdown += `**Commands Executed:** ${execCommands}\n\n`; + } + markdown += "\n## 🤖 Reasoning\n\n"; + // Second pass: process full conversation flow with interleaved reasoning, tools, and commands + let inThinkingSection = false; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + // Skip metadata lines + if ( + line.includes("OpenAI Codex") || + line.startsWith("--------") || + line.includes("workdir:") || + line.includes("model:") || + line.includes("provider:") || + line.includes("approval:") || + line.includes("sandbox:") || + line.includes("reasoning effort:") || + line.includes("reasoning summaries:") || + line.includes("tokens used:") + ) { + continue; + } + // Process thinking sections + if (line.includes("] thinking")) { + inThinkingSection = true; + continue; + } + // Process tool calls + if (line.includes("] tool ") && line.includes("(")) { + inThinkingSection = false; + const toolMatch = line.match(/\] tool ([^(]+)\(/); + if (toolMatch) { + const toolName = toolMatch[1]; + // Look ahead to find the result status + let statusIcon = "❓"; // Unknown by default + for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) { + const nextLine = lines[j]; + if (nextLine.includes("success in")) { + statusIcon = "✅"; + break; + } else if ( + nextLine.includes("failure in") || + nextLine.includes("error in") || + nextLine.includes("failed in") + ) { + statusIcon = "❌"; + break; + } + } + if (toolName.includes(".")) { + const parts = toolName.split("."); + const provider = parts[0]; + const method = parts.slice(1).join("_"); + markdown += `${statusIcon} ${provider}::${method}(...)\n\n`; + } else { + markdown += `${statusIcon} ${toolName}(...)\n\n`; + } + } + continue; + } + // Process exec commands + if (line.includes("] exec ")) { + inThinkingSection = false; + const execMatch = line.match(/exec (.+?) in/); + if (execMatch) { + const formattedCommand = formatBashCommand(execMatch[1]); + // Look ahead to find the result status + let statusIcon = "❓"; // Unknown by default + for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) { + const nextLine = lines[j]; + if (nextLine.includes("succeeded in")) { + statusIcon = "✅"; + break; + } else if ( + nextLine.includes("failed in") || + nextLine.includes("error") + ) { + statusIcon = "❌"; + break; + } + } + markdown += `${statusIcon} \`${formattedCommand}\`\n\n`; + } + continue; + } + // Process thinking content + if ( + inThinkingSection && + line.trim().length > 20 && + !line.startsWith("[2025-") + ) { + const trimmed = line.trim(); + // Add thinking content directly + markdown += `${trimmed}\n\n`; + } + } + return markdown; + } catch (error) { + core.error(`Error parsing Codex log: ${error}`); + return "## 🤖 Commands and Tools\n\nError parsing log content.\n\n## 🤖 Reasoning\n\nUnable to parse reasoning from log.\n\n"; + } + } + function formatBashCommand(command) { + if (!command) return ""; + // Convert multi-line commands to single line by replacing newlines with spaces + // and collapsing multiple spaces + let formatted = command + .replace(/\n/g, " ") // Replace newlines with spaces + .replace(/\r/g, " ") // Replace carriage returns with spaces + .replace(/\t/g, " ") // Replace tabs with spaces + .replace(/\s+/g, " ") // Collapse multiple spaces into one + .trim(); // Remove leading/trailing whitespace + // Escape backticks to prevent markdown issues + formatted = formatted.replace(/`/g, "\\`"); + // Truncate if too long (keep reasonable length for summary) + const maxLength = 80; + if (formatted.length > maxLength) { + formatted = formatted.substring(0, maxLength) + "..."; + } + return formatted; + } + function truncateString(str, maxLength) { + if (!str) return ""; + if (str.length <= maxLength) return str; + return str.substring(0, maxLength) + "..."; + } + // Export for testing + if (typeof module !== "undefined" && module.exports) { + module.exports = { parseCodexLog, formatBashCommand, truncateString }; + } + main(); + - name: Upload agent logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-github-mcp-docker-transport-with-codex.log + path: /tmp/test-github-mcp-docker-transport-with-codex.log + if-no-files-found: warn + diff --git a/.github/workflows/test-codex-github-mcp.md b/.github/workflows/test-codex-github-mcp.md new file mode 100644 index 00000000000..87ef5bcb935 --- /dev/null +++ b/.github/workflows/test-codex-github-mcp.md @@ -0,0 +1,10 @@ +--- +engine: codex +tools: + github: + allowed: [list_issues, get_issue] +--- + +# Test GitHub MCP Docker Transport with Codex + +This is a test workflow that uses Codex engine, which should still use Docker transport. \ No newline at end of file diff --git a/.github/workflows/test-example-engine-network-permissions.lock.yml b/.github/workflows/test-example-engine-network-permissions.lock.yml index 12f3196ec8b..d0685bbaacf 100644 --- a/.github/workflows/test-example-engine-network-permissions.lock.yml +++ b/.github/workflows/test-example-engine-network-permissions.lock.yml @@ -215,17 +215,11 @@ jobs: { "mcpServers": { "github": { - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "-e", - "GITHUB_PERSONAL_ACCESS_TOKEN", - "ghcr.io/github/github-mcp-server:sha-09deac4" - ], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}" + "type": "http", + "url": "https://api.github.com/mcp", + "headers": { + "Authorization": "Bearer ${{ secrets.GITHUB_TOKEN }}", + "Content-Type": "application/json" } } } diff --git a/.github/workflows/test-github-mcp.lock.yml b/.github/workflows/test-github-mcp.lock.yml new file mode 100644 index 00000000000..19e87e92f86 --- /dev/null +++ b/.github/workflows/test-github-mcp.lock.yml @@ -0,0 +1,591 @@ +# This file was automatically generated by gh-aw. DO NOT EDIT. +# To update this file, edit the corresponding .md file and run: +# gh aw compile + +name: "Test GitHub MCP HTTP Transport" +on: + # Start either every 10 minutes, or when some kind of human event occurs. + # Because of the implicit "concurrency" section, only one instance of this + # workflow will run at a time. + schedule: + - cron: "0/10 * * * *" + issues: + types: [opened, edited, closed] + issue_comment: + types: [created, edited] + pull_request: + types: [opened, edited, closed] + push: + branches: + - main + workflow_dispatch: + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}" + cancel-in-progress: true + +run-name: "Test GitHub MCP HTTP Transport" + +jobs: + test-github-mcp-http-transport: + runs-on: ubuntu-latest + permissions: read-all + steps: + - name: Check team membership for workflow + id: check-team-member + uses: actions/github-script@v7 + env: + GITHUB_AW_REQUIRED_ROLES: admin,maintainer + with: + script: | + async function main() { + const { eventName } = context; + // skip check for safe events + const safeEvents = ["workflow_dispatch", "workflow_run", "schedule"]; + if (safeEvents.includes(eventName)) { + console.log(`✅ Event ${eventName} does not require validation`); + return; + } + const actor = context.actor; + const { owner, repo } = context.repo; + const requiredPermissionsEnv = process.env.GITHUB_AW_REQUIRED_ROLES; + const requiredPermissions = requiredPermissionsEnv + ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") + : []; + if (!requiredPermissions || requiredPermissions.length === 0) { + core.error( + "❌ Configuration error: Required permissions not specified. Contact repository administrator." + ); + process.exit(1); + } + // Check if the actor has the required repository permissions + try { + console.log( + `Checking if user '${actor}' has required permissions for ${owner}/${repo}` + ); + console.log(`Required permissions: ${requiredPermissions.join(", ")}`); + const repoPermission = + await github.rest.repos.getCollaboratorPermissionLevel({ + owner: owner, + repo: repo, + username: actor, + }); + const permission = repoPermission.data.permission; + console.log(`Repository permission level: ${permission}`); + // Check if user has one of the required permission levels + for (const requiredPerm of requiredPermissions) { + if ( + permission === requiredPerm || + (requiredPerm === "maintainer" && permission === "maintain") + ) { + console.log(`✅ User has ${permission} access to repository`); + return; + } + } + console.log( + `User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}` + ); + } catch (repoError) { + const errorMessage = + repoError instanceof Error ? repoError.message : String(repoError); + core.error(`Repository permission check failed: ${errorMessage}`); + process.exit(1); + } + // Fail the job when permission check fails + core.warning( + `❌ Access denied: Only authorized users can trigger this workflow. User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` + ); + process.exit(78); + } + await main(); + - name: Checkout repository + uses: actions/checkout@v5 + - name: Setup MCPs + run: | + mkdir -p /tmp/mcp-config + cat > /tmp/mcp-config/mcp-servers.json << 'EOF' + { + "mcpServers": { + "github": { + "type": "http", + "url": "https://api.github.com/mcp", + "headers": { + "Authorization": "Bearer ${{ secrets.GITHUB_TOKEN }}", + "Content-Type": "application/json" + } + } + } + } + EOF + - name: Create prompt + env: + GITHUB_AW_PROMPT: /tmp/aw-prompts/prompt.txt + run: | + mkdir -p /tmp/aw-prompts + cat > $GITHUB_AW_PROMPT << 'EOF' + # Test GitHub MCP HTTP Transport + + This is a test workflow that uses GitHub MCP with HTTP transport. + + EOF + - name: Print prompt to step summary + run: | + echo "## Generated Prompt" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '``````markdown' >> $GITHUB_STEP_SUMMARY + cat $GITHUB_AW_PROMPT >> $GITHUB_STEP_SUMMARY + echo '``````' >> $GITHUB_STEP_SUMMARY + env: + GITHUB_AW_PROMPT: /tmp/aw-prompts/prompt.txt + - name: Generate agentic run info + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + const awInfo = { + engine_id: "claude", + engine_name: "Claude Code", + model: "", + version: "", + workflow_name: "Test GitHub MCP HTTP Transport", + experimental: false, + supports_tools_whitelist: true, + supports_http_transport: true, + run_id: context.runId, + run_number: context.runNumber, + run_attempt: process.env.GITHUB_RUN_ATTEMPT, + repository: context.repo.owner + '/' + context.repo.repo, + ref: context.ref, + sha: context.sha, + actor: context.actor, + event_name: context.eventName, + created_at: new Date().toISOString() + }; + + // Write to /tmp directory to avoid inclusion in PR + const tmpPath = '/tmp/aw_info.json'; + fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); + console.log('Generated aw_info.json at:', tmpPath); + console.log(JSON.stringify(awInfo, null, 2)); + - name: Upload agentic run info + if: always() + uses: actions/upload-artifact@v4 + with: + name: aw_info.json + path: /tmp/aw_info.json + if-no-files-found: warn + - name: Execute Claude Code Action + id: agentic_execution + uses: anthropics/claude-code-base-action@v0.0.56 + with: + # Allowed tools (sorted): + # - ExitPlanMode + # - Glob + # - Grep + # - LS + # - NotebookRead + # - Read + # - Task + # - TodoWrite + # - mcp__github__create_issue + # - mcp__github__download_workflow_run_artifact + # - mcp__github__get_code_scanning_alert + # - mcp__github__get_commit + # - mcp__github__get_dependabot_alert + # - mcp__github__get_discussion + # - mcp__github__get_discussion_comments + # - mcp__github__get_file_contents + # - mcp__github__get_issue + # - mcp__github__get_issue_comments + # - mcp__github__get_job_logs + # - mcp__github__get_me + # - mcp__github__get_notification_details + # - mcp__github__get_pull_request + # - mcp__github__get_pull_request_comments + # - mcp__github__get_pull_request_diff + # - mcp__github__get_pull_request_files + # - mcp__github__get_pull_request_reviews + # - mcp__github__get_pull_request_status + # - mcp__github__get_secret_scanning_alert + # - mcp__github__get_tag + # - mcp__github__get_workflow_run + # - mcp__github__get_workflow_run_logs + # - mcp__github__get_workflow_run_usage + # - mcp__github__list_branches + # - mcp__github__list_code_scanning_alerts + # - mcp__github__list_commits + # - mcp__github__list_dependabot_alerts + # - mcp__github__list_discussion_categories + # - mcp__github__list_discussions + # - mcp__github__list_issues + # - mcp__github__list_notifications + # - mcp__github__list_pull_requests + # - mcp__github__list_secret_scanning_alerts + # - mcp__github__list_tags + # - mcp__github__list_workflow_jobs + # - mcp__github__list_workflow_run_artifacts + # - mcp__github__list_workflow_runs + # - mcp__github__list_workflows + # - mcp__github__search_code + # - mcp__github__search_issues + # - mcp__github__search_orgs + # - mcp__github__search_pull_requests + # - mcp__github__search_repositories + # - mcp__github__search_users + allowed_tools: "ExitPlanMode,Glob,Grep,LS,NotebookRead,Read,Task,TodoWrite,mcp__github__create_issue,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__get_job_logs,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issues,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_secret_scanning_alerts,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users" + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + mcp_config: /tmp/mcp-config/mcp-servers.json + prompt_file: /tmp/aw-prompts/prompt.txt + timeout_minutes: 5 + env: + GITHUB_AW_PROMPT: /tmp/aw-prompts/prompt.txt + - name: Capture Agentic Action logs + if: always() + run: | + # Copy the detailed execution file from Agentic Action if available + if [ -n "${{ steps.agentic_execution.outputs.execution_file }}" ] && [ -f "${{ steps.agentic_execution.outputs.execution_file }}" ]; then + cp ${{ steps.agentic_execution.outputs.execution_file }} /tmp/test-github-mcp-http-transport.log + else + echo "No execution file output found from Agentic Action" >> /tmp/test-github-mcp-http-transport.log + fi + + # Ensure log file exists + touch /tmp/test-github-mcp-http-transport.log + - name: Upload engine output files + uses: actions/upload-artifact@v4 + with: + name: agent_outputs + path: | + output.txt + if-no-files-found: ignore + - name: Clean up engine output files + run: | + rm -f output.txt + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@v7 + env: + AGENT_LOG_FILE: /tmp/test-github-mcp-http-transport.log + with: + script: | + function main() { + const fs = require("fs"); + try { + // Get the log file path from environment + const logFile = process.env.AGENT_LOG_FILE; + if (!logFile) { + console.log("No agent log file specified"); + return; + } + if (!fs.existsSync(logFile)) { + console.log(`Log file not found: ${logFile}`); + return; + } + const logContent = fs.readFileSync(logFile, "utf8"); + const markdown = parseClaudeLog(logContent); + // Append to GitHub step summary + core.summary.addRaw(markdown).write(); + } catch (error) { + core.error(`Error parsing Claude log: ${error.message}`); + core.setFailed(error.message); + } + } + function parseClaudeLog(logContent) { + try { + const logEntries = JSON.parse(logContent); + if (!Array.isArray(logEntries)) { + return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; + } + let markdown = "## 🤖 Commands and Tools\n\n"; + const toolUsePairs = new Map(); // Map tool_use_id to tool_result + const commandSummary = []; // For the succinct summary + // First pass: collect tool results by tool_use_id + for (const entry of logEntries) { + if (entry.type === "user" && entry.message?.content) { + for (const content of entry.message.content) { + if (content.type === "tool_result" && content.tool_use_id) { + toolUsePairs.set(content.tool_use_id, content); + } + } + } + } + // Collect all tool uses for summary + for (const entry of logEntries) { + if (entry.type === "assistant" && entry.message?.content) { + for (const content of entry.message.content) { + if (content.type === "tool_use") { + const toolName = content.name; + const input = content.input || {}; + // Skip internal tools - only show external commands and API calls + if ( + [ + "Read", + "Write", + "Edit", + "MultiEdit", + "LS", + "Grep", + "Glob", + "TodoWrite", + ].includes(toolName) + ) { + continue; // Skip internal file operations and searches + } + // Find the corresponding tool result to get status + const toolResult = toolUsePairs.get(content.id); + let statusIcon = "❓"; + if (toolResult) { + statusIcon = toolResult.is_error === true ? "❌" : "✅"; + } + // Add to command summary (only external tools) + if (toolName === "Bash") { + const formattedCommand = formatBashCommand(input.command || ""); + commandSummary.push(`* ${statusIcon} \`${formattedCommand}\``); + } else if (toolName.startsWith("mcp__")) { + const mcpName = formatMcpName(toolName); + commandSummary.push(`* ${statusIcon} \`${mcpName}(...)\``); + } else { + // Handle other external tools (if any) + commandSummary.push(`* ${statusIcon} ${toolName}`); + } + } + } + } + } + // Add command summary + if (commandSummary.length > 0) { + for (const cmd of commandSummary) { + markdown += `${cmd}\n`; + } + } else { + markdown += "No commands or tools used.\n"; + } + // Add Information section from the last entry with result metadata + markdown += "\n## 📊 Information\n\n"; + // Find the last entry with metadata + const lastEntry = logEntries[logEntries.length - 1]; + if ( + lastEntry && + (lastEntry.num_turns || + lastEntry.duration_ms || + lastEntry.total_cost_usd || + lastEntry.usage) + ) { + if (lastEntry.num_turns) { + markdown += `**Turns:** ${lastEntry.num_turns}\n\n`; + } + if (lastEntry.duration_ms) { + const durationSec = Math.round(lastEntry.duration_ms / 1000); + const minutes = Math.floor(durationSec / 60); + const seconds = durationSec % 60; + markdown += `**Duration:** ${minutes}m ${seconds}s\n\n`; + } + if (lastEntry.total_cost_usd) { + markdown += `**Total Cost:** $${lastEntry.total_cost_usd.toFixed(4)}\n\n`; + } + if (lastEntry.usage) { + const usage = lastEntry.usage; + if (usage.input_tokens || usage.output_tokens) { + markdown += `**Token Usage:**\n`; + if (usage.input_tokens) + markdown += `- Input: ${usage.input_tokens.toLocaleString()}\n`; + if (usage.cache_creation_input_tokens) + markdown += `- Cache Creation: ${usage.cache_creation_input_tokens.toLocaleString()}\n`; + if (usage.cache_read_input_tokens) + markdown += `- Cache Read: ${usage.cache_read_input_tokens.toLocaleString()}\n`; + if (usage.output_tokens) + markdown += `- Output: ${usage.output_tokens.toLocaleString()}\n`; + markdown += "\n"; + } + } + if ( + lastEntry.permission_denials && + lastEntry.permission_denials.length > 0 + ) { + markdown += `**Permission Denials:** ${lastEntry.permission_denials.length}\n\n`; + } + } + markdown += "\n## 🤖 Reasoning\n\n"; + // Second pass: process assistant messages in sequence + for (const entry of logEntries) { + if (entry.type === "assistant" && entry.message?.content) { + for (const content of entry.message.content) { + if (content.type === "text" && content.text) { + // Add reasoning text directly (no header) + const text = content.text.trim(); + if (text && text.length > 0) { + markdown += text + "\n\n"; + } + } else if (content.type === "tool_use") { + // Process tool use with its result + const toolResult = toolUsePairs.get(content.id); + const toolMarkdown = formatToolUse(content, toolResult); + if (toolMarkdown) { + markdown += toolMarkdown; + } + } + } + } + } + return markdown; + } catch (error) { + return `## Agent Log Summary\n\nError parsing Claude log: ${error.message}\n`; + } + } + function formatToolUse(toolUse, toolResult) { + const toolName = toolUse.name; + const input = toolUse.input || {}; + // Skip TodoWrite except the very last one (we'll handle this separately) + if (toolName === "TodoWrite") { + return ""; // Skip for now, would need global context to find the last one + } + // Helper function to determine status icon + function getStatusIcon() { + if (toolResult) { + return toolResult.is_error === true ? "❌" : "✅"; + } + return "❓"; // Unknown by default + } + let markdown = ""; + const statusIcon = getStatusIcon(); + switch (toolName) { + case "Bash": + const command = input.command || ""; + const description = input.description || ""; + // Format the command to be single line + const formattedCommand = formatBashCommand(command); + if (description) { + markdown += `${description}:\n\n`; + } + markdown += `${statusIcon} \`${formattedCommand}\`\n\n`; + break; + case "Read": + const filePath = input.file_path || input.path || ""; + const relativePath = filePath.replace( + /^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, + "" + ); // Remove /home/runner/work/repo/repo/ prefix + markdown += `${statusIcon} Read \`${relativePath}\`\n\n`; + break; + case "Write": + case "Edit": + case "MultiEdit": + const writeFilePath = input.file_path || input.path || ""; + const writeRelativePath = writeFilePath.replace( + /^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, + "" + ); + markdown += `${statusIcon} Write \`${writeRelativePath}\`\n\n`; + break; + case "Grep": + case "Glob": + const query = input.query || input.pattern || ""; + markdown += `${statusIcon} Search for \`${truncateString(query, 80)}\`\n\n`; + break; + case "LS": + const lsPath = input.path || ""; + const lsRelativePath = lsPath.replace( + /^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, + "" + ); + markdown += `${statusIcon} LS: ${lsRelativePath || lsPath}\n\n`; + break; + default: + // Handle MCP calls and other tools + if (toolName.startsWith("mcp__")) { + const mcpName = formatMcpName(toolName); + const params = formatMcpParameters(input); + markdown += `${statusIcon} ${mcpName}(${params})\n\n`; + } else { + // Generic tool formatting - show the tool name and main parameters + const keys = Object.keys(input); + if (keys.length > 0) { + // Try to find the most important parameter + const mainParam = + keys.find(k => + ["query", "command", "path", "file_path", "content"].includes(k) + ) || keys[0]; + const value = String(input[mainParam] || ""); + if (value) { + markdown += `${statusIcon} ${toolName}: ${truncateString(value, 100)}\n\n`; + } else { + markdown += `${statusIcon} ${toolName}\n\n`; + } + } else { + markdown += `${statusIcon} ${toolName}\n\n`; + } + } + } + return markdown; + } + function formatMcpName(toolName) { + // Convert mcp__github__search_issues to github::search_issues + if (toolName.startsWith("mcp__")) { + const parts = toolName.split("__"); + if (parts.length >= 3) { + const provider = parts[1]; // github, etc. + const method = parts.slice(2).join("_"); // search_issues, etc. + return `${provider}::${method}`; + } + } + return toolName; + } + function formatMcpParameters(input) { + const keys = Object.keys(input); + if (keys.length === 0) return ""; + const paramStrs = []; + for (const key of keys.slice(0, 4)) { + // Show up to 4 parameters + const value = String(input[key] || ""); + paramStrs.push(`${key}: ${truncateString(value, 40)}`); + } + if (keys.length > 4) { + paramStrs.push("..."); + } + return paramStrs.join(", "); + } + function formatBashCommand(command) { + if (!command) return ""; + // Convert multi-line commands to single line by replacing newlines with spaces + // and collapsing multiple spaces + let formatted = command + .replace(/\n/g, " ") // Replace newlines with spaces + .replace(/\r/g, " ") // Replace carriage returns with spaces + .replace(/\t/g, " ") // Replace tabs with spaces + .replace(/\s+/g, " ") // Collapse multiple spaces into one + .trim(); // Remove leading/trailing whitespace + // Escape backticks to prevent markdown issues + formatted = formatted.replace(/`/g, "\\`"); + // Truncate if too long (keep reasonable length for summary) + const maxLength = 80; + if (formatted.length > maxLength) { + formatted = formatted.substring(0, maxLength) + "..."; + } + return formatted; + } + function truncateString(str, maxLength) { + if (!str) return ""; + if (str.length <= maxLength) return str; + return str.substring(0, maxLength) + "..."; + } + // Export for testing + if (typeof module !== "undefined" && module.exports) { + module.exports = { + parseClaudeLog, + formatToolUse, + formatBashCommand, + truncateString, + }; + } + main(); + - name: Upload agent logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-github-mcp-http-transport.log + path: /tmp/test-github-mcp-http-transport.log + if-no-files-found: warn + diff --git a/.github/workflows/test-github-mcp.md b/.github/workflows/test-github-mcp.md new file mode 100644 index 00000000000..ed525e61c05 --- /dev/null +++ b/.github/workflows/test-github-mcp.md @@ -0,0 +1,9 @@ +--- +tools: + github: + allowed: [list_issues, get_issue, create_issue] +--- + +# Test GitHub MCP HTTP Transport + +This is a test workflow that uses GitHub MCP with HTTP transport. \ No newline at end of file diff --git a/.github/workflows/test-proxy.lock.yml b/.github/workflows/test-proxy.lock.yml index 0df0c3d3a2a..35126b0b243 100644 --- a/.github/workflows/test-proxy.lock.yml +++ b/.github/workflows/test-proxy.lock.yml @@ -383,17 +383,11 @@ jobs: ] }, "github": { - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "-e", - "GITHUB_PERSONAL_ACCESS_TOKEN", - "ghcr.io/github/github-mcp-server:sha-09deac4" - ], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}" + "type": "http", + "url": "https://api.github.com/mcp", + "headers": { + "Authorization": "Bearer ${{ secrets.GITHUB_TOKEN }}", + "Content-Type": "application/json" } } } diff --git a/pkg/workflow/claude_engine.go b/pkg/workflow/claude_engine.go index d75552292c0..39b60539e53 100644 --- a/pkg/workflow/claude_engine.go +++ b/pkg/workflow/claude_engine.go @@ -552,24 +552,18 @@ func (e *ClaudeEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]a } // renderGitHubClaudeMCPConfig generates the GitHub MCP server configuration -// Always uses Docker MCP as the default +// Always uses HTTP MCP as the default for remote GitHub MCP server func (e *ClaudeEngine) renderGitHubClaudeMCPConfig(yaml *strings.Builder, githubTool any, isLast bool) { - githubDockerImageVersion := getGitHubDockerImageVersion(githubTool) + githubURL := getGitHubMCPURL(githubTool) yaml.WriteString(" \"github\": {\n") - // Always use Docker-based GitHub MCP server (services mode has been removed) - yaml.WriteString(" \"command\": \"docker\",\n") - yaml.WriteString(" \"args\": [\n") - yaml.WriteString(" \"run\",\n") - yaml.WriteString(" \"-i\",\n") - yaml.WriteString(" \"--rm\",\n") - yaml.WriteString(" \"-e\",\n") - yaml.WriteString(" \"GITHUB_PERSONAL_ACCESS_TOKEN\",\n") - yaml.WriteString(" \"ghcr.io/github/github-mcp-server:" + githubDockerImageVersion + "\"\n") - yaml.WriteString(" ],\n") - yaml.WriteString(" \"env\": {\n") - yaml.WriteString(" \"GITHUB_PERSONAL_ACCESS_TOKEN\": \"${{ secrets.GITHUB_TOKEN }}\"\n") + // Use HTTP-based GitHub MCP server (remote service) + yaml.WriteString(" \"type\": \"http\",\n") + yaml.WriteString(" \"url\": \"" + githubURL + "\",\n") + yaml.WriteString(" \"headers\": {\n") + yaml.WriteString(" \"Authorization\": \"Bearer ${{ secrets.GITHUB_TOKEN }}\",\n") + yaml.WriteString(" \"Content-Type\": \"application/json\"\n") yaml.WriteString(" }\n") if isLast { diff --git a/pkg/workflow/codex_test.go b/pkg/workflow/codex_test.go index f20b2e2f62f..547bbf3ecd3 100644 --- a/pkg/workflow/codex_test.go +++ b/pkg/workflow/codex_test.go @@ -395,8 +395,31 @@ This is a test workflow for MCP configuration with different AI engines. t.Errorf("Expected github section in JSON but didn't find it in:\n%s", lockContent) } - if !strings.Contains(lockContent, "\"command\": \"docker\"") { - t.Errorf("Expected docker command in mcp-servers.json but didn't find it in:\n%s", lockContent) + // For Claude engine, expect HTTP configuration; for others, expect Docker + if tt.expectedAI == "claude" { + // Claude should use HTTP transport for GitHub MCP + if !strings.Contains(lockContent, "\"type\": \"http\"") { + t.Errorf("Expected HTTP type in Claude mcp-servers.json but didn't find it in:\n%s", lockContent) + } + if !strings.Contains(lockContent, "\"url\": \"https://api.github.com/mcp\"") { + t.Errorf("Expected GitHub MCP URL in Claude mcp-servers.json but didn't find it in:\n%s", lockContent) + } + if !strings.Contains(lockContent, "\"Authorization\": \"Bearer ${{ secrets.GITHUB_TOKEN }}\"") { + t.Errorf("Expected Authorization header in Claude mcp-servers.json but didn't find it in:\n%s", lockContent) + } + // Should NOT have Docker command + if strings.Contains(lockContent, "\"command\": \"docker\"") { + t.Errorf("Expected NO docker command in Claude mcp-servers.json but found it in:\n%s", lockContent) + } + } else { + // Non-Claude engines should still use Docker transport + if !strings.Contains(lockContent, "\"command\": \"docker\"") { + t.Errorf("Expected docker command in mcp-servers.json but didn't find it in:\n%s", lockContent) + } + // Should NOT have HTTP type + if strings.Contains(lockContent, "\"type\": \"http\"") { + t.Errorf("Expected NO HTTP type in non-Claude mcp-servers.json but found it in:\n%s", lockContent) + } } // Should NOT have services section (services mode removed) if strings.Contains(lockContent, "services:") { diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index 55b560de8f4..0d56ee1a524 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -2845,6 +2845,20 @@ func getGitHubDockerImageVersion(githubTool any) string { return githubDockerImageVersion } +// getGitHubMCPURL returns the GitHub MCP server URL, with support for custom URLs +func getGitHubMCPURL(githubTool any) string { + githubURL := "https://api.github.com/mcp" // Default GitHub MCP server URL + // Extract mcp_url setting from tool properties + if toolConfig, ok := githubTool.(map[string]any); ok { + if urlSetting, exists := toolConfig["mcp_url"]; exists { + if stringValue, ok := urlSetting.(string); ok { + githubURL = stringValue + } + } + } + return githubURL +} + // generateMainJobSteps generates the steps section for the main job func (c *Compiler) generateMainJobSteps(yaml *strings.Builder, data *WorkflowData) { // Add custom steps or default checkout step diff --git a/pkg/workflow/js/collect_ndjson_output.test.cjs b/pkg/workflow/js/collect_ndjson_output.test.cjs index 50aa970599c..d7924369738 100644 --- a/pkg/workflow/js/collect_ndjson_output.test.cjs +++ b/pkg/workflow/js/collect_ndjson_output.test.cjs @@ -154,7 +154,7 @@ describe("collect_ndjson_output.cjs", () => { const failedMessage = mockCore.setFailed.mock.calls[0][0]; expect(failedMessage).toContain("requires a 'body' string field"); expect(failedMessage).toContain("requires a 'title' string field"); - + // setOutput should not be called because of early return const setOutputCalls = mockCore.setOutput.mock.calls; const outputCall = setOutputCalls.find(call => call[0] === "output"); @@ -650,7 +650,7 @@ Line 3"} expect(mockCore.setFailed).toHaveBeenCalledTimes(1); const failedMessage = mockCore.setFailed.mock.calls[0][0]; expect(failedMessage).toContain("JSON parsing failed"); - + // setOutput should not be called because of early return const setOutputCalls = mockCore.setOutput.mock.calls; const outputCall = setOutputCalls.find(call => call[0] === "output"); @@ -736,7 +736,7 @@ Line 3"} // Check if repair succeeded by looking at mock calls const setOutputCalls = mockCore.setOutput.mock.calls; const outputCall = setOutputCalls.find(call => call[0] === "output"); - + if (outputCall) { // Repair succeeded const parsedOutput = JSON.parse(outputCall[1]); @@ -830,7 +830,7 @@ Line 3"} expect(mockCore.setFailed).toHaveBeenCalledTimes(1); const failedMessage = mockCore.setFailed.mock.calls[0][0]; expect(failedMessage).toContain("JSON parsing failed"); - + // setOutput should not be called because of early return const setOutputCalls = mockCore.setOutput.mock.calls; const outputCall = setOutputCalls.find(call => call[0] === "output"); @@ -949,7 +949,7 @@ Line 3"} expect(mockCore.setFailed).toHaveBeenCalledTimes(1); const failedMessage = mockCore.setFailed.mock.calls[0][0]; expect(failedMessage).toContain("JSON parsing failed"); - + // setOutput should not be called because of early return const setOutputCalls = mockCore.setOutput.mock.calls; const outputCall = setOutputCalls.find(call => call[0] === "output"); @@ -970,7 +970,7 @@ Line 3"} expect(mockCore.setFailed).toHaveBeenCalledTimes(1); const failedMessage = mockCore.setFailed.mock.calls[0][0]; expect(failedMessage).toContain("JSON parsing failed"); - + // setOutput should not be called because of early return const setOutputCalls = mockCore.setOutput.mock.calls; const outputCall = setOutputCalls.find(call => call[0] === "output"); @@ -1015,7 +1015,7 @@ Line 3"} // Check if repair succeeded by looking at mock calls const setOutputCalls = mockCore.setOutput.mock.calls; const outputCall = setOutputCalls.find(call => call[0] === "output"); - + if (outputCall) { // Repair succeeded const parsedOutput = JSON.parse(outputCall[1]); diff --git a/pkg/workflow/mcp_config_test.go b/pkg/workflow/mcp_config_test.go index c71e261928b..312f32f47af 100644 --- a/pkg/workflow/mcp_config_test.go +++ b/pkg/workflow/mcp_config_test.go @@ -26,55 +26,45 @@ func TestGitHubMCPConfiguration(t *testing.T) { expectedDockerImage string }{ { - name: "default Docker server", + name: "default HTTP server", frontmatter: `--- tools: github: allowed: [list_issues, create_issue] ---`, - // With Docker MCP always enabled, default is docker (not services) - expectedType: "docker", - expectedCommand: "docker", - expectedDockerImage: "ghcr.io/github/github-mcp-server:sha-09deac4", + // With HTTP MCP enabled by default for Claude engine + expectedType: "http", + expectedURL: "https://api.github.com/mcp", }, { - name: "custom docker image version", + name: "default HTTP server with list_issues", frontmatter: `--- tools: github: - use_docker_mcp: true - docker_image_version: "v1.2.3" - allowed: [list_issues, create_issue] + allowed: [list_issues] ---`, - expectedType: "docker", - expectedCommand: "docker", - expectedDockerImage: "ghcr.io/github/github-mcp-server:v1.2.3", + expectedType: "http", + expectedURL: "https://api.github.com/mcp", }, { - name: "custom docker image SHA", + name: "default HTTP server with multiple tools", frontmatter: `--- tools: github: - use_docker_mcp: true - docker_image_version: "sha-abcd1234" - allowed: [list_issues, create_issue] + allowed: [get_issue, list_pull_requests, search_issues] ---`, - expectedType: "docker", - expectedCommand: "docker", - expectedDockerImage: "ghcr.io/github/github-mcp-server:sha-abcd1234", + expectedType: "http", + expectedURL: "https://api.github.com/mcp", }, { - name: "custom docker image version with services disabled", + name: "default HTTP server with different allowed tools", frontmatter: `--- tools: github: - use_docker_mcp: true - docker_image_version: "latest" - allowed: [list_issues, create_issue] + allowed: [get_issue, list_pull_requests] ---`, - expectedType: "docker", - expectedCommand: "docker", - expectedDockerImage: "ghcr.io/github/github-mcp-server:latest", + expectedType: "http", + expectedURL: "https://api.github.com/mcp", }, } @@ -163,30 +153,30 @@ func TestGenerateGitHubMCPConfig(t *testing.T) { { name: "nil github tool", githubTool: nil, - // With new defaults, nil tool defaults to docker (not services) - expectedType: "docker", + // With new defaults, nil tool defaults to http (remote MCP server) + expectedType: "http", }, { name: "empty github tool config", githubTool: map[string]any{ "allowed": []any{"list_issues"}, }, - // With Docker always enabled, empty config defaults to docker (not services) - expectedType: "docker", + // With HTTP always enabled, empty config defaults to http (remote MCP server) + expectedType: "http", }, { - name: "explicit docker config (redundant)", + name: "explicit github tool config", githubTool: map[string]any{ "allowed": []any{"list_issues"}, }, - // Docker is always enabled now - expectedType: "docker", + // HTTP is always enabled now for Claude engine + expectedType: "http", }, { name: "non-map github tool", githubTool: "invalid", - // With Docker always enabled, invalid tool config defaults to docker (not services) - expectedType: "docker", + // With HTTP always enabled, invalid tool config defaults to http (remote MCP server) + expectedType: "http", }, } @@ -201,6 +191,19 @@ func TestGenerateGitHubMCPConfig(t *testing.T) { result := yamlBuilder.String() switch tt.expectedType { + case "http": + if !strings.Contains(result, `"type": "http"`) { + t.Errorf("Expected HTTP type but got:\n%s", result) + } + if !strings.Contains(result, `"url": "https://api.github.com/mcp"`) { + t.Errorf("Expected HTTP URL but got:\n%s", result) + } + if !strings.Contains(result, `"Authorization": "Bearer ${{ secrets.GITHUB_TOKEN }}"`) { + t.Errorf("Expected Authorization header but got:\n%s", result) + } + if strings.Contains(result, `"command": "docker"`) { + t.Errorf("Expected no Docker command but found it in:\n%s", result) + } case "docker": if !strings.Contains(result, `"command": "docker"`) { t.Errorf("Expected Docker command but got:\n%s", result) @@ -224,7 +227,7 @@ func TestMCPConfigurationEdgeCases(t *testing.T) { expected string }{ { - name: "last server with docker config", + name: "last server with http config", githubTool: map[string]any{ "allowed": []any{"list_issues"}, }, @@ -232,7 +235,7 @@ func TestMCPConfigurationEdgeCases(t *testing.T) { expected: ` }`, }, { - name: "not last server with docker config", + name: "not last server with http config", githubTool: map[string]any{ "allowed": []any{"list_issues"}, }, @@ -275,7 +278,7 @@ func TestCustomDockerMCPConfiguration(t *testing.T) { expectedDockerImage string // Expected Docker image version }{ { - name: "custom docker MCP with default settings", + name: "custom docker MCP with default Claude HTTP settings", frontmatter: `--- tools: github: @@ -287,11 +290,10 @@ tools: command: "docker" args: ["run", "-i", "--rm", "custom/mcp-server:latest"] ---`, - expectedType: "docker", // GitHub always uses docker now - expectedDockerImage: "sha-09deac4", // Default version + expectedType: "http", // Claude engine now uses HTTP transport for GitHub }, { - name: "custom docker MCP with default settings", + name: "custom docker MCP with default Claude HTTP settings", frontmatter: `--- tools: github: @@ -302,8 +304,7 @@ tools: command: "docker" args: ["run", "-i", "--rm", "custom/mcp-server:latest"] ---`, - expectedType: "docker", // Services mode removed - always Docker - expectedDockerImage: "sha-09deac4", // Default version + expectedType: "http", // Claude engine now uses HTTP transport for GitHub }, { name: "custom docker MCP with different settings", @@ -317,8 +318,7 @@ tools: command: "docker" args: ["run", "-i", "--rm", "custom/mcp-server:latest"] ---`, - expectedType: "docker", - expectedDockerImage: "sha-09deac4", // Default version + expectedType: "http", // Claude engine now uses HTTP transport for GitHub }, { name: "mixed MCP configuration with defaults", @@ -337,8 +337,7 @@ tools: command: "docker" args: ["run", "-i", "--rm", "-v", "/tmp:/workspace", "custom/tool:latest"] ---`, - expectedType: "docker", // GitHub should now use docker by default (not services) - expectedDockerImage: "sha-09deac4", // Default version + expectedType: "http", // Claude engine now uses HTTP transport for GitHub }, { name: "custom docker MCP with custom Docker image version", @@ -353,8 +352,7 @@ tools: command: "docker" args: ["run", "-i", "--rm", "custom/mcp-server:latest"] ---`, - expectedType: "docker", // GitHub always uses docker now - expectedDockerImage: "v2.0.0", // Custom version + expectedType: "http", // Claude engine now uses HTTP transport for GitHub }, } @@ -389,6 +387,26 @@ This is a test workflow for custom Docker MCP configuration with different scena // Check the GitHub MCP configuration based on expected type switch tt.expectedType { + case "http": + // Should contain HTTP configuration for GitHub (Claude engine) + if !strings.Contains(lockContent, `"type": "http"`) { + t.Errorf("Expected HTTP type but didn't find it in:\n%s", lockContent) + } + if !strings.Contains(lockContent, `"url": "https://api.github.com/mcp"`) { + t.Errorf("Expected GitHub MCP URL but didn't find it in:\n%s", lockContent) + } + if !strings.Contains(lockContent, `"Authorization": "Bearer ${{ secrets.GITHUB_TOKEN }}"`) { + t.Errorf("Expected Authorization header but didn't find it in:\n%s", lockContent) + } + // Should NOT contain Docker configuration in the GitHub server section + githubSection := extractGitHubSection(lockContent) + if githubSection != "" && strings.Contains(githubSection, `"command": "docker"`) { + t.Errorf("Expected no Docker command in GitHub MCP section but found it in:\n%s", githubSection) + } + // Should NOT contain services configuration + if strings.Contains(lockContent, `services:`) { + t.Errorf("Expected no services configuration but found it in:\n%s", lockContent) + } case "docker": // Should contain Docker configuration for GitHub if !strings.Contains(lockContent, `"command": "docker"`) { @@ -435,3 +453,30 @@ This is a test workflow for custom Docker MCP configuration with different scena }) } } + +// extractGitHubSection extracts just the GitHub MCP server section from the workflow content +func extractGitHubSection(content string) string { + // Find the start of the github section + startIdx := strings.Index(content, `"github": {`) + if startIdx == -1 { + return "" + } + + // Find the matching closing brace + braceCount := 0 + inBraces := false + for i := startIdx; i < len(content); i++ { + char := content[i] + if char == '{' { + braceCount++ + inBraces = true + } else if char == '}' { + braceCount-- + if inBraces && braceCount == 0 { + return content[startIdx : i+1] + } + } + } + + return "" +}