Skip to content

ADR-002: Immediate Token Exchange in redirectToAuthorization() ​

Status: Superseded by ADR-006 (v3) Date: 2025-01-25 Tags: oauth, mcp, sdk-integration

Problem ​

The MCP SDK's auth() flow works as follows:

  1. Check provider.tokens() — if valid tokens exist, return 'AUTHORIZED'
  2. If no tokens, start authorization: call redirectToAuthorization(url)
  3. Immediately return 'REDIRECT' (without re-checking tokens)

For web-based OAuth, this makes sense: redirectToAuthorization() triggers a page redirect and control never returns synchronously. The SDK expects authentication to complete in a subsequent request.

For CLI/desktop apps using browserAuth(), control does return synchronously—we capture the callback in-process via a local HTTP server. We exchange tokens inside redirectToAuthorization(), but the SDK has already decided to return 'REDIRECT', causing UnauthorizedError.

Decision ​

Exchange tokens inside redirectToAuthorization() and document the retry pattern as the expected usage:

typescript
// First connect triggers OAuth flow and saves tokens, but SDK returns
// 'REDIRECT' before checking. Second connect finds valid tokens.
async function connectWithOAuthRetry(client, serverUrl, authProvider) {
  const transport = new StreamableHTTPClientTransport(serverUrl, {
    authProvider,
  });
  try {
    await client.connect(transport);
  } catch (error) {
    if (error.message === "Unauthorized") {
      await client.connect(
        new StreamableHTTPClientTransport(serverUrl, { authProvider }),
      );
    } else throw error;
  }
}

Why a new transport on retry? The transport caches connection state internally. A fresh transport ensures clean reconnection.

Rationale ​

  • SDK constraint: No hook exists between redirect completion and the 'REDIRECT' return. The SDK interface (Promise<void>) cannot signal "auth completed."
  • In-process capture: CLI apps don't have page redirects that would trigger a fresh auth check cycle.
  • Correctness over elegance: The retry is unusual but reliable—tokens are always saved before the error.

Alternatives Considered ​

AlternativeWhy Rejected
transport.finishAuth(callbackUrl)Breaks provider encapsulation; doesn't fit in-process capture
Return tokens from redirectToAuthorization()SDK interface expects Promise<void>
Upstream SDK changeNot viable for library consumers

Impact ​

  • Positive: Self-contained auth flow; no external coordination needed
  • Negative: First connection always throws UnauthorizedError after OAuth—must be documented clearly
  • Code: src/auth/browser-auth.ts lines 254-368
  • MCP SDK auth interface: @modelcontextprotocol/sdk/client/auth.js
  • Related: ADR-001 (no refresh tokens)