From 8a71032f60da6a9e88605014f348be66758111cf Mon Sep 17 00:00:00 2001 From: Enrico Toniato <2827496+tonxxd@users.noreply.github.com> Date: Tue, 7 Oct 2025 23:01:52 +0200 Subject: [PATCH 1/2] Fix OAuth flow and session API issues - skip ESLint for now --- examples/react/README.md | 132 ++++ examples/react/index.html | 41 + examples/react/index.tsx | 11 + examples/react/oauth-helper.ts | 451 +++++++++++ examples/react/package.json | 28 + examples/react/pnpm-lock.yaml | 1206 +++++++++++++++++++++++++++++ examples/react/react_example.html | 41 + examples/react/react_example.tsx | 369 +++++++++ examples/react/serve.js | 59 ++ examples/react/tsconfig.json | 25 + examples/react/tsconfig.node.json | 10 + examples/react/vite.config.ts | 46 ++ package.json | 5 + pnpm-lock.yaml | 11 + src/browser.ts | 95 +++ src/client.ts | 159 +--- src/client/base.ts | 155 ++++ src/logging.ts | 40 +- src/oauth-helper.ts | 537 +++++++++++++ tsconfig.json | 2 +- 20 files changed, 3277 insertions(+), 146 deletions(-) create mode 100644 examples/react/README.md create mode 100644 examples/react/index.html create mode 100644 examples/react/index.tsx create mode 100644 examples/react/oauth-helper.ts create mode 100644 examples/react/package.json create mode 100644 examples/react/pnpm-lock.yaml create mode 100644 examples/react/react_example.html create mode 100644 examples/react/react_example.tsx create mode 100644 examples/react/serve.js create mode 100644 examples/react/tsconfig.json create mode 100644 examples/react/tsconfig.node.json create mode 100644 examples/react/vite.config.ts create mode 100644 src/browser.ts create mode 100644 src/client/base.ts create mode 100644 src/oauth-helper.ts diff --git a/examples/react/README.md b/examples/react/README.md new file mode 100644 index 00000000..7eb295e3 --- /dev/null +++ b/examples/react/README.md @@ -0,0 +1,132 @@ +# MCP Use React Example + +This is a React example that demonstrates how to use the `mcp-use` library in a React/browser application. The example shows how to: + +- Import and initialize the MCPClient from `mcp-use/browser` +- Connect to MCP servers via WebSocket or HTTP/SSE +- Display available tools from connected servers +- Handle loading states and errors + +## Browser Compatibility + +The browser version (`mcp-use/browser`) supports: + +- ✅ **WebSocket connections**: Connect to MCP servers via WebSocket +- ✅ **HTTP/SSE connections**: Connect to MCP servers via HTTP or Server-Sent Events +- ❌ **Stdio connections**: Not supported (requires Node.js child_process) + +Example configurations: + +```typescript +// WebSocket connection +const config = { + mcpServers: { + myServer: { + ws_url: 'ws://localhost:8080', + authToken: 'optional-token' + } + } +} + +// HTTP connection (with automatic SSE fallback) +const config = { + mcpServers: { + myServer: { + url: 'http://localhost:8080', + authToken: 'optional-token', + preferSse: false // Set to true to force SSE + } + } +} +``` + +## Setup + +1. First, build the main `mcp-use` library: + + ```bash + cd ../../ + pnpm build + ``` + +2. Install dependencies for the React example: + + ```bash + cd examples/react + pnpm install + ``` + +3. Build the React example: + + ```bash + pnpm build + ``` + +4. Preview the example: + ```bash + pnpm preview + # or use the simple server: + pnpm serve + ``` + +## Development + +To run in development mode: + +```bash +pnpm dev +``` + +This will start a development server with hot reloading. + +## Features + +The React example includes: + +- **MCPTools Component**: A React component that displays available tools from MCP servers +- **Tool Display**: Shows tool names, descriptions, and input schemas +- **Server Management**: Connect/disconnect from MCP servers +- **Error Handling**: Displays connection errors and loading states +- **Responsive UI**: Clean, modern interface for exploring MCP tools + +## Configuration + +The example uses a default configuration with a filesystem server. You can modify the `exampleConfig` in `react_example.tsx` to use different MCP servers. + +## File Structure + +- `index.tsx` - Entry point for the React application +- `react_example.tsx` - Main React component with MCP integration +- `react_example.html` - HTML template +- `vite.config.ts` - Vite bundler configuration (includes browser polyfills) +- `package.json` - Dependencies and scripts +- `tsconfig.json` - TypeScript configuration + +## Important Notes + +### Vite Configuration + +The `vite.config.ts` includes necessary polyfills for browser compatibility: + +```typescript +const config = { + define: { + 'global': 'globalThis', + 'process.env.DEBUG': 'undefined', + 'process.platform': '""', + 'process.version': '""', + 'process.argv': '[]', + } +} +``` + +These definitions ensure that Node.js-specific code paths are properly handled in the browser environment. + +### Real MCP Client + +This example uses the **actual** MCP client code from `mcp-use/browser`, not mocks. It includes: + +- Real WebSocket and HTTP/SSE connectors +- Full MCP protocol implementation +- Actual tool listing and execution capabilities +- Browser-safe logging (falls back to console) diff --git a/examples/react/index.html b/examples/react/index.html new file mode 100644 index 00000000..67374825 --- /dev/null +++ b/examples/react/index.html @@ -0,0 +1,41 @@ + + + + + + MCP Tools Explorer - React Example + + + +
+
+

Loading MCP Tools Explorer...

+

If this message persists, check the browser console for errors.

+
+
+ + + + + diff --git a/examples/react/index.tsx b/examples/react/index.tsx new file mode 100644 index 00000000..dd2bb277 --- /dev/null +++ b/examples/react/index.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import { createRoot } from 'react-dom/client' +import ReactExample from './react_example' + +const container = document.getElementById('root') +if (container) { + const root = createRoot(container) + root.render() +} else { + console.error('Root element not found') +} diff --git a/examples/react/oauth-helper.ts b/examples/react/oauth-helper.ts new file mode 100644 index 00000000..81966d97 --- /dev/null +++ b/examples/react/oauth-helper.ts @@ -0,0 +1,451 @@ +/** + * OAuth helper for browser-based MCP authentication + * + * This helper provides OAuth 2.0 authorization code flow support for MCP servers + * that require authentication, such as Linear's MCP server. + */ + +export interface OAuthConfig { + clientId?: string // Optional - will use dynamic registration if not provided + redirectUri: string + scope?: string + state?: string + clientName?: string // For dynamic registration +} + +export interface OAuthDiscovery { + issuer: string + authorization_endpoint: string + token_endpoint: string + registration_endpoint?: string + response_types_supported: string[] + grant_types_supported: string[] + code_challenge_methods_supported: string[] + token_endpoint_auth_methods_supported?: string[] +} + +export interface ClientRegistration { + client_id: string + client_secret?: string + registration_access_token?: string + registration_client_uri?: string + client_id_issued_at?: number + client_secret_expires_at?: number +} + +export interface OAuthResult { + access_token: string + token_type: string + expires_at?: number | null + refresh_token?: string | null + scope?: string | null +} + +export interface OAuthState { + isRequired: boolean + isAuthenticated: boolean + isAuthenticating: boolean + isCompletingOAuth: boolean + authError: string | null + oauthTokens: OAuthResult | null +} + +export class OAuthHelper { + private config: OAuthConfig + private discovery?: OAuthDiscovery + private state: OAuthState + private clientRegistration?: ClientRegistration + + constructor(config: OAuthConfig) { + this.config = config + this.state = { + isRequired: false, + isAuthenticated: false, + isAuthenticating: false, + isCompletingOAuth: false, + authError: null, + oauthTokens: null, + } + } + + /** + * Get current OAuth state + */ + getState(): OAuthState { + return { ...this.state } + } + + /** + * Check if a server requires authentication by pinging the URL + */ + async checkAuthRequired(serverUrl: string): Promise { + console.log('🔍 [OAuthHelper] Checking auth requirement for:', serverUrl) + + try { + const response = await fetch(serverUrl, { + method: 'GET', + headers: { + 'Accept': 'text/event-stream', + 'Cache-Control': 'no-cache', + }, + redirect: 'manual', + signal: AbortSignal.timeout(10000), // 10 second timeout + }) + + console.log('🔍 [OAuthHelper] Auth check response:', { + status: response.status, + statusText: response.statusText, + url: serverUrl, + }) + + // 401 Unauthorized, 403 Forbidden, or 400 Bad Request means auth is required + if (response.status === 401 || response.status === 403 || response.status === 400) { + console.log('🔐 [OAuthHelper] Authentication required for:', serverUrl) + return true + } + + // Any other response (200, 404, 500, etc.) means no auth required + console.log('✅ [OAuthHelper] No authentication required for:', serverUrl) + return false + } catch (error: any) { + console.warn('⚠️ [OAuthHelper] Could not check auth requirement for:', serverUrl, error) + + // Handle specific error types + if (error.name === 'TypeError' && + (error.message?.includes('CORS') || error.message?.includes('Failed to fetch'))) { + console.log('🔍 [OAuthHelper] CORS blocked direct check, using heuristics for:', serverUrl) + return this.checkAuthByHeuristics(serverUrl) + } + + if (error.name === 'AbortError') { + console.log('⏰ [OAuthHelper] Request timeout, assuming no auth required for:', serverUrl) + return false + } + + // If we can't reach the server at all, try heuristics + return this.checkAuthByHeuristics(serverUrl) + } + } + + /** + * Fallback heuristics for determining auth requirements when direct checking fails + */ + private checkAuthByHeuristics(serverUrl: string): boolean { + console.log('🔍 [OAuthHelper] Using heuristics to determine auth for:', serverUrl) + + // Known patterns that typically require auth + const authRequiredPatterns = [ + /api\.githubcopilot\.com/i, // GitHub Copilot + /api\.github\.com/i, // GitHub API + /.*\.googleapis\.com/i, // Google APIs + /api\.openai\.com/i, // OpenAI + /api\.anthropic\.com/i, // Anthropic + /.*\.atlassian\.net/i, // Atlassian (Jira, Confluence) + /.*\.slack\.com/i, // Slack + /api\.notion\.com/i, // Notion + /api\.linear\.app/i, // Linear + ] + + // Known patterns that typically don't require auth (public MCP servers) + const noAuthPatterns = [ + /localhost/i, // Local development + /127\.0\.0\.1/i, // Local development + /\.local/i, // Local development + /mcp\..*\.com/i, // Generic MCP server pattern (often public) + ] + + // Check no-auth patterns first + for (const pattern of noAuthPatterns) { + if (pattern.test(serverUrl)) { + console.log('✅ [OAuthHelper] Heuristic: No auth required (matches no-auth pattern):', serverUrl) + return false + } + } + + // Check auth-required patterns + for (const pattern of authRequiredPatterns) { + if (pattern.test(serverUrl)) { + console.log('🔐 [OAuthHelper] Heuristic: Auth required (matches auth pattern):', serverUrl) + return true + } + } + + // Default: assume no auth required for unknown patterns + console.log('❓ [OAuthHelper] Heuristic: Unknown pattern, assuming no auth required:', serverUrl) + return false + } + + /** + * Discover OAuth configuration from a server + */ + async discoverOAuthConfig(serverUrl: string): Promise { + try { + const discoveryUrl = `${serverUrl}/.well-known/oauth-authorization-server` + const response = await fetch(discoveryUrl) + + if (!response.ok) { + throw new Error(`OAuth discovery failed: ${response.status} ${response.statusText}`) + } + + this.discovery = await response.json() + return this.discovery + } catch (error) { + throw new Error(`Failed to discover OAuth configuration: ${error}`) + } + } + + /** + * Register a new OAuth client dynamically + */ + async registerClient(serverUrl: string): Promise { + if (!this.discovery) { + throw new Error('OAuth discovery not performed. Call discoverOAuthConfig first.') + } + + if (!this.discovery.registration_endpoint) { + throw new Error('Server does not support dynamic client registration') + } + + try { + const registrationData = { + client_name: this.config.clientName || 'MCP Use Example', + redirect_uris: [this.config.redirectUri], + grant_types: ['authorization_code'], + response_types: ['code'], + token_endpoint_auth_method: 'none', // Use public client (no secret) + scope: this.config.scope || 'read write', + } + + console.log('🔐 [OAuthHelper] Registering OAuth client dynamically:', { + registration_endpoint: this.discovery.registration_endpoint, + client_name: registrationData.client_name, + redirect_uri: this.config.redirectUri, + }) + + const response = await fetch(this.discovery.registration_endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(registrationData) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Client registration failed: ${response.status} ${response.statusText} - ${errorText}`) + } + + this.clientRegistration = await response.json() + console.log('✅ [OAuthHelper] Client registered successfully:', { + client_id: this.clientRegistration.client_id, + client_secret: this.clientRegistration.client_secret ? '***' : 'none', + }) + + return this.clientRegistration + } catch (error) { + console.error('❌ [OAuthHelper] Client registration failed:', error) + throw new Error(`Failed to register OAuth client: ${error}`) + } + } + + /** + * Generate authorization URL for OAuth flow + */ + generateAuthUrl(serverUrl: string, additionalParams?: Record): string { + if (!this.discovery) { + throw new Error('OAuth discovery not performed. Call discoverOAuthConfig first.') + } + + const params = new URLSearchParams({ + client_id: this.config.clientId, + redirect_uri: this.config.redirectUri, + response_type: 'code', + scope: this.config.scope || 'read', + state: this.config.state || this.generateState(), + ...additionalParams + }) + + return `${this.discovery.authorization_endpoint}?${params.toString()}` + } + + /** + * Exchange authorization code for access token + */ + async exchangeCodeForToken( + serverUrl: string, + code: string, + codeVerifier?: string + ): Promise { + if (!this.discovery) { + throw new Error('OAuth discovery not performed. Call discoverOAuthConfig first.') + } + + const body = new URLSearchParams({ + grant_type: 'authorization_code', + client_id: this.config.clientId, + code, + redirect_uri: this.config.redirectUri, + }) + + if (codeVerifier) { + body.append('code_verifier', codeVerifier) + } + + const response = await fetch(this.discovery.token_endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: body.toString() + }) + + if (!response.ok) { + const error = await response.text() + throw new Error(`Token exchange failed: ${response.status} ${response.statusText} - ${error}`) + } + + return await response.json() + } + + /** + * Handle OAuth callback and extract authorization code + */ + handleCallback(): { code: string; state: string } | null { + const urlParams = new URLSearchParams(window.location.search) + const code = urlParams.get('code') + const state = urlParams.get('state') + + if (!code || !state) { + return null + } + + // Clean up URL + const url = new URL(window.location.href) + url.searchParams.delete('code') + url.searchParams.delete('state') + window.history.replaceState({}, '', url.toString()) + + return { code, state } + } + + /** + * Start OAuth flow by opening popup window (similar to your implementation) + */ + async startOAuthFlow(serverUrl: string): Promise { + this.setState({ + isAuthenticating: true, + authError: null, + }) + + try { + await this.discoverOAuthConfig(serverUrl) + const authUrl = this.generateAuthUrl(serverUrl) + + // Open popup window for authentication (similar to your implementation) + const authWindow = window.open( + authUrl, + 'mcp-oauth', + 'width=500,height=600,scrollbars=yes,resizable=yes,status=yes,location=yes' + ) + + if (!authWindow) { + throw new Error('Failed to open authentication window. Please allow popups for this site and try again.') + } + + console.log('✅ [OAuthHelper] OAuth popup opened successfully') + } catch (error) { + console.error('❌ [OAuthHelper] Failed to start OAuth flow:', error) + this.setState({ + isAuthenticating: false, + authError: error instanceof Error ? error.message : 'Failed to start authentication', + }) + throw error + } + } + + /** + * Complete OAuth flow by exchanging code for token + */ + async completeOAuthFlow(serverUrl: string, code: string): Promise { + this.setState({ + isCompletingOAuth: true, + authError: null, + }) + + try { + const tokenResponse = await this.exchangeCodeForToken(serverUrl, code) + + this.setState({ + isAuthenticating: false, + isAuthenticated: true, + isCompletingOAuth: false, + authError: null, + oauthTokens: tokenResponse, + }) + + console.log('✅ [OAuthHelper] OAuth flow completed successfully') + return tokenResponse + } catch (error) { + console.error('❌ [OAuthHelper] Failed to complete OAuth flow:', error) + this.setState({ + isAuthenticating: false, + isCompletingOAuth: false, + authError: error instanceof Error ? error.message : 'Failed to complete authentication', + }) + throw error + } + } + + /** + * Reset authentication state + */ + resetAuth(): void { + this.setState({ + isRequired: false, + isAuthenticated: false, + isAuthenticating: false, + isCompletingOAuth: false, + authError: null, + oauthTokens: null, + }) + } + + /** + * Set OAuth state (internal method) + */ + private setState(newState: Partial): void { + this.state = { ...this.state, ...newState } + } + + /** + * Generate a random state parameter for CSRF protection + */ + private generateState(): string { + return Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15) + } +} + +/** + * Linear-specific OAuth configuration + */ +export const LINEAR_OAUTH_CONFIG: OAuthConfig = { + clientId: 'mcp-use-example', // This should be registered with Linear + redirectUri: window.location.origin + window.location.pathname, + scope: 'read write', +} + +/** + * Helper function to create OAuth-enabled MCP configuration + */ +export function createOAuthMCPConfig(serverUrl: string, accessToken: string) { + return { + mcpServers: { + linear: { + url: serverUrl, + authToken: accessToken, + transport: 'sse' + } + } + } +} diff --git a/examples/react/package.json b/examples/react/package.json new file mode 100644 index 00000000..5bf96865 --- /dev/null +++ b/examples/react/package.json @@ -0,0 +1,28 @@ +{ + "name": "mcp-use-react-example", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "serve": "node serve.js" + }, + "dependencies": { + "mcp-use": "file:../", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@esbuild-plugins/node-globals-polyfill": "^0.2.3", + "@esbuild-plugins/node-modules-polyfill": "^0.2.2", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "typescript": "^5.0.0", + "util": "^0.12.5", + "vite": "^4.4.0" + } +} diff --git a/examples/react/pnpm-lock.yaml b/examples/react/pnpm-lock.yaml new file mode 100644 index 00000000..4187c83b --- /dev/null +++ b/examples/react/pnpm-lock.yaml @@ -0,0 +1,1206 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + mcp-use: + specifier: file:../ + version: examples@file:.. + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + devDependencies: + '@esbuild-plugins/node-globals-polyfill': + specifier: ^0.2.3 + version: 0.2.3(esbuild@0.18.20) + '@esbuild-plugins/node-modules-polyfill': + specifier: ^0.2.2 + version: 0.2.2(esbuild@0.18.20) + '@types/react': + specifier: ^18.2.0 + version: 18.3.26 + '@types/react-dom': + specifier: ^18.2.0 + version: 18.3.7(@types/react@18.3.26) + '@vitejs/plugin-react': + specifier: ^4.0.0 + version: 4.7.0(vite@4.5.14) + buffer: + specifier: ^6.0.3 + version: 6.0.3 + events: + specifier: ^3.3.0 + version: 3.3.0 + typescript: + specifier: ^5.0.0 + version: 5.9.3 + util: + specifier: ^0.12.5 + version: 0.12.5 + vite: + specifier: ^4.4.0 + version: 4.5.14 + +packages: + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.4': + resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.4': + resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + + '@esbuild-plugins/node-globals-polyfill@0.2.3': + resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==} + peerDependencies: + esbuild: '*' + + '@esbuild-plugins/node-modules-polyfill@0.2.2': + resolution: {integrity: sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==} + peerDependencies: + esbuild: '*' + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.26': + resolution: {integrity: sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.8.13: + resolution: {integrity: sha512-7s16KR8io8nIBWQyCYhmFhd+ebIzb9VKTzki+wOJXHTxTnV6+mFGH3+Jwn1zoKaY9/H9T/0BcKCZnzXljPnpSQ==} + hasBin: true + + browserslist@4.26.3: + resolution: {integrity: sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + caniuse-lite@1.0.30001748: + resolution: {integrity: sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.232: + resolution: {integrity: sha512-ENirSe7wf8WzyPCibqKUG1Cg43cPaxH4wRR7AJsX7MCABCHBIOFqvaYODSLKUuZdraxUTHRE/0A2Aq8BYKEHOg==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + estree-walker@0.6.1: + resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + examples@file:..: + resolution: {directory: .., type: directory} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.23: + resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + rollup-plugin-inject@3.0.2: + resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} + deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject. + + rollup-plugin-node-polyfills@0.2.1: + resolution: {integrity: sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==} + + rollup-pluginutils@2.8.2: + resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} + + rollup@3.29.5: + resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + + vite@4.5.14: + resolution: {integrity: sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + +snapshots: + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.4': {} + + '@babel/core@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.4 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.26.3 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@babel/traverse@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.18.20)': + dependencies: + esbuild: 0.18.20 + + '@esbuild-plugins/node-modules-polyfill@0.2.2(esbuild@0.18.20)': + dependencies: + esbuild: 0.18.20 + escape-string-regexp: 4.0.0 + rollup-plugin-node-polyfills: 0.2.1 + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.4 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.4 + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.26)': + dependencies: + '@types/react': 18.3.26 + + '@types/react@18.3.26': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.1.3 + + '@vitejs/plugin-react@4.7.0(vite@4.5.14)': + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.4) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 4.5.14 + transitivePeerDependencies: + - supports-color + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.8.13: {} + + browserslist@4.26.3: + dependencies: + baseline-browser-mapping: 2.8.13 + caniuse-lite: 1.0.30001748 + electron-to-chromium: 1.5.232 + node-releases: 2.0.23 + update-browserslist-db: 1.1.3(browserslist@4.26.3) + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + caniuse-lite@1.0.30001748: {} + + convert-source-map@2.0.0: {} + + csstype@3.1.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.232: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + estree-walker@0.6.1: {} + + events@3.3.0: {} + + examples@file:..: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + ieee754@1.2.1: {} + + inherits@2.0.4: {} + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.25.9: + dependencies: + sourcemap-codec: 1.4.8 + + math-intrinsics@1.1.0: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + node-releases@2.0.23: {} + + picocolors@1.1.1: {} + + possible-typed-array-names@1.1.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-refresh@0.17.0: {} + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + rollup-plugin-inject@3.0.2: + dependencies: + estree-walker: 0.6.1 + magic-string: 0.25.9 + rollup-pluginutils: 2.8.2 + + rollup-plugin-node-polyfills@0.2.1: + dependencies: + rollup-plugin-inject: 3.0.2 + + rollup-pluginutils@2.8.2: + dependencies: + estree-walker: 0.6.1 + + rollup@3.29.5: + optionalDependencies: + fsevents: 2.3.3 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + source-map-js@1.2.1: {} + + sourcemap-codec@1.4.8: {} + + typescript@5.9.3: {} + + update-browserslist-db@1.1.3(browserslist@4.26.3): + dependencies: + browserslist: 4.26.3 + escalade: 3.2.0 + picocolors: 1.1.1 + + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.2 + is-typed-array: 1.1.15 + which-typed-array: 1.1.19 + + vite@4.5.14: + dependencies: + esbuild: 0.18.20 + postcss: 8.5.6 + rollup: 3.29.5 + optionalDependencies: + fsevents: 2.3.3 + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + yallist@3.1.1: {} diff --git a/examples/react/react_example.html b/examples/react/react_example.html new file mode 100644 index 00000000..67374825 --- /dev/null +++ b/examples/react/react_example.html @@ -0,0 +1,41 @@ + + + + + + MCP Tools Explorer - React Example + + + +
+
+

Loading MCP Tools Explorer...

+

If this message persists, check the browser console for errors.

+
+
+ + + + + diff --git a/examples/react/react_example.tsx b/examples/react/react_example.tsx new file mode 100644 index 00000000..21eb76f6 --- /dev/null +++ b/examples/react/react_example.tsx @@ -0,0 +1,369 @@ +import React, { useState, useEffect } from 'react' +import { MCPClient, OAuthHelper, LINEAR_OAUTH_CONFIG, createOAuthMCPConfig } from 'mcp-use/browser' + +interface Tool { + name: string + description?: string + inputSchema?: any +} + +interface MCPToolsProps { + config?: Record +} + +const MCPTools: React.FC = ({ config }) => { + const [client, setClient] = useState(null) + const [tools, setTools] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [connectedServers, setConnectedServers] = useState([]) + const [oauthHelper] = useState(() => new OAuthHelper(LINEAR_OAUTH_CONFIG)) + const [oauthState, setOAuthState] = useState(oauthHelper.getState()) + const [serverUrl] = useState('https://mcp.linear.app') + + useEffect(() => { + if (config) { + const mcpClient = new MCPClient(config) + setClient(mcpClient) + } + }, [config]) + + // Handle OAuth callback on component mount + useEffect(() => { + const handleOAuthCallback = async () => { + const callback = oauthHelper.handleCallback() + if (callback) { + try { + const tokenResult = await oauthHelper.completeOAuthFlow(serverUrl, callback.code) + setOAuthState(oauthHelper.getState()) + + // Create new config with the access token + const oauthConfig = createOAuthMCPConfig(`${serverUrl}/sse`, tokenResult.access_token) + const mcpClient = new MCPClient(oauthConfig) + setClient(mcpClient) + } catch (err) { + setError(`OAuth authentication failed: ${err instanceof Error ? err.message : 'Unknown error'}`) + setOAuthState(oauthHelper.getState()) + } + } + } + + handleOAuthCallback() + }, [oauthHelper, serverUrl]) + + // Check if authentication is required on mount + useEffect(() => { + const checkAuthRequirement = async () => { + try { + const requiresAuth = await oauthHelper.checkAuthRequired(`${serverUrl}/sse`) + if (requiresAuth) { + setOAuthState(oauthHelper.getState()) + } + } catch (err) { + console.warn('Could not check auth requirement:', err) + } + } + + checkAuthRequirement() + }, [oauthHelper, serverUrl]) + + const loadTools = async () => { + if (!client) { + setError('MCP Client not initialized') + return + } + + setLoading(true) + setError(null) + + try { + // Get all server names from config + const serverNames = client.getServerNames() + + if (serverNames.length === 0) { + setError('No MCP servers configured') + setLoading(false) + return + } + + // Create sessions for all servers + const sessions = await client.createAllSessions() + setConnectedServers(Object.keys(sessions)) + + // Collect tools from all sessions + const allTools: Tool[] = [] + + for (const [serverName, session] of Object.entries(sessions)) { + try { + const sessionTools = session.connector.tools + const toolsWithServer = sessionTools.map(tool => ({ + ...tool, + server: serverName + })) + allTools.push(...toolsWithServer) + } catch (err) { + console.warn(`Failed to get tools from server ${serverName}:`, err) + } + } + + setTools(allTools) + } catch (err) { + setError(`Failed to load tools: ${err instanceof Error ? err.message : 'Unknown error'}`) + } finally { + setLoading(false) + } + } + + const disconnect = async () => { + if (client) { + await client.closeAllSessions() + setConnectedServers([]) + setTools([]) + } + } + + const startOAuthFlow = async () => { + setError(null) + try { + await oauthHelper.startOAuthFlow(serverUrl) + setOAuthState(oauthHelper.getState()) + } catch (err) { + setError(`Failed to start OAuth flow: ${err instanceof Error ? err.message : 'Unknown error'}`) + setOAuthState(oauthHelper.getState()) + } + } + + const clearAuth = () => { + oauthHelper.resetAuth() + setOAuthState(oauthHelper.getState()) + setClient(null) + setConnectedServers([]) + setTools([]) + setError(null) + } + + return ( +
+

MCP Tools Explorer

+ +
+ {!oauthState.isAuthenticated ? ( + + ) : ( + <> + + + + + + + )} +
+ + {error && ( +
+ Error: {error} +
+ )} + + {oauthState.isAuthenticated && oauthState.oauthTokens && ( +
+

✓ Authenticated with Linear

+

+ Access token: {oauthState.oauthTokens.access_token.substring(0, 20)}... + {oauthState.oauthTokens.expires_at && ( + (expires: {new Date(oauthState.oauthTokens.expires_at * 1000).toLocaleString()}) + )} +

+
+ )} + + {oauthState.authError && ( +
+ OAuth Error: {oauthState.authError} +
+ )} + + {connectedServers.length > 0 && ( +
+

Connected Servers:

+
    + {connectedServers.map(server => ( +
  • ✓ {server}
  • + ))} +
+
+ )} + +
+

Available Tools ({tools.length})

+ {tools.length === 0 && !loading && ( +

No tools loaded. Click "Load Tools" to get started.

+ )} + + {tools.map((tool, index) => ( +
+

+ {tool.name} + {tool.server && ( + + from {tool.server} + + )} +

+ + {tool.description && ( +

+ {tool.description} +

+ )} + + {tool.inputSchema && ( +
+ + Input Schema + +
+                  {JSON.stringify(tool.inputSchema, null, 2)}
+                
+
+ )} +
+ ))} +
+
+ ) +} + +// Example usage component +const ReactExample: React.FC = () => { + return ( +
+ + +
+

🔐 OAuth Authentication Required

+

+ This example demonstrates OAuth authentication with Linear's MCP server. + Click "Authenticate with Linear" to start the OAuth flow. +

+

+ Note: You'll need to register this application with Linear to get a proper client ID. + For this demo, we're using a placeholder client ID. +

+

+ Browser limitations: Stdio connections (using command and args) + are not supported in the browser. Use ws_url for WebSocket or url for HTTP/SSE connections. +

+
+
+ ) +} + +export default ReactExample diff --git a/examples/react/serve.js b/examples/react/serve.js new file mode 100644 index 00000000..aec09416 --- /dev/null +++ b/examples/react/serve.js @@ -0,0 +1,59 @@ +#!/usr/bin/env node + +import { createServer } from 'http' +import { readFileSync, existsSync } from 'fs' +import { join, extname } from 'path' +import { fileURLToPath } from 'url' +import { dirname } from 'path' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +const port = process.env.PORT || 3000 +const distDir = join(__dirname, 'dist') + +const mimeTypes = { + '.html': 'text/html', + '.js': 'application/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon' +} + +const server = createServer((req, res) => { + let filePath = join(distDir, req.url === '/' ? 'react_example.html' : req.url) + + // Security check - prevent directory traversal + if (!filePath.startsWith(distDir)) { + res.writeHead(403) + res.end('Forbidden') + return + } + + if (!existsSync(filePath)) { + res.writeHead(404) + res.end('Not Found') + return + } + + try { + const content = readFileSync(filePath) + const ext = extname(filePath) + const contentType = mimeTypes[ext] || 'application/octet-stream' + + res.writeHead(200, { 'Content-Type': contentType }) + res.end(content) + } catch (error) { + res.writeHead(500) + res.end('Internal Server Error') + } +}) + +server.listen(port, () => { + console.log(`🚀 React example server running at http://localhost:${port}`) + console.log(`📁 Serving files from: ${distDir}`) +}) diff --git a/examples/react/tsconfig.json b/examples/react/tsconfig.json new file mode 100644 index 00000000..2c731fae --- /dev/null +++ b/examples/react/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["**/*.ts", "**/*.tsx"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/react/tsconfig.node.json b/examples/react/tsconfig.node.json new file mode 100644 index 00000000..42872c59 --- /dev/null +++ b/examples/react/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/react/vite.config.ts b/examples/react/vite.config.ts new file mode 100644 index 00000000..eed0d196 --- /dev/null +++ b/examples/react/vite.config.ts @@ -0,0 +1,46 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { resolve } from 'path' +import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill' +import { NodeModulesPolyfillPlugin } from '@esbuild-plugins/node-modules-polyfill' + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + commonjsOptions: { + transformMixedEsModules: true + } + }, + resolve: { + alias: { + 'mcp-use/browser': resolve(__dirname, '../../dist/src/browser.js'), + 'mcp-use': resolve(__dirname, '../../dist/src'), + } + }, + define: { + global: 'globalThis', + 'process.env.DEBUG': 'undefined', + 'process.env.MCP_USE_ANONYMIZED_TELEMETRY': 'undefined', + 'process.env.MCP_USE_TELEMETRY_SOURCE': 'undefined', + 'process.env.MCP_USE_LANGFUSE': 'undefined', + 'process.platform': '""', + 'process.version': '""', + 'process.argv': '[]', + }, + optimizeDeps: { + include: ['react', 'react-dom'], + esbuildOptions: { + define: { + global: 'globalThis' + }, + plugins: [ + NodeGlobalsPolyfillPlugin({ + process: true, + buffer: true + }), + NodeModulesPolyfillPlugin() + ] + } + } +}) diff --git a/package.json b/package.json index 01ff97ef..7921878b 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,10 @@ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" + }, + "./browser": { + "types": "./dist/src/browser.d.ts", + "import": "./dist/src/browser.js" } }, "main": "./dist/index.js", @@ -103,6 +107,7 @@ "posthog-node": "^5.1.1", "uuid": "^11.1.0", "winston": "^3.17.0", + "winston-transport-browserconsole": "^1.0.5", "ws": "^8.18.2", "zod": "^3.25.48", "zod-to-json-schema": "^3.24.6" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a18aa20e..5d1fdc35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: winston: specifier: ^3.17.0 version: 3.17.0 + winston-transport-browserconsole: + specifier: ^1.0.5 + version: 1.0.5 ws: specifier: ^8.18.2 version: 8.18.2 @@ -2896,6 +2899,9 @@ packages: engines: {node: '>=8'} hasBin: true + winston-transport-browserconsole@1.0.5: + resolution: {integrity: sha512-BFZDvknATAmsqaRY3WatB2S0eEb0ziwqBYIv+H0Fnu9oe7GnPUVQzEJC1frmLdNsfEkfnyR1PCNDBAFtZE3SuQ==} + winston-transport@4.9.0: resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} engines: {node: '>= 12.0.0'} @@ -5903,6 +5909,11 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + winston-transport-browserconsole@1.0.5: + dependencies: + winston: 3.17.0 + winston-transport: 4.9.0 + winston-transport@4.9.0: dependencies: logform: 2.7.0 diff --git a/src/browser.ts b/src/browser.ts new file mode 100644 index 00000000..91cb6a5e --- /dev/null +++ b/src/browser.ts @@ -0,0 +1,95 @@ +/** + * Browser-compatible exports for mcp-use + * + * This module provides browser-safe versions of mcp-use components + * that avoid Node.js-specific dependencies (like fs, path for file operations). + * + * The actual MCP protocol connectors (WebSocket, HTTP/SSE) work fine in browsers. + */ + +import type { BaseConnector } from './connectors/base.js' +import { HttpConnector } from './connectors/http.js' +import { WebSocketConnector } from './connectors/websocket.js' +import { BaseMCPClient } from './client/base.js' +import { logger } from './logging.js' +import { MCPSession } from './session.js' + +/** + * Browser-compatible MCP Client + * + * Unlike the Node.js version, this doesn't support: + * - Loading config from files (loadConfigFile) + * - Saving config to files (saveConfig) + * - StdioConnector (requires child_process) + * + * Supported connectors: + * - WebSocketConnector: Connect to MCP servers via WebSocket + * - HttpConnector: Connect to MCP servers via HTTP/SSE + */ +export class MCPClient extends BaseMCPClient { + constructor(config?: Record) { + super(config) + } + + public static fromDict(cfg: Record): MCPClient { + return new MCPClient(cfg) + } + + /** + * Create a connector from server configuration (browser-safe version) + * + * Supports: + * - WebSocket connections: { ws_url: "ws://..." } + * - HTTP connections: { url: "http://..." } + * + * Does NOT support: + * - Stdio connections: { command: "...", args: [...] } + */ + protected createConnectorFromConfig(serverConfig: Record): BaseConnector { + // WebSocket connector + if ('ws_url' in serverConfig) { + return new WebSocketConnector(serverConfig.ws_url, { + headers: serverConfig.headers, + authToken: serverConfig.auth_token || serverConfig.authToken, + }) + } + + // HTTP/SSE connector + if ('url' in serverConfig) { + const transport = serverConfig.transport || 'http' + + return new HttpConnector(serverConfig.url, { + headers: serverConfig.headers, + authToken: serverConfig.auth_token || serverConfig.authToken, + preferSse: serverConfig.preferSse || transport === 'sse', + }) + } + + // Stdio is not supported in browser + if ('command' in serverConfig && 'args' in serverConfig) { + throw new Error( + 'StdioConnector is not supported in browser environments. ' + + 'Use WebSocket (ws_url) or HTTP (url) connectors instead.', + ) + } + + throw new Error('Cannot determine connector type from config. Use "url" for HTTP or "ws_url" for WebSocket.') + } +} + +// Re-export browser-safe connectors +export { HttpConnector, WebSocketConnector } + +// Re-export session +export { MCPSession } + +// Re-export logger (already browser-safe) +export { logger } + +// Re-export OAuth helper for browser authentication +export { OAuthHelper, LINEAR_OAUTH_CONFIG, createOAuthMCPConfig } from './oauth-helper.js' +export type { OAuthConfig, OAuthDiscovery, OAuthResult, OAuthState, ClientRegistration } from './oauth-helper.js' + +// Re-export types that are safe for browser +export type { BaseMessage, HumanMessage, SystemMessage, AIMessage, ToolMessage } from '@langchain/core/messages' +export type { StreamEvent } from '@langchain/core/tracers/log_stream' \ No newline at end of file diff --git a/src/client.ts b/src/client.ts index b7449612..3c7c1539 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,23 +1,30 @@ import fs from 'node:fs' import path from 'node:path' +import type { BaseConnector } from './connectors/base.js' import { createConnectorFromConfig, loadConfigFile } from './config.js' -import { logger } from './logging.js' -import { MCPSession } from './session.js' - -export class MCPClient { - private config: Record = {} - private sessions: Record = {} - public activeSessions: string[] = [] - +import { BaseMCPClient } from './client/base.js' + +/** + * Node.js-specific MCPClient implementation + * + * Extends the base client with Node.js-specific features like: + * - File system operations (saveConfig) + * - Config file loading (fromConfigFile) + * - All connector types including StdioConnector + */ +export class MCPClient extends BaseMCPClient { constructor(config?: string | Record) { if (config) { if (typeof config === 'string') { - this.config = loadConfigFile(config) + super(loadConfigFile(config)) } else { - this.config = config + super(config) } } + else { + super() + } } public static fromDict(cfg: Record): MCPClient { @@ -28,30 +35,9 @@ export class MCPClient { return new MCPClient(loadConfigFile(path)) } - public addServer(name: string, serverConfig: Record): void { - this.config.mcpServers = this.config.mcpServers || {} - this.config.mcpServers[name] = serverConfig - } - - public removeServer(name: string): void { - if (this.config.mcpServers?.[name]) { - delete this.config.mcpServers[name] - this.activeSessions = this.activeSessions.filter(n => n !== name) - } - } - - public getServerNames(): string[] { - return Object.keys(this.config.mcpServers ?? {}) - } - - public getServerConfig(name: string): Record { - return this.config.mcpServers?.[name] - } - - public getConfig(): Record { - return this.config ?? {} - } - + /** + * Save configuration to a file (Node.js only) + */ public saveConfig(filepath: string): void { const dir = path.dirname(filepath) if (!fs.existsSync(dir)) { @@ -60,104 +46,11 @@ export class MCPClient { fs.writeFileSync(filepath, JSON.stringify(this.config, null, 2), 'utf-8') } - public async createSession( - serverName: string, - autoInitialize = true, - ): Promise { - const servers = this.config.mcpServers ?? {} - - if (Object.keys(servers).length === 0) { - logger.warn('No MCP servers defined in config') - } - - if (!servers[serverName]) { - throw new Error(`Server '${serverName}' not found in config`) - } - - const connector = createConnectorFromConfig(servers[serverName]) - const session = new MCPSession(connector) - if (autoInitialize) { - await session.initialize() - } - - this.sessions[serverName] = session - if (!this.activeSessions.includes(serverName)) { - this.activeSessions.push(serverName) - } - return session - } - - public async createAllSessions( - autoInitialize = true, - ): Promise> { - const servers = this.config.mcpServers ?? {} - - if (Object.keys(servers).length === 0) { - logger.warn('No MCP servers defined in config') - } - - for (const name of Object.keys(servers)) { - await this.createSession(name, autoInitialize) - } - - return this.sessions - } - - public getSession(serverName: string): MCPSession | null { - const session = this.sessions[serverName] - // if (!session) { - // throw new Error(`No session exists for server '${serverName}'`) - // } - if (!session) { - return null - } - return session - } - - public getAllActiveSessions(): Record { - return Object.fromEntries( - this.activeSessions.map(n => [n, this.sessions[n]]), - ) - } - - public async closeSession(serverName: string): Promise { - const session = this.sessions[serverName] - if (!session) { - logger.warn(`No session exists for server ${serverName}, nothing to close`) - return - } - try { - logger.debug(`Closing session for server ${serverName}`) - await session.disconnect() - } - catch (e) { - logger.error(`Error closing session for server '${serverName}': ${e}`) - } - finally { - delete this.sessions[serverName] - this.activeSessions = this.activeSessions.filter(n => n !== serverName) - } - } - - public async closeAllSessions(): Promise { - const serverNames = Object.keys(this.sessions) - const errors: string[] = [] - for (const serverName of serverNames) { - try { - logger.debug(`Closing session for server ${serverName}`) - await this.closeSession(serverName) - } - catch (e: any) { - const errorMsg = `Failed to close session for server '${serverName}': ${e}` - logger.error(errorMsg) - errors.push(errorMsg) - } - } - if (errors.length) { - logger.error(`Encountered ${errors.length} errors while closing sessions`) - } - else { - logger.debug('All sessions closed successfully') - } + /** + * Create a connector from server configuration (Node.js version) + * Supports all connector types including StdioConnector + */ + protected createConnectorFromConfig(serverConfig: Record): BaseConnector { + return createConnectorFromConfig(serverConfig) } } diff --git a/src/client/base.ts b/src/client/base.ts new file mode 100644 index 00000000..2ca697ac --- /dev/null +++ b/src/client/base.ts @@ -0,0 +1,155 @@ +import type { BaseConnector } from '../connectors/base.js' +import { logger } from '../logging.js' +import { MCPSession } from '../session.js' + +/** + * Base MCPClient class with shared functionality + * + * This class contains all the common logic that works in both Node.js and browser environments. + * Platform-specific implementations should extend this class and override methods as needed. + */ +export abstract class BaseMCPClient { + protected config: Record = {} + protected sessions: Record = {} + public activeSessions: string[] = [] + + constructor(config?: Record) { + if (config) { + this.config = config + } + } + + public static fromDict(_cfg: Record): BaseMCPClient { + // This will be overridden by concrete implementations + throw new Error('fromDict must be implemented by concrete class') + } + + public addServer(name: string, serverConfig: Record): void { + this.config.mcpServers = this.config.mcpServers || {} + this.config.mcpServers[name] = serverConfig + } + + public removeServer(name: string): void { + if (this.config.mcpServers?.[name]) { + delete this.config.mcpServers[name] + this.activeSessions = this.activeSessions.filter(n => n !== name) + } + } + + public getServerNames(): string[] { + return Object.keys(this.config.mcpServers ?? {}) + } + + public getServerConfig(name: string): Record { + return this.config.mcpServers?.[name] + } + + public getConfig(): Record { + return this.config ?? {} + } + + /** + * Create a connector from server configuration + * This method must be implemented by platform-specific subclasses + */ + protected abstract createConnectorFromConfig(serverConfig: Record): BaseConnector + + public async createSession( + serverName: string, + autoInitialize = true, + ): Promise { + const servers = this.config.mcpServers ?? {} + + if (Object.keys(servers).length === 0) { + logger.warn('No MCP servers defined in config') + } + + if (!servers[serverName]) { + throw new Error(`Server '${serverName}' not found in config`) + } + + const connector = this.createConnectorFromConfig(servers[serverName]) + const session = new MCPSession(connector) + + if (autoInitialize) { + await session.initialize() + } + + this.sessions[serverName] = session + if (!this.activeSessions.includes(serverName)) { + this.activeSessions.push(serverName) + } + return session + } + + public async createAllSessions( + autoInitialize = true, + ): Promise> { + const servers = this.config.mcpServers ?? {} + + if (Object.keys(servers).length === 0) { + logger.warn('No MCP servers defined in config') + } + + for (const name of Object.keys(servers)) { + await this.createSession(name, autoInitialize) + } + + return this.sessions + } + + public getSession(serverName: string): MCPSession | null { + const session = this.sessions[serverName] + if (!session) { + return null + } + return session + } + + public getAllActiveSessions(): Record { + return Object.fromEntries( + this.activeSessions.map(n => [n, this.sessions[n]]), + ) + } + + public async closeSession(serverName: string): Promise { + const session = this.sessions[serverName] + if (!session) { + logger.warn(`No session exists for server ${serverName}, nothing to close`) + return + } + try { + logger.debug(`Closing session for server ${serverName}`) + await session.disconnect() + } + catch (e) { + logger.error(`Error closing session for server '${serverName}': ${e}`) + } + finally { + delete this.sessions[serverName] + this.activeSessions = this.activeSessions.filter(n => n !== serverName) + } + } + + public async closeAllSessions(): Promise { + const serverNames = Object.keys(this.sessions) + const errors: string[] = [] + for (const serverName of serverNames) { + try { + logger.debug(`Closing session for server ${serverName}`) + await this.closeSession(serverName) + } + catch (e: any) { + const errorMsg = `Failed to close session for server '${serverName}': ${e}` + logger.error(errorMsg) + errors.push(errorMsg) + } + } + if (errors.length) { + logger.error(`Encountered ${errors.length} errors while closing sessions`) + } + else { + logger.debug('All sessions closed successfully') + } + } +} diff --git a/src/logging.ts b/src/logging.ts index 37b8baff..7116fcd6 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,8 +1,21 @@ import type { Logger as WinstonLogger } from 'winston' -import fs from 'node:fs' -import path from 'node:path' import { createLogger, format, transports } from 'winston' +// Conditional imports for Node.js-only modules +async function getNodeModules() { + if (typeof process !== 'undefined' && process.platform) { + try { + // Use dynamic imports for Node.js environments + const fs = await import('node:fs') + const path = await import('node:path') + return { fs: fs.default, path: path.default } + } catch { + return { fs: null, path: null } + } + } + return { fs: null, path: null } +} + const { combine, timestamp, label, printf, colorize, splat } = format export type LogLevel = 'error' | 'warn' | 'info' | 'http' | 'verbose' | 'debug' | 'silly' @@ -38,8 +51,7 @@ function isNodeJSEnvironment(): boolean { // Check for Node.js modules const hasNodeModules = ( - typeof fs !== 'undefined' - && typeof createLogger === 'function' + typeof createLogger === 'function' ) return hasNodeGlobals && hasNodeModules @@ -195,7 +207,7 @@ export class Logger { } } - public static configure(options: LoggerOptions = {}): void { + public static async configure(options: LoggerOptions = {}): Promise { const { level, console = true, file, format = 'minimal' } = options const debugEnv = (typeof process !== 'undefined' && process.env?.DEBUG) || undefined const resolvedLevel = level ?? resolveLevel(debugEnv) @@ -205,16 +217,17 @@ export class Logger { root.level = resolvedLevel + // Winston-specific configuration for Node.js environments + const winstonRoot = root as WinstonLogger + // For non-Node.js environments, just update the level if (!isNodeJSEnvironment()) { Object.values(this.simpleInstances).forEach((logger) => { logger.level = resolvedLevel }) + return } - - // Winston-specific configuration for Node.js environments - const winstonRoot = root as WinstonLogger winstonRoot.clear() if (console) { @@ -222,11 +235,14 @@ export class Logger { } if (file) { - const dir = path.dirname(path.resolve(file)) - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }) + const { fs: nodeFs, path: nodePath } = await getNodeModules() + if (nodeFs && nodePath) { + const dir = nodePath.dirname(nodePath.resolve(file)) + if (!nodeFs.existsSync(dir)) { + nodeFs.mkdirSync(dir, { recursive: true }) + } + winstonRoot.add(new transports.File({ filename: file })) } - winstonRoot.add(new transports.File({ filename: file })) } // Update all existing Winston loggers with new format diff --git a/src/oauth-helper.ts b/src/oauth-helper.ts new file mode 100644 index 00000000..daca5c4c --- /dev/null +++ b/src/oauth-helper.ts @@ -0,0 +1,537 @@ +/** + * OAuth helper for browser-based MCP authentication + * + * This helper provides OAuth 2.0 authorization code flow support for MCP servers + * that require authentication, such as Linear's MCP server. + */ + +export interface OAuthConfig { + clientId?: string // Optional - will use dynamic registration if not provided + redirectUri: string + scope?: string + state?: string + clientName?: string // For dynamic registration +} + +export interface OAuthDiscovery { + issuer: string + authorization_endpoint: string + token_endpoint: string + registration_endpoint?: string + response_types_supported: string[] + grant_types_supported: string[] + code_challenge_methods_supported: string[] + token_endpoint_auth_methods_supported?: string[] +} + +export interface ClientRegistration { + client_id: string + client_secret?: string + registration_access_token?: string + registration_client_uri?: string + client_id_issued_at?: number + client_secret_expires_at?: number +} + +export interface OAuthResult { + access_token: string + token_type: string + expires_at?: number | null + refresh_token?: string | null + scope?: string | null +} + +export interface OAuthState { + isRequired: boolean + isAuthenticated: boolean + isAuthenticating: boolean + isCompletingOAuth: boolean + authError: string | null + oauthTokens: OAuthResult | null +} + +export class OAuthHelper { + private config: OAuthConfig + private discovery?: OAuthDiscovery + private state: OAuthState + private clientRegistration?: ClientRegistration + private serverUrl?: string + private storageKey: string + + constructor(config: OAuthConfig) { + this.config = config + this.storageKey = `mcp-oauth-${btoa(config.redirectUri)}` + this.state = { + isRequired: false, + isAuthenticated: false, + isAuthenticating: false, + isCompletingOAuth: false, + authError: null, + oauthTokens: null, + } + + // Load persisted client registration + this.loadClientRegistration() + } + + /** + * Get current OAuth state + */ + getState(): OAuthState { + return { ...this.state } + } + + /** + * Load client registration from localStorage + */ + private loadClientRegistration(): void { + try { + const stored = localStorage.getItem(this.storageKey) + if (stored) { + const data = JSON.parse(stored) + this.clientRegistration = data.clientRegistration + this.serverUrl = data.serverUrl + console.log('🔄 [OAuthHelper] Loaded persisted client registration:', { + client_id: this.clientRegistration?.client_id, + server: this.serverUrl + }) + } + } catch (error) { + console.warn('⚠️ [OAuthHelper] Failed to load client registration:', error) + } + } + + /** + * Save client registration to localStorage + */ + private saveClientRegistration(): void { + try { + const data = { + clientRegistration: this.clientRegistration, + serverUrl: this.serverUrl + } + localStorage.setItem(this.storageKey, JSON.stringify(data)) + console.log('💾 [OAuthHelper] Saved client registration to localStorage') + } catch (error) { + console.warn('⚠️ [OAuthHelper] Failed to save client registration:', error) + } + } + + /** + * Check if a server requires authentication by pinging the URL + */ + async checkAuthRequired(serverUrl: string): Promise { + console.log('🔍 [OAuthHelper] Checking auth requirement for:', serverUrl) + + try { + const response = await fetch(serverUrl, { + method: 'GET', + headers: { + 'Accept': 'text/event-stream', + 'Cache-Control': 'no-cache', + }, + redirect: 'manual', + signal: AbortSignal.timeout(10000), // 10 second timeout + }) + + console.log('🔍 [OAuthHelper] Auth check response:', { + status: response.status, + statusText: response.statusText, + url: serverUrl, + }) + + // 401 Unauthorized, 403 Forbidden, or 400 Bad Request means auth is required + if (response.status === 401 || response.status === 403 || response.status === 400) { + console.log('🔐 [OAuthHelper] Authentication required for:', serverUrl) + return true + } + + // Any other response (200, 404, 500, etc.) means no auth required + console.log('✅ [OAuthHelper] No authentication required for:', serverUrl) + return false + } catch (error: any) { + console.warn('⚠️ [OAuthHelper] Could not check auth requirement for:', serverUrl, error) + + // Handle specific error types + if (error.name === 'TypeError' && + (error.message?.includes('CORS') || error.message?.includes('Failed to fetch'))) { + console.log('🔍 [OAuthHelper] CORS blocked direct check, using heuristics for:', serverUrl) + return this.checkAuthByHeuristics(serverUrl) + } + + if (error.name === 'AbortError') { + console.log('⏰ [OAuthHelper] Request timeout, assuming no auth required for:', serverUrl) + return false + } + + // If we can't reach the server at all, try heuristics + return this.checkAuthByHeuristics(serverUrl) + } + } + + /** + * Fallback heuristics for determining auth requirements when direct checking fails + */ + private checkAuthByHeuristics(serverUrl: string): boolean { + console.log('🔍 [OAuthHelper] Using heuristics to determine auth for:', serverUrl) + + // Known patterns that typically require auth + const authRequiredPatterns = [ + /api\.githubcopilot\.com/i, // GitHub Copilot + /api\.github\.com/i, // GitHub API + /.*\.googleapis\.com/i, // Google APIs + /api\.openai\.com/i, // OpenAI + /api\.anthropic\.com/i, // Anthropic + /.*\.atlassian\.net/i, // Atlassian (Jira, Confluence) + /.*\.slack\.com/i, // Slack + /api\.notion\.com/i, // Notion + /api\.linear\.app/i, // Linear + ] + + // Known patterns that typically don't require auth (public MCP servers) + const noAuthPatterns = [ + /localhost/i, // Local development + /127\.0\.0\.1/i, // Local development + /\.local/i, // Local development + /mcp\..*\.com/i, // Generic MCP server pattern (often public) + ] + + // Check no-auth patterns first + for (const pattern of noAuthPatterns) { + if (pattern.test(serverUrl)) { + console.log('✅ [OAuthHelper] Heuristic: No auth required (matches no-auth pattern):', serverUrl) + return false + } + } + + // Check auth-required patterns + for (const pattern of authRequiredPatterns) { + if (pattern.test(serverUrl)) { + console.log('🔐 [OAuthHelper] Heuristic: Auth required (matches auth pattern):', serverUrl) + return true + } + } + + // Default: assume no auth required for unknown patterns + console.log('❓ [OAuthHelper] Heuristic: Unknown pattern, assuming no auth required:', serverUrl) + return false + } + + /** + * Discover OAuth configuration from a server + */ + async discoverOAuthConfig(serverUrl: string): Promise { + try { + const discoveryUrl = `${serverUrl}/.well-known/oauth-authorization-server` + const response = await fetch(discoveryUrl) + + if (!response.ok) { + throw new Error(`OAuth discovery failed: ${response.status} ${response.statusText}`) + } + + this.discovery = await response.json() + return this.discovery! + } catch (error) { + throw new Error(`Failed to discover OAuth configuration: ${error}`) + } + } + + /** + * Register a new OAuth client dynamically + */ + async registerClient(_serverUrl: string): Promise { + if (!this.discovery) { + throw new Error('OAuth discovery not performed. Call discoverOAuthConfig first.') + } + + if (!this.discovery.registration_endpoint) { + throw new Error('Server does not support dynamic client registration') + } + + try { + const registrationData = { + client_name: this.config.clientName || 'MCP Use Example', + redirect_uris: [this.config.redirectUri], + grant_types: ['authorization_code'], + response_types: ['code'], + token_endpoint_auth_method: 'none', // Use public client (no secret) + scope: this.config.scope || 'read write', + } + + console.log('🔐 [OAuthHelper] Registering OAuth client dynamically:', { + registration_endpoint: this.discovery.registration_endpoint, + client_name: registrationData.client_name, + redirect_uri: this.config.redirectUri, + }) + + const response = await fetch(this.discovery.registration_endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(registrationData) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Client registration failed: ${response.status} ${response.statusText} - ${errorText}`) + } + + this.clientRegistration = await response.json() + this.serverUrl = serverUrl + this.saveClientRegistration() + + console.log('✅ [OAuthHelper] Client registered successfully:', { + client_id: this.clientRegistration!.client_id, + client_secret: this.clientRegistration!.client_secret ? '***' : 'none', + }) + + return this.clientRegistration! + } catch (error) { + console.error('❌ [OAuthHelper] Client registration failed:', error) + throw new Error(`Failed to register OAuth client: ${error}`) + } + } + + /** + * Generate authorization URL for OAuth flow + */ + generateAuthUrl(serverUrl: string, additionalParams?: Record): string { + if (!this.discovery) { + throw new Error('OAuth discovery not performed. Call discoverOAuthConfig first.') + } + + if (!this.clientRegistration) { + throw new Error('Client not registered. Call registerClient first.') + } + + const params = new URLSearchParams({ + client_id: this.clientRegistration.client_id, + redirect_uri: this.config.redirectUri, + response_type: 'code', + scope: this.config.scope || 'read', + state: this.config.state || this.generateState(), + ...additionalParams + }) + + return `${this.discovery.authorization_endpoint}?${params.toString()}` + } + + /** + * Exchange authorization code for access token + */ + async exchangeCodeForToken( + serverUrl: string, + code: string, + codeVerifier?: string + ): Promise { + if (!this.discovery) { + throw new Error('OAuth discovery not performed. Call discoverOAuthConfig first.') + } + + if (!this.clientRegistration) { + throw new Error('Client not registered. Call registerClient first.') + } + + const body = new URLSearchParams({ + grant_type: 'authorization_code', + client_id: this.clientRegistration.client_id, + code, + redirect_uri: this.config.redirectUri, + }) + + if (codeVerifier) { + body.append('code_verifier', codeVerifier) + } + + const response = await fetch(this.discovery.token_endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: body.toString() + }) + + if (!response.ok) { + const error = await response.text() + throw new Error(`Token exchange failed: ${response.status} ${response.statusText} - ${error}`) + } + + return await response.json() + } + + /** + * Handle OAuth callback and extract authorization code + */ + handleCallback(): { code: string; state: string } | null { + const urlParams = new URLSearchParams(window.location.search) + const code = urlParams.get('code') + const state = urlParams.get('state') + + if (!code || !state) { + return null + } + + // Clean up URL + const url = new URL(window.location.href) + url.searchParams.delete('code') + url.searchParams.delete('state') + window.history.replaceState({}, '', url.toString()) + + return { code, state } + } + + /** + * Start OAuth flow by opening popup window (similar to your implementation) + */ + async startOAuthFlow(serverUrl: string): Promise { + this.setState({ + isAuthenticating: true, + authError: null, + }) + + try { + // Step 1: Discover OAuth configuration + await this.discoverOAuthConfig(serverUrl) + + // Step 2: Register client dynamically (if not already registered) + if (!this.clientRegistration) { + await this.registerClient(serverUrl) + } + + // Step 3: Generate authorization URL + const authUrl = this.generateAuthUrl(serverUrl) + + // Step 4: Open popup window for authentication + const authWindow = window.open( + authUrl, + 'mcp-oauth', + 'width=500,height=600,scrollbars=yes,resizable=yes,status=yes,location=yes' + ) + + if (!authWindow) { + throw new Error('Failed to open authentication window. Please allow popups for this site and try again.') + } + + console.log('✅ [OAuthHelper] OAuth popup opened successfully') + } catch (error) { + console.error('❌ [OAuthHelper] Failed to start OAuth flow:', error) + this.setState({ + isAuthenticating: false, + authError: error instanceof Error ? error.message : 'Failed to start authentication', + }) + throw error + } + } + + /** + * Complete OAuth flow by exchanging code for token + */ + async completeOAuthFlow(serverUrl: string, code: string): Promise { + this.setState({ + isCompletingOAuth: true, + authError: null, + }) + + try { + // If we don't have discovery data, re-discover it + if (!this.discovery) { + console.log('🔍 [OAuthHelper] Re-discovering OAuth configuration for callback') + await this.discoverOAuthConfig(serverUrl) + } + + // Only re-register if we don't have a client registration for this server + if (!this.clientRegistration || this.serverUrl !== serverUrl) { + console.log('🔐 [OAuthHelper] Re-registering client for callback') + await this.registerClient(serverUrl) + } else { + console.log('🔄 [OAuthHelper] Using existing client registration for callback') + } + + const tokenResponse = await this.exchangeCodeForToken(serverUrl, code) + + this.setState({ + isAuthenticating: false, + isAuthenticated: true, + isCompletingOAuth: false, + authError: null, + oauthTokens: tokenResponse, + }) + + console.log('✅ [OAuthHelper] OAuth flow completed successfully') + return tokenResponse + } catch (error) { + console.error('❌ [OAuthHelper] Failed to complete OAuth flow:', error) + this.setState({ + isAuthenticating: false, + isCompletingOAuth: false, + authError: error instanceof Error ? error.message : 'Failed to complete authentication', + }) + throw error + } + } + + /** + * Reset authentication state + */ + resetAuth(): void { + this.setState({ + isRequired: false, + isAuthenticated: false, + isAuthenticating: false, + isCompletingOAuth: false, + authError: null, + oauthTokens: null, + }) + + // Clear stored client registration + this.clientRegistration = undefined + this.serverUrl = undefined + try { + localStorage.removeItem(this.storageKey) + console.log('🗑️ [OAuthHelper] Cleared stored client registration') + } catch (error) { + console.warn('⚠️ [OAuthHelper] Failed to clear client registration:', error) + } + } + + /** + * Set OAuth state (internal method) + */ + private setState(newState: Partial): void { + this.state = { ...this.state, ...newState } + } + + /** + * Generate a random state parameter for CSRF protection + */ + private generateState(): string { + return Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15) + } +} + +/** + * Linear-specific OAuth configuration + */ +export const LINEAR_OAUTH_CONFIG: OAuthConfig = { + // No clientId needed - will use dynamic client registration + redirectUri: typeof window !== 'undefined' ? window.location.origin + window.location.pathname : 'http://localhost:5174', + scope: 'read write', + clientName: 'MCP Use Example', +} + +/** + * Helper function to create OAuth-enabled MCP configuration + */ +export function createOAuthMCPConfig(serverUrl: string, accessToken: string) { + return { + mcpServers: { + linear: { + url: serverUrl, + authToken: accessToken, + transport: 'sse' + } + } + } +} diff --git a/tsconfig.json b/tsconfig.json index 8611fad1..bb579910 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,5 +14,5 @@ "skipLibCheck": true }, "include": ["**/*.ts"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "examples"] } From 966049164d37212991c55ce278273b77ba3b906d Mon Sep 17 00:00:00 2001 From: Luigi Pederzani Date: Wed, 8 Oct 2025 09:13:07 +0200 Subject: [PATCH 2/2] format --- eslint.config.js | 1 + examples/react/index.html | 64 +++--- examples/react/index.tsx | 3 +- examples/react/oauth-helper.ts | 59 +++--- examples/react/package.json | 2 +- examples/react/react_example.html | 64 +++--- examples/react/react_example.tsx | 326 +++++++++++++++++------------- examples/react/serve.js | 20 +- examples/react/tsconfig.json | 18 +- examples/react/tsconfig.node.json | 4 +- examples/react/vite.config.ts | 26 +-- package-lock.json | 11 + src/browser.ts | 22 +- src/client.ts | 6 +- src/client/base.ts | 4 +- src/logging.ts | 15 +- src/managers/server_manager.ts | 2 +- src/oauth-helper.ts | 83 ++++---- 18 files changed, 405 insertions(+), 325 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 26c0726a..1e36a289 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -5,6 +5,7 @@ export default antfu({ typescript: true, rules: { 'node/prefer-global/process': 'off', + 'no-console': 'off', }, }, { files: ['examples/**/*.ts'], diff --git a/examples/react/index.html b/examples/react/index.html index 67374825..e06cd37f 100644 --- a/examples/react/index.html +++ b/examples/react/index.html @@ -1,41 +1,41 @@ - + - - - + + + MCP Tools Explorer - React Example - - + +
-
-

