Skip to content

MCP & Vault

The Model Context Protocol (MCP) is an open standard for connecting LLMs to external tools and data. Surogate ships a full MCP client + a credential-injecting proxy + security scanning.

You manage MCP servers in Skills & Tools → MCP SERVERS tab (Develop or Work mode). The Vault button at the top of that tab opens the credential store.

Sandbox / Worker
│ MCP tool call
MCP Proxy (separate K8s service)
│ injects credentials from vault
External MCP Server (stdio or HTTP)

The proxy is the trusted credential broker:

  • Sandboxes can only reach the MCP proxy (K8s NetworkPolicy enforces this)
  • The proxy holds credentials in memory (fetched from the Vault on connection)
  • Outbound calls to MCP servers carry injected headers/args without the sandbox ever seeing secrets

This is the structural fix for “what if my agent leaks an API key” — the agent literally cannot access keys, even if fully prompt-injected.

The blocks below show the shape of a server’s stored config — the same fields the Studio form (or /api/mcp-servers) writes into the registry. There is no config file on disk to edit.

Launch the MCP server as a subprocess; communicate via stdin/stdout. Configuration:

mcp_servers:
github:
transport: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-github"]
credential_refs: ["GITHUB_TOKEN"] # resolved from Vault
timeout: 120

Best for: locally-installable CLI-like tools, MCP servers shipped as npm packages.

Connect to a remote MCP server over HTTP:

mcp_servers:
jira:
transport: http
url: "https://mcp.acme.com/jira"
auth: oauth
oauth:
client_id: "surogates-agent"
scope: "read write"
timeout: 120

Best for: cloud-hosted MCP servers, shared infrastructure across tenants.

Feature Description
Auto-reconnect Exponential backoff with up to 5 retries on connection loss
Sampling MCP servers can request LLM completions back through the client
Environment filtering Only explicitly allowed env vars passed to stdio servers
Credential stripping Secrets scrubbed from error messages
Per-server timeout Configurable per server
Thread safety Dedicated background event loop for MCP connections
1. Agent needs to call an OAuth-protected MCP tool
2. MCP client checks for cached tokens (on disk)
3. If no valid token:
a. Start ephemeral localhost HTTP server for redirect
b. Open browser to authorization URL with PKCE challenge
c. User approves in browser
d. Callback server receives authorization code
e. Exchange code for access + refresh tokens
f. Store tokens on disk for reuse
4. Attach access token to MCP requests
5. Auto-refresh when token expires

Configuration:

mcp_servers:
salesforce:
url: "https://mcp.salesforce.com/mcp"
auth: oauth
oauth:
client_id: "pre-registered-id"
client_secret: "secret"
scope: "api refresh_token"
redirect_port: 0 # auto-pick available port
client_name: "Surogates Agent"
token_dir: "/var/lib/surogates/tokens"

The Vault button on the MCP Servers tab opens the credential store:

Field Notes
Name Vault entry name (e.g. GITHUB_TOKEN) — named in an MCP server’s credential_refs
Value Write-only. You set it, and later Update it (store a new value) or Remove it — but the value is never displayed back to you. The list API returns names + timestamps only, never the secret.
Scope Project-scoped (= org). Credentials belong to the project; every MCP server in the project can reference them.

System-owned credentials (used by platform/system agents) are protected: the API refuses to delete them (403), and a credential still consumed by a running agent’s MCP server can’t be deleted until you stop the consumer (409, in-use conflict).

A server config never carries a secret. It carries a credential_refs array naming Vault entries and saying where each value is injected. The short form is a bare name:

"credential_refs": ["GITHUB_TOKEN"]
  • stdio → injected as env.GITHUB_TOKEN
  • http → injected as headers.Authorization: Bearer <value>

Use the structured form when the target variable or header differs from the Vault name:

"credential_refs": [
{"name": "MY_TOKEN", "env": "GITHUB_PERSONAL_ACCESS_TOKEN"},
{"name": "API_KEY", "header": "X-API-Key"},
{"name": "AUTH", "header": "Authorization", "prefix": "Bearer "}
]
Field Notes
name Vault entry name (required)
env Inject as this environment variable (stdio)
header Inject as this HTTP header (http)
prefix Prepended to the value — header form only (e.g. Bearer )

With neither env nor header, the value is injected as an environment variable named after the credential.

The Auth dialog on an MCP server writes these for you: API key produces {"name": …, "header": "X-API-Key"}, Bearer produces {"name": …, "header": "Authorization", "prefix": "Bearer "}, OAuth produces {"name": …, "env": "CLIENT_SECRET"}.

At runtime the proxy resolves each ref from the Vault and injects it into the outbound connection. The sandbox never sees the value, and every lookup is written to the tenant audit log as a credential.access entry.

Vault credentials are encrypted at rest with Fernet (AES-128-CBC + HMAC-SHA256). The symmetric key lives in a K8s cluster secret.

The registry is exclusively DB-backed — the mcp_servers table, written by the Studio UI and /api/mcp-servers. There are no on-disk config layers.

Servers are project (org) scoped: every server you register is visible to the whole project. What a given session actually loads is decided per agent — the proxy loads only the servers attached to that agent, so two agents in the same project get different toolsets from the same registry. Servers toggled off are skipped entirely.

The PLATFORM / ORG / USER chip on a server row is a label for your own bookkeeping (the REST list can filter on it). It does not change who can see the server and it does not affect load precedence.

Every MCP tool definition is scanned before registration:

Threat What it catches
Invisible Unicode Zero-width chars, bidi marks in tool names/descriptions
Prompt injection Deceptive descriptions that trick the LLM
Hidden HTML HTML comments with invisible instructions
Tool poisoning Descriptions that manipulate the LLM into dangerous behaviour
Rug-pull Tool definitions that change between connections (SHA-256 fingerprinting)

Tools failing scanning are not registered. Scan results land in the tenant audit log:

SELECT * FROM audit_log
WHERE type = 'mcp.tool_scan'
AND org_id = $1
ORDER BY created_at DESC;

If a server returns a different tool definition on reconnect (different fingerprint), the scanner emits a warning. Common cause: the upstream MCP server got updated. Review the diff before re-enabling.

Method Endpoint Purpose
GET /api/mcp-servers List (filter by project_id, enabled, layer)
GET /api/mcp-servers/composio/catalog Live Composio toolkit catalog
GET /api/mcp-servers/{id} Detail with connected tools
POST /api/mcp-servers Add server
PATCH /api/mcp-servers/{id} Update config (including the enabled toggle)
GET /api/mcp-servers/{id}/in-use List agents referencing this server
DELETE /api/mcp-servers/{id} Remove server (409 if attached to a running agent)
GET /api/credentials List Vault entries (names + timestamps only, never values)
POST /api/credentials Store a credential (same name overwrites — this is how “Update” works)
GET /api/credentials/{name}/in-use List MCP servers referencing this credential
DELETE /api/credentials/{name} Remove (403 if system-owned, 409 if in use)

There is no dedicated /enable endpoint — enabling/disabling is a PATCH that flips the enabled flag. Tool security scanning happens in the MCP proxy at tool-discovery time, not as a synchronous call on this API. Updating a credential is a POST with the same name (the store upserts); there is no PUT.

Hub for the artifact versioning layer. Datasets and Training for the training pipeline. Use cases → “Build an MCP server with OAuth.”