UNPKG

@the_cfdude/productboard-mcp

Version:

Model Context Protocol server for Productboard REST API with dynamic tool loading

628 lines (627 loc) 23.7 kB
/** * High-performance lightweight tools for status checking, validation, and monitoring */ import { lightweightQueryEngine } from '../utils/lightweight-query-engine.js'; import { performanceCollector, memoryMonitor, globalCache, } from '../utils/performance-monitor.js'; import { ValidationError } from '../errors/index.js'; /** * Check status for multiple entities with minimal data transfer */ export async function checkEntityStatus(args) { const { entityType, ids, fields = ['id', 'status', 'updatedAt'], format = 'summary', useCache = true, } = args; // Validate inputs if (!Array.isArray(ids) || ids.length === 0) { throw new ValidationError('ids must be a non-empty array', 'ids'); } if (ids.length > 500) { throw new ValidationError('Maximum 500 IDs allowed per request', 'ids'); } const validEntityTypes = [ 'features', 'notes', 'companies', 'users', 'products', 'components', ]; if (!validEntityTypes.includes(entityType)) { throw new ValidationError(`Invalid entityType. Must be one of: ${validEntityTypes.join(', ')}`, 'entityType'); } try { const options = { fields, cache: useCache, cacheTtl: 180000, // 3 minutes }; const result = await lightweightQueryEngine.checkMultipleStatus(entityType, ids, options); // Format response based on requested format switch (format) { case 'summary': return { summary: result.summary, total: result.total, statusDistribution: result.byStatus, lastUpdated: result.lastUpdated, }; case 'counts': return { total: result.total, byStatus: result.byStatus, }; case 'detailed': return result; default: return result; } } catch (error) { throw new Error(`Status check failed: ${error.message}`); } } /** * Validate existence of multiple entities */ export async function validateEntityExistence(args) { const { entityType, ids, returnMissing = true, returnExisting = false, useCache = true, } = args; // Validate inputs if (!Array.isArray(ids) || ids.length === 0) { throw new ValidationError('ids must be a non-empty array', 'ids'); } if (ids.length > 1000) { throw new ValidationError('Maximum 1000 IDs allowed per existence check', 'ids'); } try { const options = { cache: useCache, cacheTtl: 300000, // 5 minutes - existence is more stable }; const result = await lightweightQueryEngine.validateExistence(entityType, ids, options); // Format response based on what caller wants const response = { total: result.total, existingCount: result.existingCount, missingCount: result.missingCount, }; if (returnMissing) { response.missing = result.missing; } if (returnExisting) { response.existing = result.existing; } // Add helpful summary if (result.missingCount > 0) { response.summary = `${result.missingCount} of ${result.total} entities not found`; } else { response.summary = `All ${result.total} entities exist`; } return response; } catch (error) { throw new Error(`Existence validation failed: ${error.message}`); } } /** * Track progress for batch operations using custom markers */ export async function trackBatchProgress(args) { const { entityType, ids, progressMarker, includeDetails = false, groupBy, useCache = true, } = args; // Validate inputs if (!Array.isArray(ids) || ids.length === 0) { throw new ValidationError('ids must be a non-empty array', 'ids'); } if (!progressMarker || typeof progressMarker !== 'string') { throw new ValidationError('progressMarker must be a non-empty string', 'progressMarker'); } if (ids.length > 500) { throw new ValidationError('Maximum 500 IDs allowed per progress check', 'ids'); } try { const options = { cache: useCache, cacheTtl: 120000, // 2 minutes - progress changes frequently }; const result = await lightweightQueryEngine.trackBatchProgress(entityType, ids, progressMarker, options); const response = { completed: result.completed, pending: result.pending, total: result.total, summary: result.summary, completionRate: Math.round((result.completed / result.total) * 100), }; if (includeDetails && result.details) { response.details = result.details; } if (groupBy && result.details) { response.groupedBy = groupProgressDetails(result.details, groupBy); } return response; } catch (error) { throw new Error(`Progress tracking failed: ${error.message}`); } } /** * Get entity counts without fetching full data */ export async function getEntityCounts(args) { const { entityType, filters = {}, useCache = true } = args; try { const options = { cache: useCache, cacheTtl: 300000, // 5 minutes }; const result = await lightweightQueryEngine.getEntityCount(entityType, filters, options); return { entityType, count: result.count, filters: Object.keys(filters).length > 0 ? filters : undefined, timestamp: result.timestamp, cached: useCache, }; } catch (error) { throw new Error(`Entity count failed: ${error.message}`); } } /** * Perform system health check */ export async function performHealthCheck(args) { const { includeDetails = true, includeCacheStats = false, includeMemoryStats = false, } = args; try { const healthResult = await lightweightQueryEngine.healthCheck(); const response = { status: healthResult.status, responseTime: healthResult.responseTime, timestamp: healthResult.timestamp, checks: healthResult.checks, }; if (includeDetails) { response.details = { apiLatency: healthResult.details.apiLatency, memoryUsage: { heapUsed: Math.round(healthResult.details.memoryUsage.heapUsed / 1024 / 1024), heapTotal: Math.round(healthResult.details.memoryUsage.heapTotal / 1024 / 1024), rss: Math.round(healthResult.details.memoryUsage.rss / 1024 / 1024), }, }; } if (includeCacheStats) { response.cache = globalCache.getStats(); } if (includeMemoryStats) { const memStats = memoryMonitor.getStats(); response.memory = { current: { heapUsed: Math.round(memStats.current.heapUsed / 1024 / 1024), heapTotal: Math.round(memStats.current.heapTotal / 1024 / 1024), }, trend: memStats.trend, isCritical: memoryMonitor.isCritical(), }; } return response; } catch (error) { return { status: 'unhealthy', responseTime: -1, timestamp: Date.now(), error: error.message, checks: { api: false, cache: false, memory: false, }, }; } } /** * Get performance statistics */ export async function getPerformanceStats(args) { const { operation, includePercentiles = true, clearOldMetrics = false, } = args; try { // Clear old metrics if requested if (clearOldMetrics) { performanceCollector.clearOldMetrics(3600000); // 1 hour } const stats = performanceCollector.getStats(operation); const response = { operation: operation || 'all', totalRequests: stats.totalRequests, averageDuration: Math.round(stats.averageDuration), cacheHitRate: Math.round(stats.cacheHitRate * 100), errorRate: Math.round(stats.errorRate * 100), averageDataSize: Math.round(stats.averageDataSize), }; if (includePercentiles && stats.totalRequests > 0) { response.percentiles = { p50: Math.round(stats.percentiles.p50), p90: Math.round(stats.percentiles.p90), p95: Math.round(stats.percentiles.p95), p99: Math.round(stats.percentiles.p99), }; } // Add memory cleanup info const memoryStats = memoryMonitor.getStats(); response.memoryTrend = memoryStats.trend; return response; } catch (error) { throw new Error(`Performance stats failed: ${error.message}`); } } /** * Clear caches and perform cleanup */ export async function performCleanup(args) { const { clearCache = false, clearMetrics = false, forceGC = false } = args; const results = { timestamp: Date.now(), actions: [], }; try { if (clearCache) { const beforeSize = globalCache.size(); globalCache.clear(); results.actions.push({ action: 'cache_cleared', beforeSize, afterSize: 0, }); } if (clearMetrics) { const beforeCount = performanceCollector.getMetricsCount(); const cleared = performanceCollector.clearOldMetrics(0); // Clear all results.actions.push({ action: 'metrics_cleared', beforeCount, clearedCount: cleared, }); } if (forceGC) { const beforeMemory = process.memoryUsage(); const gcSuccess = memoryMonitor.forceGC(); const afterMemory = process.memoryUsage(); results.actions.push({ action: 'garbage_collection', success: gcSuccess, memoryBefore: Math.round(beforeMemory.heapUsed / 1024 / 1024), memoryAfter: Math.round(afterMemory.heapUsed / 1024 / 1024), freedMB: Math.round((beforeMemory.heapUsed - afterMemory.heapUsed) / 1024 / 1024), }); } return results; } catch (error) { results.error = error.message; return results; } } /** * Group progress details by specified field */ function groupProgressDetails(details, groupBy) { const groups = {}; for (const detail of details) { const key = groupBy === 'status' ? detail.status : detail.progress ? 'completed' : 'pending'; if (!groups[key]) { groups[key] = { count: 0, ids: [], }; } groups[key].count++; groups[key].ids.push(detail.id); } return groups; } /** * Tool handler function */ export async function handlePerformanceTool(operation, args) { switch (operation) { case 'check_entity_status': return checkEntityStatus(args); case 'validate_entity_existence': return validateEntityExistence(args); case 'track_batch_progress': return trackBatchProgress(args); case 'get_entity_counts': return getEntityCounts(args); case 'perform_health_check': return performHealthCheck(args); case 'get_performance_stats': return getPerformanceStats(args); case 'perform_cleanup': return performCleanup(args); default: throw new ValidationError(`Unknown performance operation: ${operation}`, 'operation'); } } /** * Setup performance tools definitions */ export function setupPerformanceTools() { return [ { name: 'check_entity_status', description: 'Check status for multiple entities with minimal data transfer. Optimized for quick status overview of large entity sets.', inputSchema: { type: 'object', properties: { entityType: { type: 'string', enum: [ 'features', 'notes', 'companies', 'users', 'products', 'components', ], description: 'Type of entities to check status for', }, ids: { type: 'array', items: { type: 'string' }, description: 'Array of entity IDs to check (max 500)', maxItems: 500, }, fields: { type: 'array', items: { type: 'string' }, description: 'Fields to include in status check', default: ['id', 'status', 'updatedAt'], }, format: { type: 'string', enum: ['summary', 'detailed', 'counts'], description: 'Response format level', default: 'summary', }, useCache: { type: 'boolean', description: 'Whether to use intelligent caching', default: true, }, instance: { type: 'string', description: 'ProductBoard instance name', }, workspaceId: { type: 'string', description: 'Workspace ID' }, }, required: ['entityType', 'ids'], }, }, { name: 'validate_entity_existence', description: 'Validate existence of multiple entities efficiently. Returns missing/existing entity lists.', inputSchema: { type: 'object', properties: { entityType: { type: 'string', enum: [ 'features', 'notes', 'companies', 'users', 'products', 'components', ], description: 'Type of entities to validate', }, ids: { type: 'array', items: { type: 'string' }, description: 'Array of entity IDs to validate (max 1000)', maxItems: 1000, }, returnMissing: { type: 'boolean', description: 'Include missing entity IDs in response', default: true, }, returnExisting: { type: 'boolean', description: 'Include existing entity IDs in response', default: false, }, useCache: { type: 'boolean', description: 'Whether to use intelligent caching', default: true, }, instance: { type: 'string', description: 'ProductBoard instance name', }, workspaceId: { type: 'string', description: 'Workspace ID' }, }, required: ['entityType', 'ids'], }, }, { name: 'track_batch_progress', description: 'Track progress for batch operations using custom markers (e.g., status:completed, customField).', inputSchema: { type: 'object', properties: { entityType: { type: 'string', enum: [ 'features', 'notes', 'companies', 'users', 'products', 'components', ], description: 'Type of entities to track', }, ids: { type: 'array', items: { type: 'string' }, description: 'Array of entity IDs to track (max 500)', maxItems: 500, }, progressMarker: { type: 'string', description: 'Progress marker (e.g., "status:completed", "customField", "approved")', }, includeDetails: { type: 'boolean', description: 'Include detailed progress info per entity', default: false, }, groupBy: { type: 'string', enum: ['status', 'progress'], description: 'Group results by status or progress', }, useCache: { type: 'boolean', description: 'Whether to use intelligent caching', default: true, }, instance: { type: 'string', description: 'ProductBoard instance name', }, workspaceId: { type: 'string', description: 'Workspace ID' }, }, required: ['entityType', 'ids', 'progressMarker'], }, }, { name: 'get_entity_counts', description: 'Get entity counts without fetching full data. Optimized for dashboard metrics and overview statistics.', inputSchema: { type: 'object', properties: { entityType: { type: 'string', enum: [ 'features', 'notes', 'companies', 'users', 'products', 'components', ], description: 'Type of entities to count', }, filters: { type: 'object', description: 'Optional filters to apply to count', additionalProperties: true, }, useCache: { type: 'boolean', description: 'Whether to use intelligent caching', default: true, }, instance: { type: 'string', description: 'ProductBoard instance name', }, workspaceId: { type: 'string', description: 'Workspace ID' }, }, required: ['entityType'], }, }, { name: 'perform_health_check', description: 'Perform comprehensive system health check including API connectivity, cache status, and memory usage.', inputSchema: { type: 'object', properties: { includeDetails: { type: 'boolean', description: 'Include detailed health metrics', default: true, }, includeCacheStats: { type: 'boolean', description: 'Include cache performance statistics', default: false, }, includeMemoryStats: { type: 'boolean', description: 'Include detailed memory usage stats', default: false, }, instance: { type: 'string', description: 'ProductBoard instance name', }, workspaceId: { type: 'string', description: 'Workspace ID' }, }, }, }, { name: 'get_performance_stats', description: 'Get detailed performance statistics including response times, cache hit rates, and percentiles.', inputSchema: { type: 'object', properties: { operation: { type: 'string', description: 'Specific operation to get stats for (omit for all operations)', }, includePercentiles: { type: 'boolean', description: 'Include response time percentiles (p50, p90, p95, p99)', default: true, }, clearOldMetrics: { type: 'boolean', description: 'Clear metrics older than 1 hour before returning stats', default: false, }, instance: { type: 'string', description: 'ProductBoard instance name', }, workspaceId: { type: 'string', description: 'Workspace ID' }, }, }, }, { name: 'perform_cleanup', description: 'Clear caches and perform system cleanup. Useful for memory management and troubleshooting.', inputSchema: { type: 'object', properties: { clearCache: { type: 'boolean', description: 'Clear intelligent cache', default: false, }, clearMetrics: { type: 'boolean', description: 'Clear performance metrics', default: false, }, forceGC: { type: 'boolean', description: 'Force garbage collection (if available)', default: false, }, instance: { type: 'string', description: 'ProductBoard instance name', }, workspaceId: { type: 'string', description: 'Workspace ID' }, }, }, }, ]; }