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
JavaScript
// 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);
});