UNPKG

@builder.io/dev-tools

Version:

Builder.io Visual CMS Devtools

129 lines (128 loc) 5.9 kB
/** * 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>; }