UNPKG

@the_cfdude/productboard-mcp

Version:

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

629 lines (566 loc) 17.4 kB
/** * Basic context-aware features (simplified approach) * Provides intelligent response adaptation and user context awareness */ import { performanceCollector } from './performance-monitor.js'; export interface ContextData { userId?: string; sessionId?: string; userPreferences?: { dataFormat?: 'detailed' | 'standard' | 'basic'; includeMetadata?: boolean; maxResults?: number; timezone?: string; language?: string; }; recentQueries?: string[]; workspaceContext?: { id?: string; name?: string; permissions?: string[]; }; instanceContext?: { name?: string; features?: string[]; limits?: Record<string, number>; }; } export interface ContextualResponse { data: unknown; metadata?: { responseFormat?: string; adaptations?: string[]; suggestions?: string[]; relatedActions?: string[]; }; userGuidance?: { nextSteps?: string[]; tips?: string[]; warnings?: string[]; }; } export interface AdaptationRule { condition: ( context: ContextData, query: string, response: unknown ) => boolean; adaptation: ( context: ContextData, query: string, response: unknown ) => ContextualResponse; priority: number; description: string; } /** * Basic context-aware response adapter */ export class ContextAwareAdapter { private adaptationRules: AdaptationRule[] = []; private contextCache = new Map<string, ContextData>(); constructor() { this.initializeDefaultRules(); } /** * Set user context for session */ setContext(sessionId: string, context: ContextData): void { this.contextCache.set(sessionId, { ...this.contextCache.get(sessionId), ...context, }); } /** * Get user context for session */ getContext(sessionId: string): ContextData { return this.contextCache.get(sessionId) || {}; } /** * Clear context for session */ clearContext(sessionId: string): void { this.contextCache.delete(sessionId); } /** * Adapt response based on context */ adaptResponse( sessionId: string, query: string, originalResponse: unknown ): ContextualResponse { const metric = performanceCollector.start('context_adaptation'); try { const context = this.getContext(sessionId); // Update context with recent query this.updateRecentQueries(sessionId, query); // Find applicable adaptation rules const applicableRules = this.adaptationRules .filter(rule => rule.condition(context, query, originalResponse)) .sort((a, b) => b.priority - a.priority); // Apply the highest priority rule if (applicableRules.length > 0) { const adaptedResponse = applicableRules[0].adaptation( context, query, originalResponse ); performanceCollector.end(metric, true); return adaptedResponse; } // No adaptation rules applied, return basic contextual response const basicResponse: ContextualResponse = { data: originalResponse, metadata: { responseFormat: context.userPreferences?.dataFormat || 'standard', adaptations: [], suggestions: this.generateBasicSuggestions(context, query), relatedActions: this.generateRelatedActions(context, query), }, userGuidance: { nextSteps: this.generateNextSteps(context, query), tips: this.generateTips(context, query), }, }; performanceCollector.end(metric, true); return basicResponse; } catch { performanceCollector.end(metric, false); // Fallback to original response return { data: originalResponse, metadata: { responseFormat: 'standard', adaptations: ['Error in context adaptation - fallback applied'], }, }; } } /** * Add custom adaptation rule */ addAdaptationRule(rule: AdaptationRule): void { this.adaptationRules.push(rule); // Sort by priority to ensure correct order this.adaptationRules.sort((a, b) => b.priority - a.priority); } /** * Initialize default adaptation rules */ private initializeDefaultRules(): void { // Large dataset adaptation this.addAdaptationRule({ condition: (context, _query, response) => { return ( this.isLargeDataset(response) && context.userPreferences?.dataFormat !== 'detailed' ); }, adaptation: (_context, _query, response) => { return { data: this.summarizeResponse(response), metadata: { responseFormat: 'summarized', adaptations: ['Large dataset summarized for readability'], suggestions: [ 'Use "detail: detailed" for full data', 'Consider filtering results with specific criteria', ], }, userGuidance: { tips: [ 'Large datasets are automatically summarized for better readability', ], }, }; }, priority: 8, description: 'Summarize large datasets for better readability', }); // Error guidance adaptation this.addAdaptationRule({ condition: (_context, _query, response) => { return this.isErrorResponse(response); }, adaptation: (_context, _query, response) => { const errorInfo = this.extractErrorInfo(response); return { data: response, metadata: { responseFormat: 'error_enhanced', adaptations: ['Error response enhanced with guidance'], }, userGuidance: { nextSteps: this.generateErrorRecoverySteps(errorInfo), tips: this.generateErrorTips(errorInfo), warnings: [errorInfo.message || 'Operation failed'], }, }; }, priority: 10, description: 'Enhance error responses with recovery guidance', }); // Format preference adaptation this.addAdaptationRule({ condition: (context, _query, response) => { return ( context.userPreferences?.dataFormat === 'basic' && this.isDetailedResponse(response) ); }, adaptation: (_context, _query, response) => { return { data: this.simplifyResponse(response), metadata: { responseFormat: 'basic', adaptations: ['Response simplified based on user preference'], suggestions: ['Use "detail: detailed" to see full information'], }, }; }, priority: 6, description: 'Simplify responses for users preferring basic format', }); // Workspace context adaptation this.addAdaptationRule({ condition: (context, _query, response) => { return !!( context.workspaceContext?.permissions && this.containsRestrictedData( response, context.workspaceContext.permissions ) ); }, adaptation: (context, _query, response) => { return { data: this.filterByPermissions( response, context.workspaceContext?.permissions || [] ), metadata: { responseFormat: 'permission_filtered', adaptations: ['Data filtered based on workspace permissions'], }, userGuidance: { warnings: [ 'Some data may be hidden due to permission restrictions', ], }, }; }, priority: 9, description: 'Filter data based on workspace permissions', }); } /** * Update recent queries for context */ private updateRecentQueries(sessionId: string, query: string): void { const context = this.getContext(sessionId); const recentQueries = context.recentQueries || []; // Add new query and keep only last 10 recentQueries.unshift(query); context.recentQueries = recentQueries.slice(0, 10); this.setContext(sessionId, context); } /** * Check if response is a large dataset */ private isLargeDataset(response: unknown): boolean { if (Array.isArray(response)) { return response.length > 50; } if (typeof response === 'object' && response !== null) { const obj = response as Record<string, unknown>; if (obj.content && Array.isArray(obj.content)) { return obj.content.length > 50; } // Check if response has large text content const text = JSON.stringify(response); return text.length > 10000; } return false; } /** * Check if response is an error */ private isErrorResponse(response: unknown): boolean { if (typeof response === 'object' && response !== null) { const obj = response as Record<string, unknown>; return ( obj.error !== undefined || obj.code !== undefined || (typeof obj.content === 'object' && (obj.content as Record<string, unknown>)?.error !== undefined) ); } return false; } /** * Check if response contains detailed information */ private isDetailedResponse(response: unknown): boolean { if (typeof response === 'object' && response !== null) { const text = JSON.stringify(response); return ( text.length > 5000 || Object.keys(response as Record<string, unknown>).length > 20 ); } return false; } /** * Check if response contains restricted data */ private containsRestrictedData( response: unknown, permissions: string[] ): boolean { // Simplified permission check - in real implementation would be more sophisticated const hasFullAccess = permissions.includes('admin') || permissions.includes('full_access'); return !hasFullAccess && typeof response === 'object' && response !== null; } /** * Summarize large response */ private summarizeResponse(response: unknown): unknown { if (Array.isArray(response)) { return { summary: `Showing first 10 of ${response.length} items`, items: response.slice(0, 10), totalCount: response.length, }; } return response; } /** * Simplify detailed response */ private simplifyResponse(response: unknown): unknown { if (typeof response === 'object' && response !== null) { const obj = response as Record<string, unknown>; // Keep only essential fields const essentialFields = [ 'id', 'name', 'title', 'summary', 'status', 'type', ]; const simplified: Record<string, unknown> = {}; for (const field of essentialFields) { if (obj[field] !== undefined) { simplified[field] = obj[field]; } } // If no essential fields found, return first few fields if (Object.keys(simplified).length === 0) { const allKeys = Object.keys(obj); for (let i = 0; i < Math.min(5, allKeys.length); i++) { simplified[allKeys[i]] = obj[allKeys[i]]; } } return simplified; } return response; } /** * Filter response by permissions */ private filterByPermissions( response: unknown, permissions: string[] ): unknown { // Simplified implementation - would be more sophisticated in real system if (permissions.includes('admin') || permissions.includes('full_access')) { return response; } // For demo purposes, just add a filtered marker if (typeof response === 'object' && response !== null) { return { ...(response as Record<string, unknown>), _filtered: true, _reason: 'Some fields may be hidden due to permissions', }; } return response; } /** * Extract error information */ private extractErrorInfo(response: unknown): { message?: string; code?: string; type?: string; } { if (typeof response === 'object' && response !== null) { const obj = response as Record<string, unknown>; return { message: (obj.error as string) || (obj.message as string), code: obj.code as string, type: (obj.type as string) || 'unknown', }; } return {}; } /** * Generate basic suggestions based on context and query */ private generateBasicSuggestions( context: ContextData, query: string ): string[] { const suggestions: string[] = []; if ( query.toLowerCase().includes('list') || query.toLowerCase().includes('get') ) { suggestions.push('Try filtering results with specific criteria'); if (!query.includes('limit')) { suggestions.push('Use limit parameter to reduce results'); } } if (context.userPreferences?.dataFormat === 'basic') { suggestions.push( 'Use "detail: detailed" for more comprehensive information' ); } return suggestions; } /** * Generate related actions based on context and query */ private generateRelatedActions( _context: ContextData, query: string ): string[] { const actions: string[] = []; if (query.toLowerCase().includes('create')) { actions.push('Update the created item', 'List related items'); } else if (query.toLowerCase().includes('update')) { actions.push('Get updated item details', 'Create related items'); } else if ( query.toLowerCase().includes('list') || query.toLowerCase().includes('get') ) { actions.push('Update selected items', 'Create new item'); } return actions; } /** * Generate next steps based on context and query */ private generateNextSteps(_context: ContextData, query: string): string[] { const steps: string[] = []; if (query.toLowerCase().includes('create')) { steps.push('Verify the created item details'); steps.push('Consider setting up related items or workflows'); } else if (query.toLowerCase().includes('list')) { steps.push('Review the results and identify items to work with'); steps.push('Consider filtering or sorting for better focus'); } return steps; } /** * Generate contextual tips */ private generateTips(context: ContextData, _query: string): string[] { const tips: string[] = []; if (context.recentQueries && context.recentQueries.length > 5) { tips.push( 'You seem to be doing similar queries - consider using filters to save time' ); } if (!context.userPreferences?.dataFormat) { tips.push( 'Set your preferred data format in user preferences for better experience' ); } return tips; } /** * Generate error recovery steps */ private generateErrorRecoverySteps(errorInfo: { message?: string; code?: string; type?: string; }): string[] { const steps: string[] = []; if (errorInfo.code === '404' || errorInfo.message?.includes('not found')) { steps.push('Verify the item ID is correct'); steps.push('Check if the item exists in the current workspace'); } else if ( errorInfo.code === '403' || errorInfo.message?.includes('permission') ) { steps.push('Check your permissions for this operation'); steps.push('Contact your workspace admin if needed'); } else if ( errorInfo.code === '400' || errorInfo.message?.includes('validation') ) { steps.push('Review the request parameters'); steps.push('Check the API documentation for required fields'); } else { steps.push('Try the operation again'); steps.push('Check your network connection'); } return steps; } /** * Generate error-specific tips */ private generateErrorTips(errorInfo: { message?: string; code?: string; type?: string; }): string[] { const tips: string[] = []; if (errorInfo.message?.includes('rate limit')) { tips.push('Wait a moment before retrying to avoid rate limiting'); } else if (errorInfo.message?.includes('timeout')) { tips.push('Consider reducing the scope of your request'); } return tips; } /** * Get context statistics */ getContextStats(): { totalSessions: number; activeRules: number; averageAdaptations: number; } { return { totalSessions: this.contextCache.size, activeRules: this.adaptationRules.length, averageAdaptations: 0, // Would calculate from metrics in real implementation }; } /** * Clean old context data */ cleanOldContexts(_maxAgeMs: number = 3600000): number { // Simplified cleanup - in real implementation would track timestamps const beforeSize = this.contextCache.size; // For demonstration, clear contexts older than maxAge // In real implementation, would check timestamps if (this.contextCache.size > 100) { const entries = Array.from(this.contextCache.entries()); // Keep only the most recent 50 sessions this.contextCache.clear(); entries.slice(-50).forEach(([key, value]) => { this.contextCache.set(key, value); }); } return beforeSize - this.contextCache.size; } } // Export singleton instance export const contextAwareAdapter = new ContextAwareAdapter();