import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import express, { Request, Response, NextFunction } from 'express'; import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; import { randomUUID } from "node:crypto"; import { readFileSync } from "node:fs"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { InMemoryEventStore } from "@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js"; import getToolAPIServer from "./getToolServer.js"; import bodyParser from 'body-parser'; import { Logger } from "tslog"; import cors from 'cors'; // Load .env file manually (no dotenv dependency) try { const __dirname = dirname(fileURLToPath(import.meta.url)); const envPath = resolve(__dirname, '..', '.env'); const envContent = readFileSync(envPath, 'utf-8'); for (const line of envContent.split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const eqIndex = trimmed.indexOf('='); if (eqIndex === -1) continue; const key = trimmed.slice(0, eqIndex).trim(); const value = trimmed.slice(eqIndex + 1).trim(); if (!process.env[key]) process.env[key] = value; } } catch (e) { // .env file not found — rely on environment variables } const log = new Logger({stylePrettyLogs: false}); const app = express(); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(cors()); function getCredentials(req: Request): { username?: string; password?: string; clientId?: string; clientSecret?: string; baseUrl?: string } { return { username: (req.headers["x-dbplanet-username"] as string) || process.env.DBPLANET_USERNAME, password: (req.headers["x-dbplanet-password"] as string) || process.env.DBPLANET_PASSWORD, clientId: (req.headers["x-dbplanet-client-id"] as string) || process.env.DBPLANET_CLIENT_ID, clientSecret: (req.headers["x-dbplanet-client-secret"] as string) || process.env.DBPLANET_CLIENT_SECRET, baseUrl: (req.headers["x-dbplanet-base-url"] as string) || process.env.DBPLANET_BASE_URL }; } const transports: Record = {}; app.get('/health', (req: Request, res: Response) => { res.status(200).json({ status: 'ok' }); }); app.all('/servers/dbplanet/mcp', async (req: Request, res: Response) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; log.info(`Request to /mcp, session=${sessionId ?? "n/a"}, method=${req.method}`); try { let transport: StreamableHTTPServerTransport; if (sessionId && transports[sessionId]) { transport = transports[sessionId]; } else if (!sessionId && req.method === 'POST' && isInitializeRequest(req.body)) { const eventStore = new InMemoryEventStore(); transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), eventStore, onsessioninitialized: (sessionId) => { log.info('StreamableHTTP session initialized, session=', sessionId); transports[sessionId] = transport; } }); transport.onclose = () => { const sid = transport.sessionId; if (sid && transports[sid]) { log.info(`Transport closed, session=${sessionId}`); delete transports[sid]; } }; const credentials = getCredentials(req); const server = await getToolAPIServer(credentials); await server.connect(transport); } else { res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided', }, id: null, }); return; } // For GET requests (SSE streams), send periodic keep-alive pings // to prevent client-side read timeouts on idle connections. let keepAliveInterval: ReturnType | undefined; if (req.method === 'GET') { keepAliveInterval = setInterval(() => { if (!res.writableEnded) { res.write(':ping\n\n'); } else { clearInterval(keepAliveInterval); } }, 15_000); res.on('close', () => { if (keepAliveInterval) clearInterval(keepAliveInterval); }); } await transport.handleRequest(req, res, req.body); } catch (error) { log.error('Error handling MCP request:', (error as Error).message); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error', }, id: null, }); } } }); app.use((err: Error, req: Request, res: Response, next: NextFunction) => { log.error('Server error:', err.message); res.status(500).json({ error: 'Internal server error', message: process.env.NODE_ENV === 'production' ? undefined : err.message }); }); const PORT = 4000; app.listen(PORT, () => { log.info(`DB Planet MCP server listening on port ${PORT}`); log.info(`\nEndpoint: /servers/dbplanet/mcp\nMethods: GET, POST, DELETE\n`); }); process.on('SIGINT', async () => { log.info('Shutting down server...'); for (const sessionId in transports) { try { log.info(`Closing transport, session=${sessionId}`); await transports[sessionId].close(); delete transports[sessionId]; } catch (error) { log.error(`Error closing transport, session=${sessionId}:`, (error as Error).message); } } log.info('Server shutdown complete'); process.exit(0); });