Files
ontime/apps/server/src/api-mcp/mcp.router.ts
T
2026-07-01 12:48:40 +02:00

54 lines
2.0 KiB
TypeScript

import type { IncomingMessage, ServerResponse } from 'node:http';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express from 'express';
import type { Request, Response, Router } from 'express';
import { LogOrigin } from 'ontime-types';
import { logger } from '../classes/Logger.js';
import { createMcpServer } from './mcp.server.js';
export const mcpRouter: Router = express.Router();
mcpRouter.post('/', async (req, res) => {
// A new Server instance is created per request — required for stateless mode where
// each POST is independent and concurrent requests must not share transport state.
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // stateless: no session tracking
enableJsonResponse: true,
});
const server = createMcpServer();
let cleanedUp = false;
const cleanup = async () => {
if (cleanedUp) return;
cleanedUp = true;
transport.close();
await server.close?.();
};
res.on('close', () => void cleanup());
res.on('finish', () => void cleanup());
try {
await server.connect(transport);
await transport.handleRequest(req as IncomingMessage, res as ServerResponse, req.body);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error(LogOrigin.Server, `MCP request failed: ${message}`);
if (!res.headersSent) {
res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message }, id: req.body?.id ?? null });
}
} finally {
await cleanup();
}
});
// Stateless mode: GET (SSE) and DELETE (session teardown) are not applicable.
// All MCP interactions happen via POST in a single request/response cycle.
const methodNotAllowed = (_req: Request, res: Response) =>
void res.status(405).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed.' }, id: null });
mcpRouter.get('/', methodNotAllowed);
mcpRouter.delete('/', methodNotAllowed);