10BC0 Support RFC 8252 dynamic loopback port selection for native OAuth clients · Issue #1316 · modelcontextprotocol/typescript-sdk · GitHub
[go: up one dir, main page]

Skip to content

Support RFC 8252 dynamic loopback port selection for native OAuth clients #1316

@BestOwl

Description

@BestOwl

Summary

The current OAuthClientProvider interface requires redirectUrl to be a synchronous getter, which makes it difficult for native apps to implement RFC 8252-compliant dynamic port selection for loopback redirect URIs.

Background

RFC 8252 Section 7.3 specifies best practices for OAuth 2.0 in native applications:

Native apps that are able to open a port on the loopback network interface [...] can use the loopback interface to receive the OAuth redirect.

Loopback redirect URIs use the "http" scheme and are constructed with the loopback IP literal and whatever port the client is listening on.

The authorization server MUST allow any port to be specified at the time of the request for loopback IP redirect URIs, to accommodate clients that obtain an available ephemeral port from the operating system at the time of the request.

Key requirements:

  • Use 127.0.0.1 or [::1] (not localhost)
  • Let OS assign an available ephemeral port (server.listen(0))
  • Authorization servers must accept any port for loopback URIs

Current Limitation

The OAuthClientProvider interface defines redirectUrl as a synchronous getter:

// src/client/auth.ts
export interface OAuthClientProvider {
  get redirectUrl(): string | URL | undefined;
  // ...
}

This is accessed in startAuthorization() before the authorization flow begins:

// src/client/auth.ts:514-518
const { authorizationUrl, codeVerifier } = await startAuthorization(authorizationServerUrl, {
  // ...
  redirectUrl: provider.redirectUrl,  // <-- sync getter called here
  // ...
});

The problem: To get a dynamic port, native apps need to:

  1. Start a local HTTP server with server.listen(0, '127.0.0.1')
  2. Wait for the listening callback to get the assigned port
  3. Return the redirect URL with the actual port

Step 2 is async, but redirectUrl is a sync getter.

Current Workaround

Native app developers must start the callback server before creating the OAuthClientProvider, even if OAuth might not be needed (e.g., cached tokens are valid). This is wasteful and goes against the "start server only when needed" pattern shown in simpleOAuthClient.ts.

Proposed Solution

Consider making redirectUrl support async resolution. Options:

Option A: Allow async getter

get redirectUrl(): string | URL | undefined | Promise<string | URL | undefined>;

Option B: Callback-based lazy initialization

interface OAuthClientProvider {
  // Called when redirect URL is needed, provider can start server here
  prepareForAuthorization?(): Promise<void>;
  get redirectUrl(): string | URL | undefined;
}

Option B might be cleanest as it gives providers a hook to do async setup before redirectUrl is accessed.

Impact

This would enable native MCP clients to:

  • Avoid port conflicts when running multiple instances
  • Follow RFC 8252 security best practices
  • Start callback servers lazily (only when authorization is actually needed)

Related Files

  • src/client/auth.ts - OAuthClientProvider interface and startAuthorization()
  • src/examples/client/simpleOAuthClient.ts - Example that currently uses hardcoded port

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    authIssues and PRs related to Authentication / OAuthenhancementRequest for a new feature that's not currently supportedneeds designValid issue but needs maintainer alignment on design or approach

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      0