@builder.io/dev-tools
Version:
Builder.io Visual CMS Devtools
129 lines (128 loc) • 5.9 kB
TypeScript
/**
* CLI-side OAuth client provider for MCP servers.
*
* Construction adapter for {@link BaseMCPOAuthProvider} (lives in
* `vcp-common/mcp-oauth.ts`). The base provider drives the entire OAuth
* state machine (discovery, DCR, refresh, PKCE, callback exchange); this
* file wires it to file-backed persistence and a loopback redirect — the
* native-app counterpart to the server's
* `FirestoreOAuthProvider` + `ServerCallbackRedirect` pair.
*
* Construction is asynchronous because the loopback redirect needs to bind
* a port and read it back BEFORE the SDK's `clientMetadata.redirect_uris`
* is set (the auth server must see the live port at registration / discovery
* time). Use {@link LocalOAuthProvider.create} rather than `new` directly.
*
* Wiring back from a successful browser callback into the base provider:
* ```ts
* const provider = await LocalOAuthProvider.create({ ... });
* // ... SDK runs auth flow, opens browser via redirect.onAuthorizationRequired ...
* const { code, state } = await provider.awaitCallback();
* const tokens = await provider.handleCallback(code, state);
* ```
*
* The "caller awaits, caller hands code back" pattern matches how the
* Express callback route at `/mcp/oauth/callback` drives the server-side
* flow — neither redirect strategy needs a back-reference to its provider.
*/
import type { OAuthClientInformation, OAuthClientMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
import { BaseMCPOAuthProvider } from "#vcp-common/mcp-oauth.js";
import { LoopbackRedirect, type LoopbackCallback, type LoopbackRedirectOptions } from "./mcp-oauth-redirect-loopback";
export interface LocalOAuthProviderOptions {
/** OAuth-protected MCP server URL. Used as the canonical store key. */
serverUrl: string;
/** Display name (`mcp.json` key). Recorded on PKCE state rows. */
serverName: string;
/**
* Pre-registered DCR client ID. Use for IdPs that don't expose
* dynamic client registration — the CLI then skips DCR and uses these
* pinned values directly. Sourced from `mcp.json`'s `oauth.clientId`.
*/
pinnedClientId?: string;
/**
* Pre-registered DCR client secret. Optional even when `pinnedClientId`
* is set — public clients with `token_endpoint_auth_method: "none"` have
* no secret. Sourced from `mcp.json`'s `oauth.clientSecret`.
*/
pinnedClientSecret?: string;
/**
* Fixed redirect port. Use for IdPs that require pre-registered redirect
* URIs and won't accept the kernel-assigned ephemeral port. Sourced from
* `mcp.json`'s `oauth.redirectPort`.
*/
pinnedRedirectPort?: number;
/**
* If true, the loopback redirect prints the authorization URL to stderr
* instead of opening a browser. Wired up to `--no-open` for SSH workflows
* (Phase 3).
*/
noOpen?: boolean;
/**
* Override the credentials directory. See
* {@link FileMCPOAuthStoreOptions.baseDir}.
*/
storeDir?: string;
/**
* Override the static client metadata. Default is
* {@link defaultClientMetadata}; rare to need an override outside tests.
*/
clientMetadata?: OAuthClientMetadata;
/**
* Override the loopback timeout / browser opener. Mostly for tests.
*/
loopbackOptions?: Pick<LoopbackRedirectOptions, "timeoutMs" | "openImpl" | "stderrWrite">;
/**
* Optional warning sink. Production callers wire this to the CLI logger
* + Sentry; tests pass a recording callback.
*/
onWarning?: (message: string, error: unknown, context?: Record<string, unknown>) => void;
}
/**
* File-backed + loopback OAuth provider for CLI-managed MCPs. Construction
* adapter over {@link BaseMCPOAuthProvider}; the base class owns the OAuth
* state machine, this class only owns construction wiring.
*
* Construction is asynchronous (port bind happens before super()). Use
* {@link LocalOAuthProvider.create}; the constructor itself is private to
* make this contract explicit.
*/
export declare class LocalOAuthProvider extends BaseMCPOAuthProvider {
/** The loopback redirect — exposed for `awaitCallback()` and `close()`. */
readonly loopback: LoopbackRedirect;
private constructor();
/**
* Build a `LocalOAuthProvider`. Binds the loopback port before returning,
* so the caller can immediately register `provider.redirectUrl` with the
* IdP (DCR or pre-registration).
*
* If `pinnedClientId` is provided, this method also pre-seeds the
* file-backed store with metadata stub so the base provider's
* `clientInformation()` returns the pinned values without running DCR.
* Discovery still runs to populate `authorization_endpoint` /
* `token_endpoint` — which the static pin can't supply on its own.
*/
static create(options: LocalOAuthProviderOptions): Promise<LocalOAuthProvider>;
/**
* Wait for the loopback redirect to receive a `GET /oauth/callback?...`
* Resolves with the `(code, state)` tuple to feed into
* {@link BaseMCPOAuthProvider.handleCallback}.
*
* Convenience pass-through; the redirect is also reachable as
* `provider.loopback.awaitCallback()` if a caller wants finer-grained
* lifecycle control.
*/
awaitCallback(): Promise<LoopbackCallback>;
/**
* Tear down the loopback listener, releasing the bound port. Idempotent.
* Call this if the auth flow is abandoned (user cancellation, timeout
* external to the loopback's own timeout) before
* {@link awaitCallback} resolves.
*/
close(): Promise<void>;
/**
* Test helper / future inspection: returns the pinned-or-discovered
* client_id without forcing the caller through `clientInformation()`'s
* undefined-handling.
*/
getClientInformation(): Promise<OAuthClientInformation | undefined>;
}