diff --git a/App/FeatureSet/MCP/Handlers/RouteHandler.ts b/App/FeatureSet/MCP/Handlers/RouteHandler.ts index 391548b56b..378e4ffbb8 100644 --- a/App/FeatureSet/MCP/Handlers/RouteHandler.ts +++ b/App/FeatureSet/MCP/Handlers/RouteHandler.ts @@ -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 => { 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 | undefined = req.body as + | Record + | 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 { - 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[0]); // Handle the request diff --git a/App/FeatureSet/MCP/Index.ts b/App/FeatureSet/MCP/Index.ts index 5cf2549e4e..89df413fdd 100644 --- a/App/FeatureSet/MCP/Index.ts +++ b/App/FeatureSet/MCP/Index.ts @@ -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); diff --git a/App/FeatureSet/MCP/Server/MCPServer.ts b/App/FeatureSet/MCP/Server/MCPServer.ts index 96f3a52d54..873b4f8a46 100644 --- a/App/FeatureSet/MCP/Server/MCPServer.ts +++ b/App/FeatureSet/MCP/Server/MCPServer.ts @@ -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 };