Skip to content

[Feat] Deployment-scoped custom MCP servers (remote, OAuth, and stdio) - #1147

Merged
mrubens merged 18 commits into
developfrom
feat/custom-mcp-servers
Aug 7, 2026
Merged

[Feat] Deployment-scoped custom MCP servers (remote, OAuth, and stdio)#1147
mrubens merged 18 commits into
developfrom
feat/custom-mcp-servers

Conversation

@mrubens

@mrubens mrubens commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

[Feat] Deployment-scoped custom MCP servers (remote + OAuth + stdio)

Closes #1142.

Adds a Custom MCP Servers section to Settings → Integrations where deployment admins can connect MCP servers that are not in the built-in catalog. Custom servers become available to agents in every task.

What's supported

  • Remote servers (streamable HTTP) with no auth, static header auth, or OAuth. Remote servers are delivered to sandboxes as authenticated Roomote proxy URLs (/api/mcp/custom/<id>); the proxy injects credentials per request, so header values and OAuth tokens never enter sandbox-readable config or the browser.
  • OAuth per the MCP authorization spec: RFC 8414/9728 discovery, RFC 7591 Dynamic Client Registration with PKCE, manual client id/secret fallback for servers without DCR, RFC 8707 resource indicators (with a per-server opt-out), server-side token refresh, and an explicit reconnect state when a refresh is definitively rejected.
  • Local (stdio) servers, launched inside the task sandbox via the same pipeline as environment-scoped mcpServers (inheriting the reserved-env-reference defenses and the mise shim fix). Env values are encrypted at rest and write-only in the UI.
  • Per-tool deny lists, enforced at the proxy (tools/call blocked, tools/list filtered), managed from a tool dialog that lists the server's live tools.

Security

  • New safeFetch egress guard for all control-plane traffic to operator-supplied URLs (the proxy data path and every OAuth discovery hop, including URLs the remote server itself supplies): private/special-use ranges blocked, DNS answers vetted and pinned (anti-rebinding), redirects refused. Self-hosters can allow specific internal ranges with R_CUSTOM_MCP_ALLOWED_PRIVATE_CIDRS (a CIDR list, deliberately not a boolean).
  • Reserved names (roomote, catalog ids, github, slack) and reserved runtime env references are rejected at save time.
  • Header names the proxy owns (authorization, host, mcp-session-id, ...) cannot be overridden; header values are validated against CR/LF injection.
  • Editing a server's URL or auth mode clears stored OAuth tokens, so credentials can never be replayed against a different endpoint.
  • Custom-server secrets are per-value encrypted at rest; list endpoints return key names only; the decrypted stdio config endpoint is restricted to run tokens.
  • R_CUSTOM_MCP_DISABLED kill switch, independent of R_CURATED_INTEGRATIONS_DISABLED (operators who disable the catalog are the primary custom-server audience).

Notes

  • Deployment servers merge after environment-scoped mcpServers; the more specific scope wins name collisions.
  • Custom server names feed the self-setup catalog so the setup agent stops recommending services that are already wired.
  • Docs: new integrations/custom-mcp-servers page plus updates to the integrations index and the add-integration skill.
  • Rollout note: workers baked into old sandbox snapshots don't fetch custom servers; the feature appears on fresh-snapshot tasks.
  • Migration is additive (N-1 safe). OAuth connections reuse mcpConnections under a custom:<serverId> id namespace, which previous-release code ignores.

Testing

  • Schema validation, CRUD (encryption round-trip, blank-keeps-existing, token clearing), SSRF guard units (blocked ranges, IPv4-mapped bypasses, DNS pinning against a live local server, redirect refusal), proxy integration tests against a mock MCP server (header injection, OAuth, deny lists, redirect refusal, egress guard, body cap), worker merge/redaction tests, SDK delivery and run-token gating tests, and UI client tests.
  • Verified end to end on a dev deployment: server CRUD in Settings, live tool listing through the guard, per-tool disables, and an agent task calling the tools through the proxy.

Screenshots

Custom servers render as ordinary integration cards in the same Connected/Configured grids as the built-in catalog, marked with a small Custom badge. A prompt bar at the top is the entry point for adding one.

