mcp-stdio-server.ts•6.92 kB
#!/usr/bin/env node
/**
* MCP Stdio Server for Claude CLI integration
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import dotenv from 'dotenv';
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 { createMcpZodSchema } from './utils/validation';
// Load environment variables
dotenv.config();
const logger = createLogger('mcp-stdio-server');
/**
* Main server function
*/
async function main(): Promise<void> {
try {
// Initialize the MCP server
const server = new McpServer({
name: 'Quickbase MCP Server',
version: '2.0.0',
});
logger.info('MCP Server created');
// Check if environment variables are configured
const hasConfig = process.env.QUICKBASE_REALM_HOST && process.env.QUICKBASE_USER_TOKEN;
if (!hasConfig) {
logger.warn('Quickbase configuration not found in environment variables');
logger.info('Server will start but tools will not be functional until configuration is provided');
logger.info('Required environment variables: QUICKBASE_REALM_HOST, QUICKBASE_USER_TOKEN');
}
let quickbaseClient: QuickbaseClient | null = null;
let cacheService: CacheService | null = null;
if (hasConfig) {
// Initialize Quickbase client with configuration
const config: QuickbaseConfig = {
realmHost: process.env.QUICKBASE_REALM_HOST || '',
userToken: process.env.QUICKBASE_USER_TOKEN || '',
appId: process.env.QUICKBASE_APP_ID,
cacheEnabled: process.env.QUICKBASE_CACHE_ENABLED !== 'false',
cacheTtl: parseInt(process.env.QUICKBASE_CACHE_TTL || '3600', 10),
debug: process.env.DEBUG === 'true'
};
// Validate realm host format if provided
if (config.realmHost && !config.realmHost.match(/^[a-zA-Z0-9-]+\.quickbase\.com$/)) {
logger.error('QUICKBASE_REALM_HOST must be in format: yourcompany.quickbase.com');
logger.warn('Continuing without Quickbase client initialization');
} else {
// Validate cache TTL
if (isNaN(config.cacheTtl!) || config.cacheTtl! <= 0) {
config.cacheTtl = 3600; // Default to 1 hour
logger.warn('Invalid QUICKBASE_CACHE_TTL, using default: 3600');
}
try {
quickbaseClient = new QuickbaseClient(config);
cacheService = new CacheService(config.cacheTtl!, config.cacheEnabled);
// Initialize tools
initializeTools(quickbaseClient, cacheService);
logger.info('Quickbase client and tools initialized successfully');
} catch (error) {
logger.error('Failed to initialize Quickbase client', { error });
logger.warn('Server will continue without functional tools');
}
}
}
// Register a configuration check tool that's always available
server.tool(
'check_configuration',
'Check if Quickbase configuration is properly set up',
{},
async () => {
const configured = !!quickbaseClient;
const status = configured ?
'Quickbase client is configured and ready' :
'Quickbase client is not configured. Please set QUICKBASE_REALM_HOST and QUICKBASE_USER_TOKEN environment variables';
return {
content: [{
type: 'text',
text: JSON.stringify({
configured,
status,
requiredVars: ['QUICKBASE_REALM_HOST', 'QUICKBASE_USER_TOKEN'],
optionalVars: ['QUICKBASE_APP_ID', 'QUICKBASE_CACHE_ENABLED', 'QUICKBASE_CACHE_TTL', 'DEBUG']
}, null, 2)
}]
};
}
);
// Register tools with MCP server if client is initialized
if (quickbaseClient) {
const tools = toolRegistry.getAllTools();
tools.forEach(tool => {
const schema = createMcpZodSchema(tool.paramSchema);
server.tool(
tool.name,
tool.description,
schema,
async (params: Record<string, unknown>) => {
try {
logger.info(`Executing MCP tool: ${tool.name}`);
const apiResponse = await tool.execute(params);
// Handle API response - only return the data if successful
if (!apiResponse.success || apiResponse.error) {
const errorMessage = apiResponse.error?.message || 'Tool execution failed';
logger.error(`Tool ${tool.name} failed`, { error: apiResponse.error });
throw new Error(errorMessage);
}
return {
content: [{
type: 'text',
text: JSON.stringify(apiResponse.data, null, 2)
}]
};
} catch (error) {
logger.error(`Error executing MCP tool ${tool.name}`, { error });
throw error;
}
}
);
logger.info(`Registered MCP tool: ${tool.name}`);
});
logger.info(`Registered ${tools.length} Quickbase tools with MCP Server`);
} else {
logger.info('No Quickbase tools registered (configuration missing)');
}
// Create stdio transport
const transport = new StdioServerTransport();
// Connect and run
await server.connect(transport);
logger.info('MCP server connected via stdio and ready for requests');
} catch (error) {
logger.error('Failed to start MCP server', { error });
await gracefulShutdown();
process.exit(1);
}
}
/**
* Graceful shutdown handler
*/
async function gracefulShutdown(): Promise<void> {
try {
logger.info('Initiating graceful shutdown...');
// Give pending operations time to complete
await new Promise(resolve => setTimeout(resolve, 1000));
// Cleanup cache instances
const { CacheService } = await import('./utils/cache.js');
const stats = CacheService.getStats();
if (stats.instances > 0) {
logger.info(`Cleaning up ${stats.instances} cache instances`);
}
logger.info('Graceful shutdown completed');
} catch (error) {
logger.error('Error during graceful shutdown', { error });
}
}
// Install shutdown handlers
process.on('SIGTERM', async () => {
logger.info('SIGTERM received');
await gracefulShutdown();
process.exit(0);
});
process.on('SIGINT', async () => {
logger.info('SIGINT received');
await gracefulShutdown();
process.exit(0);
});
// Start the server
main().catch(async (error) => {
logger.error('Unhandled error in main', { error });
await gracefulShutdown();
process.exit(1);
});