import open from "npm:open"; import type { OAuthClientProvider } from "npm:@modelcontextprotocol/sdk/client/auth.js"; import "npm:@modelcontextprotocol/sdk/client/auth.js"; import { OAuthClientInformationSchema, OAuthTokensSchema, } from "npm:@modelcontextprotocol/sdk/shared/auth.js"; import type { OAuthClientInformation, OAuthClientInformationFull, OAuthTokens, } from "npm:@modelcontextprotocol/sdk/shared/auth.js"; import type { OAuthProviderOptions } from "./types.ts"; import { readJsonFile, readTextFile, writeJsonFile, writeTextFile, } from "./mcp-auth-config.ts"; import { getServerUrlHash, log, MCP_REMOTE_VERSION } from "./utils.ts"; /** * Implements the OAuthClientProvider interface for Node.js environments. * Handles OAuth flow and token storage for MCP clients. */ export class NodeOAuthClientProvider implements OAuthClientProvider { private serverUrlHash: string; private callbackPath: string; private clientName: string; private clientUri: string; private softwareId: string; private softwareVersion: string; /** * Creates a new NodeOAuthClientProvider * @param options Configuration options for the provider */ constructor(readonly options: OAuthProviderOptions) { this.serverUrlHash = getServerUrlHash(options.serverUrl); this.callbackPath = options.callbackPath || "/oauth/callback"; this.clientName = options.clientName || "MCP CLI Client"; this.clientUri = options.clientUri || "https://github.com/modelcontextprotocol/mcp-cli"; this.softwareId = options.softwareId || "2e6dc280-f3c3-4e01-99a7-8181dbd1d23d"; this.softwareVersion = options.softwareVersion || MCP_REMOTE_VERSION; } get redirectUrl(): string { return `http://127.0.0.1:${this.options.callbackPort}${this.callbackPath}`; } get clientMetadata() { return { redirect_uris: [this.redirectUrl], token_endpoint_auth_method: "none", grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], client_name: this.clientName, client_uri: this.clientUri, software_id: this.softwareId, software_version: this.softwareVersion, }; } /** * Gets the client information if it exists * @returns The client information or undefined */ clientInformation(): Promise { // log('Reading client info') return readJsonFile( this.serverUrlHash, "client_info.json", OAuthClientInformationSchema, ); } /** * Saves client information * @param clientInformation The client information to save */ async saveClientInformation( clientInformation: OAuthClientInformationFull, ): Promise { // log('Saving client info') await writeJsonFile( this.serverUrlHash, "client_info.json", clientInformation, ); } /** * Gets the OAuth tokens if they exist * @returns The OAuth tokens or undefined */ tokens(): Promise { // log('Reading tokens') // console.log(new Error().stack) return readJsonFile( this.serverUrlHash, "tokens.json", OAuthTokensSchema, ); } /** * Saves OAuth tokens * @param tokens The tokens to save */ async saveTokens(tokens: OAuthTokens): Promise { // log('Saving tokens') await writeJsonFile(this.serverUrlHash, "tokens.json", tokens); } /** * Redirects the user to the authorization URL * @param authorizationUrl The URL to redirect to */ async redirectToAuthorization(authorizationUrl: URL): Promise { log( `\nPlease authorize this client by visiting:\n${authorizationUrl.toString()}\n`, ); try { await open(authorizationUrl.toString()); log("Browser opened automatically."); } catch (_error) { log( "Could not open browser automatically. Please copy and paste the URL above into your browser.", ); } } /** * Saves the PKCE code verifier * @param codeVerifier The code verifier to save */ async saveCodeVerifier(codeVerifier: string): Promise { // log('Saving code verifier') await writeTextFile(this.serverUrlHash, "code_verifier.txt", codeVerifier); } /** * Gets the PKCE code verifier * @returns The code verifier */ async codeVerifier(): Promise { // log('Reading code verifier') return await readTextFile( this.serverUrlHash, "code_verifier.txt", "No code verifier saved for session", ); } }