Skip to main content
Glama
index.ts8.51 kB
#!/usr/bin/env node import { format } from "util"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { WebSocket } from "ws"; import { registerServerTools } from "./tools.js"; // Custom logger function that logs to both console and file function log(level: string, ...args: any[]) { const timestamp = new Date().toISOString(); const message = format(...args); const logMessage = `[${timestamp}] [${level}] ${message}`; // Log to console if (level === "ERROR") { console.error(logMessage); } else { console.log(logMessage); } } // Create logger convenience methods const logger = { info: (...args: any[]) => log("INFO", ...args), error: (...args: any[]) => log("ERROR", ...args), debug: (...args: any[]) => log("DEBUG", ...args), }; type rpcResponse = { client: string; rpc: { channel: number; response: { value: any }; }; }; type rpcStartedResponse = { started: boolean; }; type rpcLoginResponse = { login: boolean; accoutCreated: boolean; }; // Log the start of the application logger.info("=== MCP Server starting ==="); // Create the MCP server with stdio transport async function main() { logger.info("Creating MCP server..."); // Create server with name and version const server = new McpServer({ name: "MCP Demo Server", version: "1.0.0", }); logger.info("MCP Server created"); // WebSocket connection let wsConnection: WebSocket | null = null; const rpcMap = new Map<number, (response: any) => void>(); let rpcId = 0; logger.debug("Initial state: wsConnection=null, rpcMap=empty, rpcId=0"); // Connect to WebSocket function connectWebsocket() { logger.info("Connecting to WebSocket at ws://127.0.0.1:8765..."); const ws = new WebSocket("ws://127.0.0.1:8765"); ws.on("open", () => { logger.info("WebSocket connection established"); wsConnection = ws; logger.debug("Sending login message"); ws.send(JSON.stringify({ login: { intention: "login" } })); logger.debug("Login message sent"); }); ws.on("message", (data) => { logger.debug("WebSocket message received"); logger.debug("Raw data: %s", data.toString().substring(0, 200)); try { const parsedData: rpcResponse | rpcStartedResponse | rpcLoginResponse = JSON.parse(data.toString()); logger.debug("RPC response: %j", parsedData); logger.debug("RPC map: %j", rpcMap); if ("login" in parsedData) { logger.info("Login response: %j", parsedData); } else if ("started" in parsedData) { logger.info("Server started: ", parsedData.started); } else if ( parsedData.client === "mcp-server" && parsedData.rpc && parsedData.rpc.channel !== undefined && rpcMap.has(parsedData.rpc.channel) ) { const channel = parsedData.rpc.channel; logger.debug("Found handler for channel %d", channel); const handler = rpcMap.get(channel); if (handler) { logger.debug("Handling RPC response: %j", parsedData.rpc.response); handler(parsedData.rpc.response); // Remove one-time handlers after they're called rpcMap.delete(channel); logger.debug("Removed handler for channel %d", channel); logger.debug( "RPC map keys after removal: %s", Array.from(rpcMap.keys()) ); } } else { logger.debug( "Unexpected message or no handler found: %j", parsedData ); if (parsedData.rpc && parsedData.rpc.channel) { logger.debug( "Channel %d not found in rpcMap", parsedData.rpc.channel ); logger.debug("Available channels: %s", Array.from(rpcMap.keys())); } } } catch (error) { logger.error("Error processing WebSocket message: %o", error); } }); ws.on("close", (code, reason) => { logger.info( "WebSocket connection closed (code: %d, reason: %s)", code, reason || "No reason provided" ); wsConnection = null; logger.info("Attempting to reconnect in 5 seconds..."); setTimeout(connectWebsocket, 5000); }); ws.on("error", (err) => { logger.error("WebSocket error: %o", err); ws.close(); }); return ws; } // Initialize WebSocket connection connectWebsocket(); // Function to send RPC request and get response async function sendRpcRequest( call: string, params?: Array<any> ): Promise<any> { logger.debug( "sendRpcRequest called with: call=%s, params=%j", call, params ); logger.debug( "WebSocket readyState: %s", wsConnection ? wsConnection.readyState : "null" ); return new Promise((resolve, reject) => { logger.debug("Inside Promise constructor"); if (!wsConnection || wsConnection.readyState !== WebSocket.OPEN) { logger.error( "WebSocket connection not available, readyState: %s", wsConnection ? wsConnection.readyState : "null" ); reject(new Error("WebSocket connection not available")); return; } const channel = rpcId++; logger.debug("Assigned channel: %d", channel); // Store the resolver in the rpcMap rpcMap.set(channel, resolve); logger.debug("Added handler to rpcMap for channel %d", channel); logger.debug("RPC map keys after adding: %s", Array.from(rpcMap.keys())); // Send the request const request = { client: "mcp-server", rpc: { channel, call, args: params }, }; logger.debug("Sending request: %j", request); try { wsConnection.send(JSON.stringify(request)); logger.debug("Request sent successfully for channel %d", channel); } catch (error) { logger.error("Error sending WebSocket message: %o", error); rpcMap.delete(channel); logger.debug( "Removed handler for channel %d due to send error", channel ); reject(new Error(`Failed to send WebSocket message: ${error}`)); return; } // Set timeout to clean up if no response received logger.debug("Setting timeout for channel %d", channel); setTimeout(() => { if (rpcMap.has(channel)) { logger.error("RPC request timed out for channel %d", channel); rpcMap.delete(channel); logger.debug( "Removed handler for channel %d due to timeout", channel ); reject(new Error(`RPC request timed out: ${call}`)); } else { logger.debug( "Timeout handler called for channel %d, but handler already removed", channel ); } }, 30000); // 30 seconds timeout logger.debug("Timeout set for channel %d", channel); }); } try { logger.info("Registering tools..."); registerServerTools(server, sendRpcRequest, logger); // Add a simple text resource server.resource("help", "help://main", async (uri) => ({ contents: [ { uri: uri.href, text: `# MCP Server Help\n\nThis server provides tools to interact with the Routine API via WebSocket RPC.`, }, ], })); // Start the server with stdio transport logger.info("Starting MCP server on stdio..."); const transport = new StdioServerTransport(); await server.connect(transport); logger.info("MCP server started and ready to receive requests"); } catch (error) { logger.error("Error starting MCP server: %o", error); process.exit(1); } } // Handle graceful shutdown process.on("SIGINT", () => { logger.info("Server shutting down..."); process.exit(0); }); process.on("SIGTERM", () => { logger.info("Server shutting down..."); process.exit(0); }); // Handle uncaught exceptions process.on("uncaughtException", (err) => { logger.error("Uncaught exception: %o", err); process.exit(1); }); // Handle unhandled promise rejections process.on("unhandledRejection", (reason, _promise) => { logger.error("Unhandled promise rejection: %o", reason); process.exit(1); }); // Start the server main().catch((err) => { logger.error("Uncaught error: %o", err); process.exit(1); });

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/routineco/mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server