Successfully tested end-to-end in a browser!

This commit is contained in:
Glen Maddern 2025-03-24 15:21:18 +11:00
parent 542a66951c
commit 5175099e23
4 changed files with 117 additions and 155 deletions

View file

@ -1,9 +1,9 @@
# `mcp-remote` # `mcp-remote`
**EXPERIMENTAL PROOF OF CONCEPT**
Connect an MCP Client that only supports local (stdio) servers to a Remote MCP Server, with auth support: Connect an MCP Client that only supports local (stdio) servers to a Remote MCP Server, with auth support:
**Note: this is a working proof-of-concept** but should be considered **experimental**
E.g: Claude Desktop or Windsurf E.g: Claude Desktop or Windsurf
```json ```json

View file

@ -19,11 +19,11 @@
"build": "tsup" "build": "tsup"
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.7.0",
"express": "^4.21.2", "express": "^4.21.2",
"open": "^10.1.0" "open": "^10.1.0"
}, },
"devDependencies": { "devDependencies": {
"@modelcontextprotocol/sdk": "^1.7.0",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/node": "^22.13.10", "@types/node": "^22.13.10",
"@types/react": "^19.0.12", "@types/react": "^19.0.12",

24
pnpm-lock.yaml generated
View file

@ -8,9 +8,6 @@ importers:
.: .:
dependencies: dependencies:
'@modelcontextprotocol/sdk':
specifier: ^1.7.0
version: 1.7.0
express: express:
specifier: ^4.21.2 specifier: ^4.21.2
version: 4.21.2 version: 4.21.2
@ -18,6 +15,9 @@ importers:
specifier: ^10.1.0 specifier: ^10.1.0
version: 10.1.0 version: 10.1.0
devDependencies: devDependencies:
'@modelcontextprotocol/sdk':
specifier: ^1.7.0
version: 1.7.0
'@types/express': '@types/express':
specifier: ^5.0.0 specifier: ^5.0.0
version: 5.0.0 version: 5.0.0
@ -763,8 +763,8 @@ packages:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
mime-db@1.53.0: mime-db@1.54.0:
resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==} resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
mime-types@2.1.35: mime-types@2.1.35:
@ -1127,8 +1127,8 @@ packages:
wrappy@1.0.2: wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
zod-to-json-schema@3.24.4: zod-to-json-schema@3.24.5:
resolution: {integrity: sha512-0uNlcvgabyrni9Ag8Vghj21drk7+7tp7VTwwR7KxxXXc/3pbXz2PHlDgj3cICahgF1kHm4dExBFj7BXrZJXzig==} resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==}
peerDependencies: peerDependencies:
zod: ^3.24.1 zod: ^3.24.1
@ -1248,7 +1248,7 @@ snapshots:
pkce-challenge: 4.1.0 pkce-challenge: 4.1.0
raw-body: 3.0.0 raw-body: 3.0.0
zod: 3.24.2 zod: 3.24.2
zod-to-json-schema: 3.24.4(zod@3.24.2) zod-to-json-schema: 3.24.5(zod@3.24.2)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -1811,7 +1811,7 @@ snapshots:
mime-db@1.52.0: {} mime-db@1.52.0: {}
mime-db@1.53.0: {} mime-db@1.54.0: {}
mime-types@2.1.35: mime-types@2.1.35:
dependencies: dependencies:
@ -1819,7 +1819,7 @@ snapshots:
mime-types@3.0.0: mime-types@3.0.0:
dependencies: dependencies:
mime-db: 1.53.0 mime-db: 1.54.0
mime@1.6.0: {} mime@1.6.0: {}
@ -1991,7 +1991,7 @@ snapshots:
send@1.1.0: send@1.1.0:
dependencies: dependencies:
debug: 4.3.6 debug: 4.4.0
destroy: 1.2.0 destroy: 1.2.0
encodeurl: 2.0.0 encodeurl: 2.0.0
escape-html: 1.0.3 escape-html: 1.0.3
@ -2203,7 +2203,7 @@ snapshots:
wrappy@1.0.2: {} wrappy@1.0.2: {}
zod-to-json-schema@3.24.4(zod@3.24.2): zod-to-json-schema@3.24.5(zod@3.24.2):
dependencies: dependencies:
zod: 3.24.2 zod: 3.24.2

View file

@ -1,9 +1,9 @@
import { Tool, OAuthMetadata, JSONRPCMessage, OAuthClientInformation, OAuthTokens } from '@modelcontextprotocol/sdk/types.js' import { CallToolResultSchema, JSONRPCMessage, ListToolsResultSchema, Tool } from '@modelcontextprotocol/sdk/types.js'
import { useCallback, useEffect, useState, useRef } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js' import { discoverOAuthMetadata, exchangeAuthorization, startAuthorization } from '@modelcontextprotocol/sdk/client/auth.js'
import { discoverOAuthMetadata, startAuthorization, exchangeAuthorization } from '@modelcontextprotocol/sdk/client/auth.js' import { OAuthClientInformation, OAuthMetadata, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js'
export type UseMcpOptions = { export type UseMcpOptions = {
/** The /sse URL of your remote MCP server */ /** The /sse URL of your remote MCP server */
@ -27,8 +27,6 @@ export type UseMcpOptions = {
autoRetry?: boolean | number autoRetry?: boolean | number
/** Auto reconnect if connection is lost, with delay in ms (default: 3000) */ /** Auto reconnect if connection is lost, with delay in ms (default: 3000) */
autoReconnect?: boolean | number autoReconnect?: boolean | number
/** OAuth authentication mode (default: 'popup-with-redirect-fallback') */
authMode?: 'popup-only' | 'redirect-only' | 'popup-with-redirect-fallback'
/** Popup window features (dimensions and behavior) for OAuth */ /** Popup window features (dimensions and behavior) for OAuth */
popupFeatures?: string popupFeatures?: string
} }
@ -39,15 +37,20 @@ export type UseMcpResult = {
* The current state of the MCP connection. This will be one of: * The current state of the MCP connection. This will be one of:
* - 'discovering': Finding out whether there is in fact a server at that URL, and what its capabilities are * - 'discovering': Finding out whether there is in fact a server at that URL, and what its capabilities are
* - 'authenticating': The server has indicated we must authenticate, so we can't proceed until that's complete * - 'authenticating': The server has indicated we must authenticate, so we can't proceed until that's complete
* - 'popup-blocked': The auth popup was blocked by the browser, manual authentication is needed
* - 'connecting': The connection to the MCP server is being established. This happens before we know whether we need to authenticate or not, and then again once we have credentials * - 'connecting': The connection to the MCP server is being established. This happens before we know whether we need to authenticate or not, and then again once we have credentials
* - 'loading': We're connected to the MCP server, and now we're loading its resources/prompts/tools * - 'loading': We're connected to the MCP server, and now we're loading its resources/prompts/tools
* - 'ready': The MCP server is connected and ready to be used * - 'ready': The MCP server is connected and ready to be used
* - 'failed': The connection to the MCP server failed * - 'failed': The connection to the MCP server failed
* */ * */
state: 'discovering' | 'authenticating' | 'popup-blocked' | 'connecting' | 'loading' | 'ready' | 'failed' state: 'discovering' | 'authenticating' | 'connecting' | 'loading' | 'ready' | 'failed'
/** If the state is 'failed', this will be the error message */ /** If the state is 'failed', this will be the error message */
error?: string error?: string
/**
* If authorization was blocked, this will contain the URL to authorize manually
* The app can render this as a link with target="_blank" so the user can complete
* authorization without leaving the app
*/
authUrl?: string
/** All internal log messages */ /** All internal log messages */
log: { level: 'debug' | 'info' | 'warn' | 'error'; message: string }[] log: { level: 'debug' | 'info' | 'warn' | 'error'; message: string }[]
/** Call a tool on the MCP server */ /** Call a tool on the MCP server */
@ -57,12 +60,17 @@ export type UseMcpResult = {
/** Manually disconnect from the MCP server */ /** Manually disconnect from the MCP server */
disconnect: () => void disconnect: () => void
/** /**
* Manually trigger authentication (useful when popup is blocked) * Manually trigger authentication
* @returns Auth URL that can be used to manually open a new window * @returns Auth URL that can be used to manually open a new window
*/ */
authenticate: () => Promise<string | undefined> authenticate: () => Promise<string | undefined>
/** Authentication URL to manually open if popup is blocked */ }
authUrl?: string
type StoredState = {
authorizationUrl: string
metadata: OAuthMetadata
serverUrlHash: string
expiry: number
} }
/** /**
@ -84,7 +92,7 @@ class BrowserOAuthClientProvider {
callbackUrl?: string callbackUrl?: string
} = {}, } = {},
) { ) {
this.storageKeyPrefix = options.storageKeyPrefix || 'mcp_auth' this.storageKeyPrefix = options.storageKeyPrefix || 'mcp:auth'
this.serverUrlHash = this.hashString(serverUrl) this.serverUrlHash = this.hashString(serverUrl)
this.clientName = options.clientName || 'MCP Browser Client' this.clientName = options.clientName || 'MCP Browser Client'
this.clientUri = options.clientUri || window.location.origin this.clientUri = options.clientUri || window.location.origin
@ -157,89 +165,63 @@ class BrowserOAuthClientProvider {
async redirectToAuthorization( async redirectToAuthorization(
authorizationUrl: URL, authorizationUrl: URL,
metadata: OAuthMetadata,
options?: { options?: {
mode?: 'popup-only' | 'redirect-only' | 'popup-with-redirect-fallback'
popupFeatures?: string popupFeatures?: string
}, },
): Promise<{ success: boolean; popupBlocked?: boolean; url: string }> { ): Promise<{ success: boolean; popupBlocked?: boolean; url: string }> {
// Store the auth state for the popup flow // Store the auth state for the popup flow
const stateKey = this.getKey('auth_state')
const state = Math.random().toString(36).substring(2) const state = Math.random().toString(36).substring(2)
localStorage.setItem(stateKey, state) const stateKey = `${this.storageKeyPrefix}:state_${state}`
localStorage.setItem(
stateKey,
JSON.stringify({
authorizationUrl: authorizationUrl.toString(),
metadata,
serverUrlHash: this.serverUrlHash,
expiry: +new Date() + 1000 * 60 * 5 /* 5 minutes */,
} as StoredState),
)
authorizationUrl.searchParams.set('state', state) authorizationUrl.searchParams.set('state', state)
const authUrl = authorizationUrl.toString() const authUrl = authorizationUrl.toString()
const mode = options?.mode || 'popup-only'
const popupFeatures = options?.popupFeatures || 'width=600,height=700,resizable=yes,scrollbars=yes' const popupFeatures = options?.popupFeatures || 'width=600,height=700,resizable=yes,scrollbars=yes'
// Store the auth URL in case we need it for manual authentication // Store the auth URL in case we need it for manual authentication
localStorage.setItem(this.getKey('auth_url'), authUrl) localStorage.setItem(this.getKey('auth_url'), authUrl)
console.log({ mode })
if (mode === 'redirect-only') { try {
// Redirect in the current window // Open the authorization URL in a popup window
window.location.href = authUrl const popup = window.open(authUrl, 'mcp_auth', popupFeatures)
return { success: true, url: authUrl }
}
if (mode === 'popup-only' || mode === 'popup-with-redirect-fallback') { // Check if popup was blocked or closed immediately
try { if (!popup || popup.closed || popup.closed === undefined) {
// Open the authorization URL in a popup window console.warn('Popup blocked. Returning error.')
const popup = window.open(authUrl, 'mcp_auth', popupFeatures) return { success: false, popupBlocked: true, url: authUrl }
console.log({ popup })
// Check if popup was blocked or closed immediately
if (!popup || popup.closed || popup.closed === undefined) {
if (mode === 'popup-with-redirect-fallback') {
// Fall back to redirect
console.warn('Popup blocked. Redirecting in the same window...')
window.location.href = authUrl
return { success: true, popupBlocked: true, url: authUrl }
} else {
// Popup-only mode, return error
console.warn('Popup blocked and redirect fallback disabled')
return { success: false, popupBlocked: true, url: authUrl }
}
}
// Try to access the popup to confirm it's not blocked
try {
// Just accessing any property will throw if popup is blocked
const popupLocation = popup.location
// If we can read location.href, the popup is definitely working
if (popupLocation.href) {
// Successfully opened popup
return { success: true, url: authUrl }
}
} catch (e) {
// Access to the popup was denied, indicating it's blocked
if (mode === 'popup-with-redirect-fallback') {
console.warn('Popup blocked (security exception). Redirecting in the same window...')
window.location.href = authUrl
return { success: true, popupBlocked: true, url: authUrl }
} else {
console.warn('Popup blocked (security exception) and redirect fallback disabled')
return { success: false, popupBlocked: true, url: authUrl }
}
}
// If we got here, popup is working
return { success: true, url: authUrl }
} catch (e) {
// Error opening popup
if (mode === 'popup-with-redirect-fallback') {
console.warn('Error opening popup:', e, 'Falling back to redirect')
window.location.href = authUrl
return { success: true, popupBlocked: true, url: authUrl }
} else {
console.warn('Error opening popup:', e, 'and redirect fallback disabled')
return { success: false, popupBlocked: true, url: authUrl }
}
} }
}
// This shouldn't happen given the enum constraint, but TypeScript doesn't know that // Try to access the popup to confirm it's not blocked
throw new Error(`Invalid auth mode: ${mode}`) try {
// Just accessing any property will throw if popup is blocked
const popupLocation = popup.location
// If we can read location.href, the popup is definitely working
if (popupLocation.href) {
// Successfully opened popup
return { success: true, url: authUrl }
}
} catch (e) {
// Access to the popup was denied, indicating it's blocked
console.warn('Popup blocked (security exception).')
return { success: false, popupBlocked: true, url: authUrl }
}
// If we got here, popup is working
return { success: true, url: authUrl }
} catch (e) {
// Error opening popup
console.warn('Error opening popup:', e)
return { success: false, popupBlocked: true, url: authUrl }
}
} }
async saveCodeVerifier(codeVerifier: string): Promise<void> { async saveCodeVerifier(codeVerifier: string): Promise<void> {
@ -285,7 +267,7 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
clientName = 'MCP React Client', clientName = 'MCP React Client',
clientUri = window.location.origin, clientUri = window.location.origin,
callbackUrl = new URL('/oauth/callback', window.location.origin).toString(), callbackUrl = new URL('/oauth/callback', window.location.origin).toString(),
storageKeyPrefix = 'mcp_auth', storageKeyPrefix = 'mcp:auth',
clientConfig = { clientConfig = {
name: 'mcp-react-client', name: 'mcp-react-client',
version: '0.1.0', version: '0.1.0',
@ -293,14 +275,12 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
debug = false, debug = false,
autoRetry = false, autoRetry = false,
autoReconnect = 3000, autoReconnect = 3000,
authMode = 'popup-only',
popupFeatures = 'width=600,height=700,resizable=yes,scrollbars=yes', popupFeatures = 'width=600,height=700,resizable=yes,scrollbars=yes',
} = options } = options
// Add to log // Add to log
const addLog = useCallback( const addLog = useCallback(
(level: 'debug' | 'info' | 'warn' | 'error', message: string) => { (level: 'debug' | 'info' | 'warn' | 'error', message: string) => {
console.log(message)
if (level === 'debug' && !debug) return if (level === 'debug' && !debug) return
setLog((prevLog) => [...prevLog, { level, message }]) setLog((prevLog) => [...prevLog, { level, message }])
}, },
@ -315,10 +295,14 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
} }
try { try {
const result = await clientRef.current.request({ console.log('CALLING TOOL')
method: 'tools/call', const result = await clientRef.current.request(
params: { name, arguments: args }, {
}) method: 'tools/call',
params: { name, arguments: args },
},
CallToolResultSchema,
)
return result return result
} catch (err) { } catch (err) {
addLog('error', `Error calling tool ${name}: ${err instanceof Error ? err.message : String(err)}`) addLog('error', `Error calling tool ${name}: ${err instanceof Error ? err.message : String(err)}`)
@ -444,7 +428,7 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
// Connect transport // Connect transport
try { try {
addLog('info', 'Starting transport...') addLog('info', 'Starting transport...')
await transportRef.current.start() // await transportRef.current.start()
} catch (err) { } catch (err) {
addLog('error', `Transport start error: ${err instanceof Error ? err.message : String(err)}`) addLog('error', `Transport start error: ${err instanceof Error ? err.message : String(err)}`)
@ -596,7 +580,11 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
if (event.data && event.data.type === 'mcp_auth_callback' && event.data.code) { if (event.data && event.data.type === 'mcp_auth_callback' && event.data.code) {
window.removeEventListener('message', messageHandler) window.removeEventListener('message', messageHandler)
clearTimeout(timeoutId) clearTimeout(timeoutId)
resolve(event.data.code)
// TODO: not this, obviously
// reload window, we should find the token in local storage
window.location.reload()
// resolve(event.data.code)
} }
} }
@ -605,33 +593,24 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
// Redirect to authorization // Redirect to authorization
addLog('info', 'Opening authorization window...') addLog('info', 'Opening authorization window...')
const redirectResult = await authProviderRef.current.redirectToAuthorization(authUrlRef.current, { const redirectResult = await authProviderRef.current.redirectToAuthorization(authUrlRef.current, metadataRef.current, { popupFeatures })
mode: authMode,
popupFeatures,
})
if (!redirectResult.success) { if (!redirectResult.success) {
// Popup was blocked and we're in popup-only mode // Popup was blocked
setState('popup-blocked') setState('failed')
setError('Authentication popup was blocked by the browser') setError('Authentication popup was blocked by the browser. Please click the link to authenticate in a new window.')
setAuthUrl(redirectResult.url)
addLog('warn', 'Authentication popup was blocked. User needs to manually authorize.') addLog('warn', 'Authentication popup was blocked. User needs to manually authorize.')
throw new Error('Authentication popup blocked') throw new Error('Authentication popup blocked')
} }
if (redirectResult.popupBlocked && authMode === 'popup-with-redirect-fallback') {
// The popup was blocked but we've fallen back to redirect
// No need to wait for the auth promise since we're redirecting
addLog('info', 'Popup blocked, falling back to redirect...')
return 'redirect-in-progress'
}
// Wait for auth to complete // Wait for auth to complete
addLog('info', 'Waiting for authorization...') addLog('info', 'Waiting for authorization...')
const code = await authPromise const code = await authPromise
addLog('info', 'Authorization code received') addLog('info', 'Authorization code received')
return code return code
}, [url, addLog, authMode, popupFeatures, startAuthFlow]) }, [url, addLog, popupFeatures, startAuthFlow])
// Handle auth completion - this is called when we receive a message from the popup // Handle auth completion - this is called when we receive a message from the popup
const handleAuthCompletion = useCallback( const handleAuthCompletion = useCallback(
@ -649,11 +628,6 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
authUrlRef.current = undefined authUrlRef.current = undefined
setAuthUrl(undefined) setAuthUrl(undefined)
// Reset popup blocked state if we were in that state
if (state === 'popup-blocked') {
setState('authenticating')
}
// Reconnect with the new auth token // Reconnect with the new auth token
await disconnect() await disconnect()
connect() connect()
@ -663,7 +637,7 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
setError(`Authentication failed: ${err instanceof Error ? err.message : String(err)}`) setError(`Authentication failed: ${err instanceof Error ? err.message : String(err)}`)
} }
}, },
[addLog, disconnect, connect, state], [addLog, disconnect, connect],
) )
// Retry connection // Retry connection
@ -741,7 +715,14 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
* Once it's updated LocalStorage with the auth token, it will post a message back to the original * Once it's updated LocalStorage with the auth token, it will post a message back to the original
* window to inform any running `useMcp` hooks that the auth flow is complete. * window to inform any running `useMcp` hooks that the auth flow is complete.
*/ */
export async function onMcpAuthorization(query: Record<string, string>) { export async function onMcpAuthorization(
query: Record<string, string>,
{
storageKeyPrefix = 'mcp:auth',
}: {
storageKeyPrefix?: string
} = {},
) {
try { try {
// Extract the authorization code and state // Extract the authorization code and state
const code = query.code const code = query.code
@ -756,20 +737,23 @@ export async function onMcpAuthorization(query: Record<string, string>) {
} }
// Find the matching auth state in localStorage // Find the matching auth state in localStorage
const storageKeys = Object.keys(localStorage).filter((key) => key.includes('_auth_state') && localStorage.getItem(key) === state) // const storageKeys = Object.keys(localStorage).filter((key) => key.includes('_auth_state') && localStorage.getItem(key) === state)
if (storageKeys.length === 0) { const stateKey = `${storageKeyPrefix}:state_${state}`
const storedState = localStorage.getItem(stateKey)
console.log({ stateKey, storedState })
if (!storedState) {
throw new Error('No matching auth state found in storage') throw new Error('No matching auth state found in storage')
} }
const { authorizationUrl, serverUrlHash, metadata, expiry } = JSON.parse(storedState)
const storageKey = storageKeys[0] if (expiry < Date.now()) {
const keyParts = storageKey.split('_') throw new Error('Auth state has expired')
const serverUrlHash = keyParts[1] }
const storageKeyPrefix = keyParts[0]
// Find all related auth data with the same prefix and server hash // Find all related auth data with the same prefix and server hash
const clientInfoKey = `${storageKeyPrefix}_${serverUrlHash}_client_info` const clientInfoKey = `${storageKeyPrefix}_${serverUrlHash}_client_info`
const codeVerifierKey = `${storageKeyPrefix}_${serverUrlHash}_code_verifier` const codeVerifierKey = `${storageKeyPrefix}_${serverUrlHash}_code_verifier`
console.log({ authorizationUrl, clientInfoKey, codeVerifierKey })
const clientInfoStr = localStorage.getItem(clientInfoKey) const clientInfoStr = localStorage.getItem(clientInfoKey)
const codeVerifier = localStorage.getItem(codeVerifierKey) const codeVerifier = localStorage.getItem(codeVerifierKey)
@ -785,29 +769,7 @@ export async function onMcpAuthorization(query: Record<string, string>) {
// Parse client info // Parse client info
const clientInfo = JSON.parse(clientInfoStr) as OAuthClientInformation const clientInfo = JSON.parse(clientInfoStr) as OAuthClientInformation
// Find the server URL from other keys in localStorage const tokens = await exchangeAuthorization(new URL('/', authorizationUrl), {
const serverUrlKeys = Object.keys(localStorage).filter(
(key) => key.startsWith(`${storageKeyPrefix}_server_`) && key.includes(serverUrlHash),
)
let serverUrl: string
if (serverUrlKeys.length > 0) {
serverUrl = localStorage.getItem(serverUrlKeys[0]) || ''
} else {
// If we can't find the server URL, try to construct it from the current URL
// This is a fallback and may not always work
const currentUrl = new URL(window.location.href)
serverUrl = `${currentUrl.protocol}//${currentUrl.host}`
}
if (!serverUrl) {
throw new Error('Could not determine server URL')
}
// Exchange the code for tokens
const metadata = await discoverOAuthMetadata(serverUrl)
const tokens = await exchangeAuthorization(serverUrl, {
metadata, metadata,
clientInformation: clientInfo, clientInformation: clientInfo,
authorizationCode: code, authorizationCode: code,
@ -816,6 +778,7 @@ export async function onMcpAuthorization(query: Record<string, string>) {
// Save the tokens // Save the tokens
const tokensKey = `${storageKeyPrefix}_${serverUrlHash}_tokens` const tokensKey = `${storageKeyPrefix}_${serverUrlHash}_tokens`
console.log({ tokensKey, tokens })
localStorage.setItem(tokensKey, JSON.stringify(tokens)) localStorage.setItem(tokensKey, JSON.stringify(tokens))
// Post message back to the parent window // Post message back to the parent window
@ -823,7 +786,6 @@ export async function onMcpAuthorization(query: Record<string, string>) {
window.opener.postMessage( window.opener.postMessage(
{ {
type: 'mcp_auth_callback', type: 'mcp_auth_callback',
code,
}, },
window.location.origin, window.location.origin,
) )