Skip to main content
Glama
server.ts9.73 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import express from "express"; import path from "path"; import { readFileSync } from "fs"; import { fileURLToPath } from "url"; import { ConnectorManager } from "./connectors/manager.js"; import { ConnectorRegistry } from "./connectors/interface.js"; import { resolveTransport, resolvePort, redactDSN, resolveSourceConfigs, isReadOnlyMode, isDemoMode } from "./config/env.js"; import { buildDSNFromSource } from "./config/toml-loader.js"; import { registerTools } from "./tools/index.js"; import { listSources, getSource } from "./api/sources.js"; import { listRequests } from "./api/requests.js"; // Create __dirname equivalent for ES modules const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Load package.json to get version const packageJsonPath = path.join(__dirname, "..", "package.json"); const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")); // Server info export const SERVER_NAME = "DBHub MCP Server"; export const SERVER_VERSION = packageJson.version; /** * Generate ASCII art banner with version information */ export function generateBanner(version: string, modes: string[] = []): string { // Create a mode string that includes all active modes const modeText = modes.length > 0 ? ` [${modes.join(' | ')}]` : ''; return ` _____ ____ _ _ _ | __ \\| _ \\| | | | | | | | | | |_) | |_| |_ _| |__ | | | | _ <| _ | | | | '_ \\ | |__| | |_) | | | | |_| | |_) | |_____/|____/|_| |_|\\__,_|_.__/ v${version}${modeText} - Universal Database MCP Server `; } /** * Initialize and start the DBHub server */ export async function main(): Promise<void> { try { // Resolve source configurations from TOML or fallback to single DSN const sourceConfigsData = await resolveSourceConfigs(); if (!sourceConfigsData) { const samples = ConnectorRegistry.getAllSampleDSNs(); const sampleFormats = Object.entries(samples) .map(([id, dsn]) => ` - ${id}: ${dsn}`) .join("\n"); console.error(` ERROR: Database connection configuration is required. Please provide configuration in one of these ways (in order of priority): 1. Use demo mode: --demo (uses in-memory SQLite with sample employee database) 2. TOML config file: --config=path/to/dbhub.toml or ./dbhub.toml 3. Command line argument: --dsn="your-connection-string" 4. Environment variable: export DSN="your-connection-string" 5. .env file: DSN=your-connection-string Example DSN formats: ${sampleFormats} Example TOML config (dbhub.toml): [[sources]] id = "my_db" dsn = "postgres://user:pass@localhost:5432/dbname" See documentation for more details on configuring database connections. `); process.exit(1); } // Create MCP server factory function for HTTP transport const createServer = () => { const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION, }); // Register tools registerTools(server); return server; }; // Create server factory function (will be used for both STDIO and HTTP transports) // Create connector manager and connect to database(s) const connectorManager = new ConnectorManager(); const sources = sourceConfigsData.sources; console.error(`Configuration source: ${sourceConfigsData.source}`); // Connect to database(s) - works uniformly for all modes (demo, single DSN, multi-source TOML) console.error(`Connecting to ${sources.length} database source(s)...`); for (const source of sources) { const dsn = source.dsn || buildDSNFromSource(source); console.error(` - ${source.id}: ${redactDSN(dsn)}`); } await connectorManager.connectWithSources(sources); // Resolve transport type (for MCP server) const transportData = resolveTransport(); console.error(`MCP transport: ${transportData.type}`); // Resolve port for HTTP server (only needed for http transport) const portData = transportData.type === "http" ? resolvePort() : null; if (portData) { console.error(`HTTP server port: ${portData.port} (source: ${portData.source})`); } // Print ASCII art banner with version and slogan const readonly = isReadOnlyMode(); // Collect active modes const activeModes: string[] = []; const modeDescriptions: string[] = []; const isDemo = isDemoMode(); if (isDemo) { activeModes.push("DEMO"); modeDescriptions.push("using sample employee database"); } if (readonly) { activeModes.push("READ-ONLY"); modeDescriptions.push("only read only queries allowed"); } // Multi-source mode indicator if (sources.length > 1) { console.error(`Multi-source mode: ${sources.length} databases configured`); } // Output mode information if (activeModes.length > 0) { console.error(`Running in ${activeModes.join(' and ')} mode - ${modeDescriptions.join(', ')}`); } console.error(generateBanner(SERVER_VERSION, activeModes)); // Set up transport-specific server if (transportData.type === "http") { // HTTP transport: Start Express server with MCP endpoint and admin console const port = portData!.port; const app = express(); // Enable JSON parsing app.use(express.json()); // Handle CORS and security headers app.use((req, res, next) => { // Validate Origin header to prevent DNS rebinding attacks const origin = req.headers.origin; if (origin && !origin.startsWith('http://localhost') && !origin.startsWith('https://localhost')) { return res.status(403).json({ error: 'Forbidden origin' }); } res.header('Access-Control-Allow-Origin', origin || 'http://localhost'); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id'); res.header('Access-Control-Allow-Credentials', 'true'); if (req.method === 'OPTIONS') { return res.sendStatus(200); } next(); }); // Serve static frontend files const frontendPath = path.join(__dirname, "public"); app.use(express.static(frontendPath)); // Health check endpoint app.get("/healthz", (req, res) => { res.status(200).send("OK"); }); // Data sources API endpoints app.get("/api/sources", listSources); app.get("/api/sources/:sourceId", getSource); app.get("/api/requests", listRequests); // Main endpoint for streamable HTTP transport // SSE streaming (GET requests) is not supported in stateless mode // Return 405 Method Not Allowed for GET requests to indicate this app.get("/mcp", (req, res) => { res.status(405).json({ error: 'Method Not Allowed', message: 'SSE streaming is not supported in stateless mode. Use POST requests with JSON responses.' }); }); app.post("/mcp", async (req, res) => { try { // In stateless mode, create a new instance of transport and server for each request // to ensure complete isolation. A single instance would cause request ID collisions // when multiple clients connect concurrently. const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, // Disable session management for stateless mode enableJsonResponse: true // Use JSON responses (SSE not supported in stateless mode) }); const server = createServer(); await server.connect(transport); await transport.handleRequest(req, res, req.body); } catch (error) { console.error("Error handling request:", error); if (!res.headersSent) { res.status(500).json({ error: 'Internal server error' }); } } }); // SPA fallback - serve index.html for all non-API routes (production only) // In development, the frontend is served by Vite dev server if (process.env.NODE_ENV !== 'development') { app.get("*", (req, res) => { res.sendFile(path.join(frontendPath, "index.html")); }); } // Start the HTTP server app.listen(port, '0.0.0.0', () => { // In development mode, suggest using the Vite dev server for hot reloading if (process.env.NODE_ENV === 'development') { console.error('Development mode detected!'); console.error(' Admin console dev server (with HMR): http://localhost:5173'); console.error(' Backend API: http://localhost:8080'); console.error(''); } else { console.error(`Admin console at http://0.0.0.0:${port}/`); } console.error(`MCP server endpoint at http://0.0.0.0:${port}/mcp`); }); } else { // STDIO transport: Pure MCP-over-stdio, no HTTP server const server = createServer(); const transport = new StdioServerTransport(); await server.connect(transport); console.error("MCP server running on stdio"); // Listen for SIGINT to gracefully shut down process.on("SIGINT", async () => { console.error("Shutting down..."); await transport.close(); process.exit(0); }); } } catch (err) { console.error("Fatal error:", err); 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/bytebase/dbhub'

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