UNPKG

inboxassure-mcp-server

Version:

Comprehensive MCP server for InboxAssure email marketing platform with full API coverage including campaigns, replies, and email account management.

1,014 lines (902 loc) 44.4 kB
#!/usr/bin/env node // src/cli.js console.error("[MCP Server CLI] Script started."); process.on('uncaughtException', (err, origin) => { console.error(`[MCP Server CLI] Uncaught Exception at: ${origin}, error: ${err.stack || err}`); process.exit(1); // Ensure process exits on uncaught exception }); process.on('unhandledRejection', (reason, promise) => { console.error(`[MCP Server CLI] Unhandled Rejection at: ${promise}, reason: ${reason.stack || reason}`); // Optionally exit, or let it continue if it might recover, but for startup, exiting is safer. process.exit(1); }); import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { createMcpServer } from "./mcpSetup.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import express from "express"; import dotenv from 'dotenv'; import crypto from 'crypto'; import { BisonService } from "./bisonService.js"; dotenv.config(); // Load .env file for local development if present console.error("[MCP Server CLI] Imports completed, dotenv configured."); async function start() { console.error("[MCP Server CLI] Start function entered."); let argv; try { argv = yargs(hideBin(process.argv)) .options({ "api-key": { type: "string", description: "API key for the X-API-Key header to call the target Bison API.", demandOption: false // Changed from true to false - now optional }, port: { type: "number", description: "Port to run the HTTP server on. Not used if --stdio is present.", default: parseInt(process.env.PORT || "3000", 10) }, stdio: { type: "boolean", description: "Run in STDIO mode instead of HTTP server.", default: false } }) .help() .alias('h', 'help') .epilog('For more information, visit the project repository.') .parseSync(); console.error("[MCP Server CLI] Arguments parsed:", JSON.stringify(argv)); } catch (err) { console.error("[MCP Server CLI] Error parsing arguments:", err.stack || err); process.exit(1); } let mcpServerInstance; try { // Create the MCP server without an API key - we'll extract it from each request mcpServerInstance = createMcpServer(); console.error("[MCP Server CLI] MCP server instance created without default API key."); } catch (err) { console.error("[MCP Server CLI] Error creating MCP server instance:", err.stack || err); process.exit(1); } if (argv.stdio) { console.error("[MCP Server CLI] Starting MCP server in STDIO mode..."); try { // For STDIO mode, we still need the API key provided at startup if (!argv.apiKey) { throw new Error("[MCP Server CLI] API key is required for STDIO mode"); } const transport = new StdioServerTransport(); console.error("[MCP Server CLI] StdioServerTransport created."); // For STDIO mode, set the API key on the MCP server instance mcpServerInstance.setApiKey(argv.apiKey); await mcpServerInstance.connect(transport); console.error("[MCP Server CLI] MCP server connected via STDIO. Ready for requests."); // For STDIO, we assume connect() keeps it alive or the transport does. // To be absolutely sure PM2 keeps it alive, return a non-resolving promise. return new Promise(() => {}); } catch (err) { console.error("[MCP Server CLI] Error connecting STDIO transport:", err.stack || err); process.exit(1); // Or reject(err) if we want start().catch() to get it. } } else { console.error("[MCP Server CLI] Starting MCP server in HTTP mode..."); const app = express(); app.use(express.json()); const port = argv.port; // Add CORS middleware app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, X-API-KEY'); // Handle preflight requests if (req.method === 'OPTIONS') { return res.status(204).end(); } next(); }); // Log all requests app.use((req, res, next) => { console.error(`[MCP Server CLI] ${req.method} ${req.url} from ${req.ip}`); console.error(`[MCP Server CLI] Headers: ${JSON.stringify(req.headers)}`); // Explicitly log API key headers with different casings console.error(`[MCP Server CLI] X-API-Key header: ${req.headers['x-api-key'] || 'not found'}`); console.error(`[MCP Server CLI] x-api-key header: ${req.headers['x-api-key'] || 'not found'}`); console.error(`[MCP Server CLI] X-Api-Key header: ${req.headers['x-api-key'] || 'not found'}`); console.error(`[MCP Server CLI] X-API-KEY header: ${req.headers['x-api-key'] || 'not found'}`); console.error(`[MCP Server CLI] apiKey query param: ${req.query.apiKey || 'not found'}`); next(); }); app.post("/mcp", async (req, res) => { console.error(`[MCP Server CLI - HTTP ${new Date().toISOString()}] Received POST /mcp from ${req.ip}`); console.error(`[MCP Server CLI] POST Headers received: ${JSON.stringify(req.headers)}`); console.error(`[MCP Server CLI] Request body: ${JSON.stringify(req.body)}`); // Extract API key from headers or URL parameters const headerApiKey = req.headers['x-api-key']; // Node.js normalizes headers to lowercase const queryApiKey = req.query.apiKey || req.query.api_key || req.query.key; let apiKey = headerApiKey || queryApiKey; console.error(`[MCP Server CLI] Header API key present: ${!!headerApiKey}`); console.error(`[MCP Server CLI] Query API key present: ${!!queryApiKey}`); console.error(`[MCP Server CLI] Selected API key: ${apiKey || 'none'}`); // For development mode - still warn but don't block request if (!apiKey) { console.error("[MCP Server CLI] Warning: Request received without API key."); apiKey = process.env.DEFAULT_API_KEY || argv.apiKey || "development-mode"; } try { // Set the API key for this request mcpServerInstance.setApiKey(apiKey); console.error("[MCP Server CLI] API key set for this request"); // Parse the JSON-RPC request const { jsonrpc, id, method, params } = req.body; console.error(`[MCP Server CLI] Request details - method: ${method}, id: ${id} (type: ${typeof id})`); if (jsonrpc !== "2.0") { return res.status(400).json({ jsonrpc: "2.0", error: { code: -32600, message: "Invalid Request: jsonrpc version must be 2.0" }, id: id // Always use the client's exact ID }); } // Special handling for ListOfferings action that Cursor uses if (method === "ListOfferings") { console.error("[MCP Server CLI] Handling ListOfferings request"); return res.status(200).json({ jsonrpc: "2.0", result: { toolDefinitions: [ { name: "get_bison_hello", description: "Calls the GET https://bison.imnodev.com/api/hello endpoint.", inputSchema: { type: "object", properties: { random_string: { type: "string", description: "Dummy parameter for no-parameter tools" } }, required: [] } } ] }, id: id }); } // Handle initialize method if (method === "initialize") { console.error("[MCP Server CLI] Handling initialize request"); return res.status(200).json({ jsonrpc: "2.0", result: { serverInfo: { name: "Bison Hello Server", vendor: "Me!", version: "0.0.1" }, protocolVersion: "2024-10-07", capabilities: { tools: { canExecuteCode: false }, streaming: { type: "sse" } } }, id: id // Always use the client's exact ID }); } // Add initialized notification handler if (method === "notifications/initialized") { console.error("[MCP Server CLI] Handling notifications/initialized request"); return res.status(200).json({ jsonrpc: "2.0", result: {}, id: id }); } if (method === "tools/list") { console.error("[MCP Server CLI] Handling tools/list request"); return res.status(200).json({ jsonrpc: "2.0", result: { tools: [ { name: "get_bison_hello", description: "Calls the GET https://bison.imnodev.com/api/hello endpoint.", inputSchema: { type: "object", properties: { random_string: { type: "string", description: "Dummy parameter for no-parameter tools" } }, required: [] } } ] }, id: id }); } if (method === "tools/call") { console.error("[MCP Server CLI] Handling tools/call request"); // Extract the tool name and arguments const toolName = params.name; const toolArgs = params.arguments || {}; // Extract API key from headers or URL parameters const headerApiKey = req.headers['x-api-key']; // Node.js normalizes headers to lowercase const queryApiKey = req.query.apiKey || req.query.api_key || req.query.key; const extractedApiKey = headerApiKey || queryApiKey; console.error(`[MCP Server CLI] Tool call request: ${toolName} with args ${JSON.stringify(toolArgs)}`); console.error(`[MCP Server CLI] Header API key present: ${!!headerApiKey}`); console.error(`[MCP Server CLI] Query API key present: ${!!queryApiKey}`); console.error(`[MCP Server CLI] Selected API key: ${extractedApiKey || 'none'}`); // Check if it's our supported tool if (toolName === "get_bison_hello") { try { // Use the API key that was already set on the MCP server instance // Rather than creating a new BisonService instance here console.error("[MCP Server CLI] Calling tool directly via McpServer instance"); // Call the tool handler directly through the McpServer instance const toolResult = await mcpServerInstance.callTool(toolName, toolArgs); console.error(`[MCP Server CLI] Tool result: ${JSON.stringify(toolResult)}`); // Return the formatted JSON-RPC response return res.status(200).json({ jsonrpc: "2.0", result: toolResult, id: id }); } catch (toolError) { console.error(`[MCP Server CLI] Tool error: ${toolError.message}`); return res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: `Error calling tool: ${toolError.message}` }, id: id }); } } else { // Tool not found return res.status(404).json({ jsonrpc: "2.0", error: { code: -32601, message: `Tool not found: ${toolName}` }, id: id }); } } if (method !== "tool") { return res.status(400).json({ jsonrpc: "2.0", error: { code: -32601, message: `Method not found: ${method}` }, id: id // Always use the client's exact ID }); } if (!params || !params.name) { return res.status(400).json({ jsonrpc: "2.0", error: { code: -32602, message: "Invalid params: 'name' is required" }, id: id // Always use the client's exact ID }); } // Get the tool name const toolName = params.name; console.error(`[MCP Server CLI] Tool requested: ${toolName}`); if (toolName === "get_bison_hello") { // Extract API key from headers const extractedApiKey = req.headers['x-api-key'] || req.headers['X-API-Key'] || req.headers['X-API-KEY'] || req.headers['x-api-key'] || req.query.apiKey; console.error(`[MCP Server CLI] Using API key from headers: ${extractedApiKey ? extractedApiKey.substring(0, 8) + '...' : 'none'}`); // Create a bisonService instance with the API key const bisonService = new BisonService(extractedApiKey); try { // Call the BisonService console.error("[MCP Server CLI] Calling Bison API..."); const result = await bisonService.getHello(); console.error(`[MCP Server CLI] Bison API response: ${JSON.stringify(result)}`); // Return the formatted JSON-RPC response return res.status(200).json({ jsonrpc: "2.0", result: { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }, id: id // Always use the client's exact ID }); } catch (bisonError) { console.error(`[MCP Server CLI] Bison API error: ${bisonError.message}`); return res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: `Error calling Bison API: ${bisonError.message}` }, id: id // Always use the client's exact ID }); } } else { // Tool not found return res.status(404).json({ jsonrpc: "2.0", error: { code: -32601, message: `Tool not found: ${toolName}` }, id: id // Always use the client's exact ID }); } } catch (error) { console.error(`[MCP Server CLI] Error handling request: ${error.message}`); console.error(error.stack); // Send an error response if headers haven't been sent yet if (!res.headersSent) { res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: `Internal server error: ${error.message}` }, id: id // Always use the client's exact ID }); } } }); // Add SSE endpoint for streaming connections app.get("/mcp", (req, res) => { const requestId = crypto.randomUUID().substring(0, 8); console.error(`[DEBUG][${requestId}] SSE connection started from ${req.ip} at ${new Date().toISOString()}`); console.error(`[DEBUG][${requestId}] Protocol: ${req.protocol}`); console.error(`[DEBUG][${requestId}] Connection header: ${req.headers.connection}`); console.error(`[DEBUG][${requestId}] User-Agent: ${req.headers['user-agent']}`); console.error(`[DEBUG][${requestId}] Headers: ${JSON.stringify(req.headers)}`); console.error(`[DEBUG][${requestId}] Query params: ${JSON.stringify(req.query)}`); // NOTE: Express/Node.js normalizes all headers to lowercase // So the only valid way to check for headers is using the lowercase version console.error(`[DEBUG][${requestId}] x-api-key header value: ${req.headers['x-api-key'] || 'not found'}`); console.error(`[DEBUG][${requestId}] apiKey query param: ${req.query.apiKey || 'not found'}`); console.error(`[DEBUG][${requestId}] Client socket info: Remote ${req.socket.remoteAddress}:${req.socket.remotePort}, Local ${req.socket.localAddress}:${req.socket.localPort}`); // Track connection status let connectionState = "initializing"; // Check if this is a Cursor request const isCursorAgent = (req.headers['user-agent'] || '').toLowerCase().includes('cursor') || (req.headers['user-agent'] || '').toLowerCase().includes('node'); console.error(`[DEBUG][${requestId}] Identified as Cursor client: ${isCursorAgent}`); // Extract API key from headers or URL parameters const headerApiKey = req.headers['x-api-key']; // Node.js normalizes headers to lowercase const queryApiKey = req.query.apiKey || req.query.api_key || req.query.key; let apiKey = headerApiKey || queryApiKey; console.error(`[DEBUG][${requestId}] Header API key present: ${!!headerApiKey}`); console.error(`[DEBUG][${requestId}] Query API key present: ${!!queryApiKey}`); console.error(`[DEBUG][${requestId}] Selected API key: ${apiKey || 'none'}`); // Use fallback keys only if no key was provided in the request if (!apiKey) { if (isCursorAgent) { console.error(`[DEBUG][${requestId}] No API key found in Cursor client request. Using default API key.`); apiKey = "iak_Qm3fcmnYJHhDzRs_c_hO7q1M5h_dPuo8"; // Our specific Bison API key } else { console.error(`[DEBUG][${requestId}] No API key found. Using development fallback.`); apiKey = process.env.DEFAULT_API_KEY || argv.apiKey || "development-mode"; } } else { console.error(`[DEBUG][${requestId}] Using API key from request: ${apiKey}`); } // Always set the API key for this connection session if (mcpServerInstance) { console.error(`[DEBUG][${requestId}] Setting API key on MCP server instance: ${apiKey}`); mcpServerInstance.setApiKey(apiKey); } else { console.error(`[DEBUG][${requestId}] WARNING: No MCP server instance available`); } // Track connection timing and status const startTime = Date.now(); // Prevent connection timeout console.error(`[DEBUG][${requestId}] Setting socket parameters to prevent timeouts`); try { // Keep socket alive for a longer time (10 minutes) req.socket.setTimeout(10 * 60 * 1000); console.error(`[DEBUG][${requestId}] Socket timeout set to 10 minutes`); req.socket.setNoDelay(true); console.error(`[DEBUG][${requestId}] Socket NoDelay set to true`); req.socket.setKeepAlive(true, 30000); // Send TCP keepalive every 30 seconds console.error(`[DEBUG][${requestId}] Socket KeepAlive set to true with 30s interval`); } catch (socketError) { console.error(`[DEBUG][${requestId}] Error setting socket parameters: ${socketError.message}`); } // Tracking variables to debug connection issues let messageCount = 0; let pingCount = 0; let lastActivity = Date.now(); let isConnectionClosed = false; // Set up the SSE connection with proper headers - simplify for better compatibility console.error(`[DEBUG][${requestId}] Writing SSE headers to response at ${new Date().toISOString()}`); try { // For Cursor, use the minimal headers needed if (isCursorAgent) { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }); } else { // For other clients, use the full set of headers res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache, no-transform', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', // Disable Nginx buffering 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Cache-Control, Pragma, Origin, Authorization, Content-Type, X-Requested-With, X-API-Key', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS' }); } console.error(`[DEBUG][${requestId}] Headers written successfully`); connectionState = "headers_sent"; } catch (headerError) { console.error(`[DEBUG][${requestId}] Error writing headers: ${headerError.message}`); return; } // For Cursor, immediately send a ListOfferings response if (isCursorAgent) { try { // Send empty line to start res.write(':\n\n'); console.error(`[DEBUG][${requestId}] Initial comment sent`); // Send connected event res.write(`event: connected\ndata: {"status":"connected"}\n\n`); console.error(`[DEBUG][${requestId}] Connected event sent`); // No longer sending auto-server-info response // Just send initial ping to establish connection res.write(`event: ping\ndata: ${new Date().toISOString()}\n\n`); pingCount++; console.error(`[DEBUG][${requestId}] Initial ping sent`); connectionState = "cursor_connection_established"; } catch (writeError) { console.error(`[DEBUG][${requestId}] Error sending initial messages to Cursor: ${writeError.message}`); } } else { // For normal clients, just send the standard initial messages try { // Immediately send a simple comment to establish the connection console.error(`[DEBUG][${requestId}] Sending initial comment line at ${new Date().toISOString()}`); res.write(':\n\n'); console.error(`[DEBUG][${requestId}] Initial comment sent successfully`); connectionState = "comment_sent"; // Then send a quick connected event console.error(`[DEBUG][${requestId}] Sending connected event at ${new Date().toISOString()}`); res.write(`event: connected\ndata: {"status":"connected"}\n\n`); console.error(`[DEBUG][${requestId}] Connected event sent successfully`); connectionState = "connected_event_sent"; // Send a ping right away to confirm connection console.error(`[DEBUG][${requestId}] Sending initial ping event at ${new Date().toISOString()}`); res.write(`event: ping\ndata: ${new Date().toISOString()}\n\n`); pingCount++; console.error(`[DEBUG][${requestId}] Initial ping sent successfully`); connectionState = "ping_sent"; } catch (writeError) { console.error(`[DEBUG][${requestId}] Error sending initial messages: ${writeError.message}`); return; } } // Flush immediately console.error(`[DEBUG][${requestId}] Attempting to flush response at ${new Date().toISOString()}`); if (res.flush) { try { res.flush(); console.error(`[DEBUG][${requestId}] Response flushed successfully`); } catch (flushError) { console.error(`[DEBUG][${requestId}] Error flushing response: ${flushError.message}`); } } else { console.error(`[DEBUG][${requestId}] WARNING: res.flush not available`); } // Function to handle incoming messages - simplified for better compatibility const handleMessage = (data) => { try { messageCount++; lastActivity = Date.now(); console.error(`[DEBUG][${requestId}] Message #${messageCount} received: ${data.substring(0, 100)}...`); const message = JSON.parse(data); console.error(`[DEBUG][${requestId}] Parsed message: ${JSON.stringify(message)}`); // Get the message ID and log detailed info const messageId = message.id; console.error(`[DEBUG][${requestId}] Message ID: ${messageId} (type: ${typeof messageId})`); // Handle ListOfferings requests if (message.method === "ListOfferings") { console.error(`[DEBUG][${requestId}] Processing ListOfferings request with ID: ${messageId}`); const response = { jsonrpc: "2.0", id: messageId, result: { toolDefinitions: [ { name: "get_bison_hello", description: "Calls the GET https://bison.imnodev.com/api/hello endpoint.", inputSchema: { type: "object", properties: { random_string: { type: "string", description: "Dummy parameter for no-parameter tools" } }, required: [] } } ] } }; console.error(`[DEBUG][${requestId}] Sending ListOfferings response for ID: ${messageId}`); try { res.write(`event: message\ndata: ${JSON.stringify(response)}\n\n`); console.error(`[DEBUG][${requestId}] ListOfferings response written`); if (res.flush) { res.flush(); console.error(`[DEBUG][${requestId}] Response flushed`); } console.error(`[DEBUG][${requestId}] ListOfferings response sent`); } catch (writeError) { console.error(`[DEBUG][${requestId}] Error sending ListOfferings response: ${writeError.message}`); } } // Handle initialize method else if (message.method === "initialize") { console.error(`[DEBUG][${requestId}] Processing initialize request with ID: ${messageId}`); const response = { jsonrpc: "2.0", id: messageId, // Always use client's exact ID result: { serverInfo: { name: "Bison Hello Server", vendor: "Me!", version: "0.0.1" }, protocolVersion: "2024-10-07", capabilities: { tools: { canExecuteCode: false }, streaming: { type: "sse" } } } }; console.error(`[DEBUG][${requestId}] Sending initialize response for ID: ${messageId}`); try { res.write(`event: message\ndata: ${JSON.stringify(response)}\n\n`); console.error(`[DEBUG][${requestId}] Initialize response written`); if (res.flush) { res.flush(); console.error(`[DEBUG][${requestId}] Response flushed`); } console.error(`[DEBUG][${requestId}] Initialize response sent`); } catch (writeError) { console.error(`[DEBUG][${requestId}] Error sending initialize response: ${writeError.message}`); } } // Handle tools/call request else if (message.method === "tools/call") { console.error(`[DEBUG][${requestId}] Processing tools/call request with ID: ${messageId}`); // Extract the tool name and arguments const toolName = message.params && message.params.name; const toolArgs = message.params && message.params.arguments || {}; console.error(`[DEBUG][${requestId}] Tool call request: ${toolName} with args ${JSON.stringify(toolArgs)}`); console.error(`[DEBUG][${requestId}] Using API key from connection: ${apiKey || 'none'}`); // Check if the API key is missing or invalid if (!apiKey || apiKey === "development-mode") { console.error(`[DEBUG][${requestId}] Invalid or missing API key for tool call`); // For Cursor clients, use our hardcoded key as a last resort if (isCursorAgent) { apiKey = "iak_Qm3fcmnYJHhDzRs_c_hO7q1M5h_dPuo8"; console.error(`[DEBUG][${requestId}] Using hardcoded API key for Cursor client: ${apiKey}`); // Update the server instance with this key if (mcpServerInstance) { mcpServerInstance.setApiKey(apiKey); } } } // Check if it's our supported tool if (toolName === "get_bison_hello") { console.error(`[DEBUG][${requestId}] Calling tool directly via McpServer instance`); // Call the tool directly through mcpServerInstance mcpServerInstance.callTool(toolName, toolArgs) .then(result => { console.error(`[DEBUG][${requestId}] Tool call succeeded: ${JSON.stringify(result)}`); const response = { jsonrpc: "2.0", id: messageId, result: result }; console.error(`[DEBUG][${requestId}] Sending successful tool response for ID: ${messageId}`); res.write(`event: message\ndata: ${JSON.stringify(response)}\n\n`); if (res.flush) { res.flush(); console.error(`[DEBUG][${requestId}] Response flushed`); } console.error(`[DEBUG][${requestId}] Tool response sent`); }) .catch(error => { console.error(`[DEBUG][${requestId}] Tool call failed: ${error.message}`); const response = { jsonrpc: "2.0", id: messageId, error: { code: -32603, message: `Error: ${error.message}` } }; console.error(`[DEBUG][${requestId}] Sending error response for ID: ${messageId}`); res.write(`event: message\ndata: ${JSON.stringify(response)}\n\n`); if (res.flush) { res.flush(); console.error(`[DEBUG][${requestId}] Response flushed`); } console.error(`[DEBUG][${requestId}] Error response sent`); }); } else { // Tool not found console.error(`[DEBUG][${requestId}] Tool not found: ${toolName}`); const response = { jsonrpc: "2.0", id: messageId, error: { code: -32601, message: `Tool not found: ${toolName}` } }; console.error(`[DEBUG][${requestId}] Sending tool not found response for ID: ${messageId}`); res.write(`event: message\ndata: ${JSON.stringify(response)}\n\n`); if (res.flush) { res.flush(); console.error(`[DEBUG][${requestId}] Response flushed`); } console.error(`[DEBUG][${requestId}] Tool not found response sent`); } } // Handle initialized notification else if (message.method === "notifications/initialized") { console.error(`[DEBUG][${requestId}] Processing notifications/initialized request with ID: ${messageId}`); const response = { jsonrpc: "2.0", id: messageId, result: {} }; console.error(`[DEBUG][${requestId}] Sending initialized notification response for ID: ${messageId}`); try { res.write(`event: message\ndata: ${JSON.stringify(response)}\n\n`); console.error(`[DEBUG][${requestId}] Initialized notification response written`); if (res.flush) { res.flush(); console.error(`[DEBUG][${requestId}] Response flushed`); } console.error(`[DEBUG][${requestId}] Initialized notification response sent`); } catch (writeError) { console.error(`[DEBUG][${requestId}] Error sending initialized notification response: ${writeError.message}`); } } // Handle tools/list request else if (message.method === "tools/list") { console.error(`[DEBUG][${requestId}] Processing tools/list request with ID: ${messageId}`); const response = { jsonrpc: "2.0", id: messageId, result: { tools: [ { name: "get_bison_hello", description: "Calls the GET https://bison.imnodev.com/api/hello endpoint.", inputSchema: { type: "object", properties: { random_string: { type: "string", description: "Dummy parameter for no-parameter tools" } }, required: [] } } ] } }; console.error(`[DEBUG][${requestId}] Sending tools/list response for ID: ${messageId}`); try { res.write(`event: message\ndata: ${JSON.stringify(response)}\n\n`); console.error(`[DEBUG][${requestId}] Tools list response written`); if (res.flush) { res.flush(); console.error(`[DEBUG][${requestId}] Response flushed`); } console.error(`[DEBUG][${requestId}] Tools list response sent`); } catch (writeError) { console.error(`[DEBUG][${requestId}] Error sending tools list response: ${writeError.message}`); } } else { console.error(`[DEBUG][${requestId}] Unknown method: ${message.method}`); } } catch (error) { console.error(`[DEBUG][${requestId}] Error handling message: ${error.message}`); console.error(`[DEBUG][${requestId}] Raw data: ${data}`); } }; // Set up data handling console.error(`[DEBUG][${requestId}] Setting up data event handler at ${new Date().toISOString()}`); req.on('data', (chunk) => { const data = chunk.toString(); console.error(`[DEBUG][${requestId}] Data received (${data.length} bytes) at ${new Date().toISOString()}`); if (data.trim()) { handleMessage(data); } else { console.error(`[DEBUG][${requestId}] Empty data chunk received (heartbeat?)`); } }); // For Cursor, use a more frequent ping to maintain connection const pingInterval = isCursorAgent ? 2000 : 5000; // 2 seconds for Cursor, 5 seconds for others console.error(`[DEBUG][${requestId}] Setting up ping interval (${pingInterval}ms) at ${new Date().toISOString()}`); const pingIntervalId = setInterval(() => { if (isConnectionClosed) { console.error(`[DEBUG][${requestId}] Skipping ping as connection is already closed`); return; } pingCount++; const now = Date.now(); const timeSinceLastActivity = now - lastActivity; const uptime = now - startTime; console.error(`[DEBUG][${requestId}] Sending ping #${pingCount}. Connection uptime: ${uptime}ms. Time since last activity: ${timeSinceLastActivity}ms`); try { res.write(':\n\n'); res.write(`event: ping\ndata: ${new Date().toISOString()}\n\n`); console.error(`[DEBUG][${requestId}] Ping #${pingCount} written`); if (res.flush) { res.flush(); console.error(`[DEBUG][${requestId}] Ping #${pingCount} flushed`); } } catch (writeError) { console.error(`[DEBUG][${requestId}] Error sending ping #${pingCount}: ${writeError.message}`); // Connection may be dead unless this is Cursor if (!isCursorAgent) { isConnectionClosed = true; clearInterval(pingIntervalId); console.error(`[DEBUG][${requestId}] Cleared ping interval due to write error`); } else { console.error(`[DEBUG][${requestId}] Ignoring write error for Cursor client to maintain connection`); } } }, pingInterval); // Clean up on connection close console.error(`[DEBUG][${requestId}] Setting up close event handler at ${new Date().toISOString()}`); req.on('close', () => { isConnectionClosed = true; const connectionDuration = Date.now() - startTime; console.error(`[DEBUG][${requestId}] SSE connection closed at ${new Date().toISOString()}`); console.error(`[DEBUG][${requestId}] Connection duration: ${connectionDuration}ms`); console.error(`[DEBUG][${requestId}] Final connection state: ${connectionState}`); console.error(`[DEBUG][${requestId}] Stats: ${messageCount} messages received, ${pingCount} pings sent.`); clearInterval(pingIntervalId); console.error(`[DEBUG][${requestId}] Ping interval cleared`); }); // Handle errors console.error(`[DEBUG][${requestId}] Setting up error event handlers at ${new Date().toISOString()}`); req.on('error', (err) => { console.error(`[DEBUG][${requestId}] SSE connection error at ${new Date().toISOString()}: ${err.message}`); console.error(`[DEBUG][${requestId}] Error stack: ${err.stack}`); isConnectionClosed = true; clearInterval(pingIntervalId); }); res.on('error', (err) => { console.error(`[DEBUG][${requestId}] SSE response error at ${new Date().toISOString()}: ${err.message}`); console.error(`[DEBUG][${requestId}] Error stack: ${err.stack}`); isConnectionClosed = true; clearInterval(pingIntervalId); }); // Check for Nginx or proxy closed connections req.on('aborted', () => { console.error(`[DEBUG][${requestId}] Request aborted by client at ${new Date().toISOString()}`); isConnectionClosed = true; clearInterval(pingIntervalId); }); // Add end event handling req.on('end', () => { console.error(`[DEBUG][${requestId}] Request 'end' event received at ${new Date().toISOString()}`); isConnectionClosed = true; clearInterval(pingIntervalId); }); // Add finish event handling for response res.on('finish', () => { console.error(`[DEBUG][${requestId}] Response 'finish' event received at ${new Date().toISOString()}`); isConnectionClosed = true; clearInterval(pingIntervalId); }); // Add close event handling for response res.on('close', () => { console.error(`[DEBUG][${requestId}] Response 'close' event received at ${new Date().toISOString()}`); isConnectionClosed = true; clearInterval(pingIntervalId); }); console.error(`[DEBUG][${requestId}] SSE connection setup complete and ready for messages at ${new Date().toISOString()}`); }); // Add server info endpoint that Cursor appears to need app.get("/mcp/info", (req, res) => { console.error(`[MCP Server CLI - HTTP ${new Date().toISOString()}] Received GET /mcp/info from ${req.ip}`); console.error(`[MCP Server CLI] Info Headers received: ${JSON.stringify(req.headers)}`); // Return server information in the exact format expected by MCP protocol res.status(200).json({ serverInfo: { name: "Bison Hello Server", vendor: "Me!", version: "0.0.1" }, protocolVersion: "2024-10-07", capabilities: { tools: { canExecuteCode: false }, streaming: { type: "sse" } }, toolDefinitions: [ { name: "get_bison_hello", description: "Calls the GET https://bison.imnodev.com/api/hello endpoint.", inputSchema: { type: "object", properties: { random_string: { type: "string", description: "Dummy parameter for no-parameter tools" } }, required: [] } } ] }); }); app.get("/health", (req, res) => { console.error(`[MCP Server CLI - HTTP ${new Date().toISOString()}] Received GET /health from ${req.ip}`); res.status(200).json({ status: "ok", timestamp: new Date().toISOString() }); }); return new Promise((resolve, reject) => { const server = app.listen(port, () => { console.error(`[MCP Server CLI] MCP HTTP server listening on port ${port}.`); console.error(`[MCP Server CLI] MCP endpoint: http://localhost:${port}/mcp`); console.error(`[MCP Server CLI] Health check: http://localhost:${port}/health`); console.error("[MCP Server CLI] Server setup completed for HTTP mode."); // Do not resolve() here, so the promise stays pending and PM2 keeps the app alive. }); server.on('error', (err) => { console.error("[MCP Server CLI] HTTP server error (e.g., EADDRINUSE or other crash):", err.stack || err); reject(err); // This will make the main start().catch() handle it. }); }); } // This line is no longer reached if start() returns a promise that doesn't resolve quickly. // console.error("[MCP Server CLI] Server setup completed for the selected mode."); } console.error("[MCP Server CLI] Calling start function..."); start().catch(error => { console.error("[MCP Server CLI] Critical failure in start function promise:", error.stack || error); process.exit(1); });