From 551b5450905cd24a06e3b9b435bb2422a82c87ef Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Mon, 2 Jun 2025 19:07:59 +1000 Subject: [PATCH] Prettified --- src/auth/browser-provider.ts | 180 ++++++++++++++++++----------------- src/auth/callback.ts | 95 +++++++++--------- src/auth/types.ts | 22 ++--- src/index.ts | 8 +- src/react/README.md | 105 ++++++++++---------- src/react/index.ts | 6 +- src/react/types.ts | 52 +++++----- src/utils/assert.ts | 4 +- 8 files changed, 236 insertions(+), 236 deletions(-) diff --git a/src/auth/browser-provider.ts b/src/auth/browser-provider.ts index c6104bf..ee3da8d 100644 --- a/src/auth/browser-provider.ts +++ b/src/auth/browser-provider.ts @@ -1,41 +1,43 @@ // browser-provider.ts -import { OAuthClientInformation, OAuthMetadata, OAuthTokens, OAuthClientMetadata } from '@modelcontextprotocol/sdk/shared/auth.js'; -import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'; +import { OAuthClientInformation, OAuthMetadata, OAuthTokens, OAuthClientMetadata } from '@modelcontextprotocol/sdk/shared/auth.js' +import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js' // Assuming StoredState is defined in ./types.js and includes fields for provider options -import { StoredState } from './types.js'; // Adjust path if necessary +import { StoredState } from './types.js' // Adjust path if necessary /** * Browser-compatible OAuth client provider for MCP using localStorage. */ export class BrowserOAuthClientProvider implements OAuthClientProvider { - readonly serverUrl: string; - readonly storageKeyPrefix: string; - readonly serverUrlHash: string; - readonly clientName: string; - readonly clientUri: string; - readonly callbackUrl: string; + readonly serverUrl: string + readonly storageKeyPrefix: string + readonly serverUrlHash: string + readonly clientName: string + readonly clientUri: string + readonly callbackUrl: string constructor( serverUrl: string, options: { - storageKeyPrefix?: string; - clientName?: string; - clientUri?: string; - callbackUrl?: string; + storageKeyPrefix?: string + clientName?: string + clientUri?: string + callbackUrl?: string } = {}, ) { - this.serverUrl = serverUrl; - this.storageKeyPrefix = options.storageKeyPrefix || 'mcp:auth'; - this.serverUrlHash = this.hashString(serverUrl); - this.clientName = options.clientName || 'MCP Browser Client'; - this.clientUri = options.clientUri || (typeof window !== 'undefined' ? window.location.origin : ''); - this.callbackUrl = options.callbackUrl || (typeof window !== 'undefined' ? new URL('/oauth/callback', window.location.origin).toString() : '/oauth/callback'); + this.serverUrl = serverUrl + this.storageKeyPrefix = options.storageKeyPrefix || 'mcp:auth' + this.serverUrlHash = this.hashString(serverUrl) + this.clientName = options.clientName || 'MCP Browser Client' + this.clientUri = options.clientUri || (typeof window !== 'undefined' ? window.location.origin : '') + this.callbackUrl = + options.callbackUrl || + (typeof window !== 'undefined' ? new URL('/oauth/callback', window.location.origin).toString() : '/oauth/callback') } // --- SDK Interface Methods --- get redirectUrl(): string { - return this.callbackUrl; + return this.callbackUrl } get clientMetadata(): OAuthClientMetadata { @@ -47,68 +49,69 @@ export class BrowserOAuthClientProvider implements OAuthClientProvider { client_name: this.clientName, client_uri: this.clientUri, // scope: 'openid profile email mcp', // Example scopes, adjust as needed - }; + } } async clientInformation(): Promise { - const key = this.getKey('client_info'); - const data = localStorage.getItem(key); - if (!data) return undefined; + const key = this.getKey('client_info') + const data = localStorage.getItem(key) + if (!data) return undefined try { // TODO: Add validation using a schema - return JSON.parse(data) as OAuthClientInformation; + return JSON.parse(data) as OAuthClientInformation } catch (e) { - console.warn(`[${this.storageKeyPrefix}] Failed to parse client information:`, e); - localStorage.removeItem(key); - return undefined; + console.warn(`[${this.storageKeyPrefix}] Failed to parse client information:`, e) + localStorage.removeItem(key) + return undefined } } // NOTE: The SDK's auth() function uses this if dynamic registration is needed. // Ensure your OAuthClientInformationFull matches the expected structure if DCR is used. async saveClientInformation(clientInformation: OAuthClientInformation /* | OAuthClientInformationFull */): Promise { - const key = this.getKey('client_info'); + const key = this.getKey('client_info') // Cast needed if handling OAuthClientInformationFull specifically - localStorage.setItem(key, JSON.stringify(clientInformation)); + localStorage.setItem(key, JSON.stringify(clientInformation)) } - async tokens(): Promise { - const key = this.getKey('tokens'); - const data = localStorage.getItem(key); - if (!data) return undefined; + const key = this.getKey('tokens') + const data = localStorage.getItem(key) + if (!data) return undefined try { // TODO: Add validation - return JSON.parse(data) as OAuthTokens; + return JSON.parse(data) as OAuthTokens } catch (e) { - console.warn(`[${this.storageKeyPrefix}] Failed to parse tokens:`, e); - localStorage.removeItem(key); - return undefined; + console.warn(`[${this.storageKeyPrefix}] Failed to parse tokens:`, e) + localStorage.removeItem(key) + return undefined } } async saveTokens(tokens: OAuthTokens): Promise { - const key = this.getKey('tokens'); - localStorage.setItem(key, JSON.stringify(tokens)); + const key = this.getKey('tokens') + localStorage.setItem(key, JSON.stringify(tokens)) // Clean up code verifier and last auth URL after successful token save - localStorage.removeItem(this.getKey('code_verifier')); - localStorage.removeItem(this.getKey('last_auth_url')); + localStorage.removeItem(this.getKey('code_verifier')) + localStorage.removeItem(this.getKey('last_auth_url')) } async saveCodeVerifier(codeVerifier: string): Promise { - const key = this.getKey('code_verifier'); - localStorage.setItem(key, codeVerifier); + const key = this.getKey('code_verifier') + localStorage.setItem(key, codeVerifier) } async codeVerifier(): Promise { - const key = this.getKey('code_verifier'); - const verifier = localStorage.getItem(key); + const key = this.getKey('code_verifier') + const verifier = localStorage.getItem(key) if (!verifier) { - throw new Error(`[${this.storageKeyPrefix}] Code verifier not found in storage for key ${key}. Auth flow likely corrupted or timed out.`); + throw new Error( + `[${this.storageKeyPrefix}] Code verifier not found in storage for key ${key}. Auth flow likely corrupted or timed out.`, + ) } // SDK's auth() retrieves this BEFORE exchanging code. Don't remove it here. // It will be removed in saveTokens on success. - return verifier; + return verifier } /** @@ -118,8 +121,8 @@ export class BrowserOAuthClientProvider implements OAuthClientProvider { */ async redirectToAuthorization(authorizationUrl: URL): Promise { // Generate a unique state parameter for this authorization request - const state = crypto.randomUUID(); - const stateKey = `${this.storageKeyPrefix}:state_${state}`; + const state = crypto.randomUUID() + const stateKey = `${this.storageKeyPrefix}:state_${state}` // Store context needed by the callback handler, associated with the state param const stateData: StoredState = { @@ -132,32 +135,34 @@ export class BrowserOAuthClientProvider implements OAuthClientProvider { clientName: this.clientName, clientUri: this.clientUri, callbackUrl: this.callbackUrl, - } - }; - localStorage.setItem(stateKey, JSON.stringify(stateData)); + }, + } + localStorage.setItem(stateKey, JSON.stringify(stateData)) // Add the state parameter to the URL - authorizationUrl.searchParams.set('state', state); - const authUrlString = authorizationUrl.toString(); + authorizationUrl.searchParams.set('state', state) + const authUrlString = authorizationUrl.toString() // Persist the exact auth URL in case the popup fails and manual navigation is needed - localStorage.setItem(this.getKey('last_auth_url'), authUrlString); + localStorage.setItem(this.getKey('last_auth_url'), authUrlString) // Attempt to open the popup - const popupFeatures = 'width=600,height=700,resizable=yes,scrollbars=yes,status=yes'; // Make configurable if needed + const popupFeatures = 'width=600,height=700,resizable=yes,scrollbars=yes,status=yes' // Make configurable if needed try { - const popup = window.open(authUrlString, `mcp_auth_${this.serverUrlHash}`, popupFeatures); + const popup = window.open(authUrlString, `mcp_auth_${this.serverUrlHash}`, popupFeatures) if (!popup || popup.closed || typeof popup.closed === 'undefined') { - console.warn(`[${this.storageKeyPrefix}] Popup likely blocked by browser. Manual navigation might be required using the stored URL.`); + console.warn( + `[${this.storageKeyPrefix}] Popup likely blocked by browser. Manual navigation might be required using the stored URL.`, + ) // Cannot signal failure back via SDK auth() directly. // useMcp will need to rely on timeout or manual trigger if stuck. } else { - popup.focus(); - console.info(`[${this.storageKeyPrefix}] Redirecting to authorization URL in popup.`); + popup.focus() + console.info(`[${this.storageKeyPrefix}] Redirecting to authorization URL in popup.`) } } catch (e) { - console.error(`[${this.storageKeyPrefix}] Error opening popup window:`, e); + console.error(`[${this.storageKeyPrefix}] Error opening popup window:`, e) // Cannot signal failure back via SDK auth() directly. } // Regardless of popup success, the interface expects this method to initiate the redirect. @@ -170,60 +175,59 @@ export class BrowserOAuthClientProvider implements OAuthClientProvider { * Retrieves the last URL passed to `redirectToAuthorization`. Useful for manual fallback. */ getLastAttemptedAuthUrl(): string | null { - return localStorage.getItem(this.getKey('last_auth_url')); + return localStorage.getItem(this.getKey('last_auth_url')) } - clearStorage(): number { - const prefixPattern = `${this.storageKeyPrefix}_${this.serverUrlHash}_`; - const statePattern = `${this.storageKeyPrefix}:state_`; - const keysToRemove: string[] = []; - let count = 0; + const prefixPattern = `${this.storageKeyPrefix}_${this.serverUrlHash}_` + const statePattern = `${this.storageKeyPrefix}:state_` + const keysToRemove: string[] = [] + let count = 0 for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - if (!key) continue; + const key = localStorage.key(i) + if (!key) continue if (key.startsWith(prefixPattern)) { - keysToRemove.push(key); + keysToRemove.push(key) } else if (key.startsWith(statePattern)) { try { - const item = localStorage.getItem(key); + const item = localStorage.getItem(key) if (item) { // Check if state belongs to this provider instance based on serverUrlHash // We need to parse cautiously as the structure isn't guaranteed. - const state = JSON.parse(item) as Partial; + const state = JSON.parse(item) as Partial if (state.serverUrlHash === this.serverUrlHash) { - keysToRemove.push(key); + keysToRemove.push(key) } } } catch (e) { - console.warn(`[${this.storageKeyPrefix}] Error parsing state key ${key} during clearStorage:`, e); + console.warn(`[${this.storageKeyPrefix}] Error parsing state key ${key} during clearStorage:`, e) // Optionally remove malformed keys // keysToRemove.push(key); } } } - const uniqueKeysToRemove = [...new Set(keysToRemove)]; - uniqueKeysToRemove.forEach(key => { - localStorage.removeItem(key); - count++; - }); - return count; + const uniqueKeysToRemove = [...new Set(keysToRemove)] + uniqueKeysToRemove.forEach((key) => { + localStorage.removeItem(key) + count++ + }) + return count } private hashString(str: string): string { - let hash = 0; + let hash = 0 for (let i = 0; i < str.length; i++) { - const char = str.charCodeAt(i); - hash = (hash << 5) - hash + char; - hash = hash & hash; + const char = str.charCodeAt(i) + hash = (hash << 5) - hash + char + hash = hash & hash } - return Math.abs(hash).toString(16); + return Math.abs(hash).toString(16) } getKey(keySuffix: string): string { - return `${this.storageKeyPrefix}_${this.serverUrlHash}_${keySuffix}`; + return `${this.storageKeyPrefix}_${this.serverUrlHash}_${keySuffix}` } -} \ No newline at end of file +} diff --git a/src/auth/callback.ts b/src/auth/callback.ts index 9e9fcae..2c2c482 100644 --- a/src/auth/callback.ts +++ b/src/auth/callback.ts @@ -8,100 +8,91 @@ import { StoredState } from './types.js' // Adjust path, ensure definition inclu * Assumes it's running on the page specified as the callbackUrl. */ export async function onMcpAuthorization() { - const queryParams = new URLSearchParams(window.location.search); - const code = queryParams.get('code'); - const state = queryParams.get('state'); - const error = queryParams.get('error'); - const errorDescription = queryParams.get('error_description'); + const queryParams = new URLSearchParams(window.location.search) + const code = queryParams.get('code') + const state = queryParams.get('state') + const error = queryParams.get('error') + const errorDescription = queryParams.get('error_description') - const logPrefix = '[mcp-callback]'; // Generic prefix, or derive from stored state later - console.log(`${logPrefix} Handling callback...`, { code, state, error, errorDescription }); + const logPrefix = '[mcp-callback]' // Generic prefix, or derive from stored state later + console.log(`${logPrefix} Handling callback...`, { code, state, error, errorDescription }) - let provider: BrowserOAuthClientProvider | null = null; - let storedStateData: StoredState | null = null; - const stateKey = state ? `mcp:auth:state_${state}` : null; // Reconstruct state key prefix assumption + let provider: BrowserOAuthClientProvider | null = null + let storedStateData: StoredState | null = null + const stateKey = state ? `mcp:auth:state_${state}` : null // Reconstruct state key prefix assumption try { // --- Basic Error Handling --- if (error) { - throw new Error(`OAuth error: ${error} - ${errorDescription || 'No description provided.'}`); + throw new Error(`OAuth error: ${error} - ${errorDescription || 'No description provided.'}`) } if (!code) { - throw new Error('Authorization code not found in callback query parameters.'); + throw new Error('Authorization code not found in callback query parameters.') } if (!state || !stateKey) { - throw new Error('State parameter not found or invalid in callback query parameters.'); + throw new Error('State parameter not found or invalid in callback query parameters.') } // --- Retrieve Stored State & Provider Options --- - const storedStateJSON = localStorage.getItem(stateKey); + const storedStateJSON = localStorage.getItem(stateKey) if (!storedStateJSON) { - throw new Error(`Invalid or expired state parameter "${state}". No matching state found in storage.`); + throw new Error(`Invalid or expired state parameter "${state}". No matching state found in storage.`) } try { - storedStateData = JSON.parse(storedStateJSON) as StoredState; + storedStateData = JSON.parse(storedStateJSON) as StoredState } catch (e) { - throw new Error('Failed to parse stored OAuth state.'); + throw new Error('Failed to parse stored OAuth state.') } // Validate expiry if (!storedStateData.expiry || storedStateData.expiry < Date.now()) { - localStorage.removeItem(stateKey); // Clean up expired state - throw new Error('OAuth state has expired. Please try initiating authentication again.'); + localStorage.removeItem(stateKey) // Clean up expired state + throw new Error('OAuth state has expired. Please try initiating authentication again.') } // Ensure provider options are present if (!storedStateData.providerOptions) { - throw new Error('Stored state is missing required provider options.'); + throw new Error('Stored state is missing required provider options.') } - const { serverUrl, ...providerOptions } = storedStateData.providerOptions; - + const { serverUrl, ...providerOptions } = storedStateData.providerOptions // --- Instantiate Provider --- - console.log(`${logPrefix} Re-instantiating provider for server: ${serverUrl}`); - provider = new BrowserOAuthClientProvider(serverUrl, providerOptions); + console.log(`${logPrefix} Re-instantiating provider for server: ${serverUrl}`) + provider = new BrowserOAuthClientProvider(serverUrl, providerOptions) // --- Call SDK Auth Function --- - console.log(`${logPrefix} Calling SDK auth() to exchange code...`); + console.log(`${logPrefix} Calling SDK auth() to exchange code...`) // The SDK auth() function will internally: // 1. Use provider.clientInformation() // 2. Use provider.codeVerifier() // 3. Call exchangeAuthorization() // 4. Use provider.saveTokens() on success - const authResult = await auth(provider, { serverUrl, authorizationCode: code }); + const authResult = await auth(provider, { serverUrl, authorizationCode: code }) if (authResult === 'AUTHORIZED') { - console.log(`${logPrefix} Authorization successful via SDK auth(). Notifying opener...`); + console.log(`${logPrefix} Authorization successful via SDK auth(). Notifying opener...`) // --- Notify Opener and Close (Success) --- if (window.opener && !window.opener.closed) { - window.opener.postMessage( - { type: 'mcp_auth_callback', success: true }, - window.location.origin, - ); - window.close(); + window.opener.postMessage({ type: 'mcp_auth_callback', success: true }, window.location.origin) + window.close() } else { - console.warn(`${logPrefix} No opener window detected. Redirecting to root.`); - window.location.href = '/'; // Or a configured post-auth destination + console.warn(`${logPrefix} No opener window detected. Redirecting to root.`) + window.location.href = '/' // Or a configured post-auth destination } // Clean up state ONLY on success and after notifying opener - localStorage.removeItem(stateKey); - + localStorage.removeItem(stateKey) } else { // This case shouldn't happen if `authorizationCode` is provided to `auth()` - console.warn(`${logPrefix} SDK auth() returned unexpected status: ${authResult}`); - throw new Error(`Unexpected result from authentication library: ${authResult}`); + console.warn(`${logPrefix} SDK auth() returned unexpected status: ${authResult}`) + throw new Error(`Unexpected result from authentication library: ${authResult}`) } - } catch (err) { - console.error(`${logPrefix} Error during OAuth callback handling:`, err); - const errorMessage = err instanceof Error ? err.message : String(err); + console.error(`${logPrefix} Error during OAuth callback handling:`, err) + const errorMessage = err instanceof Error ? err.message : String(err) // --- Notify Opener and Display Error (Failure) --- if (window.opener && !window.opener.closed) { - window.opener.postMessage( - { type: 'mcp_auth_callback', success: false, error: errorMessage }, - window.location.origin, - ); + window.opener.postMessage({ type: 'mcp_auth_callback', success: false, error: errorMessage }, window.location.origin) // Optionally close even on error, depending on UX preference // window.close(); } @@ -116,22 +107,22 @@ export async function onMcpAuthorization() {

