Prettified

This commit is contained in:
Glen Maddern 2025-06-02 19:07:59 +10:00
commit 551b545090
8 changed files with 236 additions and 236 deletions

View file

@ -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<OAuthClientInformation | undefined> {
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<void> {
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<OAuthTokens | undefined> {
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<void> {
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<void> {
const key = this.getKey('code_verifier');
localStorage.setItem(key, codeVerifier);
const key = this.getKey('code_verifier')
localStorage.setItem(key, codeVerifier)
}
async codeVerifier(): Promise<string> {
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<void> {
// 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<StoredState>;
const state = JSON.parse(item) as Partial<StoredState>
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}`
}
}
}

View file

@ -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() {
</p>
<p>You can close this window or <a href="#" onclick="window.close(); return false;">click here to close</a>.</p>
<pre style="font-size: 0.8em; color: #555; margin-top: 20px; white-space: pre-wrap;">${
err instanceof Error ? err.stack : ''
}</pre>
err instanceof Error ? err.stack : ''
}</pre>
</div>
`;
`
} 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'))
}
}
}

View file

@ -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;
};
}
serverUrl: string
storageKeyPrefix: string
clientName: string
clientUri: string
callbackUrl: string
}
}

View file

@ -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';
// export type { OAuthClientInformation, OAuthMetadata, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js';

View file

@ -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 (
<div>
@ -99,8 +96,8 @@ function MyChatComponent() {
<button onClick={() => mcp.authenticate()}>Retry Auth</button>
</p>
)}
{/* Show retry button for general failures */}
{!mcp.authUrl && <button onClick={mcp.retry}>Retry Connection</button>}
{/* Show retry button for general failures */}
{!mcp.authUrl && <button onClick={mcp.retry}>Retry Connection</button>}
</div>
)}
{mcp.state === 'authenticating' && <p>Waiting for authentication...</p>}
@ -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'}
/>
<button type="submit" disabled={mcp.state !== 'ready'}>Send</button>
<button type="submit" disabled={mcp.state !== 'ready'}>
Send
</button>
</form>
{/* Optional: Display logs for debugging */}
{mcp.debug && (
<pre style={{ maxHeight: '200px', overflowY: 'auto', fontSize: '0.8em', background: '#eee', padding: '5px' }}>
{mcp.log.map((l, i) => <div key={i}>[{l.level}] {new Date(l.timestamp).toLocaleTimeString()}: {l.message}</div>)}
{mcp.log.map((l, i) => (
<div key={i}>
[{l.level}] {new Date(l.timestamp).toLocaleTimeString()}: {l.message}
</div>
))}
</pre>
)}
{/* Button to clear stored auth data */}
<button onClick={mcp.clearStorage} style={{marginTop: '10px'}}>Clear Stored Auth</button>
{/* Button to clear stored auth data */}
<button onClick={mcp.clearStorage} style={{ marginTop: '10px' }}>
Clear Stored Auth
</button>
</div>
);
)
}
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., <a href={authUrl} target="_blank">...</a>).
* 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., <a href={authUrl} target="_blank">...</a>).
- 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.
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.

View file

@ -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';
export type { Tool } from '@modelcontextprotocol/sdk/types.js'

View file

@ -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<string, string>;
customHeaders?: Record<string, string>
/** 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<string, unknown>) => Promise<any>;
callTool: (name: string, args?: Record<string, unknown>) => Promise<any>
/** 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;
};
clearStorage: () => void
}

View file

@ -6,6 +6,6 @@
*/
export function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
throw new Error(message)
}
}
}