UNPKG

@the_cfdude/productboard-mcp

Version:

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

404 lines (403 loc) 12.2 kB
/** * Dynamic field selection utilities * Provides precise field specification to replace basic/standard/full detail levels */ /** * Field selection processor for precise response filtering */ export class FieldSelector { entityFieldMaps = new Map(); constructor() { this.initializeFieldMaps(); } /** * Initialize field mappings for different entity types */ initializeFieldMaps() { // Features this.entityFieldMaps.set('feature', new Set([ 'id', 'name', 'description', 'status', 'owner', 'createdAt', 'updatedAt', 'timeframe', 'timeframe.startDate', 'timeframe.endDate', 'timeframe.duration', 'parent', 'parent.id', 'parent.name', 'parent.product', 'owner.id', 'owner.email', 'owner.name', 'status.id', 'status.name', 'status.color', 'archived', 'links', 'customFields', 'components', 'releases', ])); // Notes this.entityFieldMaps.set('note', new Set([ 'id', 'title', 'content', 'owner', 'user', 'company', 'createdAt', 'updatedAt', 'owner.id', 'owner.email', 'owner.name', 'user.id', 'user.email', 'user.name', 'user.externalId', 'company.id', 'company.name', 'company.domain', 'tags', 'links', 'displayUrl', 'source', ])); // Companies this.entityFieldMaps.set('company', new Set([ 'id', 'name', 'description', 'domain', 'externalId', 'createdAt', 'updatedAt', 'customFields', 'hasNotes', 'notesCount', ])); // Components this.entityFieldMaps.set('component', new Set([ 'id', 'name', 'description', 'product', 'createdAt', 'updatedAt', 'product.id', 'product.name', ])); // Products this.entityFieldMaps.set('product', new Set([ 'id', 'name', 'description', 'createdAt', 'updatedAt', 'components', 'componentsCount', ])); // Users this.entityFieldMaps.set('user', new Set([ 'id', 'email', 'name', 'role', 'company', 'externalId', 'createdAt', 'updatedAt', 'company.id', 'company.name', 'company.domain', ])); // Objectives this.entityFieldMaps.set('objective', new Set([ 'id', 'name', 'description', 'owner', 'startDate', 'endDate', 'createdAt', 'updatedAt', 'owner.id', 'owner.email', 'owner.name', 'keyResults', 'initiatives', 'features', ])); // Initiatives this.entityFieldMaps.set('initiative', new Set([ 'id', 'name', 'description', 'owner', 'status', 'createdAt', 'updatedAt', 'owner.id', 'owner.email', 'owner.name', 'objectives', 'features', ])); // Key Results this.entityFieldMaps.set('keyResult', new Set([ 'id', 'name', 'objective', 'type', 'startValue', 'currentValue', 'targetValue', 'createdAt', 'updatedAt', 'objective.id', 'objective.name', ])); // Releases this.entityFieldMaps.set('release', new Set([ 'id', 'name', 'description', 'releaseGroup', 'startDate', 'releaseDate', 'state', 'createdAt', 'updatedAt', 'releaseGroup.id', 'releaseGroup.name', ])); // Release Groups this.entityFieldMaps.set('releaseGroup', new Set([ 'id', 'name', 'description', 'isDefault', 'createdAt', 'updatedAt', 'releases', 'releasesCount', ])); } /** * Validate requested fields against entity schema */ validateFields(entityType, requestedFields) { const validFields = this.entityFieldMaps.get(entityType); if (!validFields) { return { valid: false, error: `Unknown entity type: ${entityType}`, }; } const invalidFields = requestedFields.filter(field => !validFields.has(field)); if (invalidFields.length === 0) { return { valid: true }; } // Generate suggestions for invalid fields const suggestions = invalidFields .map(invalidField => this.suggestSimilarField(invalidField, Array.from(validFields))) .filter((suggestion) => suggestion !== null); return { valid: false, invalidFields, suggestions, error: `Invalid fields for ${entityType}: ${invalidFields.join(', ')}`, }; } /** * Apply field selection to response data */ selectFields(data, config) { if (!data || typeof data !== 'object') { return data; } if (Array.isArray(data)) { return data.map(item => this.selectFieldsFromObject(item, config)); } return this.selectFieldsFromObject(data, config); } /** * Select fields from a single object */ selectFieldsFromObject(obj, config) { if (!obj || typeof obj !== 'object') { return obj; } const { fields, exclude = [] } = config; // If no fields specified, return all except excluded if (!fields || fields.length === 0) { if (exclude.length === 0) { return obj; } return this.excludeFields(obj, exclude); } // Build result with only requested fields const result = {}; for (const fieldPath of fields) { if (exclude.includes(fieldPath)) { continue; // Skip excluded fields } const value = this.getNestedValue(obj, fieldPath); if (value !== undefined) { this.setNestedValue(result, fieldPath, value); } } return result; } /** * Exclude specific fields from object */ excludeFields(obj, excludeFields) { const result = { ...obj }; for (const fieldPath of excludeFields) { this.deleteNestedValue(result, fieldPath); } return result; } /** * Get nested value using dot notation (e.g., "owner.email") */ getNestedValue(obj, path) { if (!path.includes('.')) { return obj[path]; } const parts = path.split('.'); let current = obj; for (const part of parts) { if (current === null || current === undefined || typeof current !== 'object') { return undefined; } current = current[part]; } return current; } /** * Set nested value using dot notation */ setNestedValue(obj, path, value) { if (!path.includes('.')) { obj[path] = value; return; } const parts = path.split('.'); const lastPart = parts.pop(); let current = obj; for (const part of parts) { if (!current[part] || typeof current[part] !== 'object') { current[part] = {}; } current = current[part]; } current[lastPart] = value; } /** * Delete nested value using dot notation */ deleteNestedValue(obj, path) { if (!path.includes('.')) { delete obj[path]; return; } const parts = path.split('.'); const lastPart = parts.pop(); let current = obj; for (const part of parts) { if (!current[part] || typeof current[part] !== 'object') { return; // Path doesn't exist } current = current[part]; } delete current[lastPart]; } /** * Suggest similar field names for typos */ suggestSimilarField(invalidField, validFields) { const lowerInvalid = invalidField.toLowerCase(); // Exact case-insensitive match const exactMatch = validFields.find(field => field.toLowerCase() === lowerInvalid); if (exactMatch) { return exactMatch; } // Partial matches const partialMatches = validFields.filter(field => field.toLowerCase().includes(lowerInvalid) || lowerInvalid.includes(field.toLowerCase())); if (partialMatches.length > 0) { return partialMatches[0]; } // Levenshtein distance for typos let closestField = null; let minDistance = Infinity; for (const field of validFields) { const distance = this.levenshteinDistance(lowerInvalid, field.toLowerCase()); if (distance < minDistance && distance <= Math.max(2, invalidField.length * 0.3)) { minDistance = distance; closestField = field; } } return closestField; } /** * Calculate Levenshtein distance for typo suggestions */ levenshteinDistance(str1, str2) { const matrix = Array(str2.length + 1) .fill(null) .map(() => Array(str1.length + 1).fill(null)); for (let i = 0; i <= str1.length; i++) matrix[0][i] = i; for (let j = 0; j <= str2.length; j++) matrix[j][0] = j; for (let j = 1; j <= str2.length; j++) { for (let i = 1; i <= str1.length; i++) { const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1; matrix[j][i] = Math.min(matrix[j][i - 1] + 1, // deletion matrix[j - 1][i] + 1, // insertion matrix[j - 1][i - 1] + indicator // substitution ); } } return matrix[str2.length][str1.length]; } /** * Get essential fields for an entity type (used as smart defaults) */ getEssentialFields(entityType) { const essentialFieldMaps = { feature: ['id', 'name', 'status.name', 'owner.email'], note: ['id', 'title', 'owner.email', 'createdAt'], company: ['id', 'name', 'domain'], component: ['id', 'name', 'product.name'], product: ['id', 'name'], user: ['id', 'email', 'name', 'role'], objective: ['id', 'name', 'owner.email', 'startDate', 'endDate'], initiative: ['id', 'name', 'owner.email', 'status'], keyResult: ['id', 'name', 'currentValue', 'targetValue'], release: ['id', 'name', 'releaseDate', 'state'], releaseGroup: ['id', 'name', 'isDefault'], }; return essentialFieldMaps[entityType] || ['id', 'name']; } /** * Get all available fields for an entity type */ getAvailableFields(entityType) { const fields = this.entityFieldMaps.get(entityType); return fields ? Array.from(fields).sort() : []; } } // Export singleton instance export const fieldSelector = new FieldSelector();