diff --git a/src/client.ts b/src/client.ts index 192c178..48d6ea4 100644 --- a/src/client.ts +++ b/src/client.ts @@ -14,7 +14,8 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' import { ListResourcesResultSchema, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js' import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js' -import { NodeOAuthClientProvider, setupOAuthCallbackServer, parseCommandLineArgs, setupSignalHandlers } from './shared' +import { NodeOAuthClientProvider } from './lib/node-oauth-client-provider' +import { parseCommandLineArgs, setupOAuthCallbackServer, setupSignalHandlers } from './lib/utils' /** * Main function to run the client diff --git a/src/shared.ts b/src/lib/node-oauth-client-provider.ts similarity index 60% rename from src/shared.ts rename to src/lib/node-oauth-client-provider.ts index 91a8e80..920b8e5 100644 --- a/src/shared.ts +++ b/src/lib/node-oauth-client-provider.ts @@ -1,15 +1,8 @@ -/** - * Shared utilities for MCP OAuth clients and proxies. - * Contains common functionality for authentication, file storage, and proxying. - */ - -import express from 'express' -import open from 'open' -import fs from 'fs/promises' +import crypto from 'crypto' import path from 'path' import os from 'os' -import crypto from 'crypto' -import net from 'net' +import fs from 'fs/promises' +import open from 'open' import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js' import { OAuthClientInformation, @@ -18,7 +11,7 @@ import { OAuthTokens, OAuthTokensSchema, } from '@modelcontextprotocol/sdk/shared/auth.js' -import { OAuthCallbackServerOptions, OAuthProviderOptions } from './lib/types' +import type { OAuthProviderOptions } from './types' /** * Implements the OAuthClientProvider interface for Node.js environments. @@ -204,131 +197,3 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { return await this.readTextFile('code_verifier.txt') } } - -/** - * Sets up an Express server to handle OAuth callbacks - * @param options The server options - * @returns An object with the server, authCode, and waitForAuthCode function - */ -export function setupOAuthCallbackServer(options: OAuthCallbackServerOptions) { - let authCode: string | null = null - const app = express() - - app.get(options.path, (req, res) => { - const code = req.query.code as string | undefined - if (!code) { - res.status(400).send('Error: No authorization code received') - return - } - - authCode = code - res.send('Authorization successful! You may close this window and return to the CLI.') - - // Notify main flow that auth code is available - options.events.emit('auth-code-received', code) - }) - - const server = app.listen(options.port, () => { - console.error(`OAuth callback server running at http://127.0.0.1:${options.port}`) - }) - - /** - * Waits for the OAuth authorization code - * @returns A promise that resolves with the authorization code - */ - const waitForAuthCode = (): Promise => { - return new Promise((resolve) => { - if (authCode) { - resolve(authCode) - return - } - - options.events.once('auth-code-received', (code) => { - resolve(code) - }) - }) - } - - return { server, authCode, waitForAuthCode } -} - -/** - * Finds an available port on the local machine - * @param preferredPort Optional preferred port to try first - * @returns A promise that resolves to an available port number - */ -export async function findAvailablePort(preferredPort?: number): Promise { - return new Promise((resolve, reject) => { - const server = net.createServer() - - server.on('error', (err: NodeJS.ErrnoException) => { - if (err.code === 'EADDRINUSE') { - // If preferred port is in use, get a random port - server.listen(0) - } else { - reject(err) - } - }) - - server.on('listening', () => { - const { port } = server.address() as net.AddressInfo - server.close(() => { - resolve(port) - }) - }) - - // Try preferred port first, or get a random port - server.listen(preferredPort || 0) - }) -} - -/** - * 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 and callbackPort - */ -export async function parseCommandLineArgs(args: string[], defaultPort: number, usage: string) { - const serverUrl = args[0] - const specifiedPort = args[1] ? parseInt(args[1]) : undefined - - if (!serverUrl) { - console.error(usage) - process.exit(1) - } - - const url = new URL(serverUrl) - const isLocalhost = (url.hostname === 'localhost' || url.hostname === '127.0.0.1') && url.protocol === 'http:' - - if (!(url.protocol == 'https:' || isLocalhost)) { - console.error(usage) - process.exit(1) - } - - // Use the specified port, or find an available one - const callbackPort = specifiedPort || (await findAvailablePort(defaultPort)) - - if (specifiedPort) { - console.error(`Using specified callback port: ${callbackPort}`) - } else { - console.error(`Using automatically selected callback port: ${callbackPort}`) - } - - return { serverUrl, callbackPort } -} - -/** - * Sets up signal handlers for graceful shutdown - * @param cleanup Cleanup function to run on shutdown - */ -export function setupSignalHandlers(cleanup: () => Promise) { - process.on('SIGINT', async () => { - console.error('\nShutting down...') - await cleanup() - process.exit(0) - }) - - // Keep the process alive - process.stdin.resume() -} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 2b09f57..33e8685 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,9 @@ import { OAuthClientProvider, UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js' import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js' +import { OAuthCallbackServerOptions } from './types' +import express from 'express' +import net from 'net' const pid = process.pid @@ -99,3 +102,131 @@ export async function connectToRemoteServer( } } } + +/** + * Sets up an Express server to handle OAuth callbacks + * @param options The server options + * @returns An object with the server, authCode, and waitForAuthCode function + */ +export function setupOAuthCallbackServer(options: OAuthCallbackServerOptions) { + let authCode: string | null = null + const app = express() + + app.get(options.path, (req, res) => { + const code = req.query.code as string | undefined + if (!code) { + res.status(400).send('Error: No authorization code received') + return + } + + authCode = code + res.send('Authorization successful! You may close this window and return to the CLI.') + + // Notify main flow that auth code is available + options.events.emit('auth-code-received', code) + }) + + const server = app.listen(options.port, () => { + console.error(`OAuth callback server running at http://127.0.0.1:${options.port}`) + }) + + /** + * Waits for the OAuth authorization code + * @returns A promise that resolves with the authorization code + */ + const waitForAuthCode = (): Promise => { + return new Promise((resolve) => { + if (authCode) { + resolve(authCode) + return + } + + options.events.once('auth-code-received', (code) => { + resolve(code) + }) + }) + } + + return { server, authCode, waitForAuthCode } +} + +/** + * Finds an available port on the local machine + * @param preferredPort Optional preferred port to try first + * @returns A promise that resolves to an available port number + */ +export async function findAvailablePort(preferredPort?: number): Promise { + return new Promise((resolve, reject) => { + const server = net.createServer() + + server.on('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EADDRINUSE') { + // If preferred port is in use, get a random port + server.listen(0) + } else { + reject(err) + } + }) + + server.on('listening', () => { + const { port } = server.address() as net.AddressInfo + server.close(() => { + resolve(port) + }) + }) + + // Try preferred port first, or get a random port + server.listen(preferredPort || 0) + }) +} + +/** + * 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 and callbackPort + */ +export async function parseCommandLineArgs(args: string[], defaultPort: number, usage: string) { + const serverUrl = args[0] + const specifiedPort = args[1] ? parseInt(args[1]) : undefined + + if (!serverUrl) { + console.error(usage) + process.exit(1) + } + + const url = new URL(serverUrl) + const isLocalhost = (url.hostname === 'localhost' || url.hostname === '127.0.0.1') && url.protocol === 'http:' + + if (!(url.protocol == 'https:' || isLocalhost)) { + console.error(usage) + process.exit(1) + } + + // Use the specified port, or find an available one + const callbackPort = specifiedPort || (await findAvailablePort(defaultPort)) + + if (specifiedPort) { + console.error(`Using specified callback port: ${callbackPort}`) + } else { + console.error(`Using automatically selected callback port: ${callbackPort}`) + } + + return { serverUrl, callbackPort } +} + +/** + * Sets up signal handlers for graceful shutdown + * @param cleanup Cleanup function to run on shutdown + */ +export function setupSignalHandlers(cleanup: () => Promise) { + process.on('SIGINT', async () => { + console.error('\nShutting down...') + await cleanup() + process.exit(0) + }) + + // Keep the process alive + process.stdin.resume() +} diff --git a/src/proxy.ts b/src/proxy.ts index d2b7e4a..23eaafe 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -11,8 +11,8 @@ import { EventEmitter } from 'events' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' -import { NodeOAuthClientProvider, setupOAuthCallbackServer, parseCommandLineArgs, setupSignalHandlers } from './shared' -import { connectToRemoteServer, mcpProxy } from './lib/utils' +import { connectToRemoteServer, mcpProxy, parseCommandLineArgs, setupOAuthCallbackServer, setupSignalHandlers } from './lib/utils' +import { NodeOAuthClientProvider } from './lib/node-oauth-client-provider' /** * Main function to run the proxy