@rhofkens/mcp-quotes-server
Version:
A Model Context Protocol (MCP) server that provides quotes based on user requests
500 lines • 19.2 kB
JavaScript
import express from "express";
import cors from "cors";
import https from "https";
import fs from "fs";
import { randomUUID } from "crypto";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import { logger } from "../utils/logger.js";
import { HttpTransportErrorType, } from "../types/http-transport-types.js";
export class HttpTransportService {
constructor(config, mcpServerFactory, options = {}) {
this.sessions = new Map();
this.startTime = new Date();
this.totalSessionsCreated = 0;
this.totalSessionsTerminated = 0;
this.isRunning = false;
this.config = config;
this.mcpServerFactory = mcpServerFactory;
this.options = {
jsonLimit: "50mb",
enableLogging: true,
sessionTimeout: 30 * 60 * 1000,
...options,
};
this.validateConfig();
this.app = this.createExpressApp();
this.setupRoutes();
this.setupSessionCleanup();
logger.info("HTTP Transport Service initialized", {
host: config.host,
port: config.port,
httpsEnabled: config.https.enabled,
allowedHosts: config.security.allowedHosts,
allowedOrigins: config.security.allowedOrigins,
});
}
validateConfig() {
if (!this.config.port || this.config.port < 1 || this.config.port > 65535) {
throw new Error("Invalid HTTP server configuration: port must be between 1 and 65535");
}
if (!this.config.host) {
throw new Error("Invalid HTTP server configuration: host is required");
}
if (this.config.https.enabled) {
if (!this.config.https.certPath || !this.config.https.keyPath) {
throw new Error("Invalid HTTP server configuration: HTTPS requires certPath and keyPath");
}
this.validateHttpsCertificates();
}
}
createExpressApp() {
const app = express();
app.use(express.json({ limit: this.options.jsonLimit }));
app.use(this.createDnsRebindingProtection());
const corsOptions = this.createCorsOptions();
app.use(cors(corsOptions));
if (this.options.enableLogging) {
app.use(this.createRequestLoggingMiddleware());
}
app.use(this.createGlobalErrorHandler());
return app;
}
createCorsOptions() {
const { allowedOrigins } = this.config.security;
return {
origin: allowedOrigins.includes("*") ? true : allowedOrigins,
exposedHeaders: ["mcp-session-id"],
allowedHeaders: ["Content-Type", "mcp-session-id"],
credentials: true,
};
}
createDnsRebindingProtection() {
const { allowedHosts } = this.config.security;
return (req, res, next) => {
const host = req.get("Host");
if (!host) {
logger.warn("Request rejected: Missing Host header");
res.status(400).json({
error: "Bad Request",
message: "Host header is required",
});
return;
}
const hostname = host.split(":")[0];
if (hostname &&
!allowedHosts.includes(hostname) &&
!allowedHosts.includes("*")) {
logger.warn("Request rejected: Invalid host", {
host: hostname,
allowedHosts,
});
res.status(403).json({
error: "Forbidden",
message: "Host not allowed",
});
return;
}
next();
};
}
validateHttpsCertificates() {
if (!this.config.https.enabled)
return;
const { certPath, keyPath } = this.config.https;
try {
if (!fs.existsSync(certPath)) {
throw new Error(`HTTPS certificate file not found: ${certPath}`);
}
if (!fs.existsSync(keyPath)) {
throw new Error(`HTTPS private key file not found: ${keyPath}`);
}
const cert = fs.readFileSync(certPath, "utf8");
const key = fs.readFileSync(keyPath, "utf8");
if (!cert.includes("-----BEGIN CERTIFICATE-----")) {
throw new Error(`Invalid certificate file format: ${certPath}`);
}
if (!key.includes("-----BEGIN") || !key.includes("PRIVATE KEY-----")) {
throw new Error(`Invalid private key file format: ${keyPath}`);
}
logger.info("HTTPS certificates validated successfully");
}
catch (error) {
logger.error("HTTPS certificate validation failed", { error });
throw error;
}
}
createRequestLoggingMiddleware() {
return (req, res, next) => {
const startTime = Date.now();
const requestId = randomUUID();
res.setHeader("x-request-id", requestId);
logger.info("HTTP request started", {
requestId,
method: req.method,
url: req.url,
userAgent: req.get("User-Agent"),
sessionId: req.headers["mcp-session-id"],
ip: req.ip,
});
res.on("finish", () => {
const duration = Date.now() - startTime;
logger.info("HTTP request completed", {
requestId,
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration,
sessionId: req.headers["mcp-session-id"],
});
});
next();
};
}
createGlobalErrorHandler() {
return (error, req, res, next) => {
const requestId = res.get("x-request-id") || "unknown";
logger.error("Unhandled HTTP error", {
requestId,
error: error.message,
stack: error.stack,
method: req.method,
url: req.url,
sessionId: req.headers["mcp-session-id"],
});
if (res.headersSent) {
return next(error);
}
this.sendError(res, {
type: HttpTransportErrorType.TRANSPORT_INIT_ERROR,
statusCode: 500,
message: "Internal Server Error",
context: {
requestId,
error: error.message,
},
});
};
}
setupRoutes() {
this.app.post("/mcp", this.handleMcpPost.bind(this));
this.app.get("/mcp", this.handleMcpGet.bind(this));
this.app.delete("/mcp", this.handleMcpDelete.bind(this));
this.app.get("/health", this.handleHealthCheck.bind(this));
this.app.get("/sessions", this.handleSessionsEndpoint.bind(this));
}
async handleMcpPost(req, res) {
try {
const sessionId = req.headers["mcp-session-id"];
let transport;
if (sessionId && this.sessions.has(sessionId)) {
const session = this.sessions.get(sessionId);
transport = session.transport;
session.lastActivity = new Date();
logger.debug("Reusing existing session", { sessionId });
}
else if (!sessionId && isInitializeRequest(req.body)) {
transport = await this.createNewSession();
logger.debug("Created new session for initialization", {
sessionId: transport.sessionId,
});
}
else {
this.sendError(res, {
type: HttpTransportErrorType.SESSION_NOT_FOUND,
statusCode: 400,
message: "Bad Request: No valid session ID provided",
});
return;
}
await transport.handleRequest(req, res, req.body);
}
catch (error) {
logger.error("Error handling MCP POST request", { error });
this.sendError(res, {
type: HttpTransportErrorType.TRANSPORT_INIT_ERROR,
statusCode: 500,
message: "Internal server error",
context: {
error: error instanceof Error ? error.message : String(error),
},
});
}
}
async handleMcpGet(req, res) {
try {
const sessionId = req.headers["mcp-session-id"];
if (!sessionId || !this.sessions.has(sessionId)) {
this.sendError(res, {
type: HttpTransportErrorType.SESSION_NOT_FOUND,
statusCode: 400,
message: "Invalid or missing session ID",
});
return;
}
const session = this.sessions.get(sessionId);
session.lastActivity = new Date();
await session.transport.handleRequest(req, res);
logger.debug("Handled SSE request", { sessionId });
}
catch (error) {
logger.error("Error handling MCP GET request", { error });
this.sendError(res, {
type: HttpTransportErrorType.TRANSPORT_INIT_ERROR,
statusCode: 500,
message: "Internal server error",
context: {
error: error instanceof Error ? error.message : String(error),
},
});
}
}
async handleMcpDelete(req, res) {
try {
const sessionId = req.headers["mcp-session-id"];
if (!sessionId || !this.sessions.has(sessionId)) {
this.sendError(res, {
type: HttpTransportErrorType.SESSION_NOT_FOUND,
statusCode: 400,
message: "Invalid or missing session ID",
});
return;
}
const session = this.sessions.get(sessionId);
await session.transport.handleRequest(req, res);
this.terminateSession(sessionId);
logger.info("Session terminated", { sessionId });
}
catch (error) {
logger.error("Error handling MCP DELETE request", { error });
this.sendError(res, {
type: HttpTransportErrorType.TRANSPORT_INIT_ERROR,
statusCode: 500,
message: "Internal server error",
context: {
error: error instanceof Error ? error.message : String(error),
},
});
}
}
handleHealthCheck(req, res) {
const stats = this.getStats();
res.json({
status: "healthy",
timestamp: new Date().toISOString(),
...stats,
});
}
handleSessionsEndpoint(req, res) {
const sessionStats = this.getSessionStats();
res.json(sessionStats);
}
async createNewSession() {
const mcpServer = this.mcpServerFactory();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sessionId) => {
const session = {
sessionId,
transport,
createdAt: new Date(),
lastActivity: new Date(),
};
this.sessions.set(sessionId, session);
this.totalSessionsCreated++;
logger.debug("Session initialized and stored", { sessionId });
},
});
transport.onclose = () => {
if (transport.sessionId) {
this.terminateSession(transport.sessionId);
}
};
await mcpServer.connect(transport);
return transport;
}
terminateSession(sessionId) {
const session = this.sessions.get(sessionId);
if (session) {
session.transport.close();
this.sessions.delete(sessionId);
this.totalSessionsTerminated++;
logger.debug("Session cleaned up", {
sessionId,
activeSessions: this.sessions.size,
});
}
}
setupSessionCleanup() {
const cleanupInterval = Math.min(this.options.sessionTimeout / 4, 5 * 60 * 1000);
this.cleanupInterval = setInterval(() => {
const now = new Date();
const expiredSessions = [];
for (const [sessionId, session] of this.sessions.entries()) {
const timeSinceLastActivity = now.getTime() - session.lastActivity.getTime();
if (timeSinceLastActivity > this.options.sessionTimeout) {
expiredSessions.push(sessionId);
}
}
expiredSessions.forEach((sessionId) => {
logger.info("Session expired, cleaning up", { sessionId });
this.terminateSession(sessionId);
});
if (expiredSessions.length > 0) {
logger.debug("Session cleanup completed", {
expiredSessions: expiredSessions.length,
activeSessions: this.sessions.size,
});
}
}, cleanupInterval);
}
sendError(res, error) {
if (!res.headersSent) {
res.status(error.statusCode || 500).json({
jsonrpc: "2.0",
error: {
code: -32000,
message: error.message || "Internal server error",
data: error.context,
},
id: null,
});
}
}
getSessionStats() {
const now = new Date();
let totalDuration = 0;
let sessionsWithDuration = 0;
for (const session of this.sessions.values()) {
const duration = now.getTime() - session.createdAt.getTime();
totalDuration += duration;
sessionsWithDuration++;
}
return {
activeSessions: this.sessions.size,
totalSessionsCreated: this.totalSessionsCreated,
totalSessionsTerminated: this.totalSessionsTerminated,
averageSessionDuration: sessionsWithDuration > 0 ? totalDuration / sessionsWithDuration : 0,
};
}
getStats() {
return {
isRunning: this.isRunning,
activeSessions: this.sessions.size,
totalSessionsCreated: this.totalSessionsCreated,
totalSessionsTerminated: this.totalSessionsTerminated,
uptime: this.isRunning ? Date.now() - this.startTime.getTime() : 0,
port: this.config.port,
host: this.config.host,
https: this.config.https.enabled,
};
}
async start() {
if (this.isRunning) {
throw new Error("HTTP transport service is already running");
}
return new Promise((resolve, reject) => {
try {
if (this.config.https.enabled) {
this.startHttpsServer(resolve, reject);
}
else {
this.startHttpServer(resolve, reject);
}
}
catch (error) {
logger.error("Failed to start HTTP server", { error });
reject(error);
}
});
}
startHttpServer(resolve, reject) {
this.server = this.app.listen(this.config.port, this.config.host, () => {
this.isRunning = true;
this.startTime = new Date();
logger.info("HTTP transport service started", {
host: this.config.host,
port: this.config.port,
protocol: "http",
});
resolve();
});
this.server.on("error", (error) => {
logger.error("HTTP server error", { error });
reject(error);
});
}
startHttpsServer(resolve, reject) {
try {
this.validateHttpsConfig();
const httpsOptions = {
cert: fs.readFileSync(this.config.https.certPath),
key: fs.readFileSync(this.config.https.keyPath),
};
this.server = https.createServer(httpsOptions, this.app);
this.server.listen(this.config.port, this.config.host, () => {
this.isRunning = true;
this.startTime = new Date();
logger.info("HTTPS transport service started", {
host: this.config.host,
port: this.config.port,
protocol: "https",
certPath: this.config.https.certPath,
});
resolve();
});
this.server.on("error", (error) => {
logger.error("HTTPS server error", { error });
reject(error);
});
}
catch (error) {
logger.error("HTTPS server startup failed", { error });
reject(error instanceof Error ? error : new Error(String(error)));
}
}
validateHttpsConfig() {
if (!this.config.https.certPath || !this.config.https.keyPath) {
throw new Error("HTTPS requires both certPath and keyPath");
}
try {
fs.accessSync(this.config.https.certPath, fs.constants.R_OK);
fs.accessSync(this.config.https.keyPath, fs.constants.R_OK);
}
catch (error) {
throw new Error(`HTTPS certificate files not accessible: ${error}`);
}
}
async stop() {
return new Promise((resolve) => {
if (!this.server || !this.isRunning) {
logger.warn("Attempted to stop HTTP transport service that is not running");
resolve();
return;
}
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = undefined;
}
for (const [sessionId, session] of this.sessions.entries()) {
try {
session.transport.close();
this.sessions.delete(sessionId);
this.totalSessionsTerminated++;
}
catch (error) {
logger.warn("Error closing session during shutdown", {
sessionId,
error,
});
}
}
this.server.close(() => {
this.isRunning = false;
logger.info("HTTP transport service stopped");
resolve();
});
});
}
}
//# sourceMappingURL=http-transport-service.js.map