Skip to content

Support OAuth 2.1 authorization for remote (SSE/HTTP) MCP servers #2229

@RockChinQ

Description

@RockChinQ

Summary

Remote MCP servers that use OAuth 2.0 (the MCP "Authorization" spec with WWW-Authenticate: Bearer realm="OAuth" + resource_metadata) cannot be connected from LangBot. The SSE/HTTP MCP connector only supports static headers, so any OAuth-protected hosted server returns 401/403 on connect, and the install UI gives the user no way to complete an authorization flow.

This affects several popular hosted MCP servers now listed on LangBot Space (Linear, Vercel, Cloudflare, and a growing number of first-party hosted servers that are moving to OAuth).

Steps to reproduce

  1. Add an SSE MCP server pointing at https://mcp.linear.app/sse (or install "Linear" from the Space marketplace).
  2. Enable it / connect.
  3. Connection fails with 401 Unauthorized.

Root cause

The MCP loader builds the transport from static config only:

  • src/langbot/pkg/provider/tools/loaders/mcp.py
    • SSE: sse_client(url, headers=server_config.get('headers', {}), ...)
    • HTTP: streamable_http_client(url, http_client=httpx.AsyncClient(headers=server_config.get('headers', {}), ...))

The install form (web/.../mcp-server/mcp-form/MCPFormDialog.tsx) only renders parameters for keys that already exist in extra_args.headers (sse/http) or extra_args.env (stdio). There is:

  • no OAuth client (no Authorization Server discovery, no Dynamic Client Registration, no PKCE authorize/redirect/token exchange), and
  • no UI affordance to launch a browser authorization flow or store the resulting tokens.

So even if the user pastes a personal API token into an Authorization header, OAuth-only servers reject it — they require a real OAuth access token obtained via the interactive flow.

Evidence (live probes)

initialize POST to each endpoint, with and without a static Bearer token:

linear   https://mcp.linear.app/sse
  401  WWW-Authenticate: Bearer realm="OAuth",
       resource_metadata="https://mcp.linear.app/.well-known/oauth-protected-resource/sse"
  (identical with a static Bearer token — PAT is not accepted)

vercel   https://mcp.vercel.com
  401  WWW-Authenticate: Bearer error="invalid_token",
       resource_metadata="https://mcp.vercel.com/.well-known/oauth-protected-resource"

cloudflare https://observability.mcp.cloudflare.com/sse
  403  Error 1010 (blocks non-browser clients) — browser OAuth required

These are standard MCP Authorization (OAuth 2.1) servers: they advertise .well-known/oauth-protected-resource, expect Authorization Server metadata discovery, Dynamic Client Registration, and a PKCE authorize → token flow.

Proposed solution

Implement the MCP Authorization spec for remote (SSE/HTTP) servers:

  1. Detect the OAuth challenge on 401 (WWW-Authenticate: Bearer ... resource_metadata=...).
  2. Discover the protected-resource metadata → authorization server → its .well-known/oauth-authorization-server metadata.
  3. Dynamic Client Registration (RFC 7591) if no client is pre-registered.
  4. PKCE authorize: open the system browser to the authorize URL with a local loopback redirect (e.g. http://127.0.0.1:<port>/callback), capture the code.
  5. Token exchange + refresh token persistence (encrypted, per MCP server config).
  6. Attach Authorization: Bearer <access_token> to the SSE/HTTP transport and auto-refresh on expiry.
  7. UI: an "Authorize" button in the MCP install/detail dialog for OAuth-type servers; show connection/authorization status.

The Python SDK (mcp) has reference OAuth client helpers that can be leveraged for the discovery/PKCE/token machinery.

Interim mitigation (already done on Space)

On the marketplace side, OAuth-only hosted servers (Linear, Vercel, Cloudflare) now carry a README banner stating they require OAuth and are not yet connectable in LangBot, to avoid users hitting silent 401s. Servers that use static auth work fine and import their parameters correctly:

  • stdio: required keys live in extra_args.env (e.g. Postgres DATABASE_URI) → rendered as fields. ✅
  • sse/http with static token: required keys live in extra_args.headers (e.g. Context7 CONTEXT7_API_KEY). ✅
  • key-in-URL: e.g. Tavily ?tavilyApiKey=.... ✅

This issue tracks adding the OAuth flow so the OAuth-only servers become first-class too.

Acceptance criteria

  • Connecting an OAuth-protected SSE/HTTP MCP server triggers a browser authorization flow and succeeds without manual token pasting.
  • Access/refresh tokens are stored securely and auto-refreshed.
  • Install/detail UI exposes an Authorize action and shows auth status for OAuth-type servers.
  • Linear / Vercel / Cloudflare hosted MCP servers connect successfully end-to-end.

Metadata

Metadata

Assignees

No one assigned

    Labels

    RoadmapIn roadmapeh: Featureenhance: 新功能添加 / add new featuresm: Plugins插件加载及管理模块 / Plugins loading and managementpd: Need designpending: 需要进一步设计的功能 / wait for us to design

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions