UNPKG

mcp-quickbase

Version:

Work with Quickbase via Model Context Protocol

319 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