mcp-connector
Version:
MCP Remote Proxy Server for Streamable HTTP with OAuth Support.
163 lines • 7.17 kB
JavaScript
import { createServer } from "http";
import { EventEmitter } from "events";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import express from "express";
import { HTMLRenderer } from "../utils/html-renderer.js";
import { DefaultAuthProvider } from "../auth/auth-provider.js";
import { AUTH_CONSTANTS } from "../constants/constants.js";
export class OAuthProxyServer extends EventEmitter {
app;
server;
htmlRenderer;
options;
isRunning = false;
clientTransport;
shutdownTimeout = null;
constructor(options) {
super();
this.options = {
port: options.port || 54757,
timeout: 5 * 60 * 1000,
maxRetries: options.maxRetries || 3,
clientTransport: options.clientTransport,
logger: options.logger,
};
this.app = express();
this.htmlRenderer = new HTMLRenderer();
this.clientTransport =
options.clientTransport;
this.setupRoutes();
}
setupRoutes() {
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
this.app.use(express.static(join(__dirname, "../../public")));
this.app.get("/health", (req, res) => {
res.json({
status: "healthy",
uptime: process.uptime(),
activeStates: AUTH_CONSTANTS.globalStateStore.size,
});
});
this.app.get("/callback", async (req, res) => {
const logger = this.options.logger;
try {
const { code, state, error } = req.query;
logger.error("OAuth callback received ::: ", { code, state, error });
if (error) {
logger.error(`OAuth error: ${error}`);
res.send(this.htmlRenderer.renderErrorPage("Authentication Failed", `OAuth Error: ${error}`));
logger.error("Authentication failed with error : ", error);
await this.stop(logger);
return;
}
if (!state || !code) {
logger.error("Missing state or code parameter");
res.send(this.htmlRenderer.renderErrorPage("Authentication Failed", "Missing required parameters."));
logger.error("Authentication failed : mandatory parameters (code, state) is not present in request");
setTimeout(async () => {
await this.stop(logger);
}, 250000);
return;
}
// Load OAuth state from memory
const oauthState = DefaultAuthProvider.verifyOAuthState(state, logger);
if (!oauthState) {
if (AUTH_CONSTANTS.globalStateStoreCompleted.has(state)) {
logger.info("Trying to access already completed state. Hence returning.");
res.send(this.htmlRenderer.renderSuccessPage(AUTH_CONSTANTS.globalStateStoreCompleted.get(state)
?.url || "null", true));
await this.stop(logger);
return;
}
logger.error(`Invalid or expired state: ${state}`);
res.send(this.htmlRenderer.renderErrorPage("Authentication Failed", "Invalid or expired authentication state."));
await this.stop(logger);
await new Promise((resolve) => setTimeout(resolve, 5 * 1000)); // Close the server after 5 seconds
await this.stop(logger);
return;
}
logger.debug(`Processing OAuth callback for state: ${state}`);
const oauthStateValue = DefaultAuthProvider.getOAuthState(state);
await this.clientTransport?.finishAuth(code.toString());
if (oauthStateValue) {
AUTH_CONSTANTS.globalStateStoreCompleted.set(state, oauthStateValue);
}
AUTH_CONSTANTS.globalStateStore.delete(state);
this.emit("oauth-success", { url: oauthStateValue?.url });
res.send(this.htmlRenderer.renderSuccessPage(oauthStateValue?.url || "null"));
return;
}
catch (error) {
logger.error("Callback handler error:", error);
res.send(this.htmlRenderer.renderErrorPage("Internal Error", "An unexpected error occurred during authentication."));
this.resetShutdownTimer(logger);
await this.stop(logger);
return;
}
});
}
resetShutdownTimer(logger) {
if (this.shutdownTimeout) {
clearTimeout(this.shutdownTimeout);
this.shutdownTimeout = null;
}
this.shutdownTimeout = setTimeout(async () => {
logger.info("OAuth proxy server timeout reached (5 seconds after last callback). Stopping server.");
await this.stop(logger);
}, 5 * 1000); // 5 seconds
}
async ensureServerRunning(logger) {
if (this.isRunning) {
return;
}
return new Promise((resolve, reject) => {
this.server = createServer(this.app);
this.server.on("error", async (error) => {
if (error.code === "EADDRINUSE") {
logger.warn(`Port ${this.options.port} is in use, trying ${this.options.port + 1}...`);
const availablePort = await DefaultAuthProvider.getAuthAvailablePort();
this.options.port = availablePort;
this.server.listen(this.options.port);
}
else {
reject(error);
}
});
this.server.listen(this.options.port, () => {
this.isRunning = true;
logger.debug(`OAuth proxy server started on http://localhost:${this.options.port}`);
setTimeout(async () => {
logger.info("OAuth proxy server timeout reached (5 minutes). Stopping server.");
await this.stop(logger);
}, this.options.timeout);
resolve();
});
});
}
async stop(logger) {
if (this.server && this.isRunning) {
return new Promise((resolve) => {
this.server.close(() => {
AUTH_CONSTANTS.globalStateStoreCompleted.clear();
DefaultAuthProvider.oauthProxy = null;
this.isRunning = false;
logger.info("OAuth proxy server stopped");
resolve();
});
});
}
}
getActiveStateCount() {
return AUTH_CONSTANTS.globalStateStore.size;
}
getServerInfo() {
return {
isRunning: this.isRunning,
port: this.options.port,
states: AUTH_CONSTANTS.globalStateStore.size,
};
}
}
//# sourceMappingURL=oauth-proxy-server.js.map