/** * HTTP Server for Gogs MCP * Provides HTTP/SSE transport for the MCP server */ import express from 'express'; import cors from 'cors'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; export interface HttpServerOptions { port: number; host: string; server: Server; } export class HttpMcpServer { private app: express.Application; private server: Server; private httpServer: any; // HTTP server instance from Express private port: number; private host: string; private transports: Map = new Map(); constructor(options: HttpServerOptions) { this.server = options.server; this.port = options.port; this.host = options.host; this.app = express(); this.setupMiddleware(); this.setupRoutes(); } private setupMiddleware() { this.app.use(cors()); this.app.use(express.json()); } private setupRoutes() { // Health check endpoint this.app.get('/health', (_req, res) => { res.json({ status: 'ok', service: 'gogs-mcp-server', transport: 'http', }); }); // SSE endpoint for MCP protocol this.app.get('/sse', async (req, res) => { console.error('New SSE connection established'); const transport = new SSEServerTransport('/messages', res); // Store transport by session ID this.transports.set(transport.sessionId, transport); console.error(`Transport stored with session ID: ${transport.sessionId}`); // Clean up when connection closes res.on('close', () => { console.error(`SSE connection closed for session: ${transport.sessionId}`); this.transports.delete(transport.sessionId); }); await this.server.connect(transport); }); // Message endpoint for client requests (must match the path given to SSEServerTransport) this.app.post('/messages', async (req, res) => { const sessionId = req.query.sessionId as string; if (!sessionId) { res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No sessionId provided' }, id: null }); return; } const transport = this.transports.get(sessionId); if (!transport) { res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No transport found for sessionId' }, id: null }); return; } try { await transport.handlePostMessage(req, res, req.body); } catch (error) { console.error('Error handling message:', error); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: error instanceof Error ? error.message : 'Internal server error' }, id: null }); } } }); } public start(): Promise { return new Promise((resolve, reject) => { try { this.httpServer = this.app.listen(this.port, this.host, () => { console.error(`Gogs MCP HTTP Server running on http://${this.host}:${this.port}`); console.error(`SSE endpoint: http://${this.host}:${this.port}/sse`); console.error(`Messages endpoint: http://${this.host}:${this.port}/messages`); console.error(`Health check: http://${this.host}:${this.port}/health`); resolve(); }); this.httpServer.on('error', (error: Error) => { console.error('HTTP server error:', error); reject(error); }); } catch (error) { console.error('Failed to start HTTP server:', error); reject(error); } }); } public async cleanup(): Promise { console.error('Cleaning up HTTP server transports...'); for (const [sessionId, transport] of this.transports) { try { console.error(`Closing transport for session ${sessionId}`); await transport.close(); } catch (error) { console.error(`Error closing transport for session ${sessionId}:`, error); } } this.transports.clear(); // Close HTTP server if (this.httpServer) { return new Promise((resolve) => { this.httpServer.close(() => { console.error('HTTP server closed'); resolve(); }); }); } } }