Loading MCP Tools Explorer...

-

If this message persists, check the browser console for errors.

-
+
+

Loading MCP Tools Explorer...

+

If this message persists, check the browser console for errors.

+
- + - + diff --git a/examples/react/index.tsx b/examples/react/index.tsx index dd2bb277..5c10a5e3 100644 --- a/examples/react/index.tsx +++ b/examples/react/index.tsx @@ -6,6 +6,7 @@ const container = document.getElementById('root') if (container) { const root = createRoot(container) root.render() -} else { +} +else { console.error('Root element not found') } diff --git a/examples/react/oauth-helper.ts b/examples/react/oauth-helper.ts index 81966d97..3b754b64 100644 --- a/examples/react/oauth-helper.ts +++ b/examples/react/oauth-helper.ts @@ -1,6 +1,6 @@ /** * OAuth helper for browser-based MCP authentication - * + * * This helper provides OAuth 2.0 authorization code flow support for MCP servers * that require authentication, such as Linear's MCP server. */ @@ -107,12 +107,13 @@ export class OAuthHelper { // Any other response (200, 404, 500, etc.) means no auth required console.log('✅ [OAuthHelper] No authentication required for:', serverUrl) return false - } catch (error: any) { + } + catch (error: any) { console.warn('⚠️ [OAuthHelper] Could not check auth requirement for:', serverUrl, error) // Handle specific error types - if (error.name === 'TypeError' && - (error.message?.includes('CORS') || error.message?.includes('Failed to fetch'))) { + if (error.name === 'TypeError' + && (error.message?.includes('CORS') || error.message?.includes('Failed to fetch'))) { console.log('🔍 [OAuthHelper] CORS blocked direct check, using heuristics for:', serverUrl) return this.checkAuthByHeuristics(serverUrl) } @@ -149,7 +150,7 @@ export class OAuthHelper { // Known patterns that typically don't require auth (public MCP servers) const noAuthPatterns = [ /localhost/i, // Local development - /127\.0\.0\.1/i, // Local development + /127\.0\.0\.1/, // Local development /\.local/i, // Local development /mcp\..*\.com/i, // Generic MCP server pattern (often public) ] @@ -182,14 +183,15 @@ export class OAuthHelper { try { const discoveryUrl = `${serverUrl}/.well-known/oauth-authorization-server` const response = await fetch(discoveryUrl) - + if (!response.ok) { throw new Error(`OAuth discovery failed: ${response.status} ${response.statusText}`) } - + this.discovery = await response.json() return this.discovery - } catch (error) { + } + catch (error) { throw new Error(`Failed to discover OAuth configuration: ${error}`) } } @@ -197,7 +199,7 @@ export class OAuthHelper { /** * Register a new OAuth client dynamically */ - async registerClient(serverUrl: string): Promise { + async registerClient(_serverUrl: string): Promise { if (!this.discovery) { throw new Error('OAuth discovery not performed. Call discoverOAuthConfig first.') } @@ -227,7 +229,7 @@ export class OAuthHelper { headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify(registrationData) + body: JSON.stringify(registrationData), }) if (!response.ok) { @@ -242,7 +244,8 @@ export class OAuthHelper { }) return this.clientRegistration - } catch (error) { + } + catch (error) { console.error('❌ [OAuthHelper] Client registration failed:', error) throw new Error(`Failed to register OAuth client: ${error}`) } @@ -262,7 +265,7 @@ export class OAuthHelper { response_type: 'code', scope: this.config.scope || 'read', state: this.config.state || this.generateState(), - ...additionalParams + ...additionalParams, }) return `${this.discovery.authorization_endpoint}?${params.toString()}` @@ -272,9 +275,9 @@ export class OAuthHelper { * Exchange authorization code for access token */ async exchangeCodeForToken( - serverUrl: string, - code: string, - codeVerifier?: string + serverUrl: string, + code: string, + codeVerifier?: string, ): Promise { if (!this.discovery) { throw new Error('OAuth discovery not performed. Call discoverOAuthConfig first.') @@ -296,7 +299,7 @@ export class OAuthHelper { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, - body: body.toString() + body: body.toString(), }) if (!response.ok) { @@ -310,7 +313,7 @@ export class OAuthHelper { /** * Handle OAuth callback and extract authorization code */ - handleCallback(): { code: string; state: string } | null { + handleCallback(): { code: string, state: string } | null { const urlParams = new URLSearchParams(window.location.search) const code = urlParams.get('code') const state = urlParams.get('state') @@ -340,12 +343,12 @@ export class OAuthHelper { try { await this.discoverOAuthConfig(serverUrl) const authUrl = this.generateAuthUrl(serverUrl) - + // Open popup window for authentication (similar to your implementation) const authWindow = window.open( authUrl, 'mcp-oauth', - 'width=500,height=600,scrollbars=yes,resizable=yes,status=yes,location=yes' + 'width=500,height=600,scrollbars=yes,resizable=yes,status=yes,location=yes', ) if (!authWindow) { @@ -353,7 +356,8 @@ export class OAuthHelper { } console.log('✅ [OAuthHelper] OAuth popup opened successfully') - } catch (error) { + } + catch (error) { console.error('❌ [OAuthHelper] Failed to start OAuth flow:', error) this.setState({ isAuthenticating: false, @@ -374,7 +378,7 @@ export class OAuthHelper { try { const tokenResponse = await this.exchangeCodeForToken(serverUrl, code) - + this.setState({ isAuthenticating: false, isAuthenticated: true, @@ -385,7 +389,8 @@ export class OAuthHelper { console.log('✅ [OAuthHelper] OAuth flow completed successfully') return tokenResponse - } catch (error) { + } + catch (error) { console.error('❌ [OAuthHelper] Failed to complete OAuth flow:', error) this.setState({ isAuthenticating: false, @@ -421,8 +426,8 @@ export class OAuthHelper { * Generate a random state parameter for CSRF protection */ private generateState(): string { - return Math.random().toString(36).substring(2, 15) + - Math.random().toString(36).substring(2, 15) + return Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15) } } @@ -444,8 +449,8 @@ export function createOAuthMCPConfig(serverUrl: string, accessToken: string) { linear: { url: serverUrl, authToken: accessToken, - transport: 'sse' - } - } + transport: 'sse', + }, + }, } } diff --git a/examples/react/package.json b/examples/react/package.json index 5bf96865..186b2ca8 100644 --- a/examples/react/package.json +++ b/examples/react/package.json @@ -1,7 +1,7 @@ { "name": "mcp-use-react-example", - "version": "1.0.0", "type": "module", + "version": "1.0.0", "scripts": { "dev": "vite", "build": "vite build", diff --git a/examples/react/react_example.html b/examples/react/react_example.html index 67374825..e06cd37f 100644 --- a/examples/react/react_example.html +++ b/examples/react/react_example.html @@ -1,41 +1,41 @@ - + - - - + + + MCP Tools Explorer - React Example - - + +
-
-

Loading MCP Tools Explorer...

-

If this message persists, check the browser console for errors.

-
+
+

Loading MCP Tools Explorer...

+

If this message persists, check the browser console for errors.

+
- + - + diff --git a/examples/react/react_example.tsx b/examples/react/react_example.tsx index 21eb76f6..97f98972 100644 --- a/examples/react/react_example.tsx +++ b/examples/react/react_example.tsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect } from 'react' -import { MCPClient, OAuthHelper, LINEAR_OAUTH_CONFIG, createOAuthMCPConfig } from 'mcp-use/browser' +import { createOAuthMCPConfig, LINEAR_OAUTH_CONFIG, MCPClient, OAuthHelper } from 'mcp-use/browser' +import React, { useEffect, useState } from 'react' interface Tool { name: string @@ -36,12 +36,13 @@ const MCPTools: React.FC = ({ config }) => { try { const tokenResult = await oauthHelper.completeOAuthFlow(serverUrl, callback.code) setOAuthState(oauthHelper.getState()) - + // Create new config with the access token const oauthConfig = createOAuthMCPConfig(`${serverUrl}/sse`, tokenResult.access_token) const mcpClient = new MCPClient(oauthConfig) setClient(mcpClient) - } catch (err) { + } + catch (err) { setError(`OAuth authentication failed: ${err instanceof Error ? err.message : 'Unknown error'}`) setOAuthState(oauthHelper.getState()) } @@ -59,7 +60,8 @@ const MCPTools: React.FC = ({ config }) => { if (requiresAuth) { setOAuthState(oauthHelper.getState()) } - } catch (err) { + } + catch (err) { console.warn('Could not check auth requirement:', err) } } @@ -79,7 +81,7 @@ const MCPTools: React.FC = ({ config }) => { try { // Get all server names from config const serverNames = client.getServerNames() - + if (serverNames.length === 0) { setError('No MCP servers configured') setLoading(false) @@ -92,24 +94,27 @@ const MCPTools: React.FC = ({ config }) => { // Collect tools from all sessions const allTools: Tool[] = [] - + for (const [serverName, session] of Object.entries(sessions)) { try { const sessionTools = session.connector.tools const toolsWithServer = sessionTools.map(tool => ({ ...tool, - server: serverName + server: serverName, })) allTools.push(...toolsWithServer) - } catch (err) { + } + catch (err) { console.warn(`Failed to get tools from server ${serverName}:`, err) } } setTools(allTools) - } catch (err) { + } + catch (err) { setError(`Failed to load tools: ${err instanceof Error ? err.message : 'Unknown error'}`) - } finally { + } + finally { setLoading(false) } } @@ -127,7 +132,8 @@ const MCPTools: React.FC = ({ config }) => { try { await oauthHelper.startOAuthFlow(serverUrl) setOAuthState(oauthHelper.getState()) - } catch (err) { + } + catch (err) { setError(`Failed to start OAuth flow: ${err instanceof Error ? err.message : 'Unknown error'}`) setOAuthState(oauthHelper.getState()) } @@ -145,118 +151,137 @@ const MCPTools: React.FC = ({ config }) => { return (

MCP Tools Explorer

- +
- {!oauthState.isAuthenticated ? ( - - ) : ( - <> - - - - - - - )} + {!oauthState.isAuthenticated + ? ( + + ) + : ( + <> + + + + + + + )}
{error && ( -
- Error: {error} + marginBottom: '20px', + }} + > + Error: + {' '} + {error}
)} {oauthState.isAuthenticated && oauthState.oauthTokens && ( -
+ borderRadius: '4px', + }} + >

✓ Authenticated with Linear

- Access token: {oauthState.oauthTokens.access_token.substring(0, 20)}... + Access token: + {' '} + {oauthState.oauthTokens.access_token.substring(0, 20)} + ... {oauthState.oauthTokens.expires_at && ( - (expires: {new Date(oauthState.oauthTokens.expires_at * 1000).toLocaleString()}) + + {' '} + (expires: + {new Date(oauthState.oauthTokens.expires_at * 1000).toLocaleString()} + ) + )}

)} {oauthState.authError && ( -
- OAuth Error: {oauthState.authError} + borderRadius: '4px', + }} + > + OAuth Error: + {' '} + {oauthState.authError}
)} @@ -265,65 +290,76 @@ const MCPTools: React.FC = ({ config }) => {

Connected Servers:

    {connectedServers.map(server => ( -
  • ✓ {server}
  • +
  • + ✓ + {server} +
  • ))}
)}
-

