refactor: Enhance MCP server initialization and routing with CORS support

This commit is contained in:
Nawaz Dhandala
2026-03-03 21:33:55 +00:00
parent a7782564a2
commit d823f81e69
3 changed files with 93 additions and 50 deletions

View File

@@ -12,7 +12,8 @@ import {
NextFunction,
ExpressJson,
} from "Common/Server/Utils/Express";
import { getMCPServer, McpServer } from "../Server/MCPServer";
import { createMCPServerInstance, McpServer } from "../Server/MCPServer";
import { registerToolHandlers } from "./ToolHandler";
import SessionManager, { SessionData } from "../Server/SessionManager";
import { McpToolInfo } from "../Types/McpTypes";
import {
@@ -22,6 +23,9 @@ import {
} from "../Config/ServerConfig";
import logger from "Common/Server/Utils/Logger";
// Tools list stored at setup time for per-session server initialization
let registeredTools: McpToolInfo[] = [];
// Type for MCP handler function
type McpHandlerFunction = (
req: ExpressRequest,
@@ -53,6 +57,7 @@ export function setupMCPRoutes(
app: ExpressApplication,
tools: McpToolInfo[],
): void {
registeredTools = tools;
ROUTE_PREFIXES.forEach((prefix: string) => {
setupRoutesForPrefix(app, prefix, tools);
});
@@ -62,6 +67,22 @@ export function setupMCPRoutes(
);
}
/**
* Middleware to add MCP-specific CORS headers (mcp-session-id must be allowed and exposed)
*/
function mcpCorsMiddleware(
_req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): void {
res.header(
"Access-Control-Allow-Headers",
"Content-Type, Accept, Authorization, mcp-session-id, x-api-key",
);
res.header("Access-Control-Expose-Headers", "mcp-session-id");
next();
}
/**
* Setup routes for a specific prefix
*/
@@ -74,14 +95,18 @@ function setupRoutesForPrefix(
const mcpHandler: McpHandlerFunction = createMCPHandler();
// MCP endpoint for all methods (GET for SSE, POST for requests, DELETE for cleanup)
app.get(mcpEndpoint, mcpHandler);
app.post(mcpEndpoint, ExpressJson(), mcpHandler);
app.delete(mcpEndpoint, mcpHandler);
app.get(mcpEndpoint, mcpCorsMiddleware, mcpHandler);
app.post(mcpEndpoint, mcpCorsMiddleware, ExpressJson(), mcpHandler);
app.delete(mcpEndpoint, mcpCorsMiddleware, mcpHandler);
// OPTIONS handler for CORS preflight requests
app.options(mcpEndpoint, (_req: ExpressRequest, res: ExpressResponse) => {
res.status(200).end();
});
app.options(
mcpEndpoint,
mcpCorsMiddleware,
(_req: ExpressRequest, res: ExpressResponse) => {
res.status(200).end();
},
);
// List tools endpoint (REST API)
setupToolsEndpoint(app, prefix, tools);
@@ -100,6 +125,22 @@ function createMCPHandler(): McpHandlerFunction {
next: NextFunction,
): Promise<void> => {
try {
// For GET requests, require Accept: text/event-stream (SSE) header
if (req.method === "GET") {
const acceptHeader: string | undefined = req.headers["accept"] as
| string
| undefined;
if (!acceptHeader || !acceptHeader.includes("text/event-stream")) {
res.status(200).json({
name: "oneuptime-mcp",
status: "running",
message:
"This is a Model Context Protocol (MCP) server endpoint. Use an MCP client to connect.",
});
return;
}
}
// Extract API key (optional - public tools work without it)
const apiKey: string | undefined = extractApiKey(req);
@@ -116,6 +157,21 @@ function createMCPHandler(): McpHandlerFunction {
return;
}
// For POST without session ID, validate it's a proper MCP initialization request
if (req.method === "POST") {
const body: Record<string, unknown> | undefined = req.body as
| Record<string, unknown>
| undefined;
if (!body || body["method"] !== "initialize") {
res.status(400).json({
error: "Bad Request",
message:
"Invalid MCP request. POST without session ID must be an 'initialize' request.",
});
return;
}
}
// Create new session for new connections
await handleNewSession(req, res, apiKey || "");
} catch (error) {
@@ -147,13 +203,16 @@ async function handleExistingSession(
/**
* Handle request for a new session (initialization)
* Creates a new McpServer instance per session to support concurrent connections.
*/
async function handleNewSession(
req: ExpressRequest,
res: ExpressResponse,
apiKey: string,
): Promise<void> {
const mcpServer: McpServer = getMCPServer();
// Create a new McpServer for this session (each can only connect to one transport)
const mcpServer: McpServer = createMCPServerInstance();
registerToolHandlers(mcpServer, registeredTools);
const transport: StreamableHTTPServerTransport =
new StreamableHTTPServerTransport({
@@ -181,7 +240,7 @@ async function handleNewSession(
logger.error(`MCP transport error: ${error.message}`);
};
// Connect the MCP server to this transport
// Connect the per-session MCP server to this transport
await mcpServer.connect(transport as Parameters<typeof mcpServer.connect>[0]);
// Handle the request

View File

@@ -8,8 +8,7 @@ import Express, { ExpressApplication } from "Common/Server/Utils/Express";
import logger from "Common/Server/Utils/Logger";
import { getApiUrl } from "./Config/ServerConfig";
import { initializeMCPServer, getMCPServer } from "./Server/MCPServer";
import { registerToolHandlers } from "./Handlers/ToolHandler";
import { initializeMCPServer } from "./Server/MCPServer";
import { setupMCPRoutes } from "./Handlers/RouteHandler";
import { generateAllTools } from "./Tools/ToolGenerator";
import OneUptimeApiService, {
@@ -29,16 +28,13 @@ const MCPFeatureSet: FeatureSet = {
OneUptimeApiService.initialize(config);
logger.info(`MCP: OneUptime API Service initialized with: ${apiUrl}`);
// Initialize MCP server
// Mark MCP subsystem as initialized
initializeMCPServer();
// Generate tools
// Generate tools (tool handlers are registered per-session in RouteHandler)
const tools: McpToolInfo[] = generateAllTools();
logger.info(`MCP: Generated ${tools.length} tools`);
// Register tool handlers
registerToolHandlers(getMCPServer(), tools);
// Setup MCP-specific routes
setupMCPRoutes(app, tools);

View File

@@ -1,24 +1,37 @@
/**
* MCP Server
* Handles MCP server initialization and configuration
* Handles MCP server initialization and configuration.
* Creates a new McpServer instance per session to support concurrent connections.
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../Config/ServerConfig";
import logger from "Common/Server/Utils/Logger";
// Singleton MCP server instance
let mcpServerInstance: McpServer | null = null;
let initialized: boolean = false;
/**
* Initialize and return the MCP server instance
* Mark the MCP subsystem as initialized (called once at startup)
*/
export function initializeMCPServer(): McpServer {
if (mcpServerInstance) {
return mcpServerInstance;
export function initializeMCPServer(): void {
initialized = true;
logger.info(
`MCP Server initialized: ${MCP_SERVER_NAME} v${MCP_SERVER_VERSION}`,
);
}
/**
* Create a new McpServer instance for a session.
* Each session needs its own McpServer because a McpServer can only connect to one transport.
*/
export function createMCPServerInstance(): McpServer {
if (!initialized) {
throw new Error(
"MCP Server not initialized. Call initializeMCPServer() first.",
);
}
mcpServerInstance = new McpServer(
return new McpServer(
{
name: MCP_SERVER_NAME,
version: MCP_SERVER_VERSION,
@@ -29,38 +42,13 @@ export function initializeMCPServer(): McpServer {
},
},
);
logger.info(
`MCP Server initialized: ${MCP_SERVER_NAME} v${MCP_SERVER_VERSION}`,
);
return mcpServerInstance;
}
/**
* Get the MCP server instance
* @throws Error if server not initialized
*/
export function getMCPServer(): McpServer {
if (!mcpServerInstance) {
throw new Error(
"MCP Server not initialized. Call initializeMCPServer() first.",
);
}
return mcpServerInstance;
}
/**
* Check if MCP server is initialized
*/
export function isMCPServerInitialized(): boolean {
return mcpServerInstance !== null;
}
/**
* Reset MCP server (useful for testing)
*/
export function resetMCPServer(): void {
mcpServerInstance = null;
return initialized;
}
export { McpServer };