From bae2ba447021ba63bcbc94d57805fef20e516c9d Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Thu, 26 Feb 2026 17:50:00 -0800 Subject: [PATCH 1/2] cp dines --- EXAMPLES.md | 31 +++++ examples/.keep | 4 - examples/mcp_github_claude_code.py | 123 +++++++++++++++++++ src/runloop_api_client/sdk/blueprint.py | 16 ++- src/runloop_api_client/sdk/network_policy.py | 15 ++- src/runloop_api_client/sdk/snapshot.py | 12 +- src/runloop_api_client/sdk/storage_object.py | 14 ++- 7 files changed, 207 insertions(+), 8 deletions(-) create mode 100644 EXAMPLES.md delete mode 100644 examples/.keep create mode 100644 examples/mcp_github_claude_code.py diff --git a/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 000000000..c77b85c42 --- /dev/null +++ b/EXAMPLES.md @@ -0,0 +1,31 @@ +# Examples + +Runnable examples live in the [`examples/`](./examples) directory. Each script is self-contained: + +```sh +python examples/.py +``` + +## Available Examples + +### [MCP Hub + Claude Code + GitHub](./examples/mcp_github_claude_code.py) + +Launches a devbox with GitHub's MCP server attached via **MCP Hub**, installs **Claude Code**, and asks Claude to list repositories — all without the devbox seeing your real GitHub credentials. + +**What it does:** + +1. Creates an MCP config pointing at `https://api.githubcopilot.com/mcp/` +2. Stores a GitHub PAT as a Runloop secret (credential isolation) +3. Launches a devbox with MCP Hub enabled — the devbox receives `$RL_MCP_URL` and `$RL_MCP_TOKEN` +4. Installs Claude Code (`@anthropic-ai/claude-code`) +5. Registers the MCP Hub endpoint with Claude Code via `claude mcp add` +6. Runs `claude --print` to ask Claude to list repos using the GitHub MCP tools +7. Cleans up all resources + +```sh +GITHUB_TOKEN=ghp_xxx ANTHROPIC_API_KEY=sk-ant-xxx python examples/mcp_github_claude_code.py +``` + +--- + +**See also:** [MCP Hub documentation](https://docs.runloop.ai/docs/devboxes/mcp-hub) · [Runloop docs](https://docs.runloop.ai) diff --git a/examples/.keep b/examples/.keep deleted file mode 100644 index d8c73e937..000000000 --- a/examples/.keep +++ /dev/null @@ -1,4 +0,0 @@ -File generated from our OpenAPI spec by Stainless. - -This directory can be used to store example files demonstrating usage of this SDK. -It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/examples/mcp_github_claude_code.py b/examples/mcp_github_claude_code.py new file mode 100644 index 000000000..54f5c4821 --- /dev/null +++ b/examples/mcp_github_claude_code.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +"""MCP Hub + Claude Code + GitHub + +Launches a devbox with GitHub's MCP server attached via MCP Hub, +installs Claude Code, registers the MCP endpoint, and asks Claude +to list repositories — all without the devbox seeing your real +GitHub credentials. + +Prerequisites: + RUNLOOP_API_KEY — your Runloop API key + GITHUB_TOKEN — a GitHub PAT with repo scope + ANTHROPIC_API_KEY — your Anthropic API key (for Claude Code) + +Usage: + GITHUB_TOKEN=ghp_xxx ANTHROPIC_API_KEY=sk-ant-xxx python examples/mcp_github_claude_code.py +""" + +from __future__ import annotations + +import os +import sys +import time + +from runloop_api_client import RunloopSDK + +GITHUB_MCP_ENDPOINT = "https://api.githubcopilot.com/mcp/" + + +def main() -> None: + github_token = os.environ.get("GITHUB_TOKEN") + anthropic_key = os.environ.get("ANTHROPIC_API_KEY") + + if not github_token: + print("Set GITHUB_TOKEN to a GitHub PAT with repo scope.", file=sys.stderr) + sys.exit(1) + if not anthropic_key: + print("Set ANTHROPIC_API_KEY for Claude Code.", file=sys.stderr) + sys.exit(1) + + sdk = RunloopSDK() + secret_name = f"example-github-mcp-{int(time.time())}" + + # 1. Create an MCP config for the GitHub MCP server + print("Creating MCP config…") + mcp_config = sdk.mcp_config.create( + name=f"github-example-{int(time.time())}", + endpoint=GITHUB_MCP_ENDPOINT, + allowed_tools=["*"], + description="GitHub MCP server — example", + ) + print(f" MCP config: {mcp_config.id}") + + # 2. Store the GitHub PAT as a Runloop secret + print("Storing GitHub token as a secret…") + sdk.api.secrets.create(name=secret_name, value=github_token) + + devbox = None + try: + # 3. Launch a devbox with MCP Hub enabled + print("Creating devbox with MCP Hub…") + devbox = sdk.devbox.create( + name=f"mcp-claude-code-{int(time.time())}", + launch_parameters={ + "resource_size_request": "SMALL", + "keep_alive_time_seconds": 300, + }, + mcp=[{"mcp_config": mcp_config.id, "secret": secret_name}], + ) + print(f" Devbox ready: {devbox.id}") + + # 4. Install Claude Code + print("\nInstalling Claude Code…") + install_result = devbox.cmd.exec("npm install -g @anthropic-ai/claude-code") + if install_result.exit_code != 0: + print("Failed to install Claude Code:", install_result.stderr(), file=sys.stderr) + return + print(" Installed.") + + # 5. Register MCP Hub with Claude Code + print("Registering MCP Hub endpoint with Claude Code…") + add_result = devbox.cmd.exec( + 'claude mcp add runloop-mcp --transport http "$RL_MCP_URL" ' + '--header "Authorization: Bearer $RL_MCP_TOKEN"' + ) + if add_result.exit_code != 0: + print("Failed to add MCP server:", add_result.stderr(), file=sys.stderr) + return + print(" MCP server registered.") + + # 6. Ask Claude Code to list repos + print("\nAsking Claude Code to list runloopai repos…\n") + claude_result = devbox.cmd.exec( + f"ANTHROPIC_API_KEY={anthropic_key} claude -p " + '"Use the MCP tools to list all repositories in the runloopai GitHub org. ' + 'Just output the repo names, one per line." ' + "--dangerously-skip-permissions" + ) + + output = claude_result.stdout().strip() + print("── Claude Code output ──────────────────────────────") + print(output) + print("────────────────────────────────────────────────────") + + finally: + print("\nCleaning up…") + if devbox: + try: + devbox.shutdown() + except Exception: + pass + try: + mcp_config.delete() + except Exception: + pass + try: + sdk.api.secrets.delete(secret_name) + except Exception: + pass + print("Done.") + + +if __name__ == "__main__": + main() diff --git a/src/runloop_api_client/sdk/blueprint.py b/src/runloop_api_client/sdk/blueprint.py index 8e9daac3e..144823c5c 100644 --- a/src/runloop_api_client/sdk/blueprint.py +++ b/src/runloop_api_client/sdk/blueprint.py @@ -12,7 +12,21 @@ class Blueprint: - """Synchronous wrapper around a blueprint resource.""" + """Synchronous wrapper around a blueprint resource. + + Blueprints are reusable devbox templates built from Dockerfiles. They define the + base image, installed packages, and system configuration. Create blueprints via + ``runloop.blueprint.create()`` and then launch devboxes from them. + + Example: + >>> runloop = RunloopSDK() + >>> blueprint = runloop.blueprint.create( + ... name="python-ml", + ... dockerfile="FROM ubuntu:22.04\\nRUN apt-get update && apt-get install -y python3", + ... ) + >>> logs = blueprint.logs() + >>> devbox = blueprint.create_devbox(name="ml-workbench") + """ def __init__( self, diff --git a/src/runloop_api_client/sdk/network_policy.py b/src/runloop_api_client/sdk/network_policy.py index d3e6a6376..dbd0c5039 100644 --- a/src/runloop_api_client/sdk/network_policy.py +++ b/src/runloop_api_client/sdk/network_policy.py @@ -10,7 +10,20 @@ class NetworkPolicy: - """Synchronous wrapper around a network policy resource.""" + """Synchronous wrapper around a network policy resource. + + Network policies control egress network access for devboxes. They specify + allowed hostnames via glob patterns and whether devbox-to-devbox traffic is + permitted. Apply policies when creating devboxes or blueprints. + + Example: + >>> runloop = RunloopSDK() + >>> policy = runloop.network_policy.create( + ... name="restricted", + ... allowed_hostnames=["github.com", "*.npmjs.org"], + ... ) + >>> devbox = runloop.devbox.create(name="locked-down", network_policy_id=policy.id) + """ def __init__( self, diff --git a/src/runloop_api_client/sdk/snapshot.py b/src/runloop_api_client/sdk/snapshot.py index 087a74e78..b987ac361 100644 --- a/src/runloop_api_client/sdk/snapshot.py +++ b/src/runloop_api_client/sdk/snapshot.py @@ -18,7 +18,17 @@ class Snapshot: - """Wrapper around synchronous snapshot operations.""" + """Synchronous wrapper around a disk snapshot resource. + + Snapshots capture the full disk state of a devbox. Create snapshots via + ``devbox.snapshot_disk()`` or ``devbox.snapshot_disk_async()``, then restore + them into new devboxes with ``snapshot.create_devbox()``. + + Example: + >>> snapshot = devbox.snapshot_disk(name="checkpoint-v1") + >>> new_devbox = snapshot.create_devbox(name="restored") + >>> snapshot.delete() + """ def __init__( self, diff --git a/src/runloop_api_client/sdk/storage_object.py b/src/runloop_api_client/sdk/storage_object.py index 859fb3a63..d1ef7c417 100644 --- a/src/runloop_api_client/sdk/storage_object.py +++ b/src/runloop_api_client/sdk/storage_object.py @@ -13,7 +13,19 @@ class StorageObject: - """Wrapper around storage object operations, including uploads and downloads.""" + """Synchronous wrapper around a storage object resource. + + Storage objects hold uploaded files and archives (text, binary, tgz). They can be + downloaded, mounted into devboxes, or used as blueprint build contexts. Use the + convenience upload helpers on ``runloop.storage_object`` to create objects from + text, bytes, files, or directories. + + Example: + >>> runloop = RunloopSDK() + >>> obj = runloop.storage_object.upload_from_text("Hello!", name="greeting.txt") + >>> print(obj.download_as_text()) # "Hello!" + >>> obj.delete() + """ def __init__(self, client: Runloop, object_id: str, upload_url: str | None) -> None: """Initialize the wrapper. From c3fe1513f6858f7f610fedc1f998e04583c593ae Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Thu, 26 Feb 2026 17:54:06 -0800 Subject: [PATCH 2/2] cp dines --- EXAMPLES.md | 7 ++- examples/mcp_github_claude_code.py | 89 +++++++++++++++--------------- 2 files changed, 49 insertions(+), 47 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index c77b85c42..50009464a 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -10,7 +10,7 @@ python examples/.py ### [MCP Hub + Claude Code + GitHub](./examples/mcp_github_claude_code.py) -Launches a devbox with GitHub's MCP server attached via **MCP Hub**, installs **Claude Code**, and asks Claude to list repositories — all without the devbox seeing your real GitHub credentials. +Launches a devbox with GitHub's MCP server attached via **MCP Hub**, installs **Claude Code**, and asks Claude to describe your latest PR — all without the devbox seeing your real GitHub credentials. **What it does:** @@ -19,11 +19,12 @@ Launches a devbox with GitHub's MCP server attached via **MCP Hub**, installs ** 3. Launches a devbox with MCP Hub enabled — the devbox receives `$RL_MCP_URL` and `$RL_MCP_TOKEN` 4. Installs Claude Code (`@anthropic-ai/claude-code`) 5. Registers the MCP Hub endpoint with Claude Code via `claude mcp add` -6. Runs `claude --print` to ask Claude to list repos using the GitHub MCP tools +6. Runs `claude --print` to ask Claude to describe your latest PR using the GitHub MCP tools 7. Cleans up all resources ```sh -GITHUB_TOKEN=ghp_xxx ANTHROPIC_API_KEY=sk-ant-xxx python examples/mcp_github_claude_code.py +GITHUB_TOKEN=ghp_xxx ANTHROPIC_API_KEY=sk-ant-xxx \ + python examples/mcp_github_claude_code.py ``` --- diff --git a/examples/mcp_github_claude_code.py b/examples/mcp_github_claude_code.py index 54f5c4821..620264798 100644 --- a/examples/mcp_github_claude_code.py +++ b/examples/mcp_github_claude_code.py @@ -3,8 +3,8 @@ Launches a devbox with GitHub's MCP server attached via MCP Hub, installs Claude Code, registers the MCP endpoint, and asks Claude -to list repositories — all without the devbox seeing your real -GitHub credentials. +to list repositories in a GitHub org — all without the devbox ever +seeing your real GitHub credentials. Prerequisites: RUNLOOP_API_KEY — your Runloop API key @@ -12,7 +12,8 @@ ANTHROPIC_API_KEY — your Anthropic API key (for Claude Code) Usage: - GITHUB_TOKEN=ghp_xxx ANTHROPIC_API_KEY=sk-ant-xxx python examples/mcp_github_claude_code.py + GITHUB_TOKEN=ghp_xxx ANTHROPIC_API_KEY=sk-ant-xxx \ + python examples/mcp_github_claude_code.py """ from __future__ import annotations @@ -24,6 +25,7 @@ from runloop_api_client import RunloopSDK GITHUB_MCP_ENDPOINT = "https://api.githubcopilot.com/mcp/" +SECRET_NAME = f"example-github-mcp-{int(time.time())}" def main() -> None: @@ -38,46 +40,57 @@ def main() -> None: sys.exit(1) sdk = RunloopSDK() - secret_name = f"example-github-mcp-{int(time.time())}" - # 1. Create an MCP config for the GitHub MCP server - print("Creating MCP config…") + # ── 1. Register GitHub's MCP server with Runloop ─────────────────── + print("[1/6] Creating MCP config…") mcp_config = sdk.mcp_config.create( name=f"github-example-{int(time.time())}", endpoint=GITHUB_MCP_ENDPOINT, - allowed_tools=["*"], + allowed_tools=[ + "get_me", + "search_pull_requests", + "get_pull_request", + "get_repository", + "get_file_contents", + ], description="GitHub MCP server — example", ) - print(f" MCP config: {mcp_config.id}") + print(f" Config: {mcp_config.id}") - # 2. Store the GitHub PAT as a Runloop secret - print("Storing GitHub token as a secret…") - sdk.api.secrets.create(name=secret_name, value=github_token) + # ── 2. Store the GitHub PAT as a Runloop secret ──────────────────── + # Runloop holds the token server-side; the devbox never sees it. + print("[2/6] Storing GitHub token as secret…") + sdk.api.secrets.create(name=SECRET_NAME, value=github_token) + print(f" Secret: {SECRET_NAME}") devbox = None try: - # 3. Launch a devbox with MCP Hub enabled - print("Creating devbox with MCP Hub…") + # ── 3. Launch a devbox with MCP Hub ────────────────────────────── + # The devbox gets $RL_MCP_URL and $RL_MCP_TOKEN — a proxy + # endpoint, not the raw GitHub token. + print("[3/6] Creating devbox…") devbox = sdk.devbox.create( name=f"mcp-claude-code-{int(time.time())}", launch_parameters={ "resource_size_request": "SMALL", "keep_alive_time_seconds": 300, }, - mcp=[{"mcp_config": mcp_config.id, "secret": secret_name}], + mcp=[{"mcp_config": mcp_config.id, "secret": SECRET_NAME}], ) - print(f" Devbox ready: {devbox.id}") + print(f" Devbox: {devbox.id}") - # 4. Install Claude Code - print("\nInstalling Claude Code…") + # ── 4. Install Claude Code ─────────────────────────────────────── + print("[4/6] Installing Claude Code…") install_result = devbox.cmd.exec("npm install -g @anthropic-ai/claude-code") if install_result.exit_code != 0: print("Failed to install Claude Code:", install_result.stderr(), file=sys.stderr) return - print(" Installed.") + print(" Installed.") - # 5. Register MCP Hub with Claude Code - print("Registering MCP Hub endpoint with Claude Code…") + # ── 5. Point Claude Code at MCP Hub ────────────────────────────── + # Claude Code ──> MCP Hub (Runloop) ──> GitHub MCP Server + # injects secret + print("[5/6] Registering MCP Hub with Claude Code…") add_result = devbox.cmd.exec( 'claude mcp add runloop-mcp --transport http "$RL_MCP_URL" ' '--header "Authorization: Bearer $RL_MCP_TOKEN"' @@ -85,37 +98,25 @@ def main() -> None: if add_result.exit_code != 0: print("Failed to add MCP server:", add_result.stderr(), file=sys.stderr) return - print(" MCP server registered.") + print(" Registered.") - # 6. Ask Claude Code to list repos - print("\nAsking Claude Code to list runloopai repos…\n") + prompt = ( + "Use the MCP tools to get my last pr and describe what it does " + "in 2-3 sentences. Also detail how you collected this information" + ) + # ── 6. Ask Claude Code to list repos via MCP ───────────────────── + print(f"[6/6] Asking Claude Code to: \n{prompt}\n") claude_result = devbox.cmd.exec( - f"ANTHROPIC_API_KEY={anthropic_key} claude -p " - '"Use the MCP tools to list all repositories in the runloopai GitHub org. ' - 'Just output the repo names, one per line." ' + f'ANTHROPIC_API_KEY={anthropic_key} claude -p "{prompt}" ' "--dangerously-skip-permissions" ) - - output = claude_result.stdout().strip() - print("── Claude Code output ──────────────────────────────") - print(output) - print("────────────────────────────────────────────────────") + print(claude_result.stdout().strip()) finally: - print("\nCleaning up…") if devbox: - try: - devbox.shutdown() - except Exception: - pass - try: - mcp_config.delete() - except Exception: - pass - try: - sdk.api.secrets.delete(secret_name) - except Exception: - pass + devbox.shutdown() + mcp_config.delete() + sdk.api.secrets.delete(SECRET_NAME) print("Done.")