MCP Proxy
Workers never hold real credentials. The gateway resolves them at request time and proxies every outbound MCP call.
Request flow
Section titled “Request flow”Every MCP URL a worker receives points back to the gateway with an X-Mcp-Id header identifying the upstream server.
- Worker sends a JSON-RPC request to the gateway proxy.
- Gateway authenticates the worker JWT, extracts
agentId/userId. - Looks up credentials for that user — auto-refreshes expired tokens.
- Injects the
Authorization: Bearer <token>header, forwards to the upstream MCP. - Response flows back to the worker.
Workers call tools/list and tools/call — credential handling is invisible.
Configuration sources
Section titled “Configuration sources”MCP servers come from two sources, merged per agent:
- Agent settings + local skills — MCP servers come from per-agent settings and local
SKILL.mdfiles. - Per-agent settings — MCPs added through the settings page or agent-driven install.
Global MCPs take precedence when IDs collide.
Authentication methods
Section titled “Authentication methods”There are three ways an MCP server can authenticate:
| Method | Config field | Use case |
|---|---|---|
| Static headers | headers | API keys, service tokens — no per-user auth needed |
| Device-code OAuth | oauth on the MCP server | Per-user OAuth — each user authenticates in their browser |
| Owletto-managed | N/A (Owletto handles internally) | Third-party APIs (GitHub, Google, Linear, etc.) |
Static headers
Section titled “Static headers”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", "mcpServers": [{ "id": "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.
Device-code OAuth (per-user auth)
Section titled “Device-code OAuth (per-user auth)”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.
How it works
Section titled “How it works”User (chat) Worker Gateway MCP Server (OAuth) | | | | | "use tool X" | | | |------------------>| | | | | tools/call X | | | |------------------>| | | | | tools/call X | | | |---------------------->| | | | 401 Unauthorized | | | |<---------------------| | | | | | | | POST /oauth/register | | | |---------------------->| | | | { client_id } | | | |<---------------------| | | | | | | | POST /oauth/device_authorization | | |---------------------->| | | | { device_code, | | | | user_code, | | | | verification_uri } | | | |<---------------------| | | | | | | login_required | | | | + link + code | | | |<------------------| | | "Click this link | | | | and enter code | | | | ABCD-1234" | | | |<------------------| | | | | | | | (user clicks link, logs in via browser) | | | | | | "done, try again"| | | |------------------>| | | | | tools/call X | | | |------------------>| | | | | poll device_code | | | |---------------------->| | | | { access_token, | | | | refresh_token } | | | |<---------------------| | | | | | | | (store credential) | | | | | | | | tools/call X + token | | | |---------------------->| | | | { result } | | | |<---------------------| | | { result } | | | |<------------------| | | "Here's the | | | | result..." | | | |<------------------| | |Step by step
Section titled “Step by step”-
Tool call fails with auth error — Worker calls a tool, gateway proxies it, upstream MCP returns 401/403.
-
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, anddevice_code
- Registers as an OAuth client at
-
User gets a link in chat — The gateway returns a
login_requiredresponse to the worker, which shows the user:Authentication required. Visit
https://mcp.example.com/oauth/deviceand enter codeABCD-1234 -
User authenticates in browser — Clicks the link, enters the code, and authorizes the application.
-
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 receivesaccess_token+refresh_token. -
Credentials stored — Encrypted in Postgres, keyed by
(agentId, userId, mcpId), with 90-day TTL. -
Future calls are transparent — Gateway injects
Authorization: Bearer <token>on every proxied request. No more user interaction needed.
Token lifecycle
Section titled “Token lifecycle”- 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_tokenbefore 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
Configuration
Section titled “Configuration”By default, the gateway auto-derives OAuth endpoints from the MCP server’s URL origin:
- 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.
Lobu-memory-managed auth
Section titled “Lobu-memory-managed auth”Third-party API integrations (GitHub, Google, Linear, Notion, etc.) are handled by Owletto MCP servers. Owletto 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 Owletto tools (e.g., owletto_github_read_repo). If Owletto needs the user to authenticate, it returns instructions for the user to call owletto_login.
Worker auth awareness
Section titled “Worker auth awareness”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.
SSRF protection
Section titled “SSRF protection”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.
Session management
Section titled “Session management”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.
Tool approval
Section titled “Tool approval”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.
Configuration reference
Section titled “Configuration reference”Adding an MCP via local skills or agent settings
Section titled “Adding an MCP via local skills or agent settings”Skills-registry entries wrap one or more MCP server definitions:
{ "id": "my-mcp", "name": "My MCP Server", "description": "What this MCP does", "mcpServers": [ /* one of the server configs below */ ]}The inner mcpServers[] entry varies by auth mode:
No auth
{ "id": "my-mcp", "name": "My MCP", "url": "https://mcp.example.com", "type": "sse" }Static auth headers (${env:VAR} substitution)
{ "id": "my-mcp", "name": "My MCP", "url": "https://mcp.example.com", "type": "sse", "headers": { "Authorization": "Bearer ${env:MY_MCP_TOKEN}" }}Per-user OAuth, auto-derived endpoints
{ "id": "my-mcp", "name": "My MCP", "url": "https://mcp.example.com", "type": "sse", "oauth": {}}Per-user OAuth, pre-registered client or custom endpoints
{ "id": "my-mcp", "name": "My MCP", "url": "https://mcp.example.com", "type": "sse", "oauth": { "clientId": "my-pre-registered-client", "tokenUrl": "https://auth.example.com/oauth/token", "deviceAuthorizationUrl": "https://auth.example.com/oauth/device_authorization", "scopes": ["read", "write"] }}