Custom MCP servers as integration cards

Adding a server starts from one dialog that chooses between a remote (HTTP) and a local (stdio) server:

Add custom MCP server dialog

Header auth (values write-only) OAuth (DCR + manual client fallback)
Edit dialog with header auth Edit dialog with OAuth
Local (stdio) config Per-tool deny list (proxy-enforced)
Local stdio edit dialog Manage tools deny list

@roomote-community

roomote-community Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

No new code issues found. See task

  • Request-size limit is applied after the body has already been buffered and parsed, allowing oversized JSON requests to exhaust API resources.
  • Importing mcp-remote Authorization headers produces a static-header configuration rejected on save.

Reviewed b9dab52

@mrubens
mrubens marked this pull request as draft August 7, 2026 10:48
Comment thread apps/api/src/handlers/mcp/proxy-utils.ts Outdated
@mrubens

mrubens commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in 97537bb. The cap was enforced after c.req.json() had already buffered and parsed the body (and JSON.stringify allocated it a second time), so the limit protected nothing.

Capped upstreams now read through readBoundedRequestText, which rejects on Content-Length before touching the stream and then enforces the limit while reading, aborting the moment the accumulated size crosses it. Because Content-Length is absent on chunked bodies and can understate the real size, the streamed bound is the actual control; the header check is just an early exit. The vetted text is forwarded as-is instead of being re-serialized, dropping the second allocation. Uncapped curated proxies keep their existing path.

Covered by three tests: an oversized parsed body, an oversized Content-Length carrying invalid JSON (a 413 rather than a 400 proves the check precedes parsing), and an endless chunked stream with no Content-Length that aborts well before exhausting memory.

isDefinitiveOAuthRejection matched 'invalid_grant|invalid_client' against
the wrapped error message, so any upstream failure whose body happened to
contain those words (a proxy error page, a 5xx with prose) would kill a
healthy connection into the reconnect state, and message rewording would
silently break the classification.

Token-endpoint failures now throw OAuthTokenRequestError carrying the
parsed RFC 6749 error code from the response body, and the classifier
decides on that code alone. Non-JSON bodies yield no code and are always
treated as transient.
MCP servers are distributed as JSON snippets, so the add dialog now has
an Import from JSON button that accepts an mcpServers wrapper (or VS
Code's servers wrapper, a single-entry map, or a bare config) and
prefills the form for review. Import is create-time only: the edit
dialog keeps field-by-field editing because blank secret values there
mean 'keep the stored value'.

Snippets that launch mcp-remote are converted to remote servers on the
upstream URL, carrying --header pairs and reporting any dropped flags,
since the proxy path keeps credentials server-side and supports
per-tool management.
Comment thread apps/web/src/lib/custom-mcp-json-import.ts
The header deny list reserved 'authorization' for the proxy, which made
it impossible to configure remote servers that expect a plain
'Authorization: Bearer <api-key>', the most common auth scheme for
hosted MCP endpoints. The reservation only exists to protect
proxy-injected credentials, and header auth can never coincide with
those: the schema rejects custom headers on OAuth servers, and the
proxy rebuilds upstream headers from scratch so the caller's run token
is never forwarded either way.

Authorization is now allowed for header-auth servers, with a proxy test
pinning that the operator value reaches the upstream while the run
token still does not.
…st edits on disabled servers

Two follow-ups from review:

- The Settings tool lister's session fallback jumped from initialize
  straight to tools/list, which spec-strict servers refuse until the
  client sends notifications/initialized. The notification is now sent
  between the two, best-effort: lenient servers that reject it still get
  the tools/list, and if a strict server required it the tools/list
  failure surfaces as before.

- setDisabledTools required the server to be enabled, so a disabled
  server's deny list could not be adjusted while preparing it for
  re-enablement. The enabled filter is dropped.
@mrubens
mrubens marked this pull request as ready for review August 7, 2026 16:04
@mrubens
mrubens merged commit e0efedf into develop Aug 7, 2026
20 checks passed
@mrubens
mrubens deleted the feat/custom-mcp-servers branch August 7, 2026 16:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Globally configurable MCP for custom integrations

2 participants