Merge pull request #3 from geelen/support-unauthed

Adding support for servers that don't use auth or have the `.well-known` URL
This commit is contained in:
Sunil Pai 2025-03-24 10:48:56 +00:00 committed by GitHub
commit ff4ecf3e5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 65 additions and 54 deletions

1
.prettierignore Normal file
View file

@ -0,0 +1 @@
pnpm-lock.yaml

View file

@ -11,10 +11,7 @@ E.g: Claude Desktop or Windsurf
"mcpServers": { "mcpServers": {
"remote-example": { "remote-example": {
"command": "npx", "command": "npx",
"args": [ "args": ["mcp-remote", "https://remote.mcp.server/sse"]
"mcp-remote",
"https://remote.mcp.server/sse"
]
} }
} }
} }
@ -23,4 +20,3 @@ E.g: Claude Desktop or Windsurf
Cursor: Cursor:
![image](https://github.com/user-attachments/assets/14338bfa-a779-4e8a-a477-71f72cc5d99d) ![image](https://github.com/user-attachments/assets/14338bfa-a779-4e8a-a477-71f72cc5d99d)

View file

@ -18,7 +18,8 @@
} }
}, },
"scripts": { "scripts": {
"build": "tsup" "build": "tsup",
"check": "prettier --check . && tsc"
}, },
"dependencies": { "dependencies": {
"express": "^4.21.2", "express": "^4.21.2",

View file

@ -11,13 +11,8 @@
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { import { NodeOAuthClientProvider, setupOAuthCallbackServer, parseCommandLineArgs, setupSignalHandlers } from './shared.js'
NodeOAuthClientProvider, import { connectToRemoteServer, mcpProxy } from '../lib/utils.js'
setupOAuthCallbackServer,
parseCommandLineArgs,
setupSignalHandlers,
} from './shared.js'
import {connectToRemoteServer, mcpProxy} from "../lib/utils.js";
/** /**
* Main function to run the proxy * Main function to run the proxy

View file

@ -18,7 +18,7 @@ import {
OAuthTokens, OAuthTokens,
OAuthTokensSchema, OAuthTokensSchema,
} from '@modelcontextprotocol/sdk/shared/auth.js' } 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. * Implements the OAuthClientProvider interface for Node.js environments.

View file

@ -1,4 +1,4 @@
import {EventEmitter} from "events"; import { EventEmitter } from 'events'
/** /**
* Options for creating an OAuth client provider * Options for creating an OAuth client provider

View file

@ -1,24 +1,23 @@
import { OAuthClientProvider, UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"; import { OAuthClientProvider, UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
/** /**
* Creates a bidirectional proxy between two transports * Creates a bidirectional proxy between two transports
* @param params The transport connections to proxy between * @param params The transport connections to proxy between
*/ */
export function mcpProxy({transportToClient, transportToServer}: { export function mcpProxy({ transportToClient, transportToServer }: { transportToClient: Transport; transportToServer: Transport }) {
transportToClient: Transport;
transportToServer: Transport
}) {
let transportToClientClosed = false let transportToClientClosed = false
let transportToServerClosed = false let transportToServerClosed = false
transportToClient.onmessage = (message) => { transportToClient.onmessage = (message) => {
// @ts-expect-error TODO
console.error('[Local→Remote]', message.method || message.id) console.error('[Local→Remote]', message.method || message.id)
transportToServer.send(message).catch(onServerError) transportToServer.send(message).catch(onServerError)
} }
transportToServer.onmessage = (message) => { transportToServer.onmessage = (message) => {
// @ts-expect-error TODO: fix this type
console.error('[Remote→Local]', message.method || message.id) console.error('[Remote→Local]', message.method || message.id)
transportToClient.send(message).catch(onClientError) transportToClient.send(message).catch(onClientError)
} }

View file

@ -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 // Create MCP client
clientRef.current = new Client( clientRef.current = new Client(
{ {
@ -491,10 +484,7 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
}, },
) )
// Set up auth flow - check if we have tokens // Create SSE transport - try connecting without auth first
const tokens = await authProviderRef.current.tokens()
// Create SSE transport
setState('connecting') setState('connecting')
addLog('info', 'Creating transport...') addLog('info', 'Creating transport...')
@ -514,13 +504,8 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
addLog('error', `Transport error: ${err.message}`) addLog('error', `Transport error: ${err.message}`)
if (err.message.includes('Unauthorized')) { if (err.message.includes('Unauthorized')) {
setState('authenticating') // Only discover OAuth metadata and authenticate if we get a 401
handleAuthentication().catch((authErr) => { discoverOAuthAndAuthenticate(err)
addLog('error', `Authentication error: ${authErr.message}`)
setState('failed')
setError(`Authentication failed: ${authErr.message}`)
connectingRef.current = false
})
} else { } else {
setState('failed') setState('failed')
setError(`Connection error: ${err.message}`) 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 { try {
addLog('info', 'Starting transport...') addLog('info', 'Starting transport...')
// await transportRef.current.start() // 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)}`) addLog('error', `Transport start error: ${err instanceof Error ? err.message : String(err)}`)
if (err instanceof Error && err.message.includes('Unauthorized')) { if (err instanceof Error && err.message.includes('Unauthorized')) {
setState('authenticating') // Only discover OAuth and authenticate if we get a 401
// Start authentication process await discoverOAuthAndAuthenticate(err)
await handleAuthentication()
// After successful auth, retry connection
return connect()
} else { } else {
setState('failed') setState('failed')
setError(`Connection error: ${err instanceof Error ? err.message : String(err)}`) setError(`Connection error: ${err instanceof Error ? err.message : String(err)}`)
@ -586,10 +599,16 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
} }
} catch (connectErr) { } catch (connectErr) {
addLog('error', `Client connect error: ${connectErr instanceof Error ? connectErr.message : String(connectErr)}`) addLog('error', `Client connect error: ${connectErr instanceof Error ? connectErr.message : String(connectErr)}`)
if (connectErr instanceof Error && connectErr.message.includes('Unauthorized')) {
// Only discover OAuth and authenticate if we get a 401
await discoverOAuthAndAuthenticate(connectErr)
} else {
setState('failed') setState('failed')
setError(`Connection error: ${connectErr instanceof Error ? connectErr.message : String(connectErr)}`) setError(`Connection error: ${connectErr instanceof Error ? connectErr.message : String(connectErr)}`)
connectingRef.current = false connectingRef.current = false
} }
}
} catch (err) { } catch (err) {
addLog('error', `Unexpected error: ${err instanceof Error ? err.message : String(err)}`) addLog('error', `Unexpected error: ${err instanceof Error ? err.message : String(err)}`)
setState('failed') setState('failed')