inspector icon indicating copy to clipboard operation
inspector copied to clipboard

working bearer tokens can still break or silently fail over SSE

Open davidawad opened this issue 9 months ago • 1 comments

I have written an MCP server that works locally and correctly with STDIO, but when trying to use MCP with a bearer token, the token will work, but the app will silently fail.

Query parameters: { transportType: 'sse', url: 'http://localhost:3001/sse' }
SSE transport: url=http://localhost:3001/sse, headers=Accept,authorization
New SSE connection
Query parameters: { transportType: 'sse', url: 'http://localhost:3001/sse' }
SSE transport: url=http://localhost:3001/sse, headers=Accept,authorization
New SSE connection
Query parameters: { transportType: 'sse', url: 'http://localhost:3001/sse' }
SSE transport: url=http://localhost:3001/sse, headers=Accept,authorization

I could click connect 10 times and not see an outout on this. Image

relevant example of my code;


const server = new McpServer({
  name: "HorseRacing",
  version: "0.1.0"
});

// --- Register Resources, Tools, and Prompts ---
registerResources(server, db, debugLog, errorLogger);
registerToolsAndPrompts(server, db, debugLog, errorLogger);


// --- Server Setup (Conditional) ---
let httpServer: http.Server | null = null;

async function runServer() {
    if (transportMode === 'sse') {
        // --- Start Server with Express and SSE ---
        infoLogger("Starting HorseRacing MCP server via HTTP/SSE..."); // Use infoLogger
        const app = express();
        const port = 3001;
        httpServer = http.createServer(app);

        const transports: { [sessionId: string]: SSEServerTransport } = {};

        app.get("/sse", async (req: Request, res: Response) => {
          // --- Authentication Check (Keep for SSE mode) ---
          const authHeader = req.headers.authorization;
          let isAuthenticated = false;
          if (authHeader && authHeader.startsWith("Bearer ")) {
            const token = authHeader.substring(7);
            if (token === EXPECTED_BEARER_TOKEN) {
              isAuthenticated = true;
            }
          }

          if (!isAuthenticated) {
            debugLog(`Authentication failed for ${req.ip}. Invalid or missing token.`); // Use debugLog
            res.writeHead(401, { 'Content-Type': 'text/plain' });
            res.end('Unauthorized');
            return;
          }
          // --- End Authentication Check ---

          debugLog(`Authenticated SSE connection request received from ${req.ip}`); // Use debugLog
          const transport = new SSEServerTransport('/messages', res);
          transports[transport.sessionId] = transport;
          debugLog(`Transport created with session ID: ${transport.sessionId}`); // Use debugLog

          res.on("close", () => {
            debugLog(`SSE connection closed for session ID: ${transport.sessionId}`); // Use debugLog
            delete transports[transport.sessionId];
          });

          try {
            await server.connect(transport);
            debugLog(`MCP Server connected to transport ${transport.sessionId}`); // Use debugLog
          } catch (error) {
            errorLogger(`Failed to connect MCP server to transport ${transport.sessionId}:`, error);
             if (!res.writableEnded) {
                res.status(500).send("Failed to establish MCP connection");
            }
             delete transports[transport.sessionId];
          }
        });

        app.post("/messages", express.json(), async (req: Request, res: Response) => {
          const sessionId = req.query.sessionId as string;
          const transport = transports[sessionId];
          debugLog(`>>> [POST /messages] Received request for session ID: ${sessionId}`); // Use debugLog
          if (transport) {
            try {
                const requestBody = JSON.stringify(req.body, null, 2);
                debugLog(`... [POST /messages] Request Body for session ${sessionId}: ${requestBody}`); // Use debugLog
                debugLog(`... [POST /messages] Calling transport.handlePostMessage for session ${sessionId}...`); // Use debugLog
                await transport.handlePostMessage(req, res);
                debugLog(`<<< [POST /messages] Successfully handled POST for session ID: ${sessionId}`); // Use debugLog
            } catch (error) {
                errorLogger(`!!! [POST /messages] Error handling POST for session ID ${sessionId}:`, error);
                 if (!res.headersSent) {
                    res.status(500).send('Error processing message');
                }
            }
          } else {
            debugLog(`No active transport found for session ID: ${sessionId}`); // Use debugLog (was warn)
            res.status(400).send('No transport found for sessionId');
          }
        });

        httpServer.listen(port, () => {
            infoLogger(`HorseRacing MCP server running via HTTP/SSE`); // Use infoLogger
            infoLogger(` -> SSE Endpoint: http://localhost:${port}/sse`); // Use infoLogger
            infoLogger(` -> MSG Endpoint: http://localhost:${port}/messages`); // Use infoLogger
        });

    } else {
        // --- Start Server with Stdio (Default) ---
        infoLogger("Starting HorseRacing MCP server via stdio..."); // Use infoLogger (goes to stderr in stdio mode)
        const transport = new StdioServerTransport();
        await server.connect(transport);
        infoLogger("HorseRacing MCP server connected via stdio."); // Use infoLogger (goes to stderr in stdio mode)
    }
}

// --- Graceful Shutdown Handling ---
let isShuttingDown = false;
const shutdown = (signal: string) => {
    if (isShuttingDown) return;
    isShuttingDown = true;
    infoLogger(`\nReceived ${signal}. Shutting down server...`); // Use infoLogger

    const closeHttpServer = (callback: () => void) => {
        if (httpServer) {
            infoLogger("Closing HTTP server..."); // Use infoLogger
            httpServer.close(() => {
                infoLogger("HTTP server closed."); // Use infoLogger
                callback();
            });
            setTimeout(() => {
                infoLogger("Could not close HTTP connections gracefully, forcefully shutting down"); // Use infoLogger
                callback();
            }, 5000);
        } else {
            callback();
        }
    };

    closeHttpServer(() => {
        try {
            db.close();
            infoLogger("Database connection closed."); // Use infoLogger
        } catch (error) {
            errorLogger("Error closing database during shutdown:", error);
        }
        process.exit(0);
    });
};

process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('exit', (code) => {
  infoLogger(`Server process exited with code ${code}`); // Use infoLogger
  try {
      if (typeof db?.close === 'function') {
          db.close();
          infoLogger("Database connection closed on process exit."); // Use infoLogger
      }
  } catch (error) {
      // Ignore
  }
});

// --- Run the Server ---
runServer().catch(error => {
    errorLogger("Failed to start server:", error);
    try {
        if (typeof db?.close === 'function') {
             db.close();
        }
    } catch (dbError) {
        errorLogger("Error closing database after startup error:", dbError);
    }
    process.exit(1);
});

davidawad avatar Apr 08 '25 23:04 davidawad

Does your MCP server log any errors in this scenario, that might not be showing up in Inspector itself?

olaservo avatar May 02 '25 14:05 olaservo

Closing this but feel free to re-open if its still a problem, I think we might need a little more info to try and repro this.

olaservo avatar May 30 '25 11:05 olaservo