Skip to content
API Blog

MCP Proxy

Workers never hold real credentials. The gateway resolves them at request time and proxies every outbound MCP call.

Every MCP URL a worker receives points back to the gateway with an X-Mcp-Id header identifying the upstream server.

  1. Worker sends a JSON-RPC request to the gateway proxy.
  2. Gateway authenticates the worker JWT, extracts agentId / userId.
  3. Looks up credentials for that user — auto-refreshes expired tokens.
  4. Injects the Authorization: Bearer <token> header, forwards to the upstream MCP.
  5. Response flows back to the worker.

Workers call tools/list and tools/call — credential handling is invisible.

MCP servers come from two sources, merged per agent:

  1. Per-agent settings + local skills — MCPs added through the settings page or an agent-driven install, plus any declared in local SKILL.md files.
  2. Global MCPs — servers registered with the gateway at boot time, available to every agent.

When an ID collides, the per-agent definition overrides the global one — global MCPs are the fallback.

There are three ways an MCP server can authenticate:

MethodConfig fieldUse case
Static headersheadersAPI keys, service tokens — no per-user auth needed
Device-code OAuthoauth on the MCP serverPer-user OAuth — each user authenticates in their browser
Lobu-managedN/A (Lobu handles internally)Third-party APIs (GitHub, Google, Linear, etc.)

Each MCP server is configured under [agents.<id>.skills.mcp.<name>] in lobu.toml, under mcpServers.<name> in a skill’s SKILL.md frontmatter, or in the agent’s settings. The JSON snippets below show the fields a single server config accepts:

{ "id": "my-mcp", "name": "My MCP", "url": "https://mcp.example.com", "type": "sse" }

For MCP servers that use a shared API key or service token. The header value supports ${env:VAR_NAME} substitution so secrets stay in environment variables.

{
"id": "my-mcp", "name": "My MCP", "url": "https://mcp.example.com", "type": "sse",
"headers": { "Authorization": "Bearer ${env:MY_MCP_TOKEN}" }
}

No user interaction needed. The gateway injects the header on every request.

For MCP servers that implement the OAuth 2.0 Device Authorization Grant. Each user authenticates individually by clicking a link and logging in via their browser.

  1. Tool call fails with auth error — Worker calls a tool, gateway proxies it, upstream MCP returns 401/403.

  2. Gateway auto-starts device-code flow — Detects the auth error and:

    • Registers as an OAuth client at {mcp-server-origin}/oauth/register (cached per MCP server)
    • Requests a device code from {mcp-server-origin}/oauth/device_authorization
    • Gets back a user_code, verification_uri, and device_code
  3. User gets a link in chat — The gateway returns a login_required response to the worker, which shows the user:

    Authentication required. Visit https://mcp.example.com/oauth/device and enter code ABCD-1234

  4. User authenticates in browser — Clicks the link, enters the code, and authorizes the application.

  5. Gateway polls for completion — On the next tool call from the worker, the gateway polls the token endpoint with the device_code. If the user has completed auth, it receives access_token + refresh_token.

  6. Credentials stored — Encrypted in Postgres, keyed by (agentId, userId, mcpId), with 90-day TTL.

  7. Future calls are transparent — Gateway injects Authorization: Bearer <token> on every proxied request. No more user interaction needed.

  • Storage: Encrypted at rest in Postgres with 90-day TTL
  • Auto-refresh: When a token is within 5 minutes of expiry, the gateway refreshes it using the refresh_token before proxying the request
  • Refresh locking: A per-process mutex prevents concurrent refresh races within a single gateway instance
  • Expiry fallback: If refresh fails (no refresh token, revoked, etc.), the next tool call triggers a new device-code flow

By default, the gateway auto-derives OAuth endpoints from the MCP server’s URL origin — so the minimal config is just an empty oauth object:

{
"id": "my-mcp", "name": "My MCP", "url": "https://mcp.example.com", "type": "sse",
"oauth": {}
}

The auto-derived endpoints are:

  • Registration: {origin}/oauth/register
  • Device authorization: {origin}/oauth/device_authorization
  • Token: {origin}/oauth/token
  • Verification (for user): {origin}/oauth/device

If the MCP server’s OAuth endpoints live at non-standard paths, or if you have a pre-registered client, you can override any of these via the oauth config. All fields are optional — omitted fields fall back to auto-derivation:

{
"oauth": {
"clientId": "my-pre-registered-client",
"clientSecret": "secret",
"tokenUrl": "https://auth.example.com/oauth/token",
"deviceAuthorizationUrl": "https://auth.example.com/oauth/device_authorization",
"registrationUrl": "https://auth.example.com/oauth/register",
"authUrl": "https://auth.example.com/oauth/device",
"scopes": ["read", "write"],
"resource": "https://api.example.com"
}
}

When clientId is provided, dynamic client registration is skipped entirely — useful when you’ve pre-registered an OAuth application with the MCP server.

Third-party API integrations (GitHub, Google, Linear, Notion, etc.) are handled by Lobu MCP servers. Lobu manages OAuth flows, token storage, and API proxying internally. The gateway acts as a thin proxy — it doesn’t know or care about the integration’s auth.

Workers access these APIs through Lobu tools (e.g., lobu_github_read_repo). If Lobu needs the user to authenticate, it returns instructions for the user to call lobu_login.

Workers receive MCP status at session startup that includes auth state:

interface McpStatus {
id: string;
name: string;
requiresAuth: boolean; // MCP config has oauth
requiresInput: boolean; // MCP needs manual config inputs
authenticated: boolean; // User has valid stored credential
configured: boolean; // Manual inputs have been provided
}

Based on this status, the worker’s system prompt includes setup instructions for any MCPs that need authentication. This lets the agent proactively guide users through login rather than waiting for a tool call to fail.

The proxy resolves upstream URLs and blocks requests to reserved/internal IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, link-local, IPv6 loopback/ULA). This prevents workers from using MCP configs to reach internal services.

MCP sessions (via Mcp-Session-Id header) are tracked in Postgres (mcp_proxy_sessions) with 30-minute TTL. If an upstream returns “Server not initialized” (stale session), the gateway automatically re-initializes with the MCP handshake (initialize + notifications/initialized) before retrying.

MCP tools can declare annotations indicating whether they are destructive or have side effects. The gateway checks these annotations and may require explicit user approval before executing a tool call. Grants are stored per agent and checked on each call.