UNPKG

mcp-quickbase

Version:

Work with Quickbase via Model Context Protocol

318 lines 10.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = __importDefault(require("express")); const dotenv_1 = __importDefault(require("dotenv")); const cors_1 = __importDefault(require("cors")); const logger_1 = require("./utils/logger"); const quickbase_1 = require("./client/quickbase"); const cache_1 = require("./utils/cache"); const tools_1 = require("./tools"); const mcp_1 = require("./mcp"); // Load environment variables dotenv_1.default.config(); const logger = (0, logger_1.createLogger)('server'); // Initialize Express app const app = (0, express_1.default)(); app.use(express_1.default.json()); app.use((0, cors_1.default)()); // Configuration const PORT = process.env.PORT || 3536; // Changed from 3000 to avoid port conflicts // Initialize Quickbase client let quickbaseClient = null; let cacheService = null; // Track connector status const connectorStatus = { status: 'disconnected', error: null }; // Initialize MCP server and transport const mcpServer = (0, mcp_1.createMcpServer)(); const mcpTransport = (0, mcp_1.createHttpTransport)(); /** * Initialize Quickbase client from environment variables */ function initializeClient() { try { // Validate required environment variables const realmHost = process.env.QUICKBASE_REALM_HOST; const userToken = process.env.QUICKBASE_USER_TOKEN; if (!realmHost) { throw new Error('QUICKBASE_REALM_HOST environment variable is required'); } if (!userToken) { throw new Error('QUICKBASE_USER_TOKEN environment variable is required'); } // Safely parse cache TTL with validation const cacheTtlStr = process.env.QUICKBASE_CACHE_TTL || '3600'; const cacheTtl = parseInt(cacheTtlStr, 10); if (isNaN(cacheTtl) || cacheTtl <= 0) { throw new Error(`Invalid QUICKBASE_CACHE_TTL value: ${cacheTtlStr}. Must be a positive integer.`); } const config = { realmHost, userToken, appId: process.env.QUICKBASE_APP_ID, cacheEnabled: process.env.QUICKBASE_CACHE_ENABLED !== 'false', cacheTtl, debug: process.env.DEBUG === 'true' }; quickbaseClient = new quickbase_1.QuickbaseClient(config); cacheService = new cache_1.CacheService(config.cacheTtl, config.cacheEnabled); // Initialize MCP tools (0, tools_1.initializeTools)(quickbaseClient, cacheService); // Register tools with MCP server after initialization (0, mcp_1.registerMcpTools)(mcpServer); connectorStatus.status = 'connected'; connectorStatus.error = null; logger.info('Quickbase client initialized successfully'); logger.info(`Registered tools: ${tools_1.toolRegistry.getToolNames().join(', ')}`); } catch (error) { connectorStatus.status = 'error'; connectorStatus.error = error instanceof Error ? error.message : 'Unknown error'; logger.error('Failed to initialize Quickbase client', { error }); } } // MCP tool execution endpoint app.post('/api/:tool', async (req, res) => { const toolName = req.params.tool; const params = req.body || {}; logger.info(`Executing tool: ${toolName}`, { params }); if (!quickbaseClient) { return res.status(500).json({ success: false, error: { message: 'Quickbase client not initialized', type: 'ConfigurationError' } }); } const tool = tools_1.toolRegistry.getTool(toolName); if (!tool) { return res.status(404).json({ success: false, error: { message: `Tool ${toolName} not found`, type: 'NotFoundError' } }); } try { const result = await tool.execute(params); res.json(result); } catch (error) { logger.error(`Error executing tool ${toolName}`, { error }); res.status(500).json({ success: false, error: { message: error instanceof Error ? error.message : 'Unknown error', type: error instanceof Error ? error.name : 'UnknownError' } }); } }); // MCP batch tool execution app.post('/api/batch', async (req, res) => { const requests = req.body.requests || []; if (!Array.isArray(requests) || requests.length === 0) { return res.status(400).json({ success: false, error: { message: 'Invalid batch request format', type: 'ValidationError' } }); } logger.info(`Executing batch request with ${requests.length} tools`); if (!quickbaseClient) { return res.status(500).json({ success: false, error: { message: 'Quickbase client not initialized', type: 'ConfigurationError' } }); } try { const results = await Promise.all(requests.map(async (request) => { const tool = tools_1.toolRegistry.getTool(request.tool); if (!tool) { return { tool: request.tool, success: false, error: { message: `Tool ${request.tool} not found`, type: 'NotFoundError' } }; } try { const result = await tool.execute(request.params || {}); return { tool: request.tool, ...result }; } catch (error) { return { tool: request.tool, success: false, error: { message: error instanceof Error ? error.message : 'Unknown error', type: error instanceof Error ? error.name : 'UnknownError' } }; } })); res.json({ success: true, results }); } catch (error) { logger.error('Error executing batch request', { error }); res.status(500).json({ success: false, error: { message: error instanceof Error ? error.message : 'Unknown error', type: error instanceof Error ? error.name : 'UnknownError' } }); } }); // MCP schema endpoint app.get('/api/schema', (_req, res) => { if (!quickbaseClient) { return res.status(500).json({ success: false, error: { message: 'Quickbase client not initialized', type: 'ConfigurationError' } }); } const tools = tools_1.toolRegistry.getAllTools().map(tool => ({ name: tool.name, description: tool.description, schema: tool.paramSchema })); res.json({ success: true, data: { tools } }); }); // Status route app.get('/status', (_req, res) => { res.json({ name: 'Quickbase MCP Server', version: '2.0.0', status: connectorStatus.status, error: connectorStatus.error, tools: quickbaseClient ? tools_1.toolRegistry.getToolNames() : [] }); }); // MCP Protocol routes // POST endpoint for MCP messages app.post('/mcp', async (req, res) => { if (!quickbaseClient) { return res.status(500).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Quickbase client not initialized' }, id: req.body?.id || null }); } try { logger.info('Received MCP protocol request'); await (0, mcp_1.handleMcpRequest)(mcpServer, mcpTransport, req, res); } catch (error) { logger.error('Error handling MCP protocol request', { error }); res.status(500).json({ jsonrpc: '2.0', error: { code: -32000, message: error instanceof Error ? error.message : 'Unknown error' }, id: req.body?.id || null }); } }); // GET endpoint for MCP long-polling notifications app.get('/mcp', async (req, res) => { try { logger.info('Received MCP protocol GET request for notifications'); res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // Keep connection open for server-sent events const interval = setInterval(() => { res.write(': keepalive\n\n'); }, 30000); req.on('close', () => { clearInterval(interval); }); } catch (error) { logger.error('Error handling MCP protocol notifications', { error }); res.status(500).end(); } }); // Start server app.listen(PORT, async () => { logger.info(`Quickbase MCP Server v2 server running on port ${PORT}`); // Initialize Quickbase client initializeClient(); // Connect the MCP server to its transport try { await mcpServer.connect(mcpTransport); logger.info('MCP server connected successfully'); } catch (error) { logger.error('Failed to connect MCP server', { error }); } }); // Graceful shutdown handling process.on('SIGTERM', () => { logger.info('SIGTERM received, shutting down gracefully'); cleanup(); }); process.on('SIGINT', () => { logger.info('SIGINT received, shutting down gracefully'); cleanup(); }); process.on('uncaughtException', (error) => { logger.error('Uncaught exception', { error }); cleanup(); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { logger.error('Unhandled rejection', { reason, promise }); cleanup(); process.exit(1); }); function cleanup() { try { // Close cache connections if (cacheService) { logger.info('Closing cache service'); // Note: Cache service cleanup should be implemented if it has cleanup methods } // Close any other resources logger.info('Cleanup completed'); } catch (error) { logger.error('Error during cleanup', { error }); } } // Export for testing exports.default = app; //# sourceMappingURL=server.js.map