diff --git a/README.md b/README.md index 4684c67c7..fdc02a82e 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ - [Dynamic Servers](#dynamic-servers) - [Low-Level Server](#low-level-server) - [Writing MCP Clients](#writing-mcp-clients) + - [OAuth Client Configuration](#oauth-client-configuration) - [Proxy Authorization Requests Upstream](#proxy-authorization-requests-upstream) - [Backwards Compatibility](#backwards-compatibility) - [Documentation](#documentation) @@ -1162,6 +1163,134 @@ const result = await client.callTool({ ``` +### OAuth Client Configuration + +The MCP SDK provides comprehensive OAuth 2.0 client support with dynamic client registration and multiple authentication methods. + +#### Basic OAuth Client Setup + +```typescript +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"; + +class MyOAuthProvider implements OAuthClientProvider { + get redirectUrl() { return "http://localhost:3000/callback"; } + + get clientMetadata() { + return { + redirect_uris: ["http://localhost:3000/callback"], + client_name: "My MCP Client", + scope: "mcp:tools mcp:resources" + }; + } + + async clientInformation() { + // Return stored client info or undefined for dynamic registration + return this.loadClientInfo(); + } + + async saveClientInformation(info) { + // Store client info after registration + await this.storeClientInfo(info); + } + + async tokens() { + // Return stored tokens or undefined + return this.loadTokens(); + } + + async saveTokens(tokens) { + // Store OAuth tokens + await this.storeTokens(tokens); + } + + async redirectToAuthorization(url) { + // Redirect user to authorization URL + window.location.href = url.toString(); + } + + async saveCodeVerifier(verifier) { + // Store PKCE code verifier + sessionStorage.setItem('code_verifier', verifier); + } + + async codeVerifier() { + // Return stored code verifier + return sessionStorage.getItem('code_verifier'); + } +} + +const authProvider = new MyOAuthProvider(); +const transport = new StreamableHTTPClientTransport(serverUrl, { + authProvider +}); + +const client = new Client({ name: "oauth-client", version: "1.0.0" }); +await client.connect(transport); +``` + +#### DCR Registration Access Token Support (RFC 7591) + +For authorization servers that require pre-authorization for dynamic client registration, the SDK supports DCR registration access tokens (called "initial access token" in RFC 7591). + +The SDK automatically checks for a `DCR_REGISTRATION_ACCESS_TOKEN` environment variable. For custom logic, implement the `dcrRegistrationAccessToken()` method in your OAuth provider: + +##### Method 1: Environment Variable (Default) +```bash +export DCR_REGISTRATION_ACCESS_TOKEN="your-initial-access-token" +``` + +##### Method 2: Custom Provider Method +```typescript +class MyOAuthProvider implements OAuthClientProvider { + // ... other methods ... + + async dcrRegistrationAccessToken() { + // Custom fallback logic: check parameter, then env var, then storage + return this.explicitToken + || process.env.DCR_REGISTRATION_ACCESS_TOKEN + || await this.loadFromSecureStorage('dcr_registration_access_token'); + } +} +``` + +The SDK will: +1. Call your `dcrRegistrationAccessToken()` method (if implemented) +2. Fall back to `DCR_REGISTRATION_ACCESS_TOKEN` environment variable +3. Proceed without token (for servers that don't require pre-authorization) + +#### Complete OAuth Flow Example + +```typescript +// After user authorization, handle the callback +async function handleAuthCallback(authorizationCode: string) { + await transport.finishAuth(authorizationCode); + // Client is now authenticated and ready to use + + const result = await client.callTool({ + name: "example-tool", + arguments: { param: "value" } + }); +} + +// Start the OAuth flow +try { + await client.connect(transport); + console.log("Already authenticated"); +} catch (error) { + if (error instanceof UnauthorizedError) { + console.log("OAuth authorization required"); + // User will be redirected to authorization server + // Handle the callback when they return + } +} +``` + +For complete working examples of OAuth with DCR token support, see: +- [`src/examples/client/simpleOAuthClient.ts`](src/examples/client/simpleOAuthClient.ts) - Basic OAuth client with DCR support +- [`src/examples/client/advancedDcrOAuthClient.ts`](src/examples/client/advancedDcrOAuthClient.ts) - Advanced DCR strategies for production + ### Proxy Authorization Requests Upstream You can proxy OAuth requests to an external authorization provider: diff --git a/package-lock.json b/package-lock.json index 01bc09539..fa1bde0eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.15.0", + "version": "1.15.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/sdk", - "version": "1.15.0", + "version": "1.15.1", "license": "MIT", "dependencies": { "ajv": "^6.12.6", diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index ce0cc7081..693405067 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -1158,6 +1158,213 @@ describe("OAuth Authorization", () => { }) ).rejects.toThrow("Dynamic client registration failed"); }); + + describe("DCR registration access token support", () => { + it("includes DCR token from provider method", async () => { + const mockProvider: OAuthClientProvider = { + get redirectUrl() { return "http://localhost:3000/callback"; }, + get clientMetadata() { return validClientMetadata; }, + clientInformation: jest.fn(), + tokens: jest.fn(), + saveTokens: jest.fn(), + redirectToAuthorization: jest.fn(), + saveCodeVerifier: jest.fn(), + codeVerifier: jest.fn(), + dcrRegistrationAccessToken: jest.fn().mockResolvedValue("provider-dcr-token"), + }; + + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => validClientInfo, + }); + + await registerClient("https://auth.example.com", { + clientMetadata: validClientMetadata, + provider: mockProvider, + }); + + expect(mockProvider.dcrRegistrationAccessToken).toHaveBeenCalled(); + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + href: "https://auth.example.com/register", + }), + expect.objectContaining({ + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer provider-dcr-token", + }, + body: JSON.stringify(validClientMetadata), + }) + ); + }); + + it("falls back to environment variable when provider method not implemented", async () => { + const originalEnv = process.env.DCR_REGISTRATION_ACCESS_TOKEN; + process.env.DCR_REGISTRATION_ACCESS_TOKEN = "env-dcr-token"; + + try { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => validClientInfo, + }); + + await registerClient("https://auth.example.com", { + clientMetadata: validClientMetadata, + // No provider passed + }); + + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + href: "https://auth.example.com/register", + }), + expect.objectContaining({ + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer env-dcr-token", + }, + body: JSON.stringify(validClientMetadata), + }) + ); + } finally { + if (originalEnv !== undefined) { + process.env.DCR_REGISTRATION_ACCESS_TOKEN = originalEnv; + } else { + delete process.env.DCR_REGISTRATION_ACCESS_TOKEN; + } + } + }); + + it("prioritizes provider method over environment variable", async () => { + const originalEnv = process.env.DCR_REGISTRATION_ACCESS_TOKEN; + process.env.DCR_REGISTRATION_ACCESS_TOKEN = "env-dcr-token"; + + try { + const mockProvider: OAuthClientProvider = { + get redirectUrl() { return "http://localhost:3000/callback"; }, + get clientMetadata() { return validClientMetadata; }, + clientInformation: jest.fn(), + tokens: jest.fn(), + saveTokens: jest.fn(), + redirectToAuthorization: jest.fn(), + saveCodeVerifier: jest.fn(), + codeVerifier: jest.fn(), + dcrRegistrationAccessToken: jest.fn().mockResolvedValue("provider-dcr-token"), + }; + + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => validClientInfo, + }); + + await registerClient("https://auth.example.com", { + clientMetadata: validClientMetadata, + provider: mockProvider, + }); + + expect(mockProvider.dcrRegistrationAccessToken).toHaveBeenCalled(); + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + href: "https://auth.example.com/register", + }), + expect.objectContaining({ + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer provider-dcr-token", + }, + body: JSON.stringify(validClientMetadata), + }) + ); + } finally { + if (originalEnv !== undefined) { + process.env.DCR_REGISTRATION_ACCESS_TOKEN = originalEnv; + } else { + delete process.env.DCR_REGISTRATION_ACCESS_TOKEN; + } + } + }); + + it("handles provider method returning undefined and falls back to env var", async () => { + const originalEnv = process.env.DCR_REGISTRATION_ACCESS_TOKEN; + process.env.DCR_REGISTRATION_ACCESS_TOKEN = "env-dcr-token"; + + try { + const mockProvider: OAuthClientProvider = { + get redirectUrl() { return "http://localhost:3000/callback"; }, + get clientMetadata() { return validClientMetadata; }, + clientInformation: jest.fn(), + tokens: jest.fn(), + saveTokens: jest.fn(), + redirectToAuthorization: jest.fn(), + saveCodeVerifier: jest.fn(), + codeVerifier: jest.fn(), + dcrRegistrationAccessToken: jest.fn().mockResolvedValue(undefined), + }; + + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => validClientInfo, + }); + + await registerClient("https://auth.example.com", { + clientMetadata: validClientMetadata, + provider: mockProvider, + }); + + expect(mockProvider.dcrRegistrationAccessToken).toHaveBeenCalled(); + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + href: "https://auth.example.com/register", + }), + expect.objectContaining({ + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer env-dcr-token", + }, + body: JSON.stringify(validClientMetadata), + }) + ); + } finally { + if (originalEnv !== undefined) { + process.env.DCR_REGISTRATION_ACCESS_TOKEN = originalEnv; + } else { + delete process.env.DCR_REGISTRATION_ACCESS_TOKEN; + } + } + }); + + it("registers without authorization header when no token available", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => validClientInfo, + }); + + await registerClient("https://auth.example.com", { + clientMetadata: validClientMetadata, + }); + + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + href: "https://auth.example.com/register", + }), + expect.objectContaining({ + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(validClientMetadata), + }) + ); + }); + }); }); describe("auth function", () => { diff --git a/src/client/auth.ts b/src/client/auth.ts index 4a8bbe2d2..323cec2fe 100644 --- a/src/client/auth.ts +++ b/src/client/auth.ts @@ -124,6 +124,17 @@ export interface OAuthClientProvider { * This avoids requiring the user to intervene manually. */ invalidateCredentials?(scope: 'all' | 'client' | 'tokens' | 'verifier'): void | Promise; + + /** + * If implemented, provides a DCR registration access token (called "initial access token" in RFC 7591) for OAuth 2.0 Dynamic Client Registration + * according to RFC 7591. This token is used to authorize the client registration request. + * + * The DCR registration access token allows the client to register with authorization servers that + * require pre-authorization for dynamic client registration. + * + * @returns The DCR registration access token string, or undefined if none is available + */ + dcrRegistrationAccessToken?(): string | undefined | Promise; } export type AuthResult = "AUTHORIZED" | "REDIRECT"; @@ -344,6 +355,7 @@ async function authInternal( const fullInformation = await registerClient(authorizationServerUrl, { metadata, clientMetadata: provider.clientMetadata, + provider, }); await provider.saveClientInformation(fullInformation); @@ -877,15 +889,25 @@ export async function refreshAuthorization( /** * Performs OAuth 2.0 Dynamic Client Registration according to RFC 7591. + * + * Supports DCR registration access tokens (called "initial access token" in RFC 7591) for authorization servers that require + * pre-authorization for dynamic client registration. The DCR registration access token + * is resolved using a clean 2-level fallback approach: + * + * 1. Provider's `dcrRegistrationAccessToken()` method (if implemented) + * 2. `DCR_REGISTRATION_ACCESS_TOKEN` environment variable (automatic fallback) + * 3. None (for servers that don't require pre-authorization) */ export async function registerClient( authorizationServerUrl: string | URL, { metadata, clientMetadata, + provider, }: { metadata?: OAuthMetadata; clientMetadata: OAuthClientMetadata; + provider?: OAuthClientProvider; }, ): Promise { let registrationUrl: URL; @@ -900,11 +922,19 @@ export async function registerClient( registrationUrl = new URL("/register", authorizationServerUrl); } + const headers: Record = { + "Content-Type": "application/json", + }; + + // Add DCR registration access token (RFC 7591 "initial access token") if available + const token = await resolveDcrToken(provider); + if (token) { + headers["Authorization"] = `Bearer ${token}`; + } + const response = await fetch(registrationUrl, { method: "POST", - headers: { - "Content-Type": "application/json", - }, + headers, body: JSON.stringify(clientMetadata), }); @@ -914,3 +944,22 @@ export async function registerClient( return OAuthClientInformationFullSchema.parse(await response.json()); } + +/** + * Internal helper to resolve DCR registration access token from provider and environment. + * Implements a clean 2-level fallback: provider method → environment variable. + */ +async function resolveDcrToken(provider?: OAuthClientProvider): Promise { + // Level 1: Provider method + if (provider?.dcrRegistrationAccessToken) { + const token = await Promise.resolve(provider.dcrRegistrationAccessToken()); + if (token) return token; + } + + // Level 2: Environment variable + if (typeof process !== 'undefined' && process.env?.DCR_REGISTRATION_ACCESS_TOKEN) { + return process.env.DCR_REGISTRATION_ACCESS_TOKEN; + } + + return undefined; +} diff --git a/src/client/sse.test.ts b/src/client/sse.test.ts index 2cc4a1dd7..e45ec6ab3 100644 --- a/src/client/sse.test.ts +++ b/src/client/sse.test.ts @@ -1107,5 +1107,79 @@ describe("SSEClientTransport", () => { await expect(() => transport.start()).rejects.toThrow(InvalidGrantError); expect(mockAuthProvider.invalidateCredentials).toHaveBeenCalledWith('tokens'); }); + + describe("DCR registration access token support via provider", () => { + it("calls auth without initialAccessToken parameter when using provider with DCR method", async () => { + // Create a mock provider with DCR token method + const providerWithDcr = { + ...mockAuthProvider, + dcrRegistrationAccessToken: jest.fn().mockResolvedValue("provider-dcr-token") + }; + + // Create a spy on the auth module + const authModule = await import("./auth.js"); + const authSpy = jest.spyOn(authModule, "auth").mockResolvedValue("AUTHORIZED"); + + transport = new SSEClientTransport( + resourceBaseUrl, + { authProvider: providerWithDcr } + ); + + // Test finishAuth method which calls auth directly + await transport.finishAuth("test-auth-code"); + + // Verify auth was called without initialAccessToken parameter + expect(authSpy).toHaveBeenCalledWith( + providerWithDcr, + expect.objectContaining({ + serverUrl: resourceBaseUrl, + authorizationCode: "test-auth-code" + }) + ); + + // Verify the deprecated parameter is NOT included + expect(authSpy).not.toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + initialAccessToken: expect.anything() + }) + ); + + authSpy.mockRestore(); + }); + + it("calls auth without initialAccessToken parameter when using provider without DCR method", async () => { + // Use the regular mock provider (no DCR method) + const authModule = await import("./auth.js"); + const authSpy = jest.spyOn(authModule, "auth").mockResolvedValue("AUTHORIZED"); + + transport = new SSEClientTransport( + resourceBaseUrl, + { authProvider: mockAuthProvider } + ); + + // Test finishAuth method which calls auth directly + await transport.finishAuth("test-auth-code"); + + // Verify auth was called correctly without the deprecated parameter + expect(authSpy).toHaveBeenCalledWith( + mockAuthProvider, + expect.objectContaining({ + serverUrl: resourceBaseUrl, + authorizationCode: "test-auth-code" + }) + ); + + // Verify no initialAccessToken parameter + expect(authSpy).not.toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + initialAccessToken: expect.anything() + }) + ); + + authSpy.mockRestore(); + }); + }); }); }); diff --git a/src/client/streamableHttp.test.ts b/src/client/streamableHttp.test.ts index c54cf2896..1db3db484 100644 --- a/src/client/streamableHttp.test.ts +++ b/src/client/streamableHttp.test.ts @@ -855,4 +855,146 @@ describe("StreamableHTTPClientTransport", () => { await expect(transport.send(message)).rejects.toThrow(UnauthorizedError); expect(mockAuthProvider.invalidateCredentials).toHaveBeenCalledWith('tokens'); }); + + describe("DCR registration access token support via provider", () => { + it("works with provider that implements dcrRegistrationAccessToken method", async () => { + // Create a mock provider with DCR token method + const providerWithDcr = { + ...mockAuthProvider, + dcrRegistrationAccessToken: jest.fn().mockResolvedValue("provider-dcr-token") + }; + + // Create a spy on the auth module + const authModule = await import("./auth.js"); + const authSpy = jest.spyOn(authModule, "auth").mockResolvedValue("REDIRECT"); + + const transport = new StreamableHTTPClientTransport( + new URL("http://localhost:1234/mcp"), + { authProvider: providerWithDcr } + ); + + // Mock fetch to trigger auth flow on send (401 response) + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + status: 401, + headers: new Headers(), + }); + + const message = { + jsonrpc: "2.0" as const, + method: "test", + params: {}, + id: "test-id" + }; + + try { + await transport.send(message); + } catch { + // Expected to fail due to mock setup, we're just testing auth call + } + + // Verify auth was called with the provider (no initialAccessToken parameter) + expect(authSpy).toHaveBeenCalledWith( + providerWithDcr, + expect.objectContaining({ + serverUrl: new URL("http://localhost:1234/mcp") + }) + ); + + // Verify the initialAccessToken parameter is NOT included + expect(authSpy).not.toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + initialAccessToken: expect.anything() + }) + ); + + authSpy.mockRestore(); + }); + + it("works with provider without dcrRegistrationAccessToken method", async () => { + // Use the regular mock provider (no DCR method) + const authModule = await import("./auth.js"); + const authSpy = jest.spyOn(authModule, "auth").mockResolvedValue("REDIRECT"); + + const transport = new StreamableHTTPClientTransport( + new URL("http://localhost:1234/mcp"), + { authProvider: mockAuthProvider } + ); + + // Mock fetch to trigger auth flow on send (401 response) + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + status: 401, + headers: new Headers(), + }); + + const message = { + jsonrpc: "2.0" as const, + method: "test", + params: {}, + id: "test-id" + }; + + try { + await transport.send(message); + } catch { + // Expected to fail due to mock setup, we're just testing auth call + } + + // Verify auth was called correctly without initialAccessToken parameter + expect(authSpy).toHaveBeenCalledWith( + mockAuthProvider, + expect.objectContaining({ + serverUrl: new URL("http://localhost:1234/mcp") + }) + ); + + // Verify no initialAccessToken parameter + expect(authSpy).not.toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + initialAccessToken: expect.anything() + }) + ); + + authSpy.mockRestore(); + }); + + it("handles DCR token during finishAuth flow", async () => { + const providerWithDcr = { + ...mockAuthProvider, + dcrRegistrationAccessToken: jest.fn().mockResolvedValue("provider-dcr-token") + }; + + const authModule = await import("./auth.js"); + const authSpy = jest.spyOn(authModule, "auth").mockResolvedValue("AUTHORIZED"); + + const transport = new StreamableHTTPClientTransport( + new URL("http://localhost:1234/mcp"), + { authProvider: providerWithDcr } + ); + + // Test the finishAuth flow which also calls auth() + await transport.finishAuth("test-auth-code"); + + expect(authSpy).toHaveBeenCalledWith( + providerWithDcr, + expect.objectContaining({ + serverUrl: new URL("http://localhost:1234/mcp"), + authorizationCode: "test-auth-code" + }) + ); + + // Verify no initialAccessToken parameter + expect(authSpy).not.toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + initialAccessToken: expect.anything() + }) + ); + + authSpy.mockRestore(); + }); + }); }); diff --git a/src/examples/README.md b/src/examples/README.md index ac92e8ded..3d12ff271 100644 --- a/src/examples/README.md +++ b/src/examples/README.md @@ -36,9 +36,69 @@ npx tsx src/examples/client/simpleStreamableHttp.ts Example client with OAuth: ```bash -npx tsx src/examples/client/simpleOAuthClient.js +npx tsx src/examples/client/simpleOAuthClient.ts ``` +#### OAuth DCR Registration Access Token Support (RFC 7591) + +The OAuth client example demonstrates comprehensive support for DCR (Dynamic Client Registration) registration access tokens (called "initial access token" in RFC 7591) for authorization servers that require pre-authorization. The example shows multiple ways to provide DCR tokens: + +##### Method 1: Environment Variable (Automatic SDK Fallback) +```bash +export DCR_REGISTRATION_ACCESS_TOKEN="your-initial-access-token" +npx tsx src/examples/client/simpleOAuthClient.ts +``` + +##### Method 2: Command Line Argument +```bash +npx tsx src/examples/client/simpleOAuthClient.ts --dcr-token "your-initial-access-token" +``` + +##### Method 3: Custom Provider Implementation +The example shows how to implement custom DCR token logic in your OAuth provider: + +```typescript +class MyOAuthProvider implements OAuthClientProvider { + // ... other methods ... + + async dcrRegistrationAccessToken(): Promise { + // Custom fallback logic: + // 1. Check explicit parameter + // 2. Check command line arguments + // 3. Check environment variables + // 4. Check secure storage (keychain, vault, etc.) + return this.getTokenFromCustomSource(); + } +} +``` + +The SDK implements a clean 2-level fallback: +1. **Provider method**: Custom `dcrRegistrationAccessToken()` implementation (if provided) +2. **Environment variable**: `DCR_REGISTRATION_ACCESS_TOKEN` (automatic fallback for RFC 7591 "initial access token") +3. **None**: Proceed without pre-authorization (for servers that don't require it) + +#### Advanced DCR Strategies Example + +For production environments requiring sophisticated DCR token management (called "initial access token" in RFC 7591), see the advanced example: + +```bash +# Demonstrate all DCR strategies +npx tsx src/examples/client/advancedDcrOAuthClient.ts + +# Demo strategies only (no connection attempt) +npx tsx src/examples/client/advancedDcrOAuthClient.ts --demo-only + +# Attempt real connection with DCR support +npx tsx src/examples/client/advancedDcrOAuthClient.ts --connect --dcr-token "your-initial-access-token" +``` + +This example demonstrates: +- **Token exchange**: Dynamic DCR initial access token acquisition via client credentials +- **Secure storage**: Integration with OS keychain, HashiCorp Vault, AWS Secrets Manager +- **Multiple environment variables**: Support for various DCR token env var names (RFC 7591 "initial access token") +- **Fallback strategies**: Comprehensive 6-level fallback approach +- **Production patterns**: Real-world deployment scenarios and security practices + ### Backwards Compatible Client A client that implements backwards compatibility according to the [MCP specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#backwards-compatibility), allowing it to work with both new and legacy servers. This client demonstrates: diff --git a/src/examples/client/simpleOAuthClient.ts b/src/examples/client/simpleOAuthClient.ts index 4531f4c2a..0580e8b8a 100644 --- a/src/examples/client/simpleOAuthClient.ts +++ b/src/examples/client/simpleOAuthClient.ts @@ -32,7 +32,8 @@ class InMemoryOAuthClientProvider implements OAuthClientProvider { constructor( private readonly _redirectUrl: string | URL, private readonly _clientMetadata: OAuthClientMetadata, - onRedirect?: (url: URL) => void + onRedirect?: (url: URL) => void, + private readonly _dcrToken?: string ) { this._onRedirect = onRedirect || ((url) => { console.log(`Redirect to: ${url.toString()}`); @@ -79,6 +80,55 @@ class InMemoryOAuthClientProvider implements OAuthClientProvider { } return this._codeVerifier; } + + /** + * DCR registration access token provider (RFC 7591) + * Provides "initial access token" as defined in RFC 7591 for Dynamic Client Registration + * This demonstrates custom DCR token logic with fallback strategies + */ + async dcrRegistrationAccessToken(): Promise { + // Strategy 1: Use explicit token provided to constructor + if (this._dcrToken) { + console.log('🔑 Using DCR token from constructor parameter'); + return this._dcrToken; + } + + // Strategy 2: Check for command line argument + const args = process.argv; + const dcrArgIndex = args.findIndex(arg => arg === '--dcr-token'); + if (dcrArgIndex !== -1 && args[dcrArgIndex + 1]) { + console.log('🔑 Using DCR token from command line argument'); + return args[dcrArgIndex + 1]; + } + + // Strategy 3: Check environment variable (this is also the SDK's automatic fallback) + if (process.env.DCR_REGISTRATION_ACCESS_TOKEN) { + console.log('🔑 Using DCR token from DCR_REGISTRATION_ACCESS_TOKEN environment variable'); + console.log(' (RFC 7591 "initial access token")'); + return process.env.DCR_REGISTRATION_ACCESS_TOKEN; + } + + // Strategy 4: Could load from secure storage (e.g., keychain, vault) + // const tokenFromStorage = await this.loadDcrTokenFromSecureStorage(); + // if (tokenFromStorage) { + // console.log('🔑 Using DCR token from secure storage'); + // return tokenFromStorage; + // } + + console.log('â„šī¸ No DCR registration access token available - proceeding without pre-authorization'); + return undefined; + } + + // Example method for secure storage (not implemented in this demo) + // private async loadDcrTokenFromSecureStorage(): Promise { + // // In production, you might load from: + // // - OS keychain/keyring + // // - HashiCorp Vault + // // - AWS Secrets Manager + // // - Azure Key Vault + // // - etc. + // return undefined; + // } } /** * Interactive MCP client with OAuth authentication @@ -224,6 +274,23 @@ class InteractiveOAuthClient { }; console.log('🔐 Creating OAuth provider...'); + + // Check for DCR token from command line (--dcr-token ) + const args = process.argv; + const dcrArgIndex = args.findIndex(arg => arg === '--dcr-token'); + const explicitDcrToken = dcrArgIndex !== -1 && args[dcrArgIndex + 1] ? args[dcrArgIndex + 1] : undefined; + + if (explicitDcrToken) { + console.log('🔑 DCR registration access token provided via command line'); + console.log(' This will be used for pre-authorized dynamic client registration (RFC 7591)'); + } else if (process.env.DCR_REGISTRATION_ACCESS_TOKEN) { + console.log('🔑 DCR registration access token available via environment variable'); + console.log(' This will be used for pre-authorized dynamic client registration (RFC 7591)'); + } else { + console.log('â„šī¸ No DCR registration access token provided (proceeding without pre-authorization)'); + console.log(' Client registration will proceed normally (if the auth server supports it)'); + } + const oauthProvider = new InMemoryOAuthClientProvider( CALLBACK_URL, clientMetadata, @@ -231,7 +298,8 @@ class InteractiveOAuthClient { console.log(`📌 OAuth redirect handler called - opening browser`); console.log(`Opening browser to: ${redirectUrl.toString()}`); this.openBrowser(redirectUrl.toString()); - } + }, + explicitDcrToken // Pass DCR token to provider ); console.log('🔐 OAuth provider created');