Merge branch 'main' into fmenezes/add-client-id-arg
This commit is contained in:
commit
b793e01d83
7 changed files with 3569 additions and 23 deletions
3509
package-lock.json
generated
Normal file
3509
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "mcp-remote",
|
"name": "mcp-remote",
|
||||||
"version": "0.1.4",
|
"version": "0.1.5",
|
||||||
"description": "Remote proxy for Model Context Protocol, allowing local-only clients to connect to remote servers using oAuth",
|
"description": "Remote proxy for Model Context Protocol, allowing local-only clients to connect to remote servers using oAuth",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"mcp",
|
"mcp",
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
"open": "^10.1.0"
|
"open": "^10.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.10.2",
|
"@modelcontextprotocol/sdk": "^1.11.2",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/node": "^22.13.10",
|
"@types/node": "^22.13.10",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
|
|
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
|
@ -16,8 +16,8 @@ importers:
|
||||||
version: 10.1.0
|
version: 10.1.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@modelcontextprotocol/sdk':
|
'@modelcontextprotocol/sdk':
|
||||||
specifier: ^1.10.2
|
specifier: ^1.11.2
|
||||||
version: 1.10.2
|
version: 1.11.2
|
||||||
'@types/express':
|
'@types/express':
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.0.0
|
version: 5.0.0
|
||||||
|
@ -211,8 +211,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.10.2':
|
'@modelcontextprotocol/sdk@1.11.2':
|
||||||
resolution: {integrity: sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==}
|
resolution: {integrity: sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
|
@ -1206,7 +1206,7 @@ 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.10.2':
|
'@modelcontextprotocol/sdk@1.11.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
cors: 2.8.5
|
cors: 2.8.5
|
||||||
|
|
|
@ -151,7 +151,7 @@ async function runClient(
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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), 'Usage: npx tsx client.ts <https://server-url> [callback-port]')
|
||||||
.then(({ serverUrl, callbackPort, headers, transportStrategy }) => {
|
.then(({ serverUrl, callbackPort, headers, transportStrategy }) => {
|
||||||
return runClient(serverUrl, callbackPort, headers, transportStrategy)
|
return runClient(serverUrl, callbackPort, headers, transportStrategy)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import open from 'open'
|
import open from 'open'
|
||||||
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'
|
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'
|
||||||
import {
|
import {
|
||||||
OAuthClientInformation,
|
|
||||||
OAuthClientInformationFull,
|
OAuthClientInformationFull,
|
||||||
OAuthClientInformationSchema,
|
OAuthClientInformationFullSchema,
|
||||||
OAuthTokens,
|
OAuthTokens,
|
||||||
OAuthTokensSchema,
|
OAuthTokensSchema,
|
||||||
} from '@modelcontextprotocol/sdk/shared/auth.js'
|
} from '@modelcontextprotocol/sdk/shared/auth.js'
|
||||||
|
@ -63,15 +62,16 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
|
||||||
* Gets the client information if it exists
|
* Gets the client information if it exists
|
||||||
* @returns The client information or undefined
|
* @returns The client information or undefined
|
||||||
*/
|
*/
|
||||||
async clientInformation(): Promise<OAuthClientInformation | undefined> {
|
async clientInformation(): Promise<OAuthClientInformationFull | undefined> {
|
||||||
// log('Reading client info')
|
// log('Reading client info')
|
||||||
if (this.clientId) {
|
if (this.clientId) {
|
||||||
return {
|
return {
|
||||||
client_id: this.clientId,
|
client_id: this.clientId,
|
||||||
client_secret: this.clientSecret,
|
client_secret: this.clientSecret,
|
||||||
|
redirect_uris: [this.redirectUrl],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return readJsonFile<OAuthClientInformation>(this.serverUrlHash, 'client_info.json', OAuthClientInformationSchema)
|
return readJsonFile<OAuthClientInformationFull>(this.serverUrlHash, 'client_info.json', OAuthClientInformationFullSchema)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -3,6 +3,13 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
||||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
||||||
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
|
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
|
||||||
|
import { OAuthClientInformationFull, OAuthClientInformationFullSchema } from '@modelcontextprotocol/sdk/shared/auth.js'
|
||||||
|
import { OAuthCallbackServerOptions } from './types'
|
||||||
|
import { getConfigFilePath, readJsonFile } from './mcp-auth-config'
|
||||||
|
import express from 'express'
|
||||||
|
import net from 'net'
|
||||||
|
import crypto from 'crypto'
|
||||||
|
import fs from 'fs/promises'
|
||||||
|
|
||||||
// Connection constants
|
// Connection constants
|
||||||
export const REASON_AUTH_NEEDED = 'authentication-needed'
|
export const REASON_AUTH_NEEDED = 'authentication-needed'
|
||||||
|
@ -10,10 +17,6 @@ export const REASON_TRANSPORT_FALLBACK = 'falling-back-to-alternate-transport'
|
||||||
|
|
||||||
// Transport strategy types
|
// Transport strategy types
|
||||||
export type TransportStrategy = 'sse-only' | 'http-only' | 'sse-first' | 'http-first'
|
export type TransportStrategy = 'sse-only' | 'http-only' | 'sse-first' | 'http-first'
|
||||||
import { OAuthCallbackServerOptions } from './types'
|
|
||||||
import express from 'express'
|
|
||||||
import net from 'net'
|
|
||||||
import crypto from 'crypto'
|
|
||||||
|
|
||||||
// Package version from package.json
|
// Package version from package.json
|
||||||
export const MCP_REMOTE_VERSION = require('../../package.json').version
|
export const MCP_REMOTE_VERSION = require('../../package.json').version
|
||||||
|
@ -352,6 +355,27 @@ export function setupOAuthCallbackServer(options: OAuthCallbackServerOptions) {
|
||||||
return { server, authCode, waitForAuthCode }
|
return { server, authCode, waitForAuthCode }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function findExistingClientPort(serverUrlHash: string): Promise<number | undefined> {
|
||||||
|
const clientInfo = await readJsonFile<OAuthClientInformationFull>(serverUrlHash, 'client_info.json', OAuthClientInformationFullSchema)
|
||||||
|
if (!clientInfo) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const localhostRedirectUri = clientInfo.redirect_uris.map((uri) => new URL(uri)).find(({ hostname }) => hostname === 'localhost')
|
||||||
|
if (!localhostRedirectUri) {
|
||||||
|
throw new Error('Cannot find localhost callback URI from existing client information')
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseInt(localhostRedirectUri.port)
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateDefaultPort(serverUrlHash: string): number {
|
||||||
|
// Convert the first 4 bytes of the serverUrlHash into a port offset
|
||||||
|
const offset = parseInt(serverUrlHash.substring(0, 4), 16)
|
||||||
|
// Pick a consistent but random-seeming port from 3335 to 49151
|
||||||
|
return 3335 + (offset % 45816)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds an available port on the local machine
|
* Finds an available port on the local machine
|
||||||
* @param preferredPort Optional preferred port to try first
|
* @param preferredPort Optional preferred port to try first
|
||||||
|
@ -385,11 +409,10 @@ export async function findAvailablePort(preferredPort?: number): Promise<number>
|
||||||
/**
|
/**
|
||||||
* Parses command line arguments for MCP clients and proxies
|
* Parses command line arguments for MCP clients and proxies
|
||||||
* @param args Command line arguments
|
* @param args Command line arguments
|
||||||
* @param defaultPort Default port for the callback server if specified port is unavailable
|
|
||||||
* @param usage Usage message to show on error
|
* @param usage Usage message to show on error
|
||||||
* @returns A promise that resolves to an object with parsed serverUrl, callbackPort and headers
|
* @returns A promise that resolves to an object with parsed serverUrl, callbackPort and headers
|
||||||
*/
|
*/
|
||||||
export async function parseCommandLineArgs(args: string[], defaultPort: number, usage: string) {
|
export async function parseCommandLineArgs(args: string[], usage: string) {
|
||||||
// Process headers
|
// Process headers
|
||||||
const headers: Record<string, string> = {}
|
const headers: Record<string, string> = {}
|
||||||
let clientId: string | undefined = undefined
|
let clientId: string | undefined = undefined
|
||||||
|
@ -479,14 +502,28 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number,
|
||||||
log(usage)
|
log(usage)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
const serverUrlHash = getServerUrlHash(serverUrl)
|
||||||
|
const defaultPort = calculateDefaultPort(serverUrlHash)
|
||||||
|
|
||||||
// Use the specified port, or find an available one
|
// Use the specified port, or the existing client port or fallback to find an available one
|
||||||
const callbackPort = specifiedPort || (await findAvailablePort(defaultPort))
|
const [existingClientPort, availablePort] = await Promise.all([findExistingClientPort(serverUrlHash), findAvailablePort(defaultPort)])
|
||||||
|
let callbackPort: number
|
||||||
|
|
||||||
if (specifiedPort) {
|
if (specifiedPort) {
|
||||||
log(`Using specified callback port: ${callbackPort}`)
|
if (existingClientPort && specifiedPort !== existingClientPort) {
|
||||||
|
log(
|
||||||
|
`Warning! Specified callback port of ${specifiedPort}, which conflicts with existing client registration port ${existingClientPort}. Deleting existing client data to force reregistration.`,
|
||||||
|
)
|
||||||
|
await fs.rm(getConfigFilePath(serverUrlHash, 'client_info.json'))
|
||||||
|
}
|
||||||
|
log(`Using specified callback port: ${specifiedPort}`)
|
||||||
|
callbackPort = specifiedPort
|
||||||
|
} else if (existingClientPort) {
|
||||||
|
log(`Using existing client port: ${existingClientPort}`)
|
||||||
|
callbackPort = existingClientPort
|
||||||
} else {
|
} else {
|
||||||
log(`Using automatically selected callback port: ${callbackPort}`)
|
log(`Using automatically selected callback port: ${availablePort}`)
|
||||||
|
callbackPort = availablePort
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(headers).length > 0) {
|
if (Object.keys(headers).length > 0) {
|
||||||
|
|
|
@ -143,7 +143,7 @@ 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), 'Usage: npx tsx proxy.ts <https://server-url> [callback-port]')
|
||||||
.then(({ serverUrl, callbackPort, headers, transportStrategy, callbackPath, clientId, clientSecret, scope }) => {
|
.then(({ serverUrl, callbackPort, headers, transportStrategy, callbackPath, clientId, clientSecret, scope }) => {
|
||||||
return runProxy(serverUrl, callbackPort, headers, transportStrategy, callbackPath, clientId, clientSecret, scope)
|
return runProxy(serverUrl, callbackPort, headers, transportStrategy, callbackPath, clientId, clientSecret, scope)
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue