commit
171938b716
4 changed files with 136 additions and 124 deletions
16
package.json
16
package.json
|
@ -32,5 +32,21 @@
|
|||
"tsup": "^8.4.0",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.2"
|
||||
},
|
||||
"tsup": {
|
||||
"entry": [
|
||||
"src/cli/client.ts",
|
||||
"src/cli/proxy.ts",
|
||||
"src/react/index.ts"
|
||||
],
|
||||
"format": [
|
||||
"esm"
|
||||
],
|
||||
"dts": true,
|
||||
"clean": true,
|
||||
"outDir": "dist",
|
||||
"external": [
|
||||
"react"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,12 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|||
import { discoverOAuthMetadata, exchangeAuthorization, startAuthorization } from '@modelcontextprotocol/sdk/client/auth.js'
|
||||
import { OAuthClientInformation, OAuthMetadata, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js'
|
||||
|
||||
function assert(condition: unknown, message: string): asserts condition {
|
||||
if (!condition) {
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
|
||||
export type UseMcpOptions = {
|
||||
/** The /sse URL of your remote MCP server */
|
||||
url: string
|
||||
|
@ -259,7 +265,6 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
|
|||
const codeVerifierRef = useRef<string | undefined>(undefined)
|
||||
const connectingRef = useRef<boolean>(false)
|
||||
const isInitialMount = useRef<boolean>(true)
|
||||
let handleAuthentication: () => Promise<string>
|
||||
|
||||
// Set up default options
|
||||
const {
|
||||
|
@ -338,6 +343,114 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
|
|||
setError(undefined)
|
||||
}, [addLog])
|
||||
|
||||
// Start the auth flow and get the auth URL
|
||||
const startAuthFlow = useCallback(async (): Promise<URL | undefined> => {
|
||||
if (!authProviderRef.current || !metadataRef.current) {
|
||||
throw new Error('Auth provider or metadata not available')
|
||||
}
|
||||
|
||||
addLog('info', 'Starting authentication flow...')
|
||||
|
||||
// Check if we have client info
|
||||
let clientInfo = await authProviderRef.current.clientInformation()
|
||||
|
||||
if (!clientInfo) {
|
||||
// Register client dynamically
|
||||
addLog('info', 'No client information found, registering...')
|
||||
// Note: In a complete implementation, you'd register the client here
|
||||
// This would be done server-side in a real application
|
||||
throw new Error('Dynamic client registration not implemented in this example')
|
||||
}
|
||||
|
||||
// Start authorization flow
|
||||
addLog('info', 'Preparing authorization...')
|
||||
const { authorizationUrl, codeVerifier } = await startAuthorization(url, {
|
||||
metadata: metadataRef.current,
|
||||
clientInformation: clientInfo,
|
||||
redirectUrl: authProviderRef.current.redirectUrl,
|
||||
})
|
||||
|
||||
// Save code verifier and auth URL for later use
|
||||
await authProviderRef.current.saveCodeVerifier(codeVerifier)
|
||||
codeVerifierRef.current = codeVerifier
|
||||
authUrlRef.current = authorizationUrl
|
||||
setAuthUrl(authorizationUrl.toString())
|
||||
|
||||
return authorizationUrl
|
||||
}, [url, addLog])
|
||||
|
||||
// Handle authentication flow
|
||||
const handleAuthentication = useCallback(async () => {
|
||||
if (!authProviderRef.current) {
|
||||
throw new Error('Auth provider not available')
|
||||
}
|
||||
|
||||
// Get or create the auth URL
|
||||
if (!authUrlRef.current) {
|
||||
try {
|
||||
await startAuthFlow()
|
||||
} catch (err) {
|
||||
addLog('error', `Failed to start auth flow: ${err instanceof Error ? err.message : String(err)}`)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
if (!authUrlRef.current) {
|
||||
throw new Error('Failed to create authorization URL')
|
||||
}
|
||||
|
||||
// Set up listener for post-auth message
|
||||
const authPromise = new Promise<string>((resolve, reject) => {
|
||||
const timeoutId = setTimeout(
|
||||
() => {
|
||||
window.removeEventListener('message', messageHandler)
|
||||
reject(new Error('Authentication timeout after 5 minutes'))
|
||||
},
|
||||
5 * 60 * 1000,
|
||||
)
|
||||
|
||||
const messageHandler = (event: MessageEvent) => {
|
||||
// Verify origin for security
|
||||
if (event.origin !== window.location.origin) return
|
||||
|
||||
if (event.data && event.data.type === 'mcp_auth_callback' && event.data.code) {
|
||||
window.removeEventListener('message', messageHandler)
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
// TODO: not this, obviously
|
||||
// reload window, we should find the token in local storage
|
||||
window.location.reload()
|
||||
// resolve(event.data.code)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', messageHandler)
|
||||
})
|
||||
|
||||
// Redirect to authorization
|
||||
addLog('info', 'Opening authorization window...')
|
||||
assert(metadataRef.current, 'Metadata not available')
|
||||
const redirectResult = await authProviderRef.current.redirectToAuthorization(authUrlRef.current, metadataRef.current, {
|
||||
popupFeatures,
|
||||
})
|
||||
|
||||
if (!redirectResult.success) {
|
||||
// Popup was blocked
|
||||
setState('failed')
|
||||
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.')
|
||||
throw new Error('Authentication popup blocked')
|
||||
}
|
||||
|
||||
// Wait for auth to complete
|
||||
addLog('info', 'Waiting for authorization...')
|
||||
const code = await authPromise
|
||||
addLog('info', 'Authorization code received')
|
||||
|
||||
return code
|
||||
}, [url, addLog, popupFeatures, startAuthFlow])
|
||||
|
||||
// Initialize connection to MCP server
|
||||
const connect = useCallback(async () => {
|
||||
// Prevent multiple simultaneous connection attempts
|
||||
|
@ -387,11 +500,13 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
|
|||
|
||||
const serverUrl = new URL(url)
|
||||
transportRef.current = new SSEClientTransport(serverUrl, {
|
||||
// @ts-expect-error TODO: fix this type, expect BrowserOAuthClientProvider
|
||||
authProvider: authProviderRef.current,
|
||||
})
|
||||
|
||||
// Set up transport handlers
|
||||
transportRef.current.onmessage = (message: JSONRPCMessage) => {
|
||||
// @ts-expect-error TODO: fix this type
|
||||
addLog('debug', `Received message: ${message.method || message.id}`)
|
||||
}
|
||||
|
||||
|
@ -507,111 +622,6 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
|
|||
return undefined
|
||||
}, [])
|
||||
|
||||
// Start the auth flow and get the auth URL
|
||||
const startAuthFlow = useCallback(async (): Promise<URL | undefined> => {
|
||||
if (!authProviderRef.current || !metadataRef.current) {
|
||||
throw new Error('Auth provider or metadata not available')
|
||||
}
|
||||
|
||||
addLog('info', 'Starting authentication flow...')
|
||||
|
||||
// Check if we have client info
|
||||
let clientInfo = await authProviderRef.current.clientInformation()
|
||||
|
||||
if (!clientInfo) {
|
||||
// Register client dynamically
|
||||
addLog('info', 'No client information found, registering...')
|
||||
// Note: In a complete implementation, you'd register the client here
|
||||
// This would be done server-side in a real application
|
||||
throw new Error('Dynamic client registration not implemented in this example')
|
||||
}
|
||||
|
||||
// Start authorization flow
|
||||
addLog('info', 'Preparing authorization...')
|
||||
const { authorizationUrl, codeVerifier } = await startAuthorization(url, {
|
||||
metadata: metadataRef.current,
|
||||
clientInformation: clientInfo,
|
||||
redirectUrl: authProviderRef.current.redirectUrl,
|
||||
})
|
||||
|
||||
// Save code verifier and auth URL for later use
|
||||
await authProviderRef.current.saveCodeVerifier(codeVerifier)
|
||||
codeVerifierRef.current = codeVerifier
|
||||
authUrlRef.current = authorizationUrl
|
||||
setAuthUrl(authorizationUrl.toString())
|
||||
|
||||
return authorizationUrl
|
||||
}, [url, addLog])
|
||||
|
||||
// Handle authentication flow
|
||||
handleAuthentication = useCallback(async () => {
|
||||
if (!authProviderRef.current) {
|
||||
throw new Error('Auth provider not available')
|
||||
}
|
||||
|
||||
// Get or create the auth URL
|
||||
if (!authUrlRef.current) {
|
||||
try {
|
||||
await startAuthFlow()
|
||||
} catch (err) {
|
||||
addLog('error', `Failed to start auth flow: ${err instanceof Error ? err.message : String(err)}`)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
if (!authUrlRef.current) {
|
||||
throw new Error('Failed to create authorization URL')
|
||||
}
|
||||
|
||||
// Set up listener for post-auth message
|
||||
const authPromise = new Promise<string>((resolve, reject) => {
|
||||
const timeoutId = setTimeout(
|
||||
() => {
|
||||
window.removeEventListener('message', messageHandler)
|
||||
reject(new Error('Authentication timeout after 5 minutes'))
|
||||
},
|
||||
5 * 60 * 1000,
|
||||
)
|
||||
|
||||
const messageHandler = (event: MessageEvent) => {
|
||||
// Verify origin for security
|
||||
if (event.origin !== window.location.origin) return
|
||||
|
||||
if (event.data && event.data.type === 'mcp_auth_callback' && event.data.code) {
|
||||
window.removeEventListener('message', messageHandler)
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
// TODO: not this, obviously
|
||||
// reload window, we should find the token in local storage
|
||||
window.location.reload()
|
||||
// resolve(event.data.code)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', messageHandler)
|
||||
})
|
||||
|
||||
// Redirect to authorization
|
||||
addLog('info', 'Opening authorization window...')
|
||||
const redirectResult = await authProviderRef.current.redirectToAuthorization(authUrlRef.current, metadataRef.current, { popupFeatures })
|
||||
|
||||
if (!redirectResult.success) {
|
||||
// Popup was blocked
|
||||
setState('failed')
|
||||
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.')
|
||||
throw new Error('Authentication popup blocked')
|
||||
}
|
||||
|
||||
// Wait for auth to complete
|
||||
addLog('info', 'Waiting for authorization...')
|
||||
const code = await authPromise
|
||||
addLog('info', 'Authorization code received')
|
||||
|
||||
return code
|
||||
}, [url, addLog, popupFeatures, startAuthFlow])
|
||||
|
||||
// Handle auth completion - this is called when we receive a message from the popup
|
||||
const handleAuthCompletion = useCallback(
|
||||
async (code: string) => {
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
|
||||
"noEmit": true,
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"types": ["node", "react"],
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["*.ts","src/**/*"],
|
||||
"exclude": ["node_modules", "packages", "**/*.spec.ts"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
import { defineConfig } from 'tsup'
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/cli/client.ts', 'src/cli/proxy.ts', 'src/react/index.ts'],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
clean: true,
|
||||
outDir: 'dist',
|
||||
external: ['react'],
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue