Files
fredy/lib/mcp/mcpHttpRoute.js

115 lines
3.5 KiB
JavaScript
Raw Normal View History

2026-03-09 15:35:29 +01:00
/*
* Copyright (c) 2026 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { createMcpServer } from './mcpAdapter.js';
import { authenticateRequest } from './mcpAuthentication.js';
import logger from '../services/logger.js';
2026-03-09 15:35:29 +01:00
import crypto from 'crypto';
/**
* Active transports keyed by session id.
* @type {Map<string, { server: McpServer, transport: StreamableHTTPServerTransport }>}
*/
const sessions = new Map();
/**
* @param {string|undefined} sessionId
* @param {{ userId: string }} auth
*/
function getOrCreateSession(sessionId, auth) {
if (sessionId && sessions.has(sessionId)) {
return sessions.get(sessionId);
}
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
onsessioninitialized: (sid) => {
sessions.set(sid, entry);
logger.debug(`MCP session created: ${sid}`);
},
});
const server = createMcpServer();
const entry = { server, transport, userId: auth.userId };
transport.onclose = () => {
const sid = transport.sessionId;
if (sid) {
sessions.delete(sid);
logger.debug(`MCP session closed: ${sid}`);
}
};
return entry;
}
/**
2026-04-27 16:56:04 +02:00
* Register MCP Streamable HTTP routes on a fastify instance.
2026-03-09 15:35:29 +01:00
*
2026-04-27 16:56:04 +02:00
* POST /api/mcp JSON-RPC messages
* GET /api/mcp SSE stream for server-initiated notifications
* DELETE /api/mcp session termination
2026-03-09 15:35:29 +01:00
*
* All endpoints require a valid Bearer token in the Authorization header.
*
2026-04-27 16:56:04 +02:00
* @param {import('fastify').FastifyInstance} fastify
2026-03-09 15:35:29 +01:00
*/
2026-04-27 16:56:04 +02:00
export function registerMcpRoutes(fastify) {
fastify.post('/api/mcp', async (request, reply) => {
const auth = authenticateRequest(request.raw);
2026-03-09 15:35:29 +01:00
if (!auth) {
2026-04-27 16:56:04 +02:00
return reply.code(401).send({ error: 'Unauthorized. Provide a valid Bearer token.' });
2026-03-09 15:35:29 +01:00
}
2026-04-27 16:56:04 +02:00
const sessionId = request.raw.headers['mcp-session-id'];
2026-03-09 15:35:29 +01:00
const { server, transport } = getOrCreateSession(sessionId, auth);
if (!transport.onmessage) {
await server.connect(transport);
}
2026-04-27 16:56:04 +02:00
request.raw.auth = { userId: auth.userId };
2026-03-09 15:35:29 +01:00
2026-04-27 16:56:04 +02:00
reply.hijack();
await transport.handleRequest(request.raw, reply.raw, request.body);
2026-03-09 15:35:29 +01:00
});
2026-04-27 16:56:04 +02:00
fastify.get('/api/mcp', async (request, reply) => {
const auth = authenticateRequest(request.raw);
2026-03-09 15:35:29 +01:00
if (!auth) {
2026-04-27 16:56:04 +02:00
return reply.code(401).send({ error: 'Unauthorized. Provide a valid Bearer token.' });
2026-03-09 15:35:29 +01:00
}
2026-04-27 16:56:04 +02:00
const sessionId = request.raw.headers['mcp-session-id'];
2026-03-09 15:35:29 +01:00
if (!sessionId || !sessions.has(sessionId)) {
2026-04-27 16:56:04 +02:00
return reply.code(400).send({ error: 'Invalid or missing session. Send an initialize request first.' });
2026-03-09 15:35:29 +01:00
}
const { transport } = sessions.get(sessionId);
2026-04-27 16:56:04 +02:00
reply.hijack();
await transport.handleRequest(request.raw, reply.raw);
2026-03-09 15:35:29 +01:00
});
2026-04-27 16:56:04 +02:00
fastify.delete('/api/mcp', async (request, reply) => {
const auth = authenticateRequest(request.raw);
2026-03-09 15:35:29 +01:00
if (!auth) {
2026-04-27 16:56:04 +02:00
return reply.code(401).send({ error: 'Unauthorized. Provide a valid Bearer token.' });
2026-03-09 15:35:29 +01:00
}
2026-04-27 16:56:04 +02:00
const sessionId = request.raw.headers['mcp-session-id'];
2026-03-09 15:35:29 +01:00
if (!sessionId || !sessions.has(sessionId)) {
2026-04-27 16:56:04 +02:00
return reply.code(404).send({ error: 'Session not found.' });
2026-03-09 15:35:29 +01:00
}
const { transport } = sessions.get(sessionId);
await transport.close();
sessions.delete(sessionId);
2026-04-27 16:56:04 +02:00
return { ok: true };
2026-03-09 15:35:29 +01:00
});
logger.debug('MCP Streamable HTTP endpoint registered at /api/mcp');
}