You can close this window or click here to close.

${
-        err instanceof Error ? err.stack : ''
-      }
+ err instanceof Error ? err.stack : '' + } - `; + ` } catch (displayError) { - console.error(`${logPrefix} Could not display error in callback window:`, displayError); + console.error(`${logPrefix} Could not display error in callback window:`, displayError) } // Clean up potentially invalid state on error if (stateKey) { - localStorage.removeItem(stateKey); + localStorage.removeItem(stateKey) } // Clean up potentially dangling verifier or last_auth_url if auth failed badly // Note: saveTokens should clean these on success if (provider) { - localStorage.removeItem(provider.getKey('code_verifier')); - localStorage.removeItem(provider.getKey('last_auth_url')); + localStorage.removeItem(provider.getKey('code_verifier')) + localStorage.removeItem(provider.getKey('last_auth_url')) } } } diff --git a/src/auth/types.ts b/src/auth/types.ts index 95159be..76baac8 100644 --- a/src/auth/types.ts +++ b/src/auth/types.ts @@ -1,19 +1,19 @@ -import { OAuthMetadata } from '@modelcontextprotocol/sdk/shared/auth.js'; +import { OAuthMetadata } from '@modelcontextprotocol/sdk/shared/auth.js' /** * Internal type for storing OAuth state in localStorage during the popup flow. * @internal */ export interface StoredState { - expiry: number; - metadata?: OAuthMetadata; // Optional: might not be needed if auth() rediscovers - serverUrlHash: string; + expiry: number + metadata?: OAuthMetadata // Optional: might not be needed if auth() rediscovers + serverUrlHash: string // Add provider options needed on callback: providerOptions: { - serverUrl: string; - storageKeyPrefix: string; - clientName: string; - clientUri: string; - callbackUrl: string; - }; -} \ No newline at end of file + serverUrl: string + storageKeyPrefix: string + clientName: string + clientUri: string + callbackUrl: string + } +} diff --git a/src/index.ts b/src/index.ts index 4e1861b..95ac23d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,12 +3,12 @@ * Provides browser-specific, framework-agnostic components for MCP OAuth. */ -export { BrowserOAuthClientProvider } from './auth/browser-provider.js'; -export { onMcpAuthorization } from './auth/callback.js'; +export { BrowserOAuthClientProvider } from './auth/browser-provider.js' +export { onMcpAuthorization } from './auth/callback.js' // Optionally re-export relevant types from SDK or internal types if needed -export type { StoredState as InternalStoredState } from './auth/types.js'; // Example if needed internally +export type { StoredState as InternalStoredState } from './auth/types.js' // Example if needed internally // It's generally better to have users import SDK types directly from the SDK // export type { Tool, JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js'; -// export type { OAuthClientInformation, OAuthMetadata, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js'; \ No newline at end of file +// export type { OAuthClientInformation, OAuthMetadata, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js'; diff --git a/src/react/README.md b/src/react/README.md index cf380f5..3e15c47 100644 --- a/src/react/README.md +++ b/src/react/README.md @@ -21,17 +21,17 @@ The useMcp hook manages the connection to an MCP server, handles authentication ## Importing ```tsx -import { useMcp } from 'use-mcp'; +import { useMcp } from 'use-mcp' // Optional: Import types if needed -import type { UseMcpOptions, UseMcpResult, Tool } from 'use-mcp'; +import type { UseMcpOptions, UseMcpResult, Tool } from 'use-mcp' ``` ## Usage ```tsx -import React from 'react'; -import { useMcp } from 'use-mcp/react'; -import { useChat } from 'ai/react'; // Example integration with Vercel AI SDK +import React from 'react' +import { useMcp } from 'use-mcp/react' +import { useChat } from 'ai/react' // Example integration with Vercel AI SDK function MyChatComponent() { const mcp = useMcp({ @@ -45,7 +45,7 @@ function MyChatComponent() { debug: process.env.NODE_ENV === 'development', // Enable debug logs in dev autoReconnect: true, // Automatically reconnect if connection drops autoRetry: 5000, // Retry connection every 5s if initial connection fails - }); + }) // Example: Using MCP tools with Vercel AI SDK's useChat const { messages, input, handleInputChange, handleSubmit } = useChat({ @@ -55,32 +55,29 @@ function MyChatComponent() { // Example: Handling tool calls made by the LLM async onToolCall({ toolCall }) { if (mcp.state !== 'ready') { - console.error('MCP not ready, cannot handle tool call:', toolCall); + console.error('MCP not ready, cannot handle tool call:', toolCall) // Return an error or indication that the tool cannot be called return { - toolCallId: toolCall.toolCallId, - result: { error: 'Tool provider not available' } - }; + toolCallId: toolCall.toolCallId, + result: { error: 'Tool provider not available' }, + } } try { - console.log('Handling tool call via MCP:', toolCall); - const result = await mcp.callTool( - toolCall.toolName, - toolCall.args, - ); + console.log('Handling tool call via MCP:', toolCall) + const result = await mcp.callTool(toolCall.toolName, toolCall.args) return { toolCallId: toolCall.toolCallId, result, - }; + } } catch (error) { - console.error('Error calling MCP tool:', error); + console.error('Error calling MCP tool:', error) return { - toolCallId: toolCall.toolCallId, - result: { error: error instanceof Error ? error.message : String(error) } - }; + toolCallId: toolCall.toolCallId, + result: { error: error instanceof Error ? error.message : String(error) }, + } } }, - }); + }) return (
@@ -99,8 +96,8 @@ function MyChatComponent() {

)} - {/* Show retry button for general failures */} - {!mcp.authUrl && } + {/* Show retry button for general failures */} + {!mcp.authUrl && }
)} {mcp.state === 'authenticating' &&

Waiting for authentication...

} @@ -124,50 +121,58 @@ function MyChatComponent() { // Disable input if MCP is not ready and tools might be needed disabled={mcp.state !== 'ready' && mcp.state !== 'loading'} /> - + {/* Optional: Display logs for debugging */} {mcp.debug && (
-          {mcp.log.map((l, i) => 
[{l.level}] {new Date(l.timestamp).toLocaleTimeString()}: {l.message}
)} + {mcp.log.map((l, i) => ( +
+ [{l.level}] {new Date(l.timestamp).toLocaleTimeString()}: {l.message} +
+ ))}
)} - {/* Button to clear stored auth data */} - + {/* Button to clear stored auth data */} + - ); + ) } -export default MyChatComponent; +export default MyChatComponent ``` ### Hook Options (UseMcpOptions) -* url (required): The /sse URL of your MCP server. -* clientName: Name used for OAuth dynamic client registration (if applicable). Defaults to "MCP React Client". -* clientUri: Client URI used for OAuth registration. Defaults to window.location.origin. -* callbackUrl: The absolute URL of your OAuth callback page. Must match the redirect URI registered with your OAuth server and the route where you call onMcpAuthorization. Defaults to /oauth/callback on the current origin. -* storageKeyPrefix: Prefix for keys used in localStorage (e.g., tokens, client info). Defaults to "mcp:auth". Useful to avoid conflicts if multiple MCP instances/apps run on the same origin. -* clientConfig: Information about the client application sent during the MCP handshake. Includes name (default: "mcp-react-client") and version (default: "0.1.0"). -* debug: Enable verbose logging to the console and the log state array. Defaults to false. -* autoRetry: If true or a number (milliseconds), automatically tries to reconnect if the initial connection fails. Defaults to false. Uses a 5000ms delay if set to true. -* autoReconnect: If true or a number (milliseconds), automatically tries to reconnect if an established connection is lost. Defaults to 3000 (3 seconds). Set to false to disable. -* popupFeatures: The features string passed to window.open for the OAuth popup. Defaults to "width=600,height=700,resizable=yes,scrollbars=yes,status=yes". +- url (required): The /sse URL of your MCP server. +- clientName: Name used for OAuth dynamic client registration (if applicable). Defaults to "MCP React Client". +- clientUri: Client URI used for OAuth registration. Defaults to window.location.origin. +- callbackUrl: The absolute URL of your OAuth callback page. Must match the redirect URI registered with your OAuth server and the route where you call onMcpAuthorization. Defaults to /oauth/callback on the current origin. +- storageKeyPrefix: Prefix for keys used in localStorage (e.g., tokens, client info). Defaults to "mcp:auth". Useful to avoid conflicts if multiple MCP instances/apps run on the same origin. +- clientConfig: Information about the client application sent during the MCP handshake. Includes name (default: "mcp-react-client") and version (default: "0.1.0"). +- debug: Enable verbose logging to the console and the log state array. Defaults to false. +- autoRetry: If true or a number (milliseconds), automatically tries to reconnect if the initial connection fails. Defaults to false. Uses a 5000ms delay if set to true. +- autoReconnect: If true or a number (milliseconds), automatically tries to reconnect if an established connection is lost. Defaults to 3000 (3 seconds). Set to false to disable. +- popupFeatures: The features string passed to window.open for the OAuth popup. Defaults to "width=600,height=700,resizable=yes,scrollbars=yes,status=yes". ### Hook Return Value (UseMcpResult) -* tools: An array of Tool objects provided by the server. Empty until state is 'ready'. -* state: The current connection state ('discovering', 'authenticating', 'connecting', 'loading', 'ready', 'failed'). Use this to conditionally render UI or enable/disable features. -* error: An error message if state is 'failed'. -* authUrl: If authentication is required and the popup was potentially blocked, this URL string can be used to let the user manually open the auth page (e.g., ...). -* log: An array of log messages { level, message, timestamp }. Useful for debugging when debug option is true. -* callTool(name, args): An async function to execute a tool on the MCP server. Throws an error if the client isn't ready or the call fails. -* retry(): Manually triggers a reconnection attempt if the state is 'failed'. -* disconnect(): Disconnects the client from the server. -* authenticate(): Manually attempts to start the authentication flow. Useful for triggering the popup via a user click if it was initially blocked. -* clearStorage(): Removes all authentication-related data (tokens, client info, code verifier, state) for the configured server URL from localStorage. Useful for development or allowing users to "log out". Automatically disconnects the client. +- tools: An array of Tool objects provided by the server. Empty until state is 'ready'. +- state: The current connection state ('discovering', 'authenticating', 'connecting', 'loading', 'ready', 'failed'). Use this to conditionally render UI or enable/disable features. +- error: An error message if state is 'failed'. +- authUrl: If authentication is required and the popup was potentially blocked, this URL string can be used to let the user manually open the auth page (e.g., ...). +- log: An array of log messages { level, message, timestamp }. Useful for debugging when debug option is true. +- callTool(name, args): An async function to execute a tool on the MCP server. Throws an error if the client isn't ready or the call fails. +- retry(): Manually triggers a reconnection attempt if the state is 'failed'. +- disconnect(): Disconnects the client from the server. +- authenticate(): Manually attempts to start the authentication flow. Useful for triggering the popup via a user click if it was initially blocked. +- clearStorage(): Removes all authentication-related data (tokens, client info, code verifier, state) for the configured server URL from localStorage. Useful for development or allowing users to "log out". Automatically disconnects the client. ### Setting up the OAuth Callback Route -Remember to create the callback route (e.g., /oauth/callback) specified in callbackUrl and have it execute the onMcpAuthorization function exported from the main use-mcp package. See the root README for an example. \ No newline at end of file +Remember to create the callback route (e.g., /oauth/callback) specified in callbackUrl and have it execute the onMcpAuthorization function exported from the main use-mcp package. See the root README for an example. diff --git a/src/react/index.ts b/src/react/index.ts index 257220f..c2aedd7 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -3,8 +3,8 @@ * Provides the useMcp hook and related types. */ -export { useMcp } from './useMcp.js'; -export type { UseMcpOptions, UseMcpResult } from './types.js'; +export { useMcp } from './useMcp.js' +export type { UseMcpOptions, UseMcpResult } from './types.js' // Re-export core Tool type for convenience when using hook result? -export type { Tool } from '@modelcontextprotocol/sdk/types.js'; \ No newline at end of file +export type { Tool } from '@modelcontextprotocol/sdk/types.js' diff --git a/src/react/types.ts b/src/react/types.ts index 8f08800..165c0ab 100644 --- a/src/react/types.ts +++ b/src/react/types.ts @@ -1,36 +1,36 @@ -import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { Tool } from '@modelcontextprotocol/sdk/types.js' export type UseMcpOptions = { /** The /sse URL of your remote MCP server */ - url: string; + url: string /** OAuth client name for registration (if dynamic registration is used) */ - clientName?: string; + clientName?: string /** OAuth client URI for registration (if dynamic registration is used) */ - clientUri?: string; + clientUri?: string /** Custom callback URL for OAuth redirect (defaults to /oauth/callback on the current origin) */ - callbackUrl?: string; + callbackUrl?: string /** Storage key prefix for OAuth data in localStorage (defaults to "mcp:auth") */ - storageKeyPrefix?: string; + storageKeyPrefix?: string /** Custom configuration for the MCP client identity */ clientConfig?: { - name?: string; - version?: string; - }; + name?: string + version?: string + } /** Custom headers that can be used to bypass auth */ - customHeaders?: Record; + customHeaders?: Record /** Whether to enable verbose debug logging to the console and the log state */ - debug?: boolean; + debug?: boolean /** Auto retry connection if initial connection fails, with delay in ms (default: false) */ - autoRetry?: boolean | number; + autoRetry?: boolean | number /** Auto reconnect if an established connection is lost, with delay in ms (default: 3000) */ - autoReconnect?: boolean | number; + autoReconnect?: boolean | number /** Popup window features string (dimensions and behavior) for OAuth */ - popupFeatures?: string; -}; + popupFeatures?: string +} export type UseMcpResult = { /** List of tools available from the connected MCP server */ - tools: Tool[]; + tools: Tool[] /** * The current state of the MCP connection: * - 'discovering': Checking server existence and capabilities (including auth requirements). @@ -40,16 +40,16 @@ export type UseMcpResult = { * - 'ready': Connected and ready for tool calls. * - 'failed': Connection or authentication failed. Check the `error` property. */ - state: 'discovering' | 'authenticating' | 'connecting' | 'loading' | 'ready' | 'failed'; + state: 'discovering' | 'authenticating' | 'connecting' | 'loading' | 'ready' | 'failed' /** If the state is 'failed', this provides the error message */ - error?: string; + error?: string /** * If authentication requires user interaction (e.g., popup was blocked), * this URL can be presented to the user to complete authentication manually in a new tab. */ - authUrl?: string; + authUrl?: string /** Array of internal log messages (useful for debugging) */ - log: { level: 'debug' | 'info' | 'warn' | 'error'; message: string; timestamp: number }[]; + log: { level: 'debug' | 'info' | 'warn' | 'error'; message: string; timestamp: number }[] /** * Function to call a tool on the MCP server. * @param name The name of the tool to call. @@ -57,18 +57,18 @@ export type UseMcpResult = { * @returns A promise that resolves with the tool's result. * @throws If the client is not in the 'ready' state or the call fails. */ - callTool: (name: string, args?: Record) => Promise; + callTool: (name: string, args?: Record) => Promise /** Manually attempts to reconnect if the state is 'failed'. */ - retry: () => void; + retry: () => void /** Disconnects the client from the MCP server. */ - disconnect: () => void; + disconnect: () => void /** * Manually triggers the authentication process. Useful if the initial attempt failed * due to a blocked popup, allowing the user to initiate it via a button click. * @returns A promise that resolves with the authorization URL opened (or intended to be opened), * or undefined if auth cannot be started. */ - authenticate: () => void; + authenticate: () => void /** Clears all stored authentication data (tokens, client info, etc.) for this server URL from localStorage. */ - clearStorage: () => void; -}; \ No newline at end of file + clearStorage: () => void +} diff --git a/src/utils/assert.ts b/src/utils/assert.ts index 7ecbc36..544eace 100644 --- a/src/utils/assert.ts +++ b/src/utils/assert.ts @@ -6,6 +6,6 @@ */ export function assert(condition: unknown, message: string): asserts condition { if (!condition) { - throw new Error(message); + throw new Error(message) } -} \ No newline at end of file +}