Vaults & MCP

Connect agents to remote MCP servers for external tools, with credentials held in encrypted vaults.

Vaults & MCP

Agents can call tools served by remote MCP (Model Context Protocol) servers. When a server needs a secret bearer token, you store it in an encrypted vault and attach the vault to a session — the agent definition itself never carries a credential.

MCP servers

MCP servers are configured on the agent, in mcp_servers. Streamable HTTP servers are supported in v1.

In the agent form, add servers under MCP servers — each entry has a name, a URL, and optional auth and tool settings.

Set mcp_servers when creating or patching an agent:

{
  "mcp_servers": [
    {
      "name": "gateway",
      "url": "https://mcp.example.com",
      "auth": { "type": "token_source", "provider": "hydra" },
      "allowed_tools": ["search", "lookup"],
      "tool_timeout_secs": 60
    }
  ]
}
FieldNotes
nameUnique within the agent; becomes the mcp__<name>__<tool> namespace.
urlThe server's Streamable HTTP endpoint.
authnone (default) or token_source. See below.
allowed_toolsAn enable-list of tool names. Omit to allow every tool the server advertises (must be non-empty when present).
tool_timeout_secsPer-call timeout, 1600. Defaults to 120.

Auth

Agent-level MCP auth is secret-free — an agent definition never stores a token:

  • none — for public, unauthenticated servers.
  • token_source — mint a service bearer at connect time from a named provider (supabase or hydra). Use this for first-party servers that accept a platform identity.

For third-party servers that require a static bearer token, store the token in a vault (next section) rather than on the agent.

Discover tools

Probe an agent's declared MCP servers and list the tools they expose:

curl https://agents.clusterbase.dev/v1/agents/agt_3f9c2a/mcp-tools \
  -H "Authorization: Bearer $CLUSTER_TOKEN"

Vaults

A vault holds encrypted credentials. Every secret is encrypted at rest and never returned by any response. Three credential types are supported, in two categories — MCP credentials keyed to a specific MCP server URL, and an environment-variable credential keyed to a VM env-var name:

  • static_bearer — a fixed bearer token (API key / PAT). No refresh.
  • mcp_oauth — an OAuth 2.0 access token with an optional refresh block. When the access token expires, the runtime re-issues it automatically at the token_endpoint (using the stored refresh token) at connect time.
  • environment_variable — a plaintext secret injected as a VM environment variable at provision time, for third-party keys the agent's in-VM tools read directly (e.g. NOTION_API_KEY). Unlike the MCP types — whose tokens are used only by the gateway — the agent process can read this value (the same way it reads GH_TOKEN), so use it only for keys the agent is meant to use. It's keyed on secret_name (not a URL) and is applied only on the /v1/runs lane (see Connecting them).

Console support

Vaults have no dedicated Console screen yet, but the agent builder can create a vault for you when you ask it to (e.g. "set up an MCP server with a vault token"). Full vault management is available over the API.

Create a vault, then add a credential to it:

# 1. Create a vault
curl https://agents.clusterbase.dev/v1/vaults \
  -H "Authorization: Bearer $CLUSTER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "display_name": "Third-party APIs" }'
# → { "id": "vlt_1a2b3c", "display_name": "Third-party APIs", ... }

# 2. Add a static-bearer credential
curl https://agents.clusterbase.dev/v1/vaults/vlt_1a2b3c/credentials \
  -H "Authorization: Bearer $CLUSTER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "display_name": "Example API token",
    "auth": {
      "type": "static_bearer",
      "mcp_server_url": "https://mcp.example.com",
      "token": "sk-the-secret-token"
    }
  }'

The token is plaintext on the request only — it's encrypted before it touches storage and is never echoed back. The credential response carries only non-secret fields — its type, the mcp_server_url or secret_name key, and display fields.

For an OAuth server, use mcp_oauth and supply a refresh block so tokens renew themselves:

