Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Examples

Runnable examples live in the [`examples/`](./examples) directory. Each script is self-contained:

```sh
python examples/<example>.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)
4 changes: 0 additions & 4 deletions examples/.keep
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we deleting this file?

This file was deleted.

124 changes: 124 additions & 0 deletions examples/mcp_github_claude_code.py
Original file line number Diff line number Diff line change
@@ -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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should prefer using AsyncRunloopSDK for all our examples/docs


# ── 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()
16 changes: 15 additions & 1 deletion src/runloop_api_client/sdk/blueprint.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we updating these docstrings in this pr? also if we're making these changes, we should mirror them for the async classes

Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 14 additions & 1 deletion src/runloop_api_client/sdk/network_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 11 additions & 1 deletion src/runloop_api_client/sdk/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 13 additions & 1 deletion src/runloop_api_client/sdk/storage_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading