-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Description
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.1or[::1](notlocalhost) - 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:
- Start a local HTTP server with
server.listen(0, '127.0.0.1') - Wait for the
listeningcallback to get the assigned port - 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-OAuthClientProviderinterface andstartAuthorization()src/examples/client/simpleOAuthClient.ts- Example that currently uses hardcoded port