Merge branch 'main' of https://github.com/OwnID/mcp-remote
This commit is contained in:
commit
85b9f0b363
9 changed files with 134 additions and 55 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
||||||
node_modules
|
node_modules
|
||||||
.mcp-cli
|
.mcp-cli
|
||||||
dist
|
dist
|
||||||
|
.idea
|
||||||
|
|
41
README.md
41
README.md
|
@ -10,7 +10,7 @@ So far, the majority of MCP servers in the wild are installed locally, using the
|
||||||
|
|
||||||
But there's a reason most software that _could_ be moved to the web _did_ get moved to the web: it's so much easier to find and fix bugs & iterate on new features when you can push updates to all your users with a single deploy.
|
But there's a reason most software that _could_ be moved to the web _did_ get moved to the web: it's so much easier to find and fix bugs & iterate on new features when you can push updates to all your users with a single deploy.
|
||||||
|
|
||||||
With the MCP [Authorization specification](https://spec.modelcontextprotocol.io/specification/draft/basic/authorization/) nearing completion, we now have a secure way of sharing our MCP servers with the world _without_ running code on user's laptops. Or at least, you would, if all the popular MCP _clients_ supported it yet. Most are stdio-only, and those that _do_ support HTTP+SSE don't yet support the OAuth flows required.
|
With the latest MCP [Authorization specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization), we now have a secure way of sharing our MCP servers with the world _without_ running code on user's laptops. Or at least, you would, if all the popular MCP _clients_ supported it yet. Most are stdio-only, and those that _do_ support HTTP+SSE don't yet support the OAuth flows required.
|
||||||
|
|
||||||
That's where `mcp-remote` comes in. As soon as your chosen MCP client supports remote, authorized servers, you can remove it. Until that time, drop in this one liner and dress for the MCP clients you want!
|
That's where `mcp-remote` comes in. As soon as your chosen MCP client supports remote, authorized servers, you can remove it. Until that time, drop in this one liner and dress for the MCP clients you want!
|
||||||
|
|
||||||
|
@ -55,6 +55,23 @@ To bypass authentication, or to emit custom headers on all requests to your remo
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note:** Cursor has a bug where spaces inside `args` aren't escaped when it invokes `npx`, which ends up mangling these values. You can work around it using:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
// rest of config...
|
||||||
|
"args": [
|
||||||
|
"mcp-remote",
|
||||||
|
"https://remote.mcp.server/sse",
|
||||||
|
"--header",
|
||||||
|
"Authorization:${AUTH_HEADER}" // note no spaces around ':'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"AUTH_HEADER": "Bearer <auth-token>" // spaces OK in env vars
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Flags
|
### Flags
|
||||||
|
|
||||||
* If `npx` is producing errors, consider adding `-y` as the first argument to auto-accept the installation of the `mcp-remote` package.
|
* If `npx` is producing errors, consider adding `-y` as the first argument to auto-accept the installation of the `mcp-remote` package.
|
||||||
|
@ -62,12 +79,22 @@ To bypass authentication, or to emit custom headers on all requests to your remo
|
||||||
```json
|
```json
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": [
|
"args": [
|
||||||
"-y"
|
"-y",
|
||||||
"mcp-remote",
|
"mcp-remote",
|
||||||
"https://remote.mcp.server/sse"
|
"https://remote.mcp.server/sse"
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
* To use Streamable HTTP instead of Server-Sent Events (SSE), add the `--streamableHttp` flag. This is recommended as SSE is deprecated:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"args": [
|
||||||
|
"mcp-remote",
|
||||||
|
"https://remote.mcp.server/sse",
|
||||||
|
"--streamableHttp"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
* To force `npx` to always check for an updated version of `mcp-remote`, add the `@latest` flag:
|
* To force `npx` to always check for an updated version of `mcp-remote`, add the `@latest` flag:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
@ -87,6 +114,16 @@ To bypass authentication, or to emit custom headers on all requests to your remo
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
* To allow HTTP connections in trusted private networks, add the `--allow-http` flag. Note: This should only be used in secure private networks where traffic cannot be intercepted.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"args": [
|
||||||
|
"mcp-remote",
|
||||||
|
"http://internal-service.vpc/sse",
|
||||||
|
"--allow-http"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
### Claude Desktop
|
### Claude Desktop
|
||||||
|
|
||||||
[Official Docs](https://modelcontextprotocol.io/quickstart/user)
|
[Official Docs](https://modelcontextprotocol.io/quickstart/user)
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
"check": "prettier --check . && tsc"
|
"check": "prettier --check . && tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.7.0",
|
"@modelcontextprotocol/sdk": "^1.10.2",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"open": "^10.1.0"
|
"open": "^10.1.0"
|
||||||
},
|
},
|
||||||
|
|
19
pnpm-lock.yaml
generated
19
pnpm-lock.yaml
generated
|
@ -9,8 +9,8 @@ importers:
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@modelcontextprotocol/sdk':
|
'@modelcontextprotocol/sdk':
|
||||||
specifier: ^1.7.0
|
specifier: ^1.10.2
|
||||||
version: 1.7.0
|
version: 1.10.2
|
||||||
express:
|
express:
|
||||||
specifier: ^4.21.2
|
specifier: ^4.21.2
|
||||||
version: 4.21.2
|
version: 4.21.2
|
||||||
|
@ -217,8 +217,8 @@ packages:
|
||||||
'@jridgewell/trace-mapping@0.3.25':
|
'@jridgewell/trace-mapping@0.3.25':
|
||||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||||
|
|
||||||
'@modelcontextprotocol/sdk@1.7.0':
|
'@modelcontextprotocol/sdk@1.10.2':
|
||||||
resolution: {integrity: sha512-IYPe/FLpvF3IZrd/f5p5ffmWhMc3aEMuM2wGJASDqC2Ge7qatVCdbfPx3n/5xFeb19xN0j/911M2AaFuircsWA==}
|
resolution: {integrity: sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
|
@ -860,8 +860,8 @@ packages:
|
||||||
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
|
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
pkce-challenge@4.1.0:
|
pkce-challenge@5.0.0:
|
||||||
resolution: {integrity: sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==}
|
resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==}
|
||||||
engines: {node: '>=16.20.0'}
|
engines: {node: '>=16.20.0'}
|
||||||
|
|
||||||
postcss-load-config@6.0.1:
|
postcss-load-config@6.0.1:
|
||||||
|
@ -1238,14 +1238,15 @@ snapshots:
|
||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.0
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
|
|
||||||
'@modelcontextprotocol/sdk@1.7.0':
|
'@modelcontextprotocol/sdk@1.10.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
cors: 2.8.5
|
cors: 2.8.5
|
||||||
|
cross-spawn: 7.0.6
|
||||||
eventsource: 3.0.5
|
eventsource: 3.0.5
|
||||||
express: 5.0.1
|
express: 5.0.1
|
||||||
express-rate-limit: 7.5.0(express@5.0.1)
|
express-rate-limit: 7.5.0(express@5.0.1)
|
||||||
pkce-challenge: 4.1.0
|
pkce-challenge: 5.0.0
|
||||||
raw-body: 3.0.0
|
raw-body: 3.0.0
|
||||||
zod: 3.24.2
|
zod: 3.24.2
|
||||||
zod-to-json-schema: 3.24.5(zod@3.24.2)
|
zod-to-json-schema: 3.24.5(zod@3.24.2)
|
||||||
|
@ -1885,7 +1886,7 @@ snapshots:
|
||||||
|
|
||||||
pirates@4.0.6: {}
|
pirates@4.0.6: {}
|
||||||
|
|
||||||
pkce-challenge@4.1.0: {}
|
pkce-challenge@5.0.0: {}
|
||||||
|
|
||||||
postcss-load-config@6.0.1(tsx@4.19.3):
|
postcss-load-config@6.0.1(tsx@4.19.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MCP Client with OAuth support
|
* MCP Client with OAuth support
|
||||||
* A command-line client that connects to an MCP server using SSE with OAuth authentication.
|
* A command-line client that connects to an MCP server using StreamableHTTP with OAuth authentication.
|
||||||
*
|
*
|
||||||
* Run with: npx tsx client.ts https://example.remote/server [callback-port]
|
* Run with: npx tsx client.ts https://example.remote/server [callback-port]
|
||||||
*
|
*
|
||||||
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
import { EventEmitter } from 'events'
|
import { EventEmitter } from 'events'
|
||||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||||
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
||||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
||||||
import { ListResourcesResultSchema, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'
|
import { ListResourcesResultSchema, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'
|
||||||
import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'
|
import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'
|
||||||
|
@ -21,7 +22,7 @@ import { coordinateAuth } from './lib/coordination'
|
||||||
/**
|
/**
|
||||||
* Main function to run the client
|
* Main function to run the client
|
||||||
*/
|
*/
|
||||||
async function runClient(serverUrl: string, callbackPort: number, headers: Record<string, string>) {
|
async function runClient(serverUrl: string, callbackPort: number, headers: Record<string, string>, useStreamableHttp: boolean = false) {
|
||||||
// Set up event emitter for auth flow
|
// Set up event emitter for auth flow
|
||||||
const events = new EventEmitter()
|
const events = new EventEmitter()
|
||||||
|
|
||||||
|
@ -60,7 +61,22 @@ async function runClient(serverUrl: string, callbackPort: number, headers: Recor
|
||||||
// Create the transport factory
|
// Create the transport factory
|
||||||
const url = new URL(serverUrl)
|
const url = new URL(serverUrl)
|
||||||
function initTransport() {
|
function initTransport() {
|
||||||
const transport = new SSEClientTransport(url, { authProvider, requestInit: { headers } })
|
// Choose between Streamable HTTP or SSE transport based on flag
|
||||||
|
const transport = useStreamableHttp
|
||||||
|
? new StreamableHTTPClientTransport(url, {
|
||||||
|
authProvider,
|
||||||
|
requestInit: { headers },
|
||||||
|
reconnectionOptions: {
|
||||||
|
initialReconnectionDelay: 1000,
|
||||||
|
maxReconnectionDelay: 10000,
|
||||||
|
reconnectionDelayGrowFactor: 1.5,
|
||||||
|
maxRetries: 10,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: new SSEClientTransport(url, {
|
||||||
|
authProvider,
|
||||||
|
requestInit: { headers }
|
||||||
|
})
|
||||||
|
|
||||||
// Set up message and error handlers
|
// Set up message and error handlers
|
||||||
transport.onmessage = (message) => {
|
transport.onmessage = (message) => {
|
||||||
|
@ -155,9 +171,9 @@ async function runClient(serverUrl: string, callbackPort: number, headers: Recor
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse command-line arguments and run the client
|
// Parse command-line arguments and run the client
|
||||||
parseCommandLineArgs(process.argv.slice(2), 3333, 'Usage: npx tsx client.ts <https://server-url> [callback-port]')
|
parseCommandLineArgs(process.argv.slice(2), 3333, 'Usage: npx tsx client.ts <https://server-url> [callback-port] [--streamableHttp]')
|
||||||
.then(({ serverUrl, callbackPort, headers }) => {
|
.then(({ serverUrl, callbackPort, headers, useStreamableHttp }) => {
|
||||||
return runClient(serverUrl, callbackPort, headers)
|
return runClient(serverUrl, callbackPort, headers, useStreamableHttp)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Fatal error:', error)
|
console.error('Fatal error:', error)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
} from '@modelcontextprotocol/sdk/shared/auth.js'
|
} from '@modelcontextprotocol/sdk/shared/auth.js'
|
||||||
import type { OAuthProviderOptions } from './types'
|
import type { OAuthProviderOptions } from './types'
|
||||||
import { readJsonFile, writeJsonFile, readTextFile, writeTextFile } from './mcp-auth-config'
|
import { readJsonFile, writeJsonFile, readTextFile, writeTextFile } from './mcp-auth-config'
|
||||||
import { getServerUrlHash, log } from './utils'
|
import { getServerUrlHash, log, MCP_REMOTE_VERSION } from './utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements the OAuthClientProvider interface for Node.js environments.
|
* Implements the OAuthClientProvider interface for Node.js environments.
|
||||||
|
@ -20,6 +20,8 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
|
||||||
private callbackPath: string
|
private callbackPath: string
|
||||||
private clientName: string
|
private clientName: string
|
||||||
private clientUri: string
|
private clientUri: string
|
||||||
|
private softwareId: string
|
||||||
|
private softwareVersion: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new NodeOAuthClientProvider
|
* Creates a new NodeOAuthClientProvider
|
||||||
|
@ -30,6 +32,8 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
|
||||||
this.callbackPath = options.callbackPath || '/oauth/callback'
|
this.callbackPath = options.callbackPath || '/oauth/callback'
|
||||||
this.clientName = options.clientName || 'MCP CLI Client'
|
this.clientName = options.clientName || 'MCP CLI Client'
|
||||||
this.clientUri = options.clientUri || 'https://github.com/modelcontextprotocol/mcp-cli'
|
this.clientUri = options.clientUri || 'https://github.com/modelcontextprotocol/mcp-cli'
|
||||||
|
this.softwareId = options.softwareId || '2e6dc280-f3c3-4e01-99a7-8181dbd1d23d'
|
||||||
|
this.softwareVersion = options.softwareVersion || MCP_REMOTE_VERSION
|
||||||
}
|
}
|
||||||
|
|
||||||
get redirectUrl(): string {
|
get redirectUrl(): string {
|
||||||
|
@ -44,6 +48,8 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
|
||||||
response_types: ['code'],
|
response_types: ['code'],
|
||||||
client_name: this.clientName,
|
client_name: this.clientName,
|
||||||
client_uri: this.clientUri,
|
client_uri: this.clientUri,
|
||||||
|
software_id: this.softwareId,
|
||||||
|
software_version: this.softwareVersion,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,10 @@ export interface OAuthProviderOptions {
|
||||||
clientName?: string
|
clientName?: string
|
||||||
/** Client URI to use for OAuth registration */
|
/** Client URI to use for OAuth registration */
|
||||||
clientUri?: string
|
clientUri?: string
|
||||||
|
/** Software ID to use for OAuth registration */
|
||||||
|
softwareId?: string
|
||||||
|
/** Software version to use for OAuth registration */
|
||||||
|
softwareVersion?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { OAuthClientProvider, UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'
|
import { OAuthClientProvider, UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'
|
||||||
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.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'
|
||||||
import { OAuthCallbackServerOptions } from './types'
|
import { OAuthCallbackServerOptions } from './types'
|
||||||
|
@ -65,13 +66,14 @@ export function mcpProxy({ transportToClient, transportToServer }: { transportTo
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates and connects to a remote SSE server with OAuth authentication
|
* Creates and connects to a remote server with OAuth authentication
|
||||||
* @param serverUrl The URL of the remote server
|
* @param serverUrl The URL of the remote server
|
||||||
* @param authProvider The OAuth client provider
|
* @param authProvider The OAuth client provider
|
||||||
* @param headers Additional headers to send with the request
|
* @param headers Additional headers to send with the request
|
||||||
* @param waitForAuthCode Function to wait for the auth code
|
* @param waitForAuthCode Function to wait for the auth code
|
||||||
* @param skipBrowserAuth Whether to skip browser auth and use shared auth
|
* @param skipBrowserAuth Whether to skip browser auth and use shared auth
|
||||||
* @returns The connected SSE client transport
|
* @param useStreamableHttp Whether to use Streamable HTTP transport instead of SSE
|
||||||
|
* @returns The connected client transport
|
||||||
*/
|
*/
|
||||||
export async function connectToRemoteServer(
|
export async function connectToRemoteServer(
|
||||||
serverUrl: string,
|
serverUrl: string,
|
||||||
|
@ -79,31 +81,26 @@ export async function connectToRemoteServer(
|
||||||
headers: Record<string, string>,
|
headers: Record<string, string>,
|
||||||
waitForAuthCode: () => Promise<string>,
|
waitForAuthCode: () => Promise<string>,
|
||||||
skipBrowserAuth: boolean = false,
|
skipBrowserAuth: boolean = false,
|
||||||
): Promise<SSEClientTransport> {
|
useStreamableHttp: boolean = false,
|
||||||
|
): Promise<StreamableHTTPClientTransport | SSEClientTransport> {
|
||||||
log(`[${pid}] Connecting to remote server: ${serverUrl}`)
|
log(`[${pid}] Connecting to remote server: ${serverUrl}`)
|
||||||
const url = new URL(serverUrl)
|
const url = new URL(serverUrl)
|
||||||
|
|
||||||
// Create transport with eventSourceInit to pass Authorization header if present
|
// Create the appropriate transport (Streamable HTTP or SSE) based on the flag
|
||||||
const eventSourceInit = {
|
const transport = useStreamableHttp
|
||||||
fetch: (url: string | URL, init?: RequestInit) => {
|
? new StreamableHTTPClientTransport(url, {
|
||||||
return Promise.resolve(authProvider?.tokens?.()).then((tokens) =>
|
|
||||||
fetch(url, {
|
|
||||||
...init,
|
|
||||||
headers: {
|
|
||||||
...(init?.headers as Record<string, string> | undefined),
|
|
||||||
...headers,
|
|
||||||
...(tokens?.access_token ? { Authorization: `Bearer ${tokens.access_token}` } : {}),
|
|
||||||
Accept: "text/event-stream",
|
|
||||||
} as Record<string, string>,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const transport = new SSEClientTransport(url, {
|
|
||||||
authProvider,
|
authProvider,
|
||||||
requestInit: { headers },
|
requestInit: { headers },
|
||||||
eventSourceInit,
|
reconnectionOptions: {
|
||||||
|
initialReconnectionDelay: 1000,
|
||||||
|
maxReconnectionDelay: 10000,
|
||||||
|
reconnectionDelayGrowFactor: 1.5,
|
||||||
|
maxRetries: 10,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: new SSEClientTransport(url, {
|
||||||
|
authProvider,
|
||||||
|
requestInit: { headers }
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -125,8 +122,22 @@ export async function connectToRemoteServer(
|
||||||
log('Completing authorization...')
|
log('Completing authorization...')
|
||||||
await transport.finishAuth(code)
|
await transport.finishAuth(code)
|
||||||
|
|
||||||
// Create a new transport after auth
|
// Create a new transport (Streamable HTTP or SSE) after auth with the same type as before
|
||||||
const newTransport = new SSEClientTransport(url, { authProvider, requestInit: { headers } })
|
const newTransport = useStreamableHttp
|
||||||
|
? new StreamableHTTPClientTransport(url, {
|
||||||
|
authProvider,
|
||||||
|
requestInit: { headers },
|
||||||
|
reconnectionOptions: {
|
||||||
|
initialReconnectionDelay: 1000,
|
||||||
|
maxReconnectionDelay: 10000,
|
||||||
|
reconnectionDelayGrowFactor: 1.5,
|
||||||
|
maxRetries: 10,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: new SSEClientTransport(url, {
|
||||||
|
authProvider,
|
||||||
|
requestInit: { headers }
|
||||||
|
})
|
||||||
await newTransport.start()
|
await newTransport.start()
|
||||||
log('Connected to remote server after authentication')
|
log('Connected to remote server after authentication')
|
||||||
return newTransport
|
return newTransport
|
||||||
|
@ -299,6 +310,8 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number,
|
||||||
|
|
||||||
const serverUrl = args[0]
|
const serverUrl = args[0]
|
||||||
const specifiedPort = args[1] ? parseInt(args[1]) : undefined
|
const specifiedPort = args[1] ? parseInt(args[1]) : undefined
|
||||||
|
const allowHttp = args.includes('--allow-http')
|
||||||
|
const useStreamableHttp = args.includes('--streamableHttp')
|
||||||
|
|
||||||
if (!serverUrl) {
|
if (!serverUrl) {
|
||||||
log(usage)
|
log(usage)
|
||||||
|
@ -308,7 +321,8 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number,
|
||||||
const url = new URL(serverUrl)
|
const url = new URL(serverUrl)
|
||||||
const isLocalhost = (url.hostname === 'localhost' || url.hostname === '127.0.0.1') && url.protocol === 'http:'
|
const isLocalhost = (url.hostname === 'localhost' || url.hostname === '127.0.0.1') && url.protocol === 'http:'
|
||||||
|
|
||||||
if (!(url.protocol == 'https:' || isLocalhost)) {
|
if (!(url.protocol == 'https:' || isLocalhost || allowHttp)) {
|
||||||
|
log('Error: Non-HTTPS URLs are only allowed for localhost or when --allow-http flag is provided')
|
||||||
log(usage)
|
log(usage)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
@ -341,7 +355,7 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return { serverUrl, callbackPort, headers }
|
return { serverUrl, callbackPort, headers, useStreamableHttp }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
14
src/proxy.ts
14
src/proxy.ts
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MCP Proxy with OAuth support
|
* MCP Proxy with OAuth support
|
||||||
* A bidirectional proxy between a local STDIO MCP server and a remote SSE server with OAuth authentication.
|
* A bidirectional proxy between a local STDIO MCP server and a remote server with OAuth authentication.
|
||||||
*
|
*
|
||||||
* Run with: npx tsx proxy.ts https://example.remote/server [callback-port]
|
* Run with: npx tsx proxy.ts https://example.remote/server [callback-port]
|
||||||
*
|
*
|
||||||
|
@ -18,7 +18,7 @@ import { coordinateAuth } from './lib/coordination'
|
||||||
/**
|
/**
|
||||||
* Main function to run the proxy
|
* Main function to run the proxy
|
||||||
*/
|
*/
|
||||||
async function runProxy(serverUrl: string, callbackPort: number, headers: Record<string, string>) {
|
async function runProxy(serverUrl: string, callbackPort: number, headers: Record<string, string>, useStreamableHttp: boolean = false) {
|
||||||
// Set up event emitter for auth flow
|
// Set up event emitter for auth flow
|
||||||
const events = new EventEmitter()
|
const events = new EventEmitter()
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ async function runProxy(serverUrl: string, callbackPort: number, headers: Record
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Connect to remote server with authentication
|
// Connect to remote server with authentication
|
||||||
const remoteTransport = await connectToRemoteServer(serverUrl, authProvider, headers, waitForAuthCode, skipBrowserAuth)
|
const remoteTransport = await connectToRemoteServer(serverUrl, authProvider, headers, waitForAuthCode, skipBrowserAuth, useStreamableHttp)
|
||||||
|
|
||||||
// Set up bidirectional proxy between local and remote transports
|
// Set up bidirectional proxy between local and remote transports
|
||||||
mcpProxy({
|
mcpProxy({
|
||||||
|
@ -59,7 +59,7 @@ async function runProxy(serverUrl: string, callbackPort: number, headers: Record
|
||||||
// Start the local STDIO server
|
// Start the local STDIO server
|
||||||
await localTransport.start()
|
await localTransport.start()
|
||||||
log('Local STDIO server running')
|
log('Local STDIO server running')
|
||||||
log('Proxy established successfully between local STDIO and remote SSE')
|
log('Proxy established successfully between local STDIO and remote server')
|
||||||
log('Press Ctrl+C to exit')
|
log('Press Ctrl+C to exit')
|
||||||
|
|
||||||
// Setup cleanup handler
|
// Setup cleanup handler
|
||||||
|
@ -99,9 +99,9 @@ to the CA certificate file. If using claude_desktop_config.json, this might look
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse command-line arguments and run the proxy
|
// Parse command-line arguments and run the proxy
|
||||||
parseCommandLineArgs(process.argv.slice(2), 3334, 'Usage: npx tsx proxy.ts <https://server-url> [callback-port]')
|
parseCommandLineArgs(process.argv.slice(2), 3334, 'Usage: npx tsx proxy.ts <https://server-url> [callback-port] [--streamableHttp]')
|
||||||
.then(({ serverUrl, callbackPort, headers }) => {
|
.then(({ serverUrl, callbackPort, headers, useStreamableHttp }) => {
|
||||||
return runProxy(serverUrl, callbackPort, headers)
|
return runProxy(serverUrl, callbackPort, headers, useStreamableHttp)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
log('Fatal error:', error)
|
log('Fatal error:', error)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue