Skip to main content
Glama
server.ts9.25 kB
#!/usr/bin/env node /** * HTTP Transport Server - Working Implementation * The Implementor's Rule: Build exactly what works, nothing more */ // CRITICAL: Load environment first import { config } from "dotenv"; config(); import express from "express"; import { randomUUID } from "node:crypto"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; // Import lazy handlers for HTTP import { createLazyHandlers } from "./lazy-handlers"; import { MCPValidationError, MCPDatabaseError, MCPServiceError, isMCPError } from "../infrastructure/errors"; import { registerPrompts } from "../prompts"; import { registerMemoryTools, HandlerSet } from "../shared-tool-definitions"; /** * Create and configure MCP server with tools * Based on index.ts - exact same functionality */ function createMCPServer(): McpServer { const server = new McpServer({ name: "neo4j-memory-server", version: "3.2.0" }); // Register prompts first registerPrompts(server); // Lazy handler factory - safe for tool scanning let handlerPromise: Promise<HandlerSet> | null = null; const getHandlers = async (): Promise<HandlerSet> => { if (!handlerPromise) { handlerPromise = (async () => { // Use lazy handlers that don't initialize until first use const handlers = await createLazyHandlers(); // Only initialize database connection if we have config const hasDbConfig = process.env.NEO4J_URI || process.env.NEO4J_USERNAME; if (hasDbConfig) { const { DIContainer } = await import("../container/di-container"); const container = DIContainer.getInstance(); await container.initializeDatabase(); } return handlers; })(); } return handlerPromise; }; // ============================================================================= // UNIFIED TOOLS IMPLEMENTATION (Exactly 4 tools as specified) // ============================================================================= // Register all memory tools using shared definitions registerMemoryTools(server, getHandlers); return server; } /** * Simple HTTP Transport Server * Based on working patterns from SDK examples */ class SimpleHTTPServer { private app: express.Application; private mcpServer: McpServer; private transports: Map<string, StreamableHTTPServerTransport> = new Map(); constructor() { this.app = express(); this.mcpServer = createMCPServer(); this.setupMiddleware(); this.setupRoutes(); } private setupMiddleware(): void { this.app.use(express.json({ limit: '10mb' })); // CORS this.app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Mcp-Session-Id'); res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id'); if (req.method === 'OPTIONS') { res.status(200).send(); return; } next(); }); } private setupRoutes(): void { // Health check this.app.get('/health', (_req, res) => { res.json({ status: 'healthy', sessions: this.transports.size, transport: 'streamable-http' }); }); // MCP endpoint - stateful session pattern this.app.all('/mcp', async (req, res) => { try { await this.handleMCPRequest(req, res); } catch (error) { let errorCode = -32603; // Default internal error let errorMessage = 'Internal server error'; let errorData: any = undefined; // Detect specific error types if (isMCPError(error)) { const mcpError = error as any; if (mcpError instanceof MCPValidationError) { errorCode = -32602; // Invalid params errorMessage = mcpError.message; errorData = mcpError.data; } else if (mcpError instanceof MCPDatabaseError) { errorCode = -32603; // Internal error errorMessage = 'Database operation failed'; errorData = { category: 'database' }; } else if (mcpError instanceof MCPServiceError) { errorCode = -32603; // Internal error errorMessage = 'Service unavailable'; errorData = { category: 'service', service: (mcpError.data as any)?.service }; } } res.status(500).json({ jsonrpc: "2.0", error: { code: errorCode, message: errorMessage, ...(errorData && { data: errorData }) }, id: null }); } }); } private async handleMCPRequest(req: express.Request, res: express.Response): Promise<void> { const sessionId = req.headers['mcp-session-id'] as string; if (req.method === 'DELETE') { // Session termination if (sessionId && this.transports.has(sessionId)) { const transport = this.transports.get(sessionId)!; try { await transport.close?.(); } catch (error) { // Ignore close errors } this.transports.delete(sessionId); res.status(204).send(); } else { res.status(404).json({ error: 'Session not found' }); } return; } if (req.method === 'GET') { // MCP status endpoint for deployment verification res.json({ status: 'MCP endpoint ready', protocol: 'streamable-http', endpoints: { health: '/health', mcp: '/mcp' }, message: 'Use POST for MCP communication' }); return; } if (req.method === 'POST') { let transport: StreamableHTTPServerTransport; let responseSessionId: string | undefined; try { if (sessionId && this.transports.has(sessionId)) { // Use existing session transport = this.transports.get(sessionId)!; responseSessionId = sessionId; } else if (isInitializeRequest(req.body)) { // Create new session for initialize request responseSessionId = randomUUID(); transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => responseSessionId!, }); // Store transport before connecting this.transports.set(responseSessionId, transport); try { // Connect to MCP server await this.mcpServer.connect(transport); } catch (connectionError) { // Clean up on connection failure this.transports.delete(responseSessionId); const errorMessage = connectionError instanceof Error ? connectionError.message : String(connectionError); res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "MCP server connection failed", data: { reason: errorMessage } }, id: (req.body as any)?.id || null }); return; } } else { res.status(400).json({ jsonrpc: "2.0", error: { code: -32600, message: "Missing session ID" }, id: (req.body as any)?.id || null }); return; } // Set session ID header before handling request if (responseSessionId) { res.setHeader('Mcp-Session-Id', responseSessionId); } // Handle the request using transport await transport.handleRequest(req, res, req.body); } catch (handlingError) { const errorMessage = handlingError instanceof Error ? handlingError.message : String(handlingError); res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Request handling failed", data: { reason: errorMessage } }, id: (req.body as any)?.id || null }); } } } public async start(port: number = 3000): Promise<void> { return new Promise((resolve) => { this.app.listen(port, '0.0.0.0', () => { // Silent startup for deployment compatibility resolve(); }); }); } } // Main entry point const main = async () => { // Silent startup for HTTP server (separate from stdio MCP) const httpServer = new SimpleHTTPServer(); const port = parseInt(process.env.PORT || process.env.HTTP_PORT || '3000'); try { await httpServer.start(port); const cleanup = () => process.exit(0); process.on("SIGINT", cleanup); process.on("SIGTERM", cleanup); } catch (error) { // Silent error handling for deployment compatibility process.exit(1); } }; // Export for testing export { SimpleHTTPServer }; // Run if executed directly if (import.meta.url === `file://${process.argv[1]}`) { main().catch(() => { process.exit(1); }); }

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/sylweriusz/mcp-neo4j-memory-server'

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