curl https://agents.clusterbase.dev/v1/vaults/vlt_1a2b3c/credentials \
  -H "Authorization: Bearer $CLUSTER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "display_name": "Slack (OAuth)",
    "auth": {
      "type": "mcp_oauth",
      "mcp_server_url": "https://mcp.slack.com/mcp",
      "access_token": "xoxp-...",
      "expires_at": "2026-01-01T00:00:00Z",
      "refresh": {
        "token_endpoint": "https://slack.com/api/oauth.v2.access",
        "client_id": "1234.5678",
        "scope": "chat:write",
        "refresh_token": "xoxe-...",
        "token_endpoint_auth": { "type": "client_secret_post", "client_secret": "..." }
      }
    }
  }'

token_endpoint_auth.type is none (public client), client_secret_basic, or client_secret_post. If a refresh ever fails, the credential is dropped for that connection (the agent proceeds unauthenticated) rather than failing the turn — to diagnose why, call POST /v1/vaults/{id}/credentials/{cid}/mcp_oauth_validate, which returns a status of valid (works), invalid (re-authorize), or unknown (transient, retry). Diagnosing may itself rotate and persist the refresh token.

For a secret your agent's in-VM tools read as an environment variable, use environment_variable — there's no mcp_server_url, just the secret_name to inject and its value:

curl https://agents.clusterbase.dev/v1/vaults/vlt_1a2b3c/credentials \
  -H "Authorization: Bearer $CLUSTER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "display_name": "Notion API key",
    "auth": {
      "type": "environment_variable",
      "secret_name": "NOTION_API_KEY",
      "value": "ntn-the-secret-value"
    }
  }'

secret_name must be a valid POSIX env-var name ([A-Za-z_][A-Za-z0-9_]*, up to 64 chars) and may not use a runtime-reserved name or prefix (PATH, IFS, LD_*, or the GH_ / GIT_ / CCP_ / CLUSTA_ system prefixes) — a name that breaks these rules is a 400. The value is write-only (encrypted at rest, never echoed).

Connecting them

Attach vaults to a session with vault_ids when you create it:

curl https://agents.clusterbase.dev/v1/sessions \
  -H "Authorization: Bearer $CLUSTER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "agent": "agt_3f9c2a", "vault_ids": ["vlt_1a2b3c"] }'

At MCP connect time, the runtime resolves each server's credential by matching the server url against credentials' mcp_server_url. The first attached vault holding a credential whose mcp_server_url exactly matches the server wins. You must own every vault you attach (a non-owned id returns 404).

environment_variable credentials are handled differently: they're injected as VM environment variables at provision time, so they apply only when you attach the vault to a run via vault_ids on POST /v1/runs — not on POST /v1/sessions, where a VM may be warm-reused (env-var creds on a session bind are ignored, with a server-side warning). On a name collision across attached vaults, the first attached vault wins, mirroring the MCP rule.

Because these secrets are load-bearing, delivery is all-or-nothing: if the run's env-var secrets can't be rendered onto the VM (a rare transient provisioning fault, with no later reconcile on an ephemeral run VM), the run fails with a 502 and the VM is torn down — rather than starting an agent that's silently missing them. Retry the run.

Manage vaults and credentials with the standard endpoints. PUT /v1/vaults/{id} updates a vault's display_name and/or metadata; archived vaults return 409. PUT /v1/vaults/{id}/credentials/{cid} rotates a static_bearer secret and/or renames it. Structural keys (mcp_server_url / secret_name) are immutable; changing mcp_server_url returns 400. Archive and recreate for mcp_oauth, environment_variable, or structural-key changes. Remove a vault or credential with DELETE (hard) or /archive (soft) on both /v1/vaults/{id} and /v1/vaults/{id}/credentials/{cid}. List endpoints are paginated (limit, has_more), return newest-first records, and exclude archived records unless you pass include_archived=true. See the API reference.

On this page