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:
commit
ff4ecf3e5b
8 changed files with 65 additions and 54 deletions
1
.prettierignore
Normal file
1
.prettierignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pnpm-lock.yaml
|
|
@ -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:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup"
|
"build": "tsup",
|
||||||
|
"check": "prettier --check . && tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -10,7 +10,7 @@ import path from 'path'
|
||||||
import os from 'os'
|
import os from 'os'
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
import net from 'net'
|
import net from 'net'
|
||||||
import {OAuthClientProvider} from '@modelcontextprotocol/sdk/client/auth.js'
|
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'
|
||||||
import {
|
import {
|
||||||
OAuthClientInformation,
|
OAuthClientInformation,
|
||||||
OAuthClientInformationFull,
|
OAuthClientInformationFull,
|
||||||
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -66,7 +65,7 @@ export async function connectToRemoteServer(
|
||||||
): Promise<SSEClientTransport> {
|
): Promise<SSEClientTransport> {
|
||||||
console.error('Connecting to remote server:', serverUrl)
|
console.error('Connecting to remote server:', serverUrl)
|
||||||
const url = new URL(serverUrl)
|
const url = new URL(serverUrl)
|
||||||
const transport = new SSEClientTransport(url, {authProvider})
|
const transport = new SSEClientTransport(url, { authProvider })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await transport.start()
|
await transport.start()
|
||||||
|
@ -84,7 +83,7 @@ export async function connectToRemoteServer(
|
||||||
await transport.finishAuth(code)
|
await transport.finishAuth(code)
|
||||||
|
|
||||||
// Create a new transport after auth
|
// Create a new transport after auth
|
||||||
const newTransport = new SSEClientTransport(url, {authProvider})
|
const newTransport = new SSEClientTransport(url, { authProvider })
|
||||||
await newTransport.start()
|
await newTransport.start()
|
||||||
console.error('Connected to remote server after authentication')
|
console.error('Connected to remote server after authentication')
|
||||||
return newTransport
|
return newTransport
|
||||||
|
|
|
@ -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')
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue