@the_cfdude/productboard-mcp
Version:
Model Context Protocol server for Productboard REST API with dynamic tool loading
458 lines (457 loc) • 17 kB
JavaScript
/**
* Basic context-aware features (simplified approach)
* Provides intelligent response adaptation and user context awareness
*/
import { performanceCollector } from './performance-monitor.js';
/**
* Basic context-aware response adapter
*/
export class ContextAwareAdapter {
adaptationRules = [];
contextCache = new Map();
constructor() {
this.initializeDefaultRules();
}
/**
* Set user context for session
*/
setContext(sessionId, context) {
this.contextCache.set(sessionId, {
...this.contextCache.get(sessionId),
...context,
});
}
/**
* Get user context for session
*/
getContext(sessionId) {
return this.contextCache.get(sessionId) || {};
}
/**
* Clear context for session
*/
clearContext(sessionId) {
this.contextCache.delete(sessionId);
}
/**
* Adapt response based on context
*/
adaptResponse(sessionId, query, originalResponse) {
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 = {
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) {
this.adaptationRules.push(rule);
// Sort by priority to ensure correct order
this.adaptationRules.sort((a, b) => b.priority - a.priority);
}
/**
* Initialize default adaptation rules
*/
initializeDefaultRules() {
// 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
*/
updateRecentQueries(sessionId, query) {
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
*/
isLargeDataset(response) {
if (Array.isArray(response)) {
return response.length > 50;
}
if (typeof response === 'object' && response !== null) {
const obj = response;
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
*/
isErrorResponse(response) {
if (typeof response === 'object' && response !== null) {
const obj = response;
return (obj.error !== undefined ||
obj.code !== undefined ||
(typeof obj.content === 'object' &&
obj.content?.error !== undefined));
}
return false;
}
/**
* Check if response contains detailed information
*/
isDetailedResponse(response) {
if (typeof response === 'object' && response !== null) {
const text = JSON.stringify(response);
return (text.length > 5000 ||
Object.keys(response).length > 20);
}
return false;
}
/**
* Check if response contains restricted data
*/
containsRestrictedData(response, permissions) {
// 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
*/
summarizeResponse(response) {
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
*/
simplifyResponse(response) {
if (typeof response === 'object' && response !== null) {
const obj = response;
// Keep only essential fields
const essentialFields = [
'id',
'name',
'title',
'summary',
'status',
'type',
];
const simplified = {};
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
*/
filterByPermissions(response, permissions) {
// 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,
_filtered: true,
_reason: 'Some fields may be hidden due to permissions',
};
}
return response;
}
/**
* Extract error information
*/
extractErrorInfo(response) {
if (typeof response === 'object' && response !== null) {
const obj = response;
return {
message: obj.error || obj.message,
code: obj.code,
type: obj.type || 'unknown',
};
}
return {};
}
/**
* Generate basic suggestions based on context and query
*/
generateBasicSuggestions(context, query) {
const suggestions = [];
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
*/
generateRelatedActions(_context, query) {
const actions = [];
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
*/
generateNextSteps(_context, query) {
const steps = [];
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
*/
generateTips(context, _query) {
const tips = [];
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
*/
generateErrorRecoverySteps(errorInfo) {
const steps = [];
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
*/
generateErrorTips(errorInfo) {
const tips = [];
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() {
return {
totalSessions: this.contextCache.size,
activeRules: this.adaptationRules.length,
averageAdaptations: 0, // Would calculate from metrics in real implementation
};
}
/**
* Clean old context data
*/
cleanOldContexts(_maxAgeMs = 3600000) {
// 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();