UNPKG

mcp-connector

Version:

MCP Remote Proxy Server for Streamable HTTP with OAuth Support.

185 lines 8.13 kB
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"; import { isJSONRPCRequest, isJSONRPCResponse, isJSONRPCError, isJSONRPCNotification, } from "@modelcontextprotocol/sdk/types.js"; import { TokenManager } from "../auth/token-manager.js"; import { hasKeyInJson } from "../utils/cli-util.js"; import { AUTH_CONSTANTS, ROOT_CONFIG } from "../constants/constants.js"; export class McpProxy { transportToClient; transportToServer; logger; url; authProvider; transportToClientClosed = false; transportToServerClosed = false; currentRetry = 0; // For 401 while connecting to server initially currentRefreshRetry = 0; // For 401 while connecting to server via refresh token initially stalledMsgByUnAuth = null; constructor({ transportToClient, transportToServer, logger, url, authProvider, }) { this.transportToClient = transportToClient; this.transportToServer = transportToServer; this.logger = logger; this.url = url; this.authProvider = authProvider; this.setupListeners(); } // --- Getters --- get clientTransport() { return this.transportToClient; } get serverTransport() { return this.transportToServer; } get log() { return this.logger; } get proxyUrl() { return this.url; } get auth() { return this.authProvider; } get isClientClosed() { return this.transportToClientClosed; } get isServerClosed() { return this.transportToServerClosed; } get retry() { return this.currentRetry; } get unauthMsg() { return this.stalledMsgByUnAuth; } get refreshRetry() { return this.currentRefreshRetry; } // --- Setters --- set retry(value) { this.currentRetry = value; } set unauthMsg(msg) { this.stalledMsgByUnAuth = msg; } set refreshRetry(value) { this.currentRefreshRetry = value; } setupListeners() { this.transportToClient.onmessage = (_message) => { const message = _message; this.logger.info(`[Client → Server] Method: ${message.method || "unknown"} | ID: ${message.id ?? "none"}`); this.logger.debug(`[Client → Server] Full Message:`, { method: message.method, id: message.id, params: message.params ? JSON.stringify(message.params).substring(0, 500) : undefined, }); if (message.method === "initialize" && message.params) { const initMsg = message; const { clientInfo } = initMsg.params; if (clientInfo) clientInfo.name = `${clientInfo.name} (via mcp-connector)`; this.logger.info(`[Client → Server] Initialization: ${JSON.stringify(message, null, 2)}`); this.logger.debug(`[Client → Server] Modified Client Info:`, { clientInfo, }); } this.sendServerTransportMessage(message); }; this.transportToServer.onmessage = (_message) => { const message = _message; this.logger.debug(`[Server → Client] :: Message :: ${JSON.stringify(message)}`); if (isJSONRPCRequest(message)) { const request = message; this.logger.info(`[Server → Client] : JSONRPCRequest → Method: ${request.method || "unknown"} | ID: ${message.id ?? "none"}`); } if (isJSONRPCResponse(message)) { const response = message; this.logger.info(`[Server → Client] : JSONRPCResponse → Result: ${response.result ? "present" : "absent"} | ID: ${response.id ?? "none"}`); } if (isJSONRPCError(message)) { const error = message; this.logger.info(`[Server → Client] : JSONRPCError → Error: ${error.error ? JSON.stringify(error.error) : "none"} | ID: ${error.id ?? "none"}`); } if (isJSONRPCNotification(message)) { const notitfication = message; this.logger.info(`[Server → Client] : JSONRPCNotification → Method: ${notitfication.method || "unknown"}`); } this.transportToClient .send(message) .catch((error) => this.onClientError(error)); }; this.transportToClient.onclose = () => { AUTH_CONSTANTS.proxyInstances.delete(TokenManager.hashUrl(this.url)); this.unauthMsg = null; if (this.transportToServerClosed) { return; } this.transportToClientClosed = true; this.logger.debug("[Transport] Local transport closed, closing remote transport"); this.transportToServer .close() .catch((error) => this.onServerError(error)); }; this.transportToServer.onclose = () => { AUTH_CONSTANTS.proxyInstances.delete(TokenManager.hashUrl(this.url)); this.unauthMsg = null; if (this.transportToClientClosed) { return; } this.transportToServerClosed = true; this.logger.debug("[Transport] Remote transport closed, closing local transport"); this.transportToClient .close() .catch((error) => this.onClientError(error)); }; this.transportToClient.onerror = (error) => this.onClientError(error); this.transportToServer.onerror = (error) => this.onServerError(error); } onClientError(error) { this.logger.error(`[Client Error] Local client transport: ${error.message}`); this.logger.debug("[Client Error] Local client transport stack:", { stack: error.stack, }); } onServerError(error) { this.logger.error(`[Server Error] Remote server transport: ${error.message}`); this.logger.debug("[Server Error] Remote server transport stack:", { stack: error.stack, }); } sendServerTransportMessage(message) { this.transportToServer .send(message) .then(() => ((this.retry = 0), (this.refreshRetry = 0))) .catch((error) => { if (error instanceof UnauthorizedError || (error instanceof Error && error.message.includes("Unauthorized"))) { if (hasKeyInJson(ROOT_CONFIG.headers, "Authorization")) { this.logger.error("[Client → Server] Received a 401 from the server because the cli headers contain a static Authorization header. Skipping the OAuth mechanism and closing the connection."); process.exit(1); } if (this.retry++ < ROOT_CONFIG.unauthRetries) { this.logger.warn(`[Client → Server] UnAuthorized error :: Retry attempt ${this.retry} of ${ROOT_CONFIG.unauthRetries}.`); } this.logger.debug("[Client → Server] UnAuthorized error :: Storing init msg in map to retry after successful auth."); this.unauthMsg = message; } else { this.logger.info("IN SERVER ERROR"); this.onServerError(error); } }); } validateRefTokenUnauth(tokenManager, url) { if (this.refreshRetry > 0) { // If refresh token present in already saved token, it'll go on for infinite recursion in MCP package. Hence we will remove the token file to restart auth. // Like bro who tf designs an open source project like this. this.logger.oauth("[Client → Server] UnAuthorized error :: New access token generated using refresh token throwing 401. Hence we will be removing token file and reauthorizing the server."); tokenManager.removeToken(url); this.refreshRetry = 0; } this.refreshRetry++; } } //# sourceMappingURL=mcp-proxy.js.map