UNPKG

formulize-mcp

Version:

A local MCP server for connecting to Formulize, the open source data management platform.

937 lines 39.2 kB
#!/usr/bin/env node /** * Formulize MCP Server * * Local TypeScript MCP server that proxies requests to remote Formulize HTTP server * Features intelligent caching for non-tool endpoints to improve performance */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; class FormulizeServer { server; version = '1.3.3'; config = this.loadConfig(); name = this.loadServerName(); // Caching system cache = new Map(); cacheStats = { hits: 0, misses: 0, evictions: 0, size: 0 }; defaultTTL = 5 * 60 * 1000; // 5 minutes maxCacheSize = 1000; // Maximum number of cache entries constructor() { this.server = new Server({ name: this.name, version: this.version, }, { capabilities: { tools: {}, resources: {}, prompts: {}, }, }); this.setupHandlers(); // Start cache cleanup interval (every 2 minutes) setInterval(() => this.cleanupExpiredCache(), 2 * 60 * 1000); } loadConfig() { if (!process.env.FORMULIZE_URL) { throw new Error('FORMULIZE_URL environment variable is required'); } if (!process.env.FORMULIZE_API_KEY) { throw new Error('FORMULIZE_API_KEY environment variable is required'); } const apiKey = process.env.FORMULIZE_API_KEY; let baseUrl = process.env.FORMULIZE_URL; if (baseUrl.endsWith('/') == false) { baseUrl += '/'; } baseUrl += 'mcp/index.php'; return { baseUrl: baseUrl, apiKey: process.env.FORMULIZE_API_KEY, timeout: parseInt(process.env.FORMULIZE_TIMEOUT || '30000'), debug: process.env.FORMULIZE_DEBUG === 'true', }; } /** * Load server name from environment variable or fall back to default */ loadServerName() { const serverName = process.env.FORMULIZE_SERVER_NAME || 'formulize'; if (this.config?.debug) { console.error(`[DEBUG] Server name: ${serverName}`); } return serverName; } /** * Generate cache key for a request */ getCacheKey(method, params) { // Sort params to ensure consistent keys const sortedParams = JSON.stringify(params, Object.keys(params).sort()); return `${method}:${sortedParams}`; } /** * Check if a request should be cached */ shouldCache(method) { // Don't cache in debug mode if (this.config.debug) { return false; } // Don't cache tool calls (they interact with live data) if (method === 'tools/call') { return false; } // Cache everything else (tools/list, resources/list, resources/read, prompts/list, prompts/get) return true; } /** * Get TTL for different request types */ getTTL(method) { switch (method) { case 'tools/list': return 10 * 60 * 1000; // 10 minutes - tools don't change often case 'resources/list': return 2 * 60 * 1000; // 2 minutes - resources might be created case 'resources/read': return 5 * 60 * 1000; // 5 minutes - resource content is fairly stable case 'prompts/list': return 10 * 60 * 1000; // 10 minutes - prompts don't change often case 'prompts/get': return 5 * 60 * 1000; // 5 minutes - prompt content is stable default: return this.defaultTTL; } } /** * Get data from cache if available and not expired */ getFromCache(cacheKey) { const entry = this.cache.get(cacheKey); if (!entry) { this.cacheStats.misses++; return null; } // Check if expired if (Date.now() > entry.timestamp + entry.ttl) { this.cache.delete(cacheKey); this.cacheStats.evictions++; this.cacheStats.misses++; this.updateCacheSize(); return null; } this.cacheStats.hits++; if (this.config.debug) { console.error(`[DEBUG] Cache HIT for key: ${cacheKey}`); } return entry.data; } /** * Store data in cache */ setCache(cacheKey, data, method) { // Enforce max cache size if (this.cache.size >= this.maxCacheSize) { // Remove oldest entry const oldestKey = this.cache.keys().next().value; if (oldestKey) { this.cache.delete(oldestKey); this.cacheStats.evictions++; } } const ttl = this.getTTL(method); const entry = { data, timestamp: Date.now(), ttl }; this.cache.set(cacheKey, entry); this.updateCacheSize(); if (this.config.debug) { console.error(`[DEBUG] Cache SET for key: ${cacheKey} (TTL: ${ttl}ms)`); } } /** * Update cache size stat */ updateCacheSize() { this.cacheStats.size = this.cache.size; } /** * Clean up expired cache entries */ cleanupExpiredCache() { const now = Date.now(); let cleaned = 0; for (const [key, entry] of this.cache.entries()) { if (now > entry.timestamp + entry.ttl) { this.cache.delete(key); cleaned++; } } if (cleaned > 0) { this.cacheStats.evictions += cleaned; this.updateCacheSize(); if (this.config.debug) { console.error(`[DEBUG] Cleaned up ${cleaned} expired cache entries`); } } } /** * Clear all cache entries */ clearCache() { const size = this.cache.size; this.cache.clear(); this.cacheStats.evictions += size; this.updateCacheSize(); if (this.config.debug) { console.error(`[DEBUG] Cleared entire cache (${size} entries)`); } } /** * Get cache statistics */ getCacheStats() { const totalRequests = this.cacheStats.hits + this.cacheStats.misses; const hitRate = totalRequests > 0 ? (this.cacheStats.hits / totalRequests * 100).toFixed(2) : '0.00'; return { ...this.cacheStats, hit_rate_percent: hitRate, total_requests: totalRequests, cache_enabled: !this.config.debug, max_size: this.maxCacheSize, default_ttl_ms: this.defaultTTL, }; } /** * Get local proxy tools that should always be available */ getLocalProxyTools() { return [ { name: 'test_connection', description: 'Test proxy and remote Formulize server connectivity with configurable detail levels', inputSchema: { type: 'object', properties: { verbose: { type: 'boolean', description: 'Include detailed response data in output', default: false }, quick: { type: 'boolean', description: 'Quick status check only (lightweight alternative to full diagnostics)', default: false }, skip_remote: { type: 'boolean', description: 'Only test proxy server, skip remote server tests', default: false } }, }, }, { name: 'cache_refresh', description: 'Clear the local cache and force fresh data from remote server', inputSchema: { type: 'object', properties: { clear_all: { type: 'boolean', description: 'Clear entire cache (default: true)', default: true } }, }, }, { name: 'cache_stats', description: 'Show cache performance statistics and configuration', inputSchema: { type: 'object', properties: {}, }, } ]; } /** * Generic handler for listing MCP items (tools, resources, prompts) with caching */ async handleListMCP(type) { const method = `${type}/list`; const cacheKey = this.getCacheKey(method, {}); // For non-tools, handle normally if (type !== 'tools') { // Try cache first if (this.shouldCache(method)) { const cachedData = this.getFromCache(cacheKey); if (cachedData) { return cachedData; } } if (this.config.debug) { console.error(`[DEBUG] Fetching ${type} from remote server...`); } try { const response = await this.makeRequest(method, {}); if (response.result && response.result[type]) { const result = { [type]: response.result[type], }; // Cache the result if (this.shouldCache(method)) { this.setCache(cacheKey, result, method); } if (this.config.debug) { console.error(`[DEBUG] Successfully fetched ${response.result[type].length} ${type}`); } return result; } else { throw new Error(`Invalid ${type} response from remote server`); } } catch (error) { if (this.config.debug) { console.error(`[DEBUG] Error fetching ${type}:`, error); } // FOR RESOURCES AND PROMPTS: Throw the error instead of returning empty arrays throw new McpError(ErrorCode.InternalError, `Failed to fetch ${type} from remote server: ${error instanceof Error ? error.message : String(error)}`); } } // SPECIAL HANDLING FOR TOOLS: Always include local proxy tools if (this.config.debug) { console.error(`[DEBUG] Fetching tools from remote server...`); } let remoteTools = []; let remoteError = null; // Try to get remote tools first try { // Check cache first if (this.shouldCache(method)) { const cachedData = this.getFromCache(cacheKey); if (cachedData && cachedData.tools) { // Extract only remote tools from cache (exclude local proxy tools) remoteTools = cachedData.tools.filter((tool) => !['cache_refresh', 'cache_stats', 'test_connection'].includes(tool.name)); if (this.config.debug) { console.error(`[DEBUG] Found ${remoteTools.length} remote tools in cache`); } } } // If not in cache, fetch from remote server if (remoteTools.length === 0) { const response = await this.makeRequest(method, {}); if (response.result && response.result.tools) { // Filter out remote test_connection tool since our local version is more comprehensive remoteTools = response.result.tools.filter((tool) => tool.name !== 'test_connection'); if (this.config.debug) { console.error(`[DEBUG] Successfully fetched ${response.result.tools.length} tools from remote server (${remoteTools.length} after filtering)`); } } else { throw new Error(`Invalid tools response from remote server`); } } } catch (error) { remoteError = error; if (this.config.debug) { console.error(`[DEBUG] Error fetching tools from remote server:`, error); } } // Always include local proxy tools const localTools = this.getLocalProxyTools(); // Note: We always include our local test_connection tool because it provides // proxy-specific diagnostics that the remote tool cannot provide // If remote server failed, we already have test_connection in localTools, // but we can add a note about the failure context if (remoteError) { console.error(`[WARNING] Remote server unavailable for tools, providing local tools with diagnostics`); } // Combine remote tools with local proxy tools const allTools = [...remoteTools, ...localTools]; const result = { tools: allTools }; // Only cache if we successfully got remote tools (don't cache error states) if (this.shouldCache(method) && !remoteError && remoteTools.length > 0) { this.setCache(cacheKey, result, method); } if (this.config.debug) { console.error(`[DEBUG] Returning ${allTools.length} total tools (${remoteTools.length} remote + ${localTools.length} local)`); } return result; } /** * Generic handler for using MCP items (calling tools, reading resources, getting prompts) with caching */ async handleUseMCP(type, action, params) { const identifier = params.name || params.uri || ''; const args = params.arguments; const method = `${type}/${action}`; if (this.config.debug) { console.error(`[DEBUG] ${action} ${type}: ${identifier}${args ? ' with args:' : ''}`, args); } // Handle special proxy tools locally if (type === 'tools' && params.name === 'test_connection') { return await this.handleTestConnection(args); } if (type === 'tools' && params.name === 'cache_refresh') { return await this.handleCacheRefresh(args); } if (type === 'tools' && params.name === 'cache_stats') { return await this.handleCacheStats(); } // Check cache for non-tool requests const cacheKey = this.getCacheKey(method, params); if (this.shouldCache(method)) { const cachedData = this.getFromCache(cacheKey); if (cachedData) { return cachedData; } } try { // Forward request to remote server const requestParams = {}; // Set the appropriate parameter based on the type if (type === 'resources') { requestParams.uri = identifier; } else { requestParams.name = identifier; if (args !== undefined) { requestParams.arguments = args; } } const response = await this.makeRequest(method, requestParams); if (response.result) { // Cache the result if appropriate if (this.shouldCache(method)) { this.setCache(cacheKey, response.result, method); } return response.result; } else if (response.error) { if (this.config.debug) { console.error(`[DEBUG] ${type} ${action} error - Full response:`, JSON.stringify(response, null, 2)); } /** * For 200 series responses without results, and for 422 errors, return the error * as if it were a successful response so the AI will more likely handle the details correctly */ if (response.httpStatus == 422 || (response.httpStatus >= 200 && response.httpStatus < 300)) { return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: response.error, source: 'formulize_remote_server' }, null, 2) }] }; } else { const fullErrorInfo = { source: 'formulize_remote_server', remote_error: response.error, ...(response.id && { request_id: response.id }), ...(response.timestamp && { response_timestamp: response.timestamp }), proxy_info: { proxy_version: this.version, request_method: method, request_params: params } }; throw new McpError(ErrorCode.InvalidRequest, JSON.stringify(fullErrorInfo, null, 2)); } } else { throw new Error('Invalid response from remote server'); } } catch (error) { if (this.config.debug) { console.error(`[DEBUG] ${type} ${action} error:`, error); } // Provide helpful error context if (error instanceof McpError) { throw error; } // Enhanced error message with troubleshooting hints let errorMessage = `${type} ${action} failed: ${error instanceof Error ? error.message : String(error)}`; if (error instanceof Error && error.message.includes('timeout')) { errorMessage += '\n\nTroubleshooting: Try increasing FORMULIZE_TIMEOUT environment variable or check network connectivity.'; } else if (error instanceof Error && error.message.includes('HTTP')) { errorMessage += '\n\nTroubleshooting: Check if the Formulize server is running and the API key is valid.'; } throw new McpError(ErrorCode.InternalError, errorMessage); } } setupHandlers() { // Tools handlers this.server.setRequestHandler(ListToolsRequestSchema, async () => { return this.handleListMCP('tools'); }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { return this.handleUseMCP('tools', 'call', { name: request.params.name, arguments: request.params.arguments, }); }); // Resources handlers this.server.setRequestHandler(ListResourcesRequestSchema, async () => { return this.handleListMCP('resources'); }); this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { return this.handleUseMCP('resources', 'read', { uri: request.params.uri, }); }); // Prompts handlers this.server.setRequestHandler(ListPromptsRequestSchema, async () => { return this.handleListMCP('prompts'); }); this.server.setRequestHandler(GetPromptRequestSchema, async (request) => { return this.handleUseMCP('prompts', 'get', { name: request.params.name, arguments: request.params.arguments, }); }); } async makeRequest(method, params) { const url = `${this.config.baseUrl}/mcp`; const requestBody = { jsonrpc: '2.0', method, params, localServerDetails: { name: this.name, version: this.version }, id: Date.now(), }; const headers = { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache', }; if (this.config.apiKey) { headers['Authorization'] = `Bearer ${this.config.apiKey}`; } if (this.config.debug) { console.error(`[DEBUG] Making request to ${url}:`, requestBody); } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.config.timeout); try { const response = await fetch(url, { method: 'POST', headers, body: JSON.stringify(requestBody), signal: controller.signal, }); clearTimeout(timeoutId); const data = await response.json(); if (this.config.debug) { console.error('[DEBUG] Response from server:', JSON.stringify(data, null, 2)); } /** * Error out on 500 and above, but below that we want the client to interact with the entire result like normal * Formulize MCP will return 422 for PHP errors internally */ if (!response.ok && response.status >= 500) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } data.httpStatus = response.status; return data; } catch (error) { clearTimeout(timeoutId); if (error instanceof Error && error.name === 'AbortError') { throw new Error(`Request timeout after ${this.config.timeout}ms`); } throw error; } } async handleCacheRefresh(args = {}) { const clearAll = args?.clear_all !== false; // Default to true const statsBeforeClear = { ...this.cacheStats }; if (clearAll) { this.clearCache(); } const results = { action: 'cache_refresh', timestamp: new Date().toISOString(), cache_cleared: clearAll, stats_before_clear: statsBeforeClear, stats_after_clear: this.getCacheStats(), cache_enabled: !this.config.debug, message: clearAll ? 'Cache completely cleared. Next requests will fetch fresh data from remote server.' : 'Cache refresh requested but clear_all was false.', }; if (this.config.debug) { results.message += ' Note: Caching is disabled in debug mode.'; } return { content: [ { type: 'text', text: JSON.stringify(results, null, 2), }, ], }; } async handleCacheStats() { const stats = this.getCacheStats(); // Get cache entry details const entryDetails = []; for (const [key, entry] of this.cache.entries()) { const ageMs = Date.now() - entry.timestamp; const remainingMs = Math.max(0, entry.ttl - ageMs); entryDetails.push({ key: key.length > 50 ? key.substring(0, 50) + '...' : key, age_ms: ageMs, remaining_ttl_ms: remainingMs, expired: remainingMs === 0, size_estimate: JSON.stringify(entry.data).length }); } // Sort by age (newest first) entryDetails.sort((a, b) => a.age_ms - b.age_ms); const results = { cache_statistics: stats, cache_entries: entryDetails.slice(0, 10), // Show up to 10 most recent entries total_entries_shown: Math.min(10, entryDetails.length), total_entries: entryDetails.length, memory_usage_estimate_kb: Math.round(Array.from(this.cache.values()) .reduce((total, entry) => total + JSON.stringify(entry.data).length, 0) / 1024), configuration: { max_cache_size: this.maxCacheSize, default_ttl_ms: this.defaultTTL, debug_mode: this.config.debug, cache_enabled: !this.config.debug, }, timestamp: new Date().toISOString(), }; return { content: [ { type: 'text', text: JSON.stringify(results, null, 2), }, ], }; } async handleTestConnection(args = {}) { const verbose = args?.verbose || false; const quick = args?.quick || false; const skipRemote = args?.skip_remote || false; // If quick mode is requested, return lightweight status if (quick) { try { // Test connection to remote server const startTime = Date.now(); await this.makeRequest('initialize', {}); const responseTime = Date.now() - startTime; return { content: [ { type: 'text', text: JSON.stringify({ mode: 'quick_status', status: 'connected', version: this.version, remote_url: this.config.baseUrl, response_time_ms: responseTime, config: { timeout: this.config.timeout, debug: this.config.debug, api_key_configured: !!this.config.apiKey, }, cache: this.getCacheStats(), timestamp: new Date().toISOString(), }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: JSON.stringify({ mode: 'quick_status', status: 'disconnected', version: this.version, remote_url: this.config.baseUrl, error: error instanceof Error ? error.message : String(error), config: { timeout: this.config.timeout, debug: this.config.debug, api_key_configured: !!this.config.apiKey, }, cache: this.getCacheStats(), timestamp: new Date().toISOString(), }, null, 2), }, ], }; } } // Full comprehensive diagnostics (existing implementation) const results = { timestamp: new Date().toISOString(), proxy_server: { status: 'operational', version: this.version, environment: { node_version: process.version, platform: process.platform, api_key_configured: !!this.config.apiKey, api_key_length: this.config.apiKey ? this.config.apiKey.length : 0, base_url: this.config.baseUrl, timeout: this.config.timeout, debug_mode: this.config.debug, }, cache: this.getCacheStats(), }, remote_server: { url: this.config.baseUrl, status: 'unknown', tests: {}, capabilities: {}, error: null, }, recommendations: [], summary: { overall_status: 'unknown', issues_found: 0, capabilities_working: 0, total_capabilities: 3, } }; if (skipRemote) { results.remote_server.status = 'skipped'; results.summary.overall_status = 'proxy_only'; return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }], }; } // Test 1: Basic HTTP Health Check try { const healthUrl = `${this.config.baseUrl}/health`; const startTime = Date.now(); const response = await fetch(healthUrl, { method: 'GET', headers: { 'Authorization': `Bearer ${this.config.apiKey}` }, signal: AbortSignal.timeout(5000), }); const responseTime = Date.now() - startTime; results.remote_server.tests.health_check = { status: response.ok ? 'pass' : 'fail', http_status: response.status, response_time_ms: responseTime, }; if (verbose && response.ok) { try { const healthData = await response.json(); results.remote_server.tests.health_check.details = healthData; } catch (e) { results.remote_server.tests.health_check.raw_response = await response.text(); } } if (!response.ok) { results.summary.issues_found++; results.recommendations.push(`Health check failed with HTTP ${response.status} - check server status and API key`); } } catch (error) { results.remote_server.tests.health_check = { status: 'error', error: error instanceof Error ? error.message : String(error), }; results.summary.issues_found++; results.recommendations.push('Health endpoint unreachable - check if Formulize server is running and URL is correct'); } // Test 2: MCP Capabilities Endpoint try { const capUrl = `${this.config.baseUrl}/capabilities`; const startTime = Date.now(); const response = await fetch(capUrl, { method: 'GET', headers: { 'Authorization': `Bearer ${this.config.apiKey}` }, signal: AbortSignal.timeout(5000), }); const responseTime = Date.now() - startTime; results.remote_server.tests.capabilities_endpoint = { status: response.ok ? 'pass' : 'fail', http_status: response.status, response_time_ms: responseTime, }; if (response.ok) { try { const capData = await response.json(); results.remote_server.tests.capabilities_endpoint.tools_count = capData.capabilities?.tools?.length || 0; results.remote_server.tests.capabilities_endpoint.resources_count = capData.capabilities?.resources?.length || 0; results.remote_server.tests.capabilities_endpoint.prompts_count = capData.capabilities?.prompts?.length || 0; if (verbose) { results.remote_server.tests.capabilities_endpoint.full_response = capData; } } catch (e) { results.remote_server.tests.capabilities_endpoint.parse_error = 'Failed to parse JSON response'; } } else { results.summary.issues_found++; results.recommendations.push('Capabilities endpoint failed - MCP features may not work'); } } catch (error) { results.remote_server.tests.capabilities_endpoint = { status: 'error', error: error instanceof Error ? error.message : String(error), }; results.summary.issues_found++; } // Test 3: Individual MCP Capabilities const capabilities = [ { name: 'tools', method: 'tools/list' }, { name: 'resources', method: 'resources/list' }, { name: 'prompts', method: 'prompts/list' } ]; for (const capability of capabilities) { try { const startTime = Date.now(); const response = await this.makeRequest(capability.method, {}); const responseTime = Date.now() - startTime; results.remote_server.capabilities[capability.name] = { status: 'pass', response_time_ms: responseTime, count: response.result?.[capability.name]?.length || 0, }; if (verbose && response.result?.[capability.name] && Array.isArray(response.result[capability.name]) && response.result[capability.name].length > 0) { results.remote_server.capabilities[capability.name].sample_items = response.result[capability.name].slice(0, 3).map((item) => ({ name: item.name || item.uri || 'unknown', description: (item.description || '').substring(0, 100) })); } results.summary.capabilities_working++; } catch (error) { results.remote_server.capabilities[capability.name] = { status: 'error', error: error instanceof Error ? error.message : String(error), }; results.summary.issues_found++; results.recommendations.push(`${capability.name} capability failed - may need to restart AI assistant to refresh cache`); } } // Test 4: Try the remote test_connection tool for detailed server info try { const response = await this.makeRequest('tools/call', { name: 'test_connection', arguments: {}, }); if (response.result?.content?.[0]?.text) { try { const remoteTestData = JSON.parse(response.result.content[0].text); results.remote_server.tests.formulize_connection = { status: 'pass', details: remoteTestData, }; } catch (e) { results.remote_server.tests.formulize_connection = { status: 'pass', raw_response: response.result.content[0].text, }; } } } catch (error) { results.remote_server.tests.formulize_connection = { status: 'error', error: error instanceof Error ? error.message : String(error), }; results.summary.issues_found++; } // Determine overall status if (results.summary.issues_found === 0 && results.summary.capabilities_working === 3) { results.summary.overall_status = 'healthy'; results.remote_server.status = 'connected'; } else if (results.summary.capabilities_working > 0) { results.summary.overall_status = 'partial'; results.remote_server.status = 'partial'; results.recommendations.push('Some capabilities are working but issues detected - see individual test results'); } else { results.summary.overall_status = 'failed'; results.remote_server.status = 'error'; results.recommendations.push('All capabilities failed - check server connectivity and authentication'); } // Add specific recommendations based on patterns if (results.summary.capabilities_working === 1 && results.remote_server.capabilities.tools?.status === 'pass') { results.recommendations.push('Only tools working - this suggests AI assistant cached empty resources/prompts during startup. Try using cache_refresh tool or restart AI assistant.'); } if (results.remote_server.tests.health_check?.response_time_ms > 2000 || results.remote_server.tests.capabilities_endpoint?.response_time_ms > 2000) { results.recommendations.push('Slow response times detected - consider increasing FORMULIZE_TIMEOUT or checking network'); } // Add cache-specific recommendations if (results.proxy_server.cache.hit_rate_percent < 50 && results.proxy_server.cache.total_requests > 10) { results.recommendations.push('Low cache hit rate - this is normal for first use but may indicate frequently changing data'); } return { content: [ { type: 'text', text: JSON.stringify(results, null, 2), }, ], }; } async run() { const transport = new StdioServerTransport(); if (this.config.debug) { console.error(`[DEBUG] Starting Formulize MCP Server v${this.version}`); console.error(`[DEBUG] Remote URL: ${this.config.baseUrl}`); console.error(`[DEBUG] Timeout: ${this.config.timeout}ms`); console.error(`[DEBUG] Capabilities: tools, resources, prompts`); console.error(`[DEBUG] Caching: DISABLED (debug mode)`); } else { console.error(`[INFO] Starting Formulize MCP Server v${this.version} with caching enabled`); console.error(`[INFO] Cache settings: max size=${this.maxCacheSize}, default TTL=${this.defaultTTL}ms`); } await this.server.connect(transport); } } // Handle uncaught errors process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled rejection at:', promise, 'reason:', reason); process.exit(1); }); // Start the server const server = new FormulizeServer(); server.run().catch((error) => { console.error('Failed to start server:', error); process.exit(1); }); //# sourceMappingURL=formulize-mcp.js.map