diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..eeeb7be --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +pnpm-lock.yaml \ No newline at end of file diff --git a/README.md b/README.md index 99b1e8a..7aebd26 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,7 @@ E.g: Claude Desktop or Windsurf "mcpServers": { "remote-example": { "command": "npx", - "args": [ - "mcp-remote", - "https://remote.mcp.server/sse" - ] + "args": ["mcp-remote", "https://remote.mcp.server/sse"] } } } @@ -23,4 +20,3 @@ E.g: Claude Desktop or Windsurf Cursor: ![image](https://github.com/user-attachments/assets/14338bfa-a779-4e8a-a477-71f72cc5d99d) - diff --git a/package.json b/package.json index 76d1b41..7b5b680 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ } }, "scripts": { - "build": "tsup" + "build": "tsup", + "check": "prettier --check . && tsc" }, "dependencies": { "express": "^4.21.2", diff --git a/src/cli/proxy.ts b/src/cli/proxy.ts index 763a9b1..60f4966 100644 --- a/src/cli/proxy.ts +++ b/src/cli/proxy.ts @@ -11,13 +11,8 @@ import { EventEmitter } from 'events' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' -import { - NodeOAuthClientProvider, - setupOAuthCallbackServer, - parseCommandLineArgs, - setupSignalHandlers, -} from './shared.js' -import {connectToRemoteServer, mcpProxy} from "../lib/utils.js"; +import { NodeOAuthClientProvider, setupOAuthCallbackServer, parseCommandLineArgs, setupSignalHandlers } from './shared.js' +import { connectToRemoteServer, mcpProxy } from '../lib/utils.js' /** * Main function to run the proxy diff --git a/src/cli/shared.ts b/src/cli/shared.ts index 28bc1b1..a4101aa 100644 --- a/src/cli/shared.ts +++ b/src/cli/shared.ts @@ -10,7 +10,7 @@ import path from 'path' import os from 'os' import crypto from 'crypto' import net from 'net' -import {OAuthClientProvider} from '@modelcontextprotocol/sdk/client/auth.js' +import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js' import { OAuthClientInformation, OAuthClientInformationFull, @@ -18,7 +18,7 @@ import { OAuthTokens, OAuthTokensSchema, } from '@modelcontextprotocol/sdk/shared/auth.js' -import {OAuthCallbackServerOptions, OAuthProviderOptions} from "../lib/types.js"; +import { OAuthCallbackServerOptions, OAuthProviderOptions } from '../lib/types.js' /** * Implements the OAuthClientProvider interface for Node.js environments. diff --git a/src/lib/types.ts b/src/lib/types.ts index 2201eb5..188fccb 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,4 +1,4 @@ -import {EventEmitter} from "events"; +import { EventEmitter } from 'events' /** * Options for creating an OAuth client provider @@ -28,4 +28,4 @@ export interface OAuthCallbackServerOptions { path: string /** Event emitter to signal when auth code is received */ events: EventEmitter -} \ No newline at end of file +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 2a51a47..0340315 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,24 +1,23 @@ -import { OAuthClientProvider, UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; -import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; +import { OAuthClientProvider, UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js' +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' +import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js' /** * Creates a bidirectional proxy between two transports * @param params The transport connections to proxy between */ -export function mcpProxy({transportToClient, transportToServer}: { - transportToClient: Transport; - transportToServer: Transport -}) { +export function mcpProxy({ transportToClient, transportToServer }: { transportToClient: Transport; transportToServer: Transport }) { let transportToClientClosed = false let transportToServerClosed = false transportToClient.onmessage = (message) => { + // @ts-expect-error TODO console.error('[Local→Remote]', message.method || message.id) transportToServer.send(message).catch(onServerError) } transportToServer.onmessage = (message) => { + // @ts-expect-error TODO: fix this type console.error('[Remote→Local]', message.method || message.id) transportToClient.send(message).catch(onClientError) } @@ -66,7 +65,7 @@ export async function connectToRemoteServer( ): Promise { console.error('Connecting to remote server:', serverUrl) const url = new URL(serverUrl) - const transport = new SSEClientTransport(url, {authProvider}) + const transport = new SSEClientTransport(url, { authProvider }) try { await transport.start() @@ -84,7 +83,7 @@ export async function connectToRemoteServer( await transport.finishAuth(code) // Create a new transport after auth - const newTransport = new SSEClientTransport(url, {authProvider}) + const newTransport = new SSEClientTransport(url, { authProvider }) await newTransport.start() console.error('Connected to remote server after authentication') return newTransport @@ -97,4 +96,4 @@ export async function connectToRemoteServer( throw error } } -} \ No newline at end of file +} diff --git a/src/react/index.ts b/src/react/index.ts index 9538c8f..40948c2 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -471,13 +471,6 @@ export function useMcp(options: UseMcpOptions): UseMcpResult { }) } - // Discover OAuth metadata if not already discovered - if (!metadataRef.current) { - addLog('info', 'Discovering OAuth metadata...') - metadataRef.current = await discoverOAuthMetadata(url) - addLog('debug', `OAuth metadata: ${metadataRef.current ? 'Found' : 'Not available'}`) - } - // Create MCP client clientRef.current = new Client( { @@ -491,10 +484,7 @@ export function useMcp(options: UseMcpOptions): UseMcpResult { }, ) - // Set up auth flow - check if we have tokens - const tokens = await authProviderRef.current.tokens() - - // Create SSE transport + // Create SSE transport - try connecting without auth first setState('connecting') addLog('info', 'Creating transport...') @@ -514,13 +504,8 @@ export function useMcp(options: UseMcpOptions): UseMcpResult { addLog('error', `Transport error: ${err.message}`) if (err.message.includes('Unauthorized')) { - setState('authenticating') - handleAuthentication().catch((authErr) => { - addLog('error', `Authentication error: ${authErr.message}`) - setState('failed') - setError(`Authentication failed: ${authErr.message}`) - connectingRef.current = false - }) + // Only discover OAuth metadata and authenticate if we get a 401 + discoverOAuthAndAuthenticate(err) } else { setState('failed') setError(`Connection error: ${err.message}`) @@ -540,7 +525,38 @@ export function useMcp(options: UseMcpOptions): UseMcpResult { } } - // Connect transport + // Helper function to handle OAuth discovery and authentication + const discoverOAuthAndAuthenticate = async (error: Error) => { + try { + // Discover OAuth metadata now that we know we need it + if (!metadataRef.current) { + addLog('info', 'Discovering OAuth metadata...') + metadataRef.current = await discoverOAuthMetadata(url) + addLog('debug', `OAuth metadata: ${metadataRef.current ? 'Found' : 'Not available'}`) + } + + // If metadata is found, start auth flow + if (metadataRef.current) { + setState('authenticating') + // Start authentication process + await handleAuthentication() + // After successful auth, retry connection + return connect() + } else { + // No OAuth metadata available + setState('failed') + setError(`Authentication required but no OAuth metadata found: ${error.message}`) + connectingRef.current = false + } + } catch (oauthErr) { + addLog('error', `OAuth discovery error: ${oauthErr instanceof Error ? oauthErr.message : String(oauthErr)}`) + setState('failed') + setError(`Authentication setup failed: ${oauthErr instanceof Error ? oauthErr.message : String(oauthErr)}`) + connectingRef.current = false + } + } + + // Try connecting transport first without OAuth discovery try { addLog('info', 'Starting transport...') // await transportRef.current.start() @@ -548,11 +564,8 @@ export function useMcp(options: UseMcpOptions): UseMcpResult { addLog('error', `Transport start error: ${err instanceof Error ? err.message : String(err)}`) if (err instanceof Error && err.message.includes('Unauthorized')) { - setState('authenticating') - // Start authentication process - await handleAuthentication() - // After successful auth, retry connection - return connect() + // Only discover OAuth and authenticate if we get a 401 + await discoverOAuthAndAuthenticate(err) } else { setState('failed') setError(`Connection error: ${err instanceof Error ? err.message : String(err)}`) @@ -586,9 +599,15 @@ export function useMcp(options: UseMcpOptions): UseMcpResult { } } catch (connectErr) { addLog('error', `Client connect error: ${connectErr instanceof Error ? connectErr.message : String(connectErr)}`) - setState('failed') - setError(`Connection error: ${connectErr instanceof Error ? connectErr.message : String(connectErr)}`) - connectingRef.current = false + + if (connectErr instanceof Error && connectErr.message.includes('Unauthorized')) { + // Only discover OAuth and authenticate if we get a 401 + await discoverOAuthAndAuthenticate(connectErr) + } else { + setState('failed') + setError(`Connection error: ${connectErr instanceof Error ? connectErr.message : String(connectErr)}`) + connectingRef.current = false + } } } catch (err) { addLog('error', `Unexpected error: ${err instanceof Error ? err.message : String(err)}`)