diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 005fbc3..c58f197 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,12 +1,11 @@ name: Publish Any Commit on: - workflow_dispatch: pull_request: push: branches: - "**" tags: - - "v*" + - "!**" jobs: build: @@ -15,19 +14,14 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - - - name: Add git.kvant.cloud scope - run: npm config set @kvant:registry=https://git.kvant.cloud/api/packages/${{ github.repository_owner }}/npm/ - - - name: Login to git.kvant.cloud npm - run: npm config set -- '//git.kvant.cloud/api/packages/${{ github.repository_owner }}/npm/:_authToken' "${{ secrets.PHOENIX_PACKAGE_WRITER_TOKEN }}" - name: Setup pnpm & install - uses: https://github.com/wyvox/action-setup-pnpm@v3 + uses: wyvox/action-setup-pnpm@v3 with: node-version: 22 + pnpm-version: 10 - name: Build run: pnpm build - - run: pnpm dlx publish --compact --bin + - run: pnpm dlx pkg-pr-new publish --compact --bin diff --git a/README.md b/README.md index bc0a4a7..bc70b15 100644 --- a/README.md +++ b/README.md @@ -46,11 +46,11 @@ To bypass authentication, or to emit custom headers on all requests to your remo "https://remote.mcp.server/sse", "--header", "Authorization: Bearer ${AUTH_TOKEN}" - ], - "env": { - "AUTH_TOKEN": "..." - } + ] }, + "env": { + "AUTH_TOKEN": "..." + } } } ``` @@ -65,11 +65,11 @@ To bypass authentication, or to emit custom headers on all requests to your remo "https://remote.mcp.server/sse", "--header", "Authorization:${AUTH_HEADER}" // note no spaces around ':' - ], - "env": { - "AUTH_HEADER": "Bearer " // spaces OK in env vars - } + ] }, +"env": { + "AUTH_HEADER": "Bearer " // spaces OK in env vars +} ``` ### Flags diff --git a/package.json b/package.json index bf4892f..ba21bf7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "@kvant/mcp-remote", - "version": "0.1.5", + "name": "mcp-remote", + "version": "0.1.2", "description": "Remote proxy for Model Context Protocol, allowing local-only clients to connect to remote servers using oAuth", "keywords": [ "mcp", @@ -31,9 +31,8 @@ "express": "^4.21.2", "open": "^10.1.0" }, - "packageManager": "pnpm@10.11.0", "devDependencies": { - "@modelcontextprotocol/sdk": "^1.11.2", + "@modelcontextprotocol/sdk": "^1.10.2", "@types/express": "^5.0.0", "@types/node": "^22.13.10", "prettier": "^3.5.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d0720bf..a987cf4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,8 +16,8 @@ importers: version: 10.1.0 devDependencies: '@modelcontextprotocol/sdk': - specifier: ^1.11.2 - version: 1.11.2 + specifier: ^1.10.2 + version: 1.10.2 '@types/express': specifier: ^5.0.0 version: 5.0.0 @@ -211,8 +211,8 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@modelcontextprotocol/sdk@1.11.2': - resolution: {integrity: sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==} + '@modelcontextprotocol/sdk@1.10.2': + resolution: {integrity: sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==} engines: {node: '>=18'} '@pkgjs/parseargs@0.11.0': @@ -1206,7 +1206,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@modelcontextprotocol/sdk@1.11.2': + '@modelcontextprotocol/sdk@1.10.2': dependencies: content-type: 1.0.5 cors: 2.8.5 diff --git a/src/client.ts b/src/client.ts index d87599c..4e0c14c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -151,7 +151,7 @@ async function runClient( } // Parse command-line arguments and run the client -parseCommandLineArgs(process.argv.slice(2), 'Usage: npx tsx client.ts [callback-port]') +parseCommandLineArgs(process.argv.slice(2), 3333, 'Usage: npx tsx client.ts [callback-port]') .then(({ serverUrl, callbackPort, headers, transportStrategy }) => { return runClient(serverUrl, callbackPort, headers, transportStrategy) }) diff --git a/src/lib/node-oauth-client-provider.ts b/src/lib/node-oauth-client-provider.ts index 0826844..1e58b7e 100644 --- a/src/lib/node-oauth-client-provider.ts +++ b/src/lib/node-oauth-client-provider.ts @@ -1,8 +1,9 @@ import open from 'open' import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js' import { + OAuthClientInformation, OAuthClientInformationFull, - OAuthClientInformationFullSchema, + OAuthClientInformationSchema, OAuthTokens, OAuthTokensSchema, } from '@modelcontextprotocol/sdk/shared/auth.js' @@ -36,7 +37,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { } get redirectUrl(): string { - return `http://localhost:${this.options.callbackPort}${this.callbackPath}` + return `http://127.0.0.1:${this.options.callbackPort}${this.callbackPath}` } get clientMetadata() { @@ -56,9 +57,9 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { * Gets the client information if it exists * @returns The client information or undefined */ - async clientInformation(): Promise { + async clientInformation(): Promise { // log('Reading client info') - return readJsonFile(this.serverUrlHash, 'client_info.json', OAuthClientInformationFullSchema) + return readJsonFile(this.serverUrlHash, 'client_info.json', OAuthClientInformationSchema) } /** diff --git a/src/lib/utils.ts b/src/lib/utils.ts index a0a60dc..e86aaac 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -3,13 +3,6 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.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 export const REASON_AUTH_NEEDED = 'authentication-needed' @@ -17,6 +10,10 @@ export const REASON_TRANSPORT_FALLBACK = 'falling-back-to-alternate-transport' // Transport strategy types 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 export const MCP_REMOTE_VERSION = require('../../package.json').version @@ -355,27 +352,6 @@ export function setupOAuthCallbackServer(options: OAuthCallbackServerOptions) { return { server, authCode, waitForAuthCode } } -async function findExistingClientPort(serverUrlHash: string): Promise { - const clientInfo = await readJsonFile(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 * @param preferredPort Optional preferred port to try first @@ -409,10 +385,11 @@ export async function findAvailablePort(preferredPort?: number): Promise /** * Parses command line arguments for MCP clients and proxies * @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 * @returns A promise that resolves to an object with parsed serverUrl, callbackPort and headers */ -export async function parseCommandLineArgs(args: string[], usage: string) { +export async function parseCommandLineArgs(args: string[], defaultPort: number, usage: string) { // Process headers const headers: Record = {} let i = 0 @@ -462,28 +439,14 @@ export async function parseCommandLineArgs(args: string[], usage: string) { log(usage) 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 - const [existingClientPort, availablePort] = await Promise.all([findExistingClientPort(serverUrlHash), findAvailablePort(defaultPort)]) - let callbackPort: number + // Use the specified port, or find an available one + const callbackPort = specifiedPort || (await findAvailablePort(defaultPort)) if (specifiedPort) { - 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 + log(`Using specified callback port: ${callbackPort}`) } else { - log(`Using automatically selected callback port: ${availablePort}`) - callbackPort = availablePort + log(`Using automatically selected callback port: ${callbackPort}`) } if (Object.keys(headers).length > 0) { @@ -521,11 +484,6 @@ export function setupSignalHandlers(cleanup: () => Promise) { // Keep the process alive process.stdin.resume() - process.stdin.on('end', async () => { - log('\nShutting down...') - await cleanup() - process.exit(0) - }) } /** diff --git a/src/proxy.ts b/src/proxy.ts index 535bfe2..7263a95 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -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 -parseCommandLineArgs(process.argv.slice(2), 'Usage: npx tsx proxy.ts [callback-port]') +parseCommandLineArgs(process.argv.slice(2), 3334, 'Usage: npx tsx proxy.ts [callback-port]') .then(({ serverUrl, callbackPort, headers, transportStrategy }) => { return runProxy(serverUrl, callbackPort, headers, transportStrategy) })