Picking a default port based on the server hash

This commit is contained in:
Glen Maddern 2025-05-14 21:21:38 +10:00 committed by Glen Maddern
parent 6f2399bbfb
commit b1dfa9fe5b
3 changed files with 21 additions and 14 deletions

View file

@ -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)
}) })

View file

@ -4,6 +4,12 @@ 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 { 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'
@ -11,12 +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 { getConfigFilePath, readJsonFile } from './mcp-auth-config'
import express from 'express'
import net from 'net'
import crypto from 'crypto'
import fs from 'fs/promises'
// 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
@ -355,8 +355,7 @@ export function setupOAuthCallbackServer(options: OAuthCallbackServerOptions) {
return { server, authCode, waitForAuthCode } return { server, authCode, waitForAuthCode }
} }
async function findExistingClientPort(serverUrl: string): Promise<number | undefined> { async function findExistingClientPort(serverUrlHash: string): Promise<number | undefined> {
const serverUrlHash = getServerUrlHash(serverUrl)
const clientInfo = await readJsonFile<OAuthClientInformationFull>(serverUrlHash, 'client_info.json', OAuthClientInformationFullSchema) const clientInfo = await readJsonFile<OAuthClientInformationFull>(serverUrlHash, 'client_info.json', OAuthClientInformationFullSchema)
if (!clientInfo) { if (!clientInfo) {
return undefined return undefined
@ -370,6 +369,13 @@ async function findExistingClientPort(serverUrl: string): Promise<number | undef
return parseInt(localhostRedirectUri.port) 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
@ -403,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 i = 0 let i = 0
@ -457,9 +462,11 @@ 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 the existing client port or fallback to find an available one // Use the specified port, or the existing client port or fallback to find an available one
const [existingClientPort, availablePort] = await Promise.all([findExistingClientPort(serverUrl), findAvailablePort(defaultPort)]) const [existingClientPort, availablePort] = await Promise.all([findExistingClientPort(serverUrlHash), findAvailablePort(defaultPort)])
let callbackPort: number let callbackPort: number
if (specifiedPort) { if (specifiedPort) {
@ -467,7 +474,7 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number,
log( log(
`Warning! Specified callback port of ${specifiedPort}, which conflicts with existing client registration port ${existingClientPort}. Deleting existing client data to force reregistration.`, `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(getServerUrlHash(serverUrl), 'client_info.json')) await fs.rm(getConfigFilePath(serverUrlHash, 'client_info.json'))
} }
log(`Using specified callback port: ${specifiedPort}`) log(`Using specified callback port: ${specifiedPort}`)
callbackPort = specifiedPort callbackPort = specifiedPort

View file

@ -135,7 +135,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 }) => { .then(({ serverUrl, callbackPort, headers, transportStrategy }) => {
return runProxy(serverUrl, callbackPort, headers, transportStrategy) return runProxy(serverUrl, callbackPort, headers, transportStrategy)
}) })