Преглед изворни кода

Fix HTTP server SSE transport implementation

This commit fixes the HTTP/SSE transport to work correctly with MCP clients:

**HTTP Server Fixes:**
- Store SSE transports by session ID in a Map for proper message routing
- Change endpoint path from /message to /messages (required by MCP SDK)
- Implement proper message handling via transport.handlePostMessage()
- Route messages to correct transport using sessionId query parameter
- Keep reference to HTTP server instance to prevent process exit
- Add error handling and proper JSON-RPC error responses

**Graceful Shutdown:**
- Add cleanup() method to close all transports and HTTP server
- Implement SIGINT and SIGTERM handlers for graceful shutdown
- Ensure all resources are properly released on exit

**Testing:**
- Server stays running (no longer exits after 12 seconds)
- SSE connections properly established with session IDs
- Message routing works correctly
- Health check endpoint operational

Note: Claude CLI only supports stdio transport, not SSE/HTTP.
For Claude CLI, configure with: claude mcp add --transport stdio

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
claude пре 9 месеци
родитељ
комит
2b058a51a2
2 измењених фајлова са 116 додато и 20 уклоњено
  1. 95 19
      src/http-server.ts
  2. 21 1
      src/index.ts

+ 95 - 19
src/http-server.ts

@@ -17,8 +17,10 @@ export interface HttpServerOptions {
 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<string, SSEServerTransport> = new Map();
 
   constructor(options: HttpServerOptions) {
     this.server = options.server;
@@ -48,37 +50,111 @@ export class HttpMcpServer {
     this.app.get('/sse', async (req, res) => {
       console.error('New SSE connection established');
 
-      const transport = new SSEServerTransport('/message', res);
-      await this.server.connect(transport);
+      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}`);
 
-      // Keep connection alive
-      req.on('close', () => {
-        console.error('SSE connection closed');
+      // 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
-    this.app.post('/message', async (req, res) => {
+    // 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 {
-        // The SSE transport will handle the message
-        res.status(200).json({ received: true });
+        await transport.handlePostMessage(req, res, req.body);
       } catch (error) {
         console.error('Error handling message:', error);
-        res.status(500).json({
-          error: error instanceof Error ? error.message : 'Unknown 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<void> {
-    return new Promise((resolve) => {
-      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(`Health check: http://${this.host}:${this.port}/health`);
-        resolve();
-      });
+    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<void> {
+    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();
+        });
+      });
+    }
+  }
 }

+ 21 - 1
src/index.ts

@@ -36,16 +36,36 @@ const gogsClient = new GogsClient({
 // Create MCP server with all tools and handlers
 const server = createMcpServer(gogsClient);
 
+// Store http server instance for cleanup
+let httpServer: HttpMcpServer | null = null;
+
 // Start server with appropriate transport
 async function main() {
   if (TRANSPORT_MODE === 'http') {
     // Start HTTP server
-    const httpServer = new HttpMcpServer({
+    httpServer = new HttpMcpServer({
       server,
       port: HTTP_PORT,
       host: HTTP_HOST,
     });
     await httpServer.start();
+
+    // Handle graceful shutdown
+    process.on('SIGINT', async () => {
+      console.error('Received SIGINT, shutting down gracefully...');
+      if (httpServer) {
+        await httpServer.cleanup();
+      }
+      process.exit(0);
+    });
+
+    process.on('SIGTERM', async () => {
+      console.error('Received SIGTERM, shutting down gracefully...');
+      if (httpServer) {
+        await httpServer.cleanup();
+      }
+      process.exit(0);
+    });
   } else {
     // Start stdio server (default)
     const transport = new StdioServerTransport();