Skip to main content
Glama

MCP Firebird

index.ts19.5 kB
/** * MCP Firebird Server Implementation * Main server module that initializes and configures the MCP server */ import { z } from 'zod'; // --- Global Error Handlers --- import { createLogger } from '../utils/logger.js'; import { MCPError } from '../utils/errors.js'; const logger = createLogger('server:index'); // Set up global error handlers process.on('uncaughtException', (err, origin) => { logger.error(`Uncaught exception: ${err instanceof Error ? err.message : String(err)}`, { error: err, origin }); logger.error('Server will exit due to uncaught exception'); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { logger.error('Unhandled promise rejection', { reason, promise }); }); // ------------------------------------ // --- SDK Imports --- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import express from "express"; import cors from "cors"; import crypto from "crypto"; // SDK types will be imported as needed // --- Local Imports --- import { type ToolDefinition as DbToolDefinition } from '../tools/database.js'; import { type ToolDefinition as MetaToolDefinition } from '../tools/metadata.js'; import { type PromptDefinition } from '../prompts/types.js'; import { setupDatabaseResources, type ResourceDefinition } from '../resources/database.js'; import { setupDatabaseTools } from '../tools/database.js'; import { setupMetadataTools } from '../tools/metadata.js'; import { setupDatabasePrompts } from '../prompts/database.js'; import { setupSqlPrompts } from '../prompts/sql.js'; import { initSecurity } from '../security/index.js'; import { ConfigError } from '../utils/errors.js'; import pkg from '../../package.json' with { type: 'json' }; /** * Factory function to create a configured MCP server instance * @returns A configured McpServer instance */ async function createMcpServerInstance(): Promise<any> { logger.debug('Creating new MCP server instance...'); // Load tools, prompts and resources const databaseTools = setupDatabaseTools(); const metadataTools = setupMetadataTools(databaseTools); const databasePrompts = setupDatabasePrompts(); const sqlPrompts = setupSqlPrompts(); // Temporarily disable resources due to path-to-regexp compatibility issues const allResources: Map<string, ResourceDefinition> = new Map(); const allPrompts = new Map<string, PromptDefinition>([...databasePrompts, ...sqlPrompts]); const allTools = new Map<string, DbToolDefinition | MetaToolDefinition>([...databaseTools, ...metadataTools]); // Create MCP server instance const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js"); const server = new McpServer({ name: pkg.name, version: pkg.version, capabilities: { tools: { listChanged: true }, prompts: { listChanged: true }, resources: { listChanged: true, subscribe: false } } }); // Register tools, prompts and resources logger.debug('Registering tools, prompts and resources...'); // Tools - using registerTool (modern method) for (const [name, tool] of allTools.entries()) { server.registerTool( name, { title: tool.title || name, description: tool.description, inputSchema: (tool.inputSchema && tool.inputSchema instanceof z.ZodObject) ? tool.inputSchema.shape : {} }, async (args: any): Promise<{ content: any[], isError?: boolean }> => { try { const result = await tool.handler(args); if (typeof result === 'object' && result !== null && 'content' in result) { return result; } return { content: [{ type: 'text', text: JSON.stringify(result) }] }; } catch (error) { logger.error(`Error executing tool ${name}: ${error instanceof Error ? error.message : String(error)}`, { error }); const message = error instanceof Error ? error.message : 'Unknown error'; return { content: [{ type: "text", text: `Error executing tool ${name}: ${message}` }], isError: true }; } } ); } // Prompts - using registerPrompt (modern method) for (const [name, promptDef] of allPrompts.entries()) { server.registerPrompt( name, { title: promptDef.title || name, description: promptDef.description, argsSchema: (promptDef.inputSchema && promptDef.inputSchema instanceof z.ZodObject) ? promptDef.inputSchema.shape : {} }, async (args: any) => { try { let result; if (!args || Object.keys(args).length === 0) { result = { messages: [ { role: 'assistant', content: { type: 'text', text: `Prompt '${name}' metadata: ${promptDef.description}` } } ] }; } else { result = await promptDef.handler(args); } if (!result || !result.messages || !Array.isArray(result.messages)) { return { messages: [ { role: 'assistant', content: { type: 'text', text: `Internal error: invalid response format` } } ] }; } const safeMessages = result.messages.map((msg: any) => ({ role: (msg.role === 'user' || msg.role === 'assistant') ? msg.role : 'assistant', content: (msg.content && typeof msg.content === 'object' && msg.content.type === 'text') ? msg.content : { type: 'text', text: String(msg.content) } })); return { messages: safeMessages }; } catch (error) { logger.error(`Error executing prompt ${name}: ${error instanceof Error ? error.message : String(error)}`, { error }); return { messages: [ { role: 'assistant', content: { type: 'text', text: `Error executing prompt: ${error instanceof Error ? error.message : String(error)}` } } ] }; } } ); } // Resources - temporarily disabled due to path-to-regexp compatibility issues logger.info(`Skipping ${allResources.size} resources - Resource registration temporarily disabled in alpha version`); // TODO: Re-enable resources once path-to-regexp issues are resolved logger.debug(`Server instance created with ${allTools.size} tools, ${allPrompts.size} prompts, ${allResources.size} resources`); return server; } /** * Main function to start the MCP Firebird server * @returns A promise that resolves when the server is started */ export async function main() { logger.info(`Starting MCP Firebird Server - Name: ${pkg.name}, Version: ${pkg.version}`); try { // Initialize security module logger.info('Initializing security module...'); await initSecurity(); // Determine transport type const transportType = process.env.TRANSPORT_TYPE?.toLowerCase() || 'stdio'; logger.info(`Configuring ${transportType} transport...`); if (transportType === 'stdio') { // Use stdio transport with a single server instance logger.info('Starting stdio transport...'); const server = await createMcpServerInstance(); const transport = new StdioServerTransport(); await server.connect(transport); // Setup cleanup for stdio const cleanup = async () => { logger.info('Closing stdio transport...'); await server.close(); logger.info('Server closed successfully'); }; process.on('SIGINT', async () => { logger.info('Received SIGINT signal, cleaning up...'); await cleanup(); process.exit(0); }); process.on('SIGTERM', async () => { logger.info('Received SIGTERM signal, cleaning up...'); await cleanup(); process.exit(0); }); logger.info('MCP Firebird server with stdio transport ready to receive requests.'); } else if (transportType === 'sse' || transportType === 'http' || transportType === 'unified') { // Use backwards compatible server for HTTP-based transports logger.info('Starting backwards compatible server with SSE and Streamable HTTP...'); const port = parseInt(process.env.SSE_PORT || process.env.HTTP_PORT || '3003', 10); if (isNaN(port)) { throw new ConfigError(`Invalid port: ${process.env.SSE_PORT || process.env.HTTP_PORT}`); } await startBackwardsCompatibleServer(port); // Setup cleanup for backwards compatible server process.on('SIGINT', async () => { logger.info('Received SIGINT signal, cleaning up...'); process.exit(0); }); process.on('SIGTERM', async () => { logger.info('Received SIGTERM signal, cleaning up...'); process.exit(0); }); logger.info('MCP Firebird backwards compatible server ready to receive requests.'); } else { throw new ConfigError( `Unsupported transport type: ${transportType}. Supported types are 'stdio', 'sse', 'http', and 'unified'.`, undefined, { transportType } ); } } catch (error) { if (error instanceof MCPError) { logger.error(`Fatal error during server initialization: ${error.message}`, { type: error.type, context: error.context, originalError: error.originalError }); } else if (error instanceof Error) { logger.error(`Fatal error during server initialization: ${error.message}`, { stack: error.stack }); } else { logger.error(`Fatal error during server initialization: ${String(error)}`); } // Exit with error code process.exit(1); } } /** * Starts a backwards compatible server that supports both Streamable HTTP and SSE transports * Based on the official MCP TypeScript SDK documentation */ async function startBackwardsCompatibleServer(port: number): Promise<void> { const app = express(); // Configure CORS to allow web clients app.use(cors({ origin: '*', methods: ['GET', 'POST', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'mcp-session-id', 'Cache-Control', 'Accept'], credentials: false })); app.use(express.json()); // Middleware to handle Smithery configuration via query parameters // Smithery passes configuration as query params using dot-notation // Example: ?host=localhost&port=3050&database=/path/to/db.fdb app.use((req, res, next) => { const queryParams = req.query; // Map Smithery query parameters to environment variables if (queryParams.host && typeof queryParams.host === 'string') { process.env.FIREBIRD_HOST = queryParams.host; logger.debug(`Set FIREBIRD_HOST from query param: ${queryParams.host}`); } if (queryParams.port && typeof queryParams.port === 'string') { process.env.FIREBIRD_PORT = queryParams.port; logger.debug(`Set FIREBIRD_PORT from query param: ${queryParams.port}`); } if (queryParams.database && typeof queryParams.database === 'string') { process.env.FIREBIRD_DATABASE = queryParams.database; logger.debug(`Set FIREBIRD_DATABASE from query param: ${queryParams.database}`); } if (queryParams.user && typeof queryParams.user === 'string') { process.env.FIREBIRD_USER = queryParams.user; logger.debug(`Set FIREBIRD_USER from query param: ${queryParams.user}`); } if (queryParams.password && typeof queryParams.password === 'string') { process.env.FIREBIRD_PASSWORD = queryParams.password; logger.debug(`Set FIREBIRD_PASSWORD from query param: [REDACTED]`); } if (queryParams.useNativeDriver && typeof queryParams.useNativeDriver === 'string') { process.env.USE_NATIVE_DRIVER = queryParams.useNativeDriver; logger.debug(`Set USE_NATIVE_DRIVER from query param: ${queryParams.useNativeDriver}`); } if (queryParams.logLevel && typeof queryParams.logLevel === 'string') { process.env.LOG_LEVEL = queryParams.logLevel; logger.debug(`Set LOG_LEVEL from query param: ${queryParams.logLevel}`); } next(); }); // Store transports for each session type const transports = { streamable: {} as Record<string, StreamableHTTPServerTransport>, sse: {} as Record<string, SSEServerTransport> }; // Modern Streamable HTTP endpoint app.all('/mcp', async (req, res) => { try { // Check for existing session ID const sessionId = req.headers['mcp-session-id'] as string | undefined; let transport: StreamableHTTPServerTransport; if (sessionId && transports.streamable[sessionId]) { // Reuse existing transport transport = transports.streamable[sessionId]; } else if (!sessionId && req.method === 'POST') { // New initialization request transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID(), }); // Store the transport by session ID when initialized transport.onclose = () => { if (transport.sessionId) { delete transports.streamable[transport.sessionId]; logger.debug(`Cleaned up streamable transport for session: ${transport.sessionId}`); } }; // Create and connect server const server = await createMcpServerInstance(); await server.connect(transport); // Store transport after connection if (transport.sessionId) { transports.streamable[transport.sessionId] = transport; logger.debug(`Created streamable transport for session: ${transport.sessionId}`); } } else { // Invalid request res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided', }, id: null, }); return; } // Handle the request await transport.handleRequest(req, res, req.body); } catch (error) { logger.error('Error handling Streamable HTTP request:', { error }); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error', }, id: null, }); } } }); // Legacy SSE endpoint for older clients app.get('/sse', async (req, res) => { try { logger.info('Creating SSE transport for legacy client'); // Create SSE transport for legacy clients const transport = new SSEServerTransport('/messages', res); const sessionId = transport.sessionId || crypto.randomUUID(); transports.sse[sessionId] = transport; res.on("close", () => { delete transports.sse[sessionId]; logger.debug(`Cleaned up SSE transport for session: ${sessionId}`); }); // Create and connect server const server = await createMcpServerInstance(); await server.connect(transport); logger.info(`SSE transport connected for session: ${sessionId}`); } catch (error) { logger.error('Error establishing SSE connection:', { error }); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error', }, id: null, }); } } }); // Legacy message endpoint for older clients app.post('/messages', async (req, res) => { try { const sessionId = req.query.sessionId as string; const transport = transports.sse[sessionId]; if (transport) { await transport.handlePostMessage(req, res, req.body); } else { res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'No transport found for sessionId', }, id: null, }); } } catch (error) { logger.error('Error handling SSE message:', { error }); if (!res.headersSent) { res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error', }, id: null, }); } } }); // Start the server app.listen(port, () => { logger.info(`MCP Backwards Compatible Server listening on port ${port}`); logger.info('Endpoints:'); logger.info(' - Modern Streamable HTTP: POST/GET/DELETE /mcp'); logger.info(' - Legacy SSE: GET /sse'); logger.info(' - Legacy Messages: POST /messages'); }); }

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/PuroDelphi/mcpFirebird'

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