diff --git a/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 000000000..50009464a --- /dev/null +++ b/EXAMPLES.md @@ -0,0 +1,32 @@ +# 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 describe your latest PR — 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 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 +``` + +--- + +**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..620264798 --- /dev/null +++ b/examples/mcp_github_claude_code.py @@ -0,0 +1,124 @@ +#!/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 in a GitHub org — all without the devbox ever +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/" +SECRET_NAME = f"example-github-mcp-{int(time.time())}" + + +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() + + # ── 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=[ + "get_me", + "search_pull_requests", + "get_pull_request", + "get_repository", + "get_file_contents", + ], + description="GitHub MCP server — example", + ) + print(f" Config: {mcp_config.id}") + + # ── 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 ────────────────────────────── + # 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}], + ) + print(f" Devbox: {devbox.id}") + + # ── 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.") + + # ── 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"' + ) + if add_result.exit_code != 0: + print("Failed to add MCP server:", add_result.stderr(), file=sys.stderr) + return + print(" Registered.") + + 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 "{prompt}" ' + "--dangerously-skip-permissions" + ) + print(claude_result.stdout().strip()) + + finally: + if devbox: + devbox.shutdown() + mcp_config.delete() + sdk.api.secrets.delete(SECRET_NAME) + 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.