MCP proxy
DevBoy can proxy tool calls to upstream MCP servers, exposing their tools alongside its own. This lets you combine tools from multiple MCP servers into a single endpoint.
Quick setup
The fastest way to add a proxy server:
Or during project initialization:
The token is automatically stored in keychain as proxy.my-server.token.
Use case
You have a remote MCP server with additional tools (knowledge base, meeting notes, messengers). Instead of configuring multiple MCP servers in your AI assistant, you configure DevBoy to proxy them all through one connection.
Configuration
Add upstream servers to your config.toml or .devboy.toml:
Store the token in keychain:
Fields
Transport types
sse— Legacy MCP transport. Uses GET for SSE stream, POST for requests. Used by most self-hosted MCP servers.streamable-http— Modern HTTP POST-based transport withmcp-session-idheader. Used by hosted MCP services.
OAuth 2.1 authentication (device flow)
For upstream MCP servers that require OAuth 2.1 (per the MCP authorization spec),
set auth_type = "oauth2" and log in once with the device flow. The proxy then
injects a fresh Bearer per request and refreshes tokens automatically, so
sessions survive the access-token TTL without any manual re-configuration.
Then authorize:
devboy login (1) discovers the authorization server from the upstream's
WWW-Authenticate challenge (RFC 9728 → RFC 8414), (2) registers a client if
needed (RFC 7591) and caches the client_id, (3) runs the device authorization
grant (RFC 8628) — you approve the printed user_code at the verification URL —
and (4) stores the access + refresh tokens in the OS keychain.
The proxy refreshes transparently (the refresh token is long-lived and rotated
on use, so refreshes are single-flight and persisted immediately). Check state
anytime with devboy doctor — it reports each oauth2 proxy as logged in or
needs login with the exact command to run. No token_key is needed for
oauth2; tokens live under proxy.<name>.oauth.
Multiple servers
You can proxy multiple upstream servers:
How it works
- On startup, DevBoy connects to each configured upstream server and performs the MCP
initializehandshake. - Upstream tools are fetched and exposed with a prefix:
<prefix>__<tool_name>(e.g.devboy-cloud__get_issues). - When a proxied tool is called, DevBoy strips the prefix and forwards the request to the matching upstream server.
CLI commands
Add a proxy server
Add a new proxy server without editing the config file manually:
Remove a proxy server
List proxied tools
Call a proxied tool
MCP server integration
When running as an MCP server (devboy mcp), proxied tools are automatically included in tools/list and routed via tools/call. No additional configuration is needed on the client side — AI assistants see all tools (both local and proxied) as a flat list.
Transparent routing: local fallback for upstream tools
When the same tool is advertised by both the local ToolHandler and a connected upstream MCP server, DevBoy can optionally dispatch the call locally instead of round-tripping through the upstream. This is useful when:
- The upstream cannot reach a provider that is available from the developer's network (GitLab / Jira behind corporate VPN).
- The cloud integration is degraded and you want a local fallback.
- You prefer lower latency for interactive tools.
The feature is opt-in. By default, every matched call goes to the upstream (cloud has priority).
Enabling
Add a [proxy.routing] section to your config.toml:
Strategies
Graceful degradation
If the upstream schema requires arguments the local schema does not declare, DevBoy routes that specific tool to the upstream automatically — regardless of the strategy. This keeps existing calls working even when the two implementations drift. You can inspect such mismatches with devboy proxy status.
Per-server override
A routing block under [[proxy_mcp_servers]] overrides the global policy for that upstream only. Only the fields you set win over the global config — omitted fields keep their global values (a per-server block that just sets strategy does not silently reset fallback_on_error to its default):
Supported override fields: strategy, fallback_on_error, tool_overrides. When tool_overrides is set it is prepended to the global list so per-server rules match first.
Secrets cache
Local-first routing means secrets come from the OS keychain on every call. A short-lived in-memory cache prevents repeated keychain prompts without compromising rotation semantics.
- Cached values are zeroized on eviction and on process exit.
- Writing via
devboy config set-secret …invalidates the corresponding cache entry immediately. - Set
cache_ttl_secs = 0for high-security setups where every prompt should hit the keychain directly.
Telemetry
When routing happens locally the cloud backend loses visibility into usage. DevBoy forwards a minimal event to the configured telemetry endpoint so cloud dashboards stay accurate.
The payload is intentionally minimal — it never contains tool arguments or responses. Only:
tool— unprefixed tool namerouting_decision— short label (strategy_remote,override_rule,schema_incompatible, …)routing_detail— foroverride_rule, the glob pattern that matchedupstream— prefix when the call went remotestatus—success/errorlatency_ms— observed latencytimestamp_secs— unix epoch secondswas_fallback— true if the primary executor failed and we retried
Set enabled = false or omit endpoint to collect events locally without uploading (useful for CLI debugging).
Observability
devboy proxy status
Prints a human-readable snapshot of the routing table: what is routable locally, what stays remote, which pairs have incompatible schemas, and the currently active override rules. Exit with --json for a machine-readable form.
Structured logs
Every routing decision is emitted at tracing::info level with fields:
Filter with RUST_LOG=devboy_mcp::routing=info to see only routing events.
Response metadata
Routing details are currently exposed through tracing logs (tracing::info on every decision) rather than as a _meta.routing object on tools/call responses. Clients should not rely on response-level metadata for routing diagnostics unless and until that behavior is explicitly documented in a future release. For now, capture stderr (2> routing.log) and grep for routing decision records.
Cloud priority — summary of invariants
- The default strategy is
remote; no local routing happens unless the user opts in. - Missing upstream schemas disable local routing for that specific tool.
- Telemetry is on by default so cloud usage statistics remain accurate even when calls execute locally.
Validation rules
Config CLI (devboy config set|get)
Keys under proxy.{routing|secrets|telemetry}.* are a structured schema. Typos surface as explicit errors, not silent fallbacks — both on write and on read:
Provider paths (github.*, gitlab.*, …) keep historical behaviour — unknown fields return (not set) with exit 0 so pre-existing scripts don't break. Only proxy.* paths were tightened.
Type-specific rules enforced by devboy config set:
Negative integers (-1) are accepted by the CLI argument parser (allow_hyphen_values = true) and rejected by the domain validator with a clear message.
Telemetry endpoint payload
The backend enforces a strict shape on the POST body so malformed events don't create garbage rows in mcp_tool_usages:
On any validation failure the whole batch is rejected (400 Bad Request) — no partial acceptance. Clients should retry after fixing the payload.
MCP protocol and stdout hygiene
Not every devboy command keeps stdout log-free. The commands whose stdout is reserved for machine-readable output route tracing to stderr:
- JSON-RPC messages when running
devboy mcp - Machine-readable status from
devboy proxy status --json
For other commands (devboy config get, devboy init, regular interactive subcommands) human-oriented INFO logs stay on stdout by design — do not assume stdout is free of logs when piping into jq, python, or another client. Use 2> /dev/null (or a log file) to suppress/capture the log stream in those cases.
Use RUST_LOG=devboy_mcp::routing=info devboy mcp 2> routing.log to capture routing decisions without polluting the JSON-RPC channel.