Available Tools ({tools.length})

+

+ Available Tools ( + {tools.length} + ) +

{tools.length === 0 && !loading && (

No tools loaded. Click "Load Tools" to get started.

)} - + {tools.map((tool, index) => ( -

{tool.name} {tool.server && ( - - from {tool.server} + borderRadius: '3px', + }} + > + from + {' '} + {tool.server} )}

- + {tool.description && (

{tool.description}

)} - + {tool.inputSchema && (
Input Schema -
+                  fontSize: '0.9em',
+                }}
+                >
                   {JSON.stringify(tool.inputSchema, null, 2)}
                 
@@ -340,26 +376,42 @@ const ReactExample: React.FC = () => { return (
- -
+ borderRadius: '4px', + }} + >

🔐 OAuth Authentication Required

- This example demonstrates OAuth authentication with Linear's MCP server. + This example demonstrates OAuth authentication with Linear's MCP server. Click "Authenticate with Linear" to start the OAuth flow.

- Note: You'll need to register this application with Linear to get a proper client ID. + Note: + {' '} + You'll need to register this application with Linear to get a proper client ID. For this demo, we're using a placeholder client ID.

- Browser limitations: Stdio connections (using command and args) - are not supported in the browser. Use ws_url for WebSocket or url for HTTP/SSE connections. + Browser limitations: + {' '} + Stdio connections (using + command + {' '} + and + args + ) + are not supported in the browser. Use + ws_url + {' '} + for WebSocket or + url + {' '} + for HTTP/SSE connections.

diff --git a/examples/react/serve.js b/examples/react/serve.js index aec09416..9d723118 100644 --- a/examples/react/serve.js +++ b/examples/react/serve.js @@ -1,10 +1,9 @@ #!/usr/bin/env node -import { createServer } from 'http' -import { readFileSync, existsSync } from 'fs' -import { join, extname } from 'path' -import { fileURLToPath } from 'url' -import { dirname } from 'path' +import { existsSync, readFileSync } from 'node:fs' +import { createServer } from 'node:http' +import { dirname, extname, join } from 'node:path' +import { fileURLToPath } from 'node:url' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) @@ -21,12 +20,12 @@ const mimeTypes = { '.jpg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', - '.ico': 'image/x-icon' + '.ico': 'image/x-icon', } const server = createServer((req, res) => { - let filePath = join(distDir, req.url === '/' ? 'react_example.html' : req.url) - + const filePath = join(distDir, req.url === '/' ? 'react_example.html' : req.url) + // Security check - prevent directory traversal if (!filePath.startsWith(distDir)) { res.writeHead(403) @@ -44,10 +43,11 @@ const server = createServer((req, res) => { const content = readFileSync(filePath) const ext = extname(filePath) const contentType = mimeTypes[ext] || 'application/octet-stream' - + res.writeHead(200, { 'Content-Type': contentType }) res.end(content) - } catch (error) { + } + catch { res.writeHead(500) res.end('Internal Server Error') } diff --git a/examples/react/tsconfig.json b/examples/react/tsconfig.json index 2c731fae..17ac59b0 100644 --- a/examples/react/tsconfig.json +++ b/examples/react/tsconfig.json @@ -1,25 +1,25 @@ { "compilerOptions": { "target": "ES2020", - "useDefineForClassFields": true, + "jsx": "react-jsx", "lib": ["ES2020", "DOM", "DOM.Iterable"], + "useDefineForClassFields": true, "module": "ESNext", - "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", - "allowImportingTsExtensions": true, "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", + "allowImportingTsExtensions": true, /* Linting */ "strict": true, + "noFallthroughCasesInSwitch": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noEmit": true, + "isolatedModules": true, + "skipLibCheck": true }, - "include": ["**/*.ts", "**/*.tsx"], - "references": [{ "path": "./tsconfig.node.json" }] + "references": [{ "path": "./tsconfig.node.json" }], + "include": ["**/*.ts", "**/*.tsx"] } diff --git a/examples/react/tsconfig.node.json b/examples/react/tsconfig.node.json index 42872c59..dde08948 100644 --- a/examples/react/tsconfig.node.json +++ b/examples/react/tsconfig.node.json @@ -1,10 +1,10 @@ { "compilerOptions": { "composite": true, - "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "skipLibCheck": true }, "include": ["vite.config.ts"] } diff --git a/examples/react/vite.config.ts b/examples/react/vite.config.ts index eed0d196..7de8b540 100644 --- a/examples/react/vite.config.ts +++ b/examples/react/vite.config.ts @@ -1,25 +1,25 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' -import { resolve } from 'path' +import { resolve } from 'node:path' import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill' import { NodeModulesPolyfillPlugin } from '@esbuild-plugins/node-modules-polyfill' +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' export default defineConfig({ plugins: [react()], build: { outDir: 'dist', commonjsOptions: { - transformMixedEsModules: true - } + transformMixedEsModules: true, + }, }, resolve: { alias: { 'mcp-use/browser': resolve(__dirname, '../../dist/src/browser.js'), 'mcp-use': resolve(__dirname, '../../dist/src'), - } + }, }, define: { - global: 'globalThis', + 'global': 'globalThis', 'process.env.DEBUG': 'undefined', 'process.env.MCP_USE_ANONYMIZED_TELEMETRY': 'undefined', 'process.env.MCP_USE_TELEMETRY_SOURCE': 'undefined', @@ -32,15 +32,15 @@ export default defineConfig({ include: ['react', 'react-dom'], esbuildOptions: { define: { - global: 'globalThis' + global: 'globalThis', }, plugins: [ NodeGlobalsPolyfillPlugin({ process: true, - buffer: true + buffer: true, }), - NodeModulesPolyfillPlugin() - ] - } - } + NodeModulesPolyfillPlugin(), + ], + }, + }, }) diff --git a/package-lock.json b/package-lock.json index 4fdadb1c..14e059a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "posthog-node": "^5.1.1", "uuid": "^11.1.0", "winston": "^3.17.0", + "winston-transport-browserconsole": "^1.0.5", "ws": "^8.18.2", "zod": "^3.25.48", "zod-to-json-schema": "^3.24.6" @@ -8842,6 +8843,16 @@ "node": ">= 12.0.0" } }, + "node_modules/winston-transport-browserconsole": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/winston-transport-browserconsole/-/winston-transport-browserconsole-1.0.5.tgz", + "integrity": "sha512-BFZDvknATAmsqaRY3WatB2S0eEb0ziwqBYIv+H0Fnu9oe7GnPUVQzEJC1frmLdNsfEkfnyR1PCNDBAFtZE3SuQ==", + "license": "ISC", + "dependencies": { + "winston": "^3.2.1", + "winston-transport": "^4.3.0" + } + }, "node_modules/winston/node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", diff --git a/src/browser.ts b/src/browser.ts index 91cb6a5e..ce11949a 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -1,27 +1,27 @@ /** * Browser-compatible exports for mcp-use - * + * * This module provides browser-safe versions of mcp-use components * that avoid Node.js-specific dependencies (like fs, path for file operations). - * + * * The actual MCP protocol connectors (WebSocket, HTTP/SSE) work fine in browsers. */ import type { BaseConnector } from './connectors/base.js' +import { BaseMCPClient } from './client/base.js' import { HttpConnector } from './connectors/http.js' import { WebSocketConnector } from './connectors/websocket.js' -import { BaseMCPClient } from './client/base.js' import { logger } from './logging.js' import { MCPSession } from './session.js' /** * Browser-compatible MCP Client - * + * * Unlike the Node.js version, this doesn't support: * - Loading config from files (loadConfigFile) * - Saving config to files (saveConfig) * - StdioConnector (requires child_process) - * + * * Supported connectors: * - WebSocketConnector: Connect to MCP servers via WebSocket * - HttpConnector: Connect to MCP servers via HTTP/SSE @@ -37,11 +37,11 @@ export class MCPClient extends BaseMCPClient { /** * Create a connector from server configuration (browser-safe version) - * + * * Supports: * - WebSocket connections: { ws_url: "ws://..." } * - HTTP connections: { url: "http://..." } - * + * * Does NOT support: * - Stdio connections: { command: "...", args: [...] } */ @@ -87,9 +87,9 @@ export { MCPSession } export { logger } // Re-export OAuth helper for browser authentication -export { OAuthHelper, LINEAR_OAUTH_CONFIG, createOAuthMCPConfig } from './oauth-helper.js' -export type { OAuthConfig, OAuthDiscovery, OAuthResult, OAuthState, ClientRegistration } from './oauth-helper.js' +export { createOAuthMCPConfig, LINEAR_OAUTH_CONFIG, OAuthHelper } from './oauth-helper.js' +export type { ClientRegistration, OAuthConfig, OAuthDiscovery, OAuthResult, OAuthState } from './oauth-helper.js' // Re-export types that are safe for browser -export type { BaseMessage, HumanMessage, SystemMessage, AIMessage, ToolMessage } from '@langchain/core/messages' -export type { StreamEvent } from '@langchain/core/tracers/log_stream' \ No newline at end of file +export type { AIMessage, BaseMessage, HumanMessage, SystemMessage, ToolMessage } from '@langchain/core/messages' +export type { StreamEvent } from '@langchain/core/tracers/log_stream' diff --git a/src/client.ts b/src/client.ts index 3c7c1539..17eafa85 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,12 +1,12 @@ +import type { BaseConnector } from './connectors/base.js' import fs from 'node:fs' import path from 'node:path' -import type { BaseConnector } from './connectors/base.js' -import { createConnectorFromConfig, loadConfigFile } from './config.js' import { BaseMCPClient } from './client/base.js' +import { createConnectorFromConfig, loadConfigFile } from './config.js' /** * Node.js-specific MCPClient implementation - * + * * Extends the base client with Node.js-specific features like: * - File system operations (saveConfig) * - Config file loading (fromConfigFile) diff --git a/src/client/base.ts b/src/client/base.ts index 2ca697ac..c8706867 100644 --- a/src/client/base.ts +++ b/src/client/base.ts @@ -4,7 +4,7 @@ import { MCPSession } from '../session.js' /** * Base MCPClient class with shared functionality - * + * * This class contains all the common logic that works in both Node.js and browser environments. * Platform-specific implementations should extend this class and override methods as needed. */ @@ -70,7 +70,7 @@ export abstract class BaseMCPClient { const connector = this.createConnectorFromConfig(servers[serverName]) const session = new MCPSession(connector) - + if (autoInitialize) { await session.initialize() } diff --git a/src/logging.ts b/src/logging.ts index 7116fcd6..c858864e 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -9,7 +9,8 @@ async function getNodeModules() { const fs = await import('node:fs') const path = await import('node:path') return { fs: fs.default, path: path.default } - } catch { + } + catch { return { fs: null, path: null } } } @@ -97,31 +98,31 @@ class SimpleConsoleLogger { info(message: string): void { if (this.shouldLog('info')) { - console.info(this.formatMessage('info', message)) // eslint-disable-line no-console + console.info(this.formatMessage('info', message)) } } debug(message: string): void { if (this.shouldLog('debug')) { - console.debug(this.formatMessage('debug', message)) // eslint-disable-line no-console + console.debug(this.formatMessage('debug', message)) } } http(message: string): void { if (this.shouldLog('http')) { - console.log(this.formatMessage('http', message)) // eslint-disable-line no-console + console.log(this.formatMessage('http', message)) } } verbose(message: string): void { if (this.shouldLog('verbose')) { - console.log(this.formatMessage('verbose', message)) // eslint-disable-line no-console + console.log(this.formatMessage('verbose', message)) } } silly(message: string): void { if (this.shouldLog('silly')) { - console.log(this.formatMessage('silly', message)) // eslint-disable-line no-console + console.log(this.formatMessage('silly', message)) } } @@ -225,7 +226,7 @@ export class Logger { Object.values(this.simpleInstances).forEach((logger) => { logger.level = resolvedLevel }) - + return } winstonRoot.clear() diff --git a/src/managers/server_manager.ts b/src/managers/server_manager.ts index 84cf9be7..49878592 100644 --- a/src/managers/server_manager.ts +++ b/src/managers/server_manager.ts @@ -55,7 +55,7 @@ export class ServerManager { })) logger.info(`Server Manager State: [${context}]`) - console.table(tableData) // eslint-disable-line no-console + console.table(tableData) } initialize(): void { diff --git a/src/oauth-helper.ts b/src/oauth-helper.ts index daca5c4c..0ced866b 100644 --- a/src/oauth-helper.ts +++ b/src/oauth-helper.ts @@ -1,6 +1,6 @@ /** * OAuth helper for browser-based MCP authentication - * + * * This helper provides OAuth 2.0 authorization code flow support for MCP servers * that require authentication, such as Linear's MCP server. */ @@ -69,7 +69,7 @@ export class OAuthHelper { authError: null, oauthTokens: null, } - + // Load persisted client registration this.loadClientRegistration() } @@ -93,10 +93,11 @@ export class OAuthHelper { this.serverUrl = data.serverUrl console.log('🔄 [OAuthHelper] Loaded persisted client registration:', { client_id: this.clientRegistration?.client_id, - server: this.serverUrl + server: this.serverUrl, }) } - } catch (error) { + } + catch (error) { console.warn('⚠️ [OAuthHelper] Failed to load client registration:', error) } } @@ -108,11 +109,12 @@ export class OAuthHelper { try { const data = { clientRegistration: this.clientRegistration, - serverUrl: this.serverUrl + serverUrl: this.serverUrl, } localStorage.setItem(this.storageKey, JSON.stringify(data)) console.log('💾 [OAuthHelper] Saved client registration to localStorage') - } catch (error) { + } + catch (error) { console.warn('⚠️ [OAuthHelper] Failed to save client registration:', error) } } @@ -149,12 +151,13 @@ export class OAuthHelper { // Any other response (200, 404, 500, etc.) means no auth required console.log('✅ [OAuthHelper] No authentication required for:', serverUrl) return false - } catch (error: any) { + } + catch (error: any) { console.warn('⚠️ [OAuthHelper] Could not check auth requirement for:', serverUrl, error) // Handle specific error types - if (error.name === 'TypeError' && - (error.message?.includes('CORS') || error.message?.includes('Failed to fetch'))) { + if (error.name === 'TypeError' + && (error.message?.includes('CORS') || error.message?.includes('Failed to fetch'))) { console.log('🔍 [OAuthHelper] CORS blocked direct check, using heuristics for:', serverUrl) return this.checkAuthByHeuristics(serverUrl) } @@ -191,7 +194,7 @@ export class OAuthHelper { // Known patterns that typically don't require auth (public MCP servers) const noAuthPatterns = [ /localhost/i, // Local development - /127\.0\.0\.1/i, // Local development + /127\.0\.0\.1/, // Local development /\.local/i, // Local development /mcp\..*\.com/i, // Generic MCP server pattern (often public) ] @@ -224,14 +227,15 @@ export class OAuthHelper { try { const discoveryUrl = `${serverUrl}/.well-known/oauth-authorization-server` const response = await fetch(discoveryUrl) - + if (!response.ok) { throw new Error(`OAuth discovery failed: ${response.status} ${response.statusText}`) } - + this.discovery = await response.json() return this.discovery! - } catch (error) { + } + catch (error) { throw new Error(`Failed to discover OAuth configuration: ${error}`) } } @@ -269,7 +273,7 @@ export class OAuthHelper { headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify(registrationData) + body: JSON.stringify(registrationData), }) if (!response.ok) { @@ -280,14 +284,15 @@ export class OAuthHelper { this.clientRegistration = await response.json() this.serverUrl = serverUrl this.saveClientRegistration() - + console.log('✅ [OAuthHelper] Client registered successfully:', { client_id: this.clientRegistration!.client_id, client_secret: this.clientRegistration!.client_secret ? '***' : 'none', }) return this.clientRegistration! - } catch (error) { + } + catch (error) { console.error('❌ [OAuthHelper] Client registration failed:', error) throw new Error(`Failed to register OAuth client: ${error}`) } @@ -311,7 +316,7 @@ export class OAuthHelper { response_type: 'code', scope: this.config.scope || 'read', state: this.config.state || this.generateState(), - ...additionalParams + ...additionalParams, }) return `${this.discovery.authorization_endpoint}?${params.toString()}` @@ -321,9 +326,9 @@ export class OAuthHelper { * Exchange authorization code for access token */ async exchangeCodeForToken( - serverUrl: string, - code: string, - codeVerifier?: string + serverUrl: string, + code: string, + codeVerifier?: string, ): Promise { if (!this.discovery) { throw new Error('OAuth discovery not performed. Call discoverOAuthConfig first.') @@ -349,7 +354,7 @@ export class OAuthHelper { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, - body: body.toString() + body: body.toString(), }) if (!response.ok) { @@ -363,7 +368,7 @@ export class OAuthHelper { /** * Handle OAuth callback and extract authorization code */ - handleCallback(): { code: string; state: string } | null { + handleCallback(): { code: string, state: string } | null { const urlParams = new URLSearchParams(window.location.search) const code = urlParams.get('code') const state = urlParams.get('state') @@ -393,20 +398,20 @@ export class OAuthHelper { try { // Step 1: Discover OAuth configuration await this.discoverOAuthConfig(serverUrl) - + // Step 2: Register client dynamically (if not already registered) if (!this.clientRegistration) { await this.registerClient(serverUrl) } - + // Step 3: Generate authorization URL const authUrl = this.generateAuthUrl(serverUrl) - + // Step 4: Open popup window for authentication const authWindow = window.open( authUrl, 'mcp-oauth', - 'width=500,height=600,scrollbars=yes,resizable=yes,status=yes,location=yes' + 'width=500,height=600,scrollbars=yes,resizable=yes,status=yes,location=yes', ) if (!authWindow) { @@ -414,7 +419,8 @@ export class OAuthHelper { } console.log('✅ [OAuthHelper] OAuth popup opened successfully') - } catch (error) { + } + catch (error) { console.error('❌ [OAuthHelper] Failed to start OAuth flow:', error) this.setState({ isAuthenticating: false, @@ -444,12 +450,13 @@ export class OAuthHelper { if (!this.clientRegistration || this.serverUrl !== serverUrl) { console.log('🔐 [OAuthHelper] Re-registering client for callback') await this.registerClient(serverUrl) - } else { + } + else { console.log('🔄 [OAuthHelper] Using existing client registration for callback') } const tokenResponse = await this.exchangeCodeForToken(serverUrl, code) - + this.setState({ isAuthenticating: false, isAuthenticated: true, @@ -460,7 +467,8 @@ export class OAuthHelper { console.log('✅ [OAuthHelper] OAuth flow completed successfully') return tokenResponse - } catch (error) { + } + catch (error) { console.error('❌ [OAuthHelper] Failed to complete OAuth flow:', error) this.setState({ isAuthenticating: false, @@ -483,14 +491,15 @@ export class OAuthHelper { authError: null, oauthTokens: null, }) - + // Clear stored client registration this.clientRegistration = undefined this.serverUrl = undefined try { localStorage.removeItem(this.storageKey) console.log('🗑️ [OAuthHelper] Cleared stored client registration') - } catch (error) { + } + catch (error) { console.warn('⚠️ [OAuthHelper] Failed to clear client registration:', error) } } @@ -506,8 +515,8 @@ export class OAuthHelper { * Generate a random state parameter for CSRF protection */ private generateState(): string { - return Math.random().toString(36).substring(2, 15) + - Math.random().toString(36).substring(2, 15) + return Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15) } } @@ -530,8 +539,8 @@ export function createOAuthMCPConfig(serverUrl: string, accessToken: string) { linear: { url: serverUrl, authToken: accessToken, - transport: 'sse' - } - } + transport: 'sse', + }, + }, } }