server.ts•9.52 kB
import express from 'express';
import dotenv from 'dotenv';
import cors from 'cors';
import { createLogger } from './utils/logger';
import { QuickbaseClient } from './client/quickbase';
import { QuickbaseConfig } from './types/config';
import { CacheService } from './utils/cache';
import { initializeTools, toolRegistry } from './tools';
import { McpRequest } from './types/mcp';
import { createMcpServer, createHttpTransport, handleMcpRequest, registerMcpTools } from './mcp';
// Load environment variables
dotenv.config();
const logger = createLogger('server');
// Initialize Express app
const app = express();
app.use(express.json());
app.use(cors());
// Configuration
const PORT = process.env.PORT || 3536; // Changed from 3000 to avoid port conflicts
// Initialize Quickbase client
let quickbaseClient: QuickbaseClient | null = null;
let cacheService: CacheService | null = null;
// Track connector status
const connectorStatus = {
status: 'disconnected',
error: null as string | null
};
// Initialize MCP server and transport
const mcpServer = createMcpServer();
const mcpTransport = createHttpTransport();
/**
* Initialize Quickbase client from environment variables
*/
function initializeClient(): void {
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: QuickbaseConfig = {
realmHost,
userToken,
appId: process.env.QUICKBASE_APP_ID,
cacheEnabled: process.env.QUICKBASE_CACHE_ENABLED !== 'false',
cacheTtl,
debug: process.env.DEBUG === 'true'
};
quickbaseClient = new QuickbaseClient(config);
cacheService = new CacheService(
config.cacheTtl,
config.cacheEnabled
);
// Initialize MCP tools
initializeTools(quickbaseClient, cacheService);
// Register tools with MCP server after initialization
registerMcpTools(mcpServer);
connectorStatus.status = 'connected';
connectorStatus.error = null;
logger.info('Quickbase client initialized successfully');
logger.info(`Registered tools: ${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 = 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: McpRequest) => {
const tool = 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 = 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 ? 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 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(): void {
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
export default app;