mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
refactor: Enhance MCP server initialization and routing with CORS support
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user