http-server.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. /**
  2. * HTTP Server for Gogs MCP
  3. * Provides HTTP/SSE transport for the MCP server
  4. */
  5. import express from 'express';
  6. import cors from 'cors';
  7. import { Server } from '@modelcontextprotocol/sdk/server/index.js';
  8. import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
  9. export interface HttpServerOptions {
  10. port: number;
  11. host: string;
  12. server: Server;
  13. }
  14. export class HttpMcpServer {
  15. private app: express.Application;
  16. private server: Server;
  17. private httpServer: any; // HTTP server instance from Express
  18. private port: number;
  19. private host: string;
  20. private transports: Map<string, SSEServerTransport> = new Map();
  21. constructor(options: HttpServerOptions) {
  22. this.server = options.server;
  23. this.port = options.port;
  24. this.host = options.host;
  25. this.app = express();
  26. this.setupMiddleware();
  27. this.setupRoutes();
  28. }
  29. private setupMiddleware() {
  30. this.app.use(cors());
  31. this.app.use(express.json());
  32. }
  33. private setupRoutes() {
  34. // Health check endpoint
  35. this.app.get('/health', (_req, res) => {
  36. res.json({
  37. status: 'ok',
  38. service: 'gogs-mcp-server',
  39. transport: 'http',
  40. });
  41. });
  42. // SSE endpoint for MCP protocol
  43. this.app.get('/sse', async (req, res) => {
  44. console.error('New SSE connection established');
  45. const transport = new SSEServerTransport('/messages', res);
  46. // Store transport by session ID
  47. this.transports.set(transport.sessionId, transport);
  48. console.error(`Transport stored with session ID: ${transport.sessionId}`);
  49. // Clean up when connection closes
  50. res.on('close', () => {
  51. console.error(`SSE connection closed for session: ${transport.sessionId}`);
  52. this.transports.delete(transport.sessionId);
  53. });
  54. await this.server.connect(transport);
  55. });
  56. // Message endpoint for client requests (must match the path given to SSEServerTransport)
  57. this.app.post('/messages', async (req, res) => {
  58. const sessionId = req.query.sessionId as string;
  59. if (!sessionId) {
  60. res.status(400).json({
  61. jsonrpc: '2.0',
  62. error: {
  63. code: -32000,
  64. message: 'Bad Request: No sessionId provided'
  65. },
  66. id: null
  67. });
  68. return;
  69. }
  70. const transport = this.transports.get(sessionId);
  71. if (!transport) {
  72. res.status(400).json({
  73. jsonrpc: '2.0',
  74. error: {
  75. code: -32000,
  76. message: 'Bad Request: No transport found for sessionId'
  77. },
  78. id: null
  79. });
  80. return;
  81. }
  82. try {
  83. await transport.handlePostMessage(req, res, req.body);
  84. } catch (error) {
  85. console.error('Error handling message:', error);
  86. if (!res.headersSent) {
  87. res.status(500).json({
  88. jsonrpc: '2.0',
  89. error: {
  90. code: -32603,
  91. message: error instanceof Error ? error.message : 'Internal server error'
  92. },
  93. id: null
  94. });
  95. }
  96. }
  97. });
  98. }
  99. public start(): Promise<void> {
  100. return new Promise((resolve, reject) => {
  101. try {
  102. this.httpServer = this.app.listen(this.port, this.host, () => {
  103. console.error(`Gogs MCP HTTP Server running on http://${this.host}:${this.port}`);
  104. console.error(`SSE endpoint: http://${this.host}:${this.port}/sse`);
  105. console.error(`Messages endpoint: http://${this.host}:${this.port}/messages`);
  106. console.error(`Health check: http://${this.host}:${this.port}/health`);
  107. resolve();
  108. });
  109. this.httpServer.on('error', (error: Error) => {
  110. console.error('HTTP server error:', error);
  111. reject(error);
  112. });
  113. } catch (error) {
  114. console.error('Failed to start HTTP server:', error);
  115. reject(error);
  116. }
  117. });
  118. }
  119. public async cleanup(): Promise<void> {
  120. console.error('Cleaning up HTTP server transports...');
  121. for (const [sessionId, transport] of this.transports) {
  122. try {
  123. console.error(`Closing transport for session ${sessionId}`);
  124. await transport.close();
  125. } catch (error) {
  126. console.error(`Error closing transport for session ${sessionId}:`, error);
  127. }
  128. }
  129. this.transports.clear();
  130. // Close HTTP server
  131. if (this.httpServer) {
  132. return new Promise((resolve) => {
  133. this.httpServer.close(() => {
  134. console.error('HTTP server closed');
  135. resolve();
  136. });
  137. });
  138. }
  139. }
  140. }