Skip to content

fix(core): surface OAuth authorization for SSE MCP servers on 401 - #13050

Merged
saoudrizwan merged 3 commits into
mainfrom
saoudrizwan/mcp-sse-oauth-401-detection-b65a
Aug 7, 2026
Merged

fix(core): surface OAuth authorization for SSE MCP servers on 401#13050
saoudrizwan merged 3 commits into
mainfrom
saoudrizwan/mcp-sse-oauth-401-detection-b65a

Conversation

@saoudrizwan

@saoudrizwan saoudrizwan commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Related Issue

Found during desktop v0.0.10 regression testing (MCP OAuth shipped in #12983 / #12984).

Description

Enabling a remote MCP server that uses the SSE (legacy) transport, which is also what a bare {"url": ...} config entry resolves to, never surfaced the OAuth flow. The connection probe hit the server's 401, but the toggle silently snapped back off with no banner, no error, and no way to authorize.

Root cause: for passive connections createMcpSdkTransport throws a typed UnauthorizedError at the fetch boundary. The streamable HTTP transport propagates that rejection, but the SSE transport runs the stream request inside EventSource, which consumes the thrown error and re-emits it as a status-less SseError ("SSE error: MCP server requires authorization", code: undefined). The error instanceof UnauthorizedError checks in client.ts then routed the failure to markConnectionError instead of markAuthorizationRequired, so authorizationRequired was never persisted and hosts (like the desktop MCP settings view) never rendered the Connect action.

The fix keeps 401 handling typed end to end:

  • createMcpSdkTransport now passes the SSE transport a raw stream fetch via eventSourceInit.fetch, so a 401 on the stream request fails the connection with the SDK's own SseError carrying code: 401 (this also avoids EventSource's reconnect scheduling on thrown fetch errors). The throwing wrapper stays in place for streamable HTTP and for SSE message POSTs, where the typed UnauthorizedError propagates correctly.
  • A single isMcpUnauthorizedError predicate recognizes a 401 in both shapes (UnauthorizedError, or SseError with code === 401) and replaces the four instanceof UnauthorizedError checks in client.ts connect/operation error handling and the check in authorizeMcpServerOAuth.

Test Procedure

  • New unit tests: predicate shape coverage; a real SSEClientTransport built by createMcpSdkTransport with an injected fetch returning 401 rejects with an SseError the predicate recognizes; a streamable HTTP transport still rejects with the typed UnauthorizedError; and client-level tests assert authorizationRequired is persisted for SSE 401s and typed 401s, while a non-401 SseError stays a plain connection error.
  • bunx vitest run src/extensions/mcp in sdk/packages/core: 8 files, 70 tests, all passing. tsc --noEmit clean. Full @cline/core test:unit: 1758 passed; the 2 failures are the documented cloud-VM git insteadOf artifact and an unrelated checkpoint-restore hook timeout that passes in isolation.
  • Manual end-to-end verification in the desktop app (rebuilt SDK, restarted tauri dev): configured linear as a bare-URL server (https://mcp.linear.app/mcp, resolves to SSE legacy) and toggled it on in Settings -> MCP. The probe now persists authorizationRequired, the amber "OAuth authorization required" banner with Connect appears, Connect opens Linear's real authorization page in the browser and switches the banner to "Waiting for OAuth authorization" with Cancel, and Cancel reverts it. Before the fix this exact flow ended with the toggle silently flipping back off.

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • ♻️ Refactor Changes
  • 💅 Cosmetic Changes
  • 📚 Documentation update
  • 🏃 Workflow Changes

Pre-flight Checklist

  • Changes are limited to a single feature, bugfix or chore (split larger changes into separate PRs)
  • Tests are passing (bun test) and code is formatted and linted (bun run format && bun run lint)
  • I have reviewed contributor guidelines

Screenshots

SSE (legacy) server toggled on after the fix, probe detects the 401 and offers Connect:

OAuth authorization required banner on SSE legacy server

Connect opens the browser authorization flow and shows a cancellable waiting state:

Waiting for OAuth authorization with Cancel

Cancel reverts to the authorization-required state:

Banner reverted after cancel

Additional Notes

The desktop app needed no changes: its MCP view already renders the banner from oauthStatus.authorizationRequired, which now gets persisted for SSE servers too.

A 401 from an SSE MCP server never persisted authorizationRequired: the
fetch-boundary UnauthorizedError was consumed by EventSource and re-thrown
as a status-less SseError, so the instanceof check routed it to
markConnectionError and hosts never offered the OAuth connect action.

Give the SSE stream request a raw fetch so a 401 fails the connection with
the SDK's typed SseError(401), and recognize 401s across transports with a
single isMcpUnauthorizedError predicate at every detection site.
@github-actions github-actions Bot deleted a comment from cursor Bot Aug 7, 2026
@greptile-apps

greptile-apps Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR preserves HTTP 401 status information from legacy SSE MCP connections so OAuth-required state can be surfaced consistently.

  • Adds a shared predicate covering typed UnauthorizedError and status-bearing SSE errors.
  • Uses a raw EventSource fetch for SSE stream requests while retaining the wrapped fetch for other transport requests.
  • Adds transport-level and client-level tests for authorization detection and persistence.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
sdk/packages/core/src/extensions/mcp/oauth.ts Preserves SSE stream response status and centralizes recognition of unauthorized transport errors.
sdk/packages/core/src/extensions/mcp/client.ts Uses the shared unauthorized predicate when persisting connection and operation failures.
sdk/packages/core/src/extensions/mcp/oauth.test.ts Covers unauthorized error shapes and verifies transport-specific handling of passive 401 responses.
sdk/packages/core/src/extensions/mcp/client-url.test.ts Verifies authorization-required persistence for SSE and streamable HTTP while preserving ordinary SSE errors.

Sequence Diagram

sequenceDiagram
  participant Client as MCP Client
  participant Transport as SSE Transport
  participant Server as MCP Server
  participant State as OAuth Status Store
  Client->>Transport: Connect
  Transport->>Server: SSE stream request
  Server-->>Transport: HTTP 401
  Transport-->>Client: SseError(code: 401)
  Client->>Client: isMcpUnauthorizedError(error)
  Client->>State: markAuthorizationRequired(message)
Loading

Reviews (2): Last reviewed commit: "Merge branch 'main' into saoudrizwan/mcp..." | Re-trigger Greptile

@saoudrizwan
saoudrizwan merged commit 031b8d9 into main Aug 7, 2026
8 checks passed
@saoudrizwan
saoudrizwan deleted the saoudrizwan/mcp-sse-oauth-401-detection-b65a branch August 7, 2026 23:42
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.

1 participant