Skip to main content
Glama
setup.ts7.47 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from '@modelcontextprotocol/sdk/types.js'; import { DatabaseConnection, ServerConfig } from '../types.js'; import { Logger } from '../utils/logger.js'; import { RateLimiter } from '../utils/rate-limiter.js'; import { tools, executeTool } from '../tools/index.js'; export function createMCPServer( connection: DatabaseConnection, logger: Logger, config: ServerConfig ): Server { const server = new Server( { name: 'postgres-scout-mcp', version: '0.1.0', }, { capabilities: { tools: {}, }, } ); const rateLimiter = new RateLimiter( config.rateLimitMaxRequests, config.rateLimitWindowMs, config.enableRateLimit ); server.setRequestHandler(ListToolsRequestSchema, async () => { logger.debug('mcp', 'Listing available tools'); const toolList: Tool[] = Object.entries(tools).map(([name, tool]) => ({ name, description: getToolDescription(name, config.mode), inputSchema: zodToJsonSchema(tool.schema) })); return { tools: toolList }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; logger.info('mcp', `Tool called: ${name}`, { args }); try { rateLimiter.checkLimit(); const result = await executeTool(name, connection, logger, args || {}); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2) } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error('mcp', `Error executing tool ${name}`, { error: errorMessage }); return { content: [ { type: 'text', text: JSON.stringify({ error: errorMessage, tool: name }, null, 2) } ], isError: true }; } }); return server; } export async function startServer(server: Server, logger: Logger): Promise<void> { const transport = new StdioServerTransport(); logger.info('server', 'Starting MCP server'); await server.connect(transport); logger.info('server', 'MCP server running on stdio'); } function getToolDescription(name: string, mode: string): string { const descriptions: Record<string, string> = { // Database operations listDatabases: 'List all databases the user has access to', getDatabaseStats: 'Get comprehensive database statistics including size, cache hit ratio, and connection info', // Schema operations listSchemas: 'List all schemas in the current database', listTables: 'List all tables in a schema with size and statistics', describeTable: 'Get comprehensive table information including columns, constraints, and indexes', // Query operations executeQuery: `Execute SELECT queries${mode === 'read-write' ? ' or write operations' : ' (read-only)'}`, explainQuery: `Analyze query performance using EXPLAIN${mode === 'read-only' ? ' (ANALYZE disabled)' : ' ANALYZE'}`, // Data quality tools findDuplicates: 'Find duplicate rows based on column combinations', findMissingValues: 'Find NULL values or missing data in columns', findOrphans: 'Find orphaned records with invalid foreign key references', checkConstraintViolations: 'Check for rows that would violate a constraint before adding it', analyzeTypeConsistency: 'Analyze if text columns contain consistent data types', // Temporal tools findRecent: 'Find rows within a time window', analyzeTimeSeries: 'Advanced time-series analysis with window functions and anomaly detection', detectSeasonality: 'Detect seasonal patterns in time-series data', // Monitoring tools getCurrentActivity: 'Get current active queries and connections', analyzeLocks: 'Analyze current locks and blocking queries', getIndexUsage: 'Analyze index usage and identify unused indexes', // Relationship tools exploreRelationships: 'Follow foreign key relationships to explore related records', analyzeForeignKeys: 'Analyze foreign key health and performance', // Export tools exportTable: 'Export table data to various formats (CSV, JSON, SQL)', generateInsertStatements: 'Generate INSERT statements for data migration', // Maintenance & health tools analyzeTableBloat: 'Detect table and index bloat for VACUUM planning', suggestVacuum: 'Analyze and recommend VACUUM operations based on dead tuples and bloat', getHealthScore: 'Calculate overall database health score with component breakdown', getSlowQueries: 'Analyze slow queries from pg_stat_statements extension', // Optimization tools suggestIndexes: 'Analyze query patterns and table scans to recommend missing indexes', suggestPartitioning: 'Analyze large tables and recommend partitioning strategies', detectAnomalies: 'Detect anomalies in query performance, connections, and data patterns', optimizeQuery: 'Analyze a specific query and provide optimization recommendations', // Mutation tools (safe write operations) previewUpdate: 'Preview which rows would be affected by an UPDATE without modifying data', previewDelete: 'Preview which rows would be deleted without actually deleting them', safeUpdate: 'Execute UPDATE with safety guards: dry-run mode, maxRows limit, empty WHERE protection. Raw SET strings require allowRawSet=true', safeDelete: 'Execute DELETE with safety guards: dry-run mode, maxRows limit, empty WHERE protection', // Live monitoring tools getLiveMetrics: 'Collect real-time database metrics over a time period with configurable intervals', getHottestTables: 'Identify tables with highest activity during a sample period', getTableMetrics: 'Get comprehensive metrics for a specific table including I/O, scans, and maintenance stats' }; return descriptions[name] || `Execute ${name} operation`; } function zodToJsonSchema(schema: any): any { const shape = schema._def?.shape?.() || {}; const properties: Record<string, any> = {}; const required: string[] = []; for (const [key, value] of Object.entries(shape)) { const field = value as any; properties[key] = convertZodType(field); if (!field._def?.defaultValue) { required.push(key); } } return { type: 'object', properties, required: required.length > 0 ? required : undefined }; } function convertZodType(zodType: any): any { const typeName = zodType._def?.typeName; switch (typeName) { case 'ZodString': return { type: 'string' }; case 'ZodNumber': return { type: 'number' }; case 'ZodBoolean': return { type: 'boolean' }; case 'ZodArray': return { type: 'array', items: convertZodType(zodType._def?.type) }; case 'ZodEnum': return { type: 'string', enum: zodType._def?.values || [] }; case 'ZodOptional': return convertZodType(zodType._def?.innerType); case 'ZodDefault': const inner = convertZodType(zodType._def?.innerType); inner.default = zodType._def?.defaultValue; return inner; default: return { type: 'string' }; } }

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/bluwork/postgres-scout-mcp'

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