> ## Documentation Index
> Fetch the complete documentation index at: https://docs.runloop.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP Hub

> Give your agents access to MCP tool servers without exposing credentials

## Overview

MCP Hub lets your agents call tools on any [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server **through a single, secure endpoint**. Your real credentials stay on Runloop's servers — the agent only gets opaque, per-server tokens scoped to the tools you allow.

Configure multiple MCP servers, and your agent registers each one using the shared endpoint URL and its dedicated token.

## Why Use MCP Hub?

### Single Endpoint, Per-Server Tokens

Instead of configuring your agent to connect to each MCP server's real endpoint individually, MCP Hub proxies them all through one URL (`$RL_MCP_URL`). Each MCP server gets its own opaque token delivered via a named environment variable (e.g., `$GITHUB_MCP_SECRET`, `$SLACK_MCP_SECRET`), so you can register each server separately with fine-grained control.

### Credential Isolation

Your MCP server credentials (GitHub PATs, Slack tokens, etc.) **never enter the devbox**. The agent only sees:

* A shared gateway URL (`$RL_MCP_URL`)
* Per-server opaque tokens (e.g., `$GITHUB_MCP_SECRET`, `$SLACK_MCP_SECRET`)

Even if an attacker gains full access to the devbox, they cannot extract your actual API credentials.

### Tool-Level Access Control

MCP configs let you specify exactly which tools an agent can use, using glob patterns:

* `["*"]` — all tools from the server
* `["github.search_*", "github.get_*"]` — only read operations
* `["slack.post_message"]` — a single specific tool

Tools that don't match the allowed patterns are invisible to the agent — they won't appear in `tools/list` and calls to them are rejected.

## Quickstart

<Tabs>
  <Tab title="Claude Code Agent">
    Run Claude Code as an autonomous agent inside a devbox with MCP tools — orchestrated entirely from your application code.

    <Steps>
      <Step title="Create an MCP config and secret">
        Register the GitHub MCP server, choose which tools to expose, and store your GitHub PAT as a Runloop secret. The credential is stored server-side — the devbox never sees it.

        <CodeGroup>
          ```python Python theme={null}
          from runloop_api_client import RunloopSDK

          sdk = RunloopSDK()

          mcp_config = sdk.mcp_config.create(
              name="github-example",
              endpoint="https://api.githubcopilot.com/mcp/",
              allowed_tools=["*"],
              description="GitHub MCP server",
          )

          sdk.api.secrets.create(name="GITHUB_MCP_TOKEN", value="ghp_abc123...")
          ```

          ```typescript TypeScript theme={null}
          import { RunloopSDK } from "@runloop/api-client";

          const sdk = new RunloopSDK();

          const mcpConfig = await sdk.mcpConfig.create({
              name: "github-example",
              endpoint: "https://api.githubcopilot.com/mcp/",
              allowed_tools: ["*"],
              description: "GitHub MCP server",
          });

          await sdk.api.secrets.create({ name: "GITHUB_MCP_TOKEN", value: "ghp_abc123..." });
          ```
        </CodeGroup>
      </Step>

      <Step title="Launch a devbox with MCP Hub">
        The devbox receives `$RL_MCP_URL` (the shared proxy endpoint) and a per-server token environment variable — not the raw GitHub token. The map key (`GITHUB_MCP_SECRET`) becomes the environment variable name for that server's opaque token.

        <CodeGroup>
          ```python Python theme={null}
          devbox = sdk.devbox.create(
              name="mcp-claude-code",
              launch_parameters={
                  "resource_size_request": "SMALL",
                  "keep_alive_time_seconds": 300,
              },
              mcp={"GITHUB_MCP_SECRET": {"mcp_config": mcp_config.id, "secret": "GITHUB_MCP_TOKEN"}},
          )
          ```

          ```typescript TypeScript theme={null}
          const devbox = await sdk.devbox.create({
              name: "mcp-claude-code",
              launch_parameters: {
                  resource_size_request: "SMALL",
                  keep_alive_time_seconds: 300,
              },
              mcp: { GITHUB_MCP_SECRET: { mcp_config: mcpConfig.id, secret: "GITHUB_MCP_TOKEN" } },
          });
          ```
        </CodeGroup>
      </Step>

      <Step title="Install Claude Code and register MCP Hub">
        Install Claude Code inside the devbox, then register each MCP server using its own token environment variable.

        <CodeGroup>
          ```python Python theme={null}
          devbox.cmd.exec("npm install -g @anthropic-ai/claude-code")

          devbox.cmd.exec(
              'claude mcp add github-mcp --transport http "$RL_MCP_URL" '
              '--header "Authorization: Bearer $GITHUB_MCP_SECRET"'
          )
          ```

          ```typescript TypeScript theme={null}
          await devbox.cmd.exec("npm install -g @anthropic-ai/claude-code");

          await devbox.cmd.exec(
              'claude mcp add github-mcp --transport http "$RL_MCP_URL" '
              + '--header "Authorization: Bearer $GITHUB_MCP_SECRET"'
          );
          ```
        </CodeGroup>
      </Step>

      <Step title="Run a prompt with MCP tools">
        Claude Code automatically discovers the MCP tools and uses them to answer the prompt.

        <CodeGroup>
          ```python Python theme={null}
          result = devbox.cmd.exec(
              f"ANTHROPIC_API_KEY={anthropic_key} claude -p "
              '"Use the MCP tools to get my last PR and describe what it does." '
              "--dangerously-skip-permissions"
          )
          print(result.stdout())
          ```

          ```typescript TypeScript theme={null}
          const result = await devbox.cmd.exec(
              `ANTHROPIC_API_KEY=${anthropicKey} claude -p `
              + `"Use the MCP tools to get my last PR and describe what it does." `
              + `--dangerously-skip-permissions`
          );
          console.log((await result.stdout()).trim());
          ```
        </CodeGroup>

        <Note>
          See the full runnable examples: [Python](https://github.com/runloopai/api-client-python/blob/main/examples/mcp_github_claude_code.py) · [TypeScript](https://github.com/runloopai/api-client-ts/blob/main/examples/mcp-github-tools.ts)
        </Note>
      </Step>
    </Steps>
  </Tab>

  <Tab title="Claude API with MCP">
    Use the Anthropic API's `mcp_servers` parameter to give Claude direct access to MCP tools — no agent framework needed.

    <Steps>
      <Step title="Create an MCP config, secret, and devbox">
        Set up the MCP config, store your GitHub PAT, and launch a devbox with MCP Hub. The devbox receives `$RL_MCP_URL` (shared endpoint) and `$GITHUB_MCP_SECRET` (per-server token).

        <CodeGroup>
          ```python Python theme={null}
          from runloop_api_client import RunloopSDK

          sdk = RunloopSDK()

          mcp_config = sdk.mcp_config.create(
              name="github-example",
              endpoint="https://api.githubcopilot.com/mcp/",
              allowed_tools=["*"],
          )

          sdk.api.secrets.create(name="GITHUB_MCP_TOKEN", value="ghp_abc123...")

          devbox = sdk.devbox.create(
              name="mcp-claude-api",
              mcp={"GITHUB_MCP_SECRET": {"mcp_config": mcp_config.id, "secret": "GITHUB_MCP_TOKEN"}},
          )
          ```

          ```typescript TypeScript theme={null}
          import { RunloopSDK } from "@runloop/api-client";

          const sdk = new RunloopSDK();

          const mcpConfig = await sdk.mcpConfig.create({
              name: "github-example",
              endpoint: "https://api.githubcopilot.com/mcp/",
              allowed_tools: ["*"],
          });

          await sdk.api.secrets.create({ name: "GITHUB_MCP_TOKEN", value: "ghp_abc123..." });

          const devbox = await sdk.devbox.create({
              name: "mcp-claude-api",
              mcp: { GITHUB_MCP_SECRET: { mcp_config: mcpConfig.id, secret: "GITHUB_MCP_TOKEN" } },
          });
          ```
        </CodeGroup>
      </Step>

      <Step title="Call the Anthropic API with MCP servers">
        Pass the MCP Hub endpoint to the Anthropic SDK's `mcp_servers` parameter, registering each MCP server with its own token. Claude automatically discovers and calls the tools — no manual tool definitions needed.

        <CodeGroup>
          ```python Python theme={null}
          import anthropic

          anthropic_client = anthropic.Anthropic()

          mcp_url = devbox.cmd.exec("echo $RL_MCP_URL").stdout().strip()
          github_token = devbox.cmd.exec("echo $GITHUB_MCP_SECRET").stdout().strip()

          response = anthropic_client.messages.create(
              model="claude-sonnet-4-20250514",
              max_tokens=1024,
              mcp_servers=[
                  {
                      "type": "url",
                      "url": mcp_url,
                      "authorization_token": github_token,
                  }
              ],
              messages=[{
                  "role": "user",
                  "content": "Search for authentication bugs in runloopai/demo"
              }],
          )

          for block in response.content:
              if block.type == "text":
                  print(block.text)
              elif block.type == "mcp_tool_result":
                  print(f"Tool result from: {block.server_label}")
          ```

          ```typescript TypeScript theme={null}
          import Anthropic from "@anthropic-ai/sdk";

          const anthropicClient = new Anthropic();

          const mcpUrl = (await devbox.cmd.exec("echo $RL_MCP_URL")).stdout().trim();
          const githubToken = (await devbox.cmd.exec("echo $GITHUB_MCP_SECRET")).stdout().trim();

          const response = await anthropicClient.messages.create({
              model: "claude-sonnet-4-20250514",
              max_tokens: 1024,
              mcp_servers: [
                  {
                      type: "url",
                      url: mcpUrl,
                      authorization_token: githubToken,
                  }
              ],
              messages: [{
                  role: "user",
                  content: "Search for authentication bugs in runloopai/demo"
              }],
          });

          for (const block of response.content) {
              if (block.type === "text") {
                  console.log(block.text);
              } else if (block.type === "mcp_tool_result") {
                  console.log(`Tool result from: ${block.server_label}`);
              }
          }
          ```
        </CodeGroup>
      </Step>
    </Steps>
  </Tab>
</Tabs>

Your code never sees your real GitHub PAT, Slack token, or any other credential — only opaque per-server tokens like `$GITHUB_MCP_SECRET` or `$SLACK_MCP_SECRET`. This protects your credentials from:

* **Prompt injection attacks** — Even if an attacker tricks your agent into printing environment variables, they only get useless opaque tokens
* **Malicious code** — Code running in the devbox cannot access your actual API credentials
* **Scoped access** — Each MCP server gets its own token, so you can grant and revoke access per server independently

<Tip>
  Combine MCP Hub with [Network Policies](/docs/network-policies) to further lock down the devbox. Set `allow_mcp_gateway=True` on a restrictive policy to allow MCP Hub traffic while blocking all other unauthorized outbound requests.
</Tip>

## How It Works

```mermaid theme={null}
sequenceDiagram
    participant Agent as Devbox Agent
    participant Hub as Runloop MCP Hub
    participant GH as GitHub MCP Server

    Agent->>Hub: tools/list (with $GITHUB_MCP_SECRET)
    Note over Hub: Resolve server from token, inject real credentials
    Hub->>GH: tools/list (with real GitHub PAT)
    GH-->>Hub: github.search_code, github.get_issue, ...
    Note over Hub: Filter by allowed_tools
    Hub-->>Agent: Permitted GitHub tools

    Agent->>Hub: tools/call "github.search_code" (with $GITHUB_MCP_SECRET)
    Hub->>GH: tools/call "github.search_code" (with real GitHub PAT)
    GH-->>Hub: Search results
    Hub-->>Agent: Search results
```

1. **Configure MCP Configs**: Define which MCP servers to connect to and which tools are allowed
2. **Store Secrets**: Create account secrets containing the credentials for each MCP server
3. **Launch with MCP**: Create a devbox with your MCP configs — it receives `$RL_MCP_URL` (shared endpoint) and a per-server token environment variable for each MCP server
4. **Register Each Server**: Your agent registers each MCP server using the shared URL and its per-server token

## MCP Config Options

| Option          | Description                                                              | Required |
| --------------- | ------------------------------------------------------------------------ | -------- |
| `name`          | Unique name for the MCP config                                           | Yes      |
| `endpoint`      | Target MCP server URL                                                    | Yes      |
| `allowed_tools` | Glob patterns for permitted tools (e.g., `["*"]`, `["github.search_*"]`) | Yes      |
| `description`   | Optional description                                                     | No       |

### Tool Access Patterns

MCP configs use glob patterns to control which tools are exposed to the agent:

| Pattern              | Matches                                            | Use Case                 |
| -------------------- | -------------------------------------------------- | ------------------------ |
| `*`                  | All tools                                          | Full access              |
| `github.search_*`    | `github.search_code`, `github.search_issues`, etc. | Read-only search         |
| `github.get_*`       | `github.get_repo`, `github.get_issue`, etc.        | Read-only data retrieval |
| `slack.post_message` | Only `slack.post_message`                          | Single specific tool     |

<Note>
  Tools that don't match your allowed patterns are completely hidden — they won't appear when the agent lists tools, and any attempt to call them is rejected.
</Note>

## Multiple MCP Servers

You can configure multiple MCP servers for a single devbox. Each server gets its own token environment variable, and all share the same `$RL_MCP_URL` endpoint.

<CodeGroup>
  ```python Python theme={null}
  github_config = await runloop.mcp_config.create(
      name="github-readonly",
      endpoint="https://api.githubcopilot.com/mcp/",
      allowed_tools=["github.search_*", "github.get_*"]
  )

  slack_config = await runloop.mcp_config.create(
      name="slack-notify",
      endpoint="https://slack-mcp.example.com",
      allowed_tools=["slack.post_message"]
  )

  devbox = await runloop.devbox.create(
      name="multi-mcp-agent",
      mcp={
          "GITHUB_MCP_SECRET": {"mcp_config": github_config.id, "secret": "GITHUB_MCP_TOKEN"},
          "SLACK_MCP_SECRET": {"mcp_config": slack_config.id, "secret": "SLACK_MCP_TOKEN"},
      }
  )

  # The devbox receives:
  #   $RL_MCP_URL     — shared endpoint
  #   $GITHUB_MCP_SECRET  — opaque token for GitHub tools
  #   $SLACK_MCP_SECRET   — opaque token for Slack tools
  ```

  ```typescript TypeScript theme={null}
  const githubConfig = await runloop.mcpConfig.create({
      name: "github-readonly",
      endpoint: "https://api.githubcopilot.com/mcp/",
      allowed_tools: ["github.search_*", "github.get_*"]
  });

  const slackConfig = await runloop.mcpConfig.create({
      name: "slack-notify",
      endpoint: "https://slack-mcp.example.com",
      allowed_tools: ["slack.post_message"]
  });

  const devbox = await runloop.devbox.create({
      name: "multi-mcp-agent",
      mcp: {
          GITHUB_MCP_SECRET: { mcp_config: githubConfig.id, secret: "GITHUB_MCP_TOKEN" },
          SLACK_MCP_SECRET: { mcp_config: slackConfig.id, secret: "SLACK_MCP_TOKEN" },
      }
  });

  // The devbox receives:
  //   $RL_MCP_URL     — shared endpoint
  //   $GITHUB_MCP_SECRET  — opaque token for GitHub tools
  //   $SLACK_MCP_SECRET   — opaque token for Slack tools
  ```
</CodeGroup>

Register each MCP server separately in your agent using the shared URL and its per-server token:

```bash theme={null}
claude mcp add github-mcp --transport http "$RL_MCP_URL" --header "Authorization: Bearer $GITHUB_MCP_SECRET"
claude mcp add slack-mcp  --transport http "$RL_MCP_URL" --header "Authorization: Bearer $SLACK_MCP_SECRET"
```

MCP Hub routes each tool call to the correct upstream server, so the agent doesn't need to know which server hosts which tool.

## Managing MCP Configs

### List MCP Configs

<CodeGroup>
  ```python Python theme={null}
  configs = await runloop.mcp_config.list()
  for config in configs:
      print(f"{config.name}: {config.endpoint}")
  ```

  ```typescript TypeScript theme={null}
  const configs = await runloop.mcpConfig.list();
  for (const config of configs) {
      console.log(`${config.name}: ${config.endpoint}`);
  }
  ```
</CodeGroup>

### Update an MCP Config

<CodeGroup>
  ```python Python theme={null}
  mcp_config = runloop.mcp_config.from_id("mcp_1234567890")
  updated = await mcp_config.update(
      allowed_tools=["github.*"],
      description="Expanded GitHub access"
  )
  ```

  ```typescript TypeScript theme={null}
  const mcpConfig = runloop.mcpConfig.fromId("mcp_1234567890");
  const updated = await mcpConfig.update({
      allowed_tools: ["github.*"],
      description: "Expanded GitHub access"
  });
  ```
</CodeGroup>

### Delete an MCP Config

<CodeGroup>
  ```python Python theme={null}
  mcp_config = runloop.mcp_config.from_id("mcp_1234567890")
  await mcp_config.delete()
  ```

  ```typescript TypeScript theme={null}
  const mcpConfig = runloop.mcpConfig.fromId("mcp_1234567890");
  await mcpConfig.delete();
  ```
</CodeGroup>

<Warning>
  Deleting an MCP config is permanent and cannot be undone. Ensure no devboxes are actively using the config before deletion.
</Warning>

## Security Best Practices

### 1. Use Minimal Tool Permissions

Start with the narrowest set of tools your agent actually needs, and expand only as required.

* ✅ `["github.search_code", "github.get_issue"]` — specific tools
* ⚠️ `["github.*"]` — all tools from one server
* ❌ `["*"]` — all tools (use only for development/testing)

### 2. Separate Configs by Permission Level

Create distinct MCP configs for different access levels rather than one permissive config:

```
github-research   → ["github.search_*", "github.get_*"]   (read-only)
github-contributor → ["github.*"]                          (read + write)
```

### 3. Use Descriptive Config Names

Choose clear names that describe the access level and purpose:

* ✅ `github-readonly`, `slack-notify-only`, `jira-read-write`
* ❌ `config1`, `test`, `my-mcp`

## Multi-Tenant Pattern

MCP configs define the access policy, while secrets provide credentials. Reuse the same config with different secrets for each tenant:

<CodeGroup>
  ```python Python theme={null}
  devbox_a = await runloop.devbox.create(
      mcp={"GITHUB_MCP_SECRET": {"mcp_config": "github-readonly", "secret": "TENANT_A_GITHUB_TOKEN"}}
  )

  devbox_b = await runloop.devbox.create(
      mcp={"GITHUB_MCP_SECRET": {"mcp_config": "github-readonly", "secret": "TENANT_B_GITHUB_TOKEN"}}
  )
  ```

  ```typescript TypeScript theme={null}
  const devboxA = await runloop.devbox.create({
      mcp: { GITHUB_MCP_SECRET: { mcp_config: "github-readonly", secret: "TENANT_A_GITHUB_TOKEN" } }
  });

  const devboxB = await runloop.devbox.create({
      mcp: { GITHUB_MCP_SECRET: { mcp_config: "github-readonly", secret: "TENANT_B_GITHUB_TOKEN" } }
  });
  ```
</CodeGroup>

## Comparison: MCP Hub vs Agent Gateways

| Feature              | MCP Hub                                                             | Agent Gateways                           |
| -------------------- | ------------------------------------------------------------------- | ---------------------------------------- |
| **Protocol**         | MCP (Model Context Protocol)                                        | HTTP REST APIs                           |
| **Use case**         | Tool-based agent capabilities (code search, issue management, etc.) | LLM API access (Anthropic, OpenAI, etc.) |
| **Access control**   | Tool-level with glob patterns                                       | Endpoint-level                           |
| **Aggregation**      | Multiple MCP servers → single endpoint, per-server tokens           | One API per gateway                      |
| **Environment vars** | `$RL_MCP_URL` (shared), `$<NAME>` (per server)                      | `$NAME_URL`, `$NAME`                     |

Use **MCP Hub** when your agent needs to use MCP tool servers. Use **[Agent Gateways](/docs/devboxes/agent-gateways)** when your agent needs to call LLM REST APIs.

## Related Documentation

* [Account Secrets](/docs/devboxes/configuration/account-secrets) — Managing secrets for your account
* [Agent Gateways](/docs/devboxes/agent-gateways) — Securely proxy API requests
* [Network Policies](/docs/network-policies) — Controlling network access for devboxes
* [Agents API](/docs/devboxes/agents/using-agents-api) — Building AI agents with Runloop
