|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""MCP Hub + Claude Code + GitHub |
| 3 | +
|
| 4 | +Launches a devbox with GitHub's MCP server attached via MCP Hub, |
| 5 | +installs Claude Code, registers the MCP endpoint, and asks Claude |
| 6 | +to list repositories — all without the devbox seeing your real |
| 7 | +GitHub credentials. |
| 8 | +
|
| 9 | +Prerequisites: |
| 10 | + RUNLOOP_API_KEY — your Runloop API key |
| 11 | + GITHUB_TOKEN — a GitHub PAT with repo scope |
| 12 | + ANTHROPIC_API_KEY — your Anthropic API key (for Claude Code) |
| 13 | +
|
| 14 | +Usage: |
| 15 | + GITHUB_TOKEN=ghp_xxx ANTHROPIC_API_KEY=sk-ant-xxx python examples/mcp_github_claude_code.py |
| 16 | +""" |
| 17 | + |
| 18 | +from __future__ import annotations |
| 19 | + |
| 20 | +import os |
| 21 | +import sys |
| 22 | +import time |
| 23 | + |
| 24 | +from runloop_api_client import RunloopSDK |
| 25 | + |
| 26 | +GITHUB_MCP_ENDPOINT = "https://api.githubcopilot.com/mcp/" |
| 27 | + |
| 28 | + |
| 29 | +def main() -> None: |
| 30 | + github_token = os.environ.get("GITHUB_TOKEN") |
| 31 | + anthropic_key = os.environ.get("ANTHROPIC_API_KEY") |
| 32 | + |
| 33 | + if not github_token: |
| 34 | + print("Set GITHUB_TOKEN to a GitHub PAT with repo scope.", file=sys.stderr) |
| 35 | + sys.exit(1) |
| 36 | + if not anthropic_key: |
| 37 | + print("Set ANTHROPIC_API_KEY for Claude Code.", file=sys.stderr) |
| 38 | + sys.exit(1) |
| 39 | + |
| 40 | + sdk = RunloopSDK() |
| 41 | + secret_name = f"example-github-mcp-{int(time.time())}" |
| 42 | + |
| 43 | + # 1. Create an MCP config for the GitHub MCP server |
| 44 | + print("Creating MCP config…") |
| 45 | + mcp_config = sdk.mcp_config.create( |
| 46 | + name=f"github-example-{int(time.time())}", |
| 47 | + endpoint=GITHUB_MCP_ENDPOINT, |
| 48 | + allowed_tools=["*"], |
| 49 | + description="GitHub MCP server — example", |
| 50 | + ) |
| 51 | + print(f" MCP config: {mcp_config.id}") |
| 52 | + |
| 53 | + # 2. Store the GitHub PAT as a Runloop secret |
| 54 | + print("Storing GitHub token as a secret…") |
| 55 | + sdk.api.secrets.create(name=secret_name, value=github_token) |
| 56 | + |
| 57 | + devbox = None |
| 58 | + try: |
| 59 | + # 3. Launch a devbox with MCP Hub enabled |
| 60 | + print("Creating devbox with MCP Hub…") |
| 61 | + devbox = sdk.devbox.create( |
| 62 | + name=f"mcp-claude-code-{int(time.time())}", |
| 63 | + launch_parameters={ |
| 64 | + "resource_size_request": "SMALL", |
| 65 | + "keep_alive_time_seconds": 300, |
| 66 | + }, |
| 67 | + mcp=[{"mcp_config": mcp_config.id, "secret": secret_name}], |
| 68 | + ) |
| 69 | + print(f" Devbox ready: {devbox.id}") |
| 70 | + |
| 71 | + # 4. Install Claude Code |
| 72 | + print("\nInstalling Claude Code…") |
| 73 | + install_result = devbox.cmd.exec("npm install -g @anthropic-ai/claude-code") |
| 74 | + if install_result.exit_code != 0: |
| 75 | + print("Failed to install Claude Code:", install_result.stderr(), file=sys.stderr) |
| 76 | + return |
| 77 | + print(" Installed.") |
| 78 | + |
| 79 | + # 5. Register MCP Hub with Claude Code |
| 80 | + print("Registering MCP Hub endpoint with Claude Code…") |
| 81 | + add_result = devbox.cmd.exec( |
| 82 | + 'claude mcp add runloop-mcp --transport http "$RL_MCP_URL" ' |
| 83 | + '--header "Authorization: Bearer $RL_MCP_TOKEN"' |
| 84 | + ) |
| 85 | + if add_result.exit_code != 0: |
| 86 | + print("Failed to add MCP server:", add_result.stderr(), file=sys.stderr) |
| 87 | + return |
| 88 | + print(" MCP server registered.") |
| 89 | + |
| 90 | + # 6. Ask Claude Code to list repos |
| 91 | + print("\nAsking Claude Code to list runloopai repos…\n") |
| 92 | + claude_result = devbox.cmd.exec( |
| 93 | + f"ANTHROPIC_API_KEY={anthropic_key} claude -p " |
| 94 | + '"Use the MCP tools to list all repositories in the runloopai GitHub org. ' |
| 95 | + 'Just output the repo names, one per line." ' |
| 96 | + "--dangerously-skip-permissions" |
| 97 | + ) |
| 98 | + |
| 99 | + output = claude_result.stdout().strip() |
| 100 | + print("── Claude Code output ──────────────────────────────") |
| 101 | + print(output) |
| 102 | + print("────────────────────────────────────────────────────") |
| 103 | + |
| 104 | + finally: |
| 105 | + print("\nCleaning up…") |
| 106 | + if devbox: |
| 107 | + try: |
| 108 | + devbox.shutdown() |
| 109 | + except Exception: |
| 110 | + pass |
| 111 | + try: |
| 112 | + mcp_config.delete() |
| 113 | + except Exception: |
| 114 | + pass |
| 115 | + try: |
| 116 | + sdk.api.secrets.delete(secret_name) |
| 117 | + except Exception: |
| 118 | + pass |
| 119 | + print("Done.") |
| 120 | + |
| 121 | + |
| 122 | +if __name__ == "__main__": |
| 123 | + main() |
0 commit comments