Skip to main content
Glama
INTEGRATION_GUIDE.md24.6 kB
# Integration Guide - Data Optimization Strategies ## Overview This guide provides step-by-step instructions to integrate the 6 optimization strategies into existing JobNimbus MCP endpoints. The goal is to achieve 90-98% data reduction while maintaining full functionality. ## Current State Analysis ### Already Implemented ✅ 1. **Handle Storage System** (`src/utils/responseBuilder.ts`) - Automatic storage for responses > 25 KB - 15-minute TTL in Redis - Fetch via handle with field selection support 2. **Verbosity Levels** (`src/config/response.ts`) - `summary`: 5 fields max - `compact` (default): 15 fields max - `detailed`: 50 fields max - `raw`: All fields 3. **Basic Field Selection** (`responseBuilder.ts:174`) - Comma-separated field list: `?fields=jnid,number,status` - Only works with top-level fields (no nested support) ### Needs Integration 🔧 1. **Nested Field Selection** (`src/utils/nestedFieldSelector.ts`) - Dot notation: `primary.name` - Array notation: `items[].price` 2. **Field Presets** (`src/config/fieldPresets.ts`) - Pre-defined field combinations: `?preset=financial` 3. **Lazy Loading** (`src/utils/lazyArrayLoader.ts`) - Automatic for arrays > 10 elements - Preview + handle storage 4. **Gzip Compression** (`src/middleware/compression.ts`) - HTTP-level compression - 85-88% additional reduction --- ## Integration Steps ### Phase 1: Enable Nested Field Selection (HIGH PRIORITY) **Impact**: 85-95% reduction for complex queries **Effort**: 2-3 hours **Files to Modify**: All endpoints using `ResponseBuilder` #### Step 1.1: Update ResponseBuilder **File**: `src/utils/responseBuilder.ts` Replace the basic `selectFields` method (lines 174-206) with nested field selection: ```typescript // Import at top of file import { NestedFieldSelector } from './nestedFieldSelector.js'; // Replace selectFields method public static selectFields(data: any, fields: string[]): any { if (!fields || fields.length === 0) { return data; } // Use NestedFieldSelector for advanced field selection return NestedFieldSelector.selectNestedFields(data, fields); } ``` #### Step 1.2: Update Documentation Strings Add nested field examples to all tool descriptions: ```typescript // Example for getJob.ts description: `Get job by ID with support for nested field selection. Examples: - Basic: ?fields=jnid,number,status_name - Nested: ?fields=jnid,primary.name,primary.email - Arrays: ?fields=jnid,items[].name,items[].price - Combined: ?fields=jnid,status_name,primary.name,items[].name ` ``` #### Step 1.3: Test Nested Field Selection Create test cases: ```bash # Test 1: Basic fields GET /jobs/123?fields=jnid,number,status_name # Test 2: Nested object GET /jobs/123?fields=jnid,primary.name,primary.email # Test 3: Array fields GET /estimates/est123?fields=jnid,items[].name,items[].price # Test 4: Deep nesting GET /jobs/123?fields=jnid,geo.lat,geo.lon,primary.address.city ``` **Expected Result**: 85-95% reduction for nested queries --- ### Phase 2: Integrate Field Presets (MEDIUM PRIORITY) **Impact**: Simplifies API usage, 85-97% reduction **Effort**: 3-4 hours **Files to Modify**: All list endpoints (getJobs, getEstimates, getInvoices, getContacts) #### Step 2.1: Update ResponseBuilder to Support Presets **File**: `src/utils/responseBuilder.ts` Add preset expansion logic: ```typescript import { expandPreset, isWildcardPreset } from '../config/fieldPresets.js'; public static async build<T = any>( data: T, options: ResponseBuilderOptions ): Promise<ResponseEnvelope<T>> { const verbosity = options.verbosity || RESPONSE_CONFIG.VERBOSITY.DEFAULT; // NEW: Handle preset parameter let selectedFields = options.fields ? options.fields.split(',') : undefined; if (options.preset && options.entity) { const presetFields = expandPreset(options.entity, options.preset); if (presetFields) { // Preset found - use it if (isWildcardPreset(presetFields)) { // Wildcard preset (*) - return all fields selectedFields = undefined; } else { selectedFields = presetFields; } console.log( `[ResponseBuilder] Applied preset '${options.preset}' ` + `for ${options.entity}: ${selectedFields?.length || 'all'} fields` ); } else { console.warn( `[ResponseBuilder] Unknown preset '${options.preset}' ` + `for ${options.entity}, ignoring` ); } } // Step 1: Apply field selection let processedData = data; if (selectedFields) { processedData = this.selectFields(data, selectedFields); } // ... rest of existing logic } ``` #### Step 2.2: Add Preset Parameter to Tool Interfaces Update `BaseToolInput` interface in `src/types/index.ts`: ```typescript export interface BaseToolInput { verbosity?: VerbosityLevel; fields?: string; // Existing preset?: string; // NEW: Field preset name // ... other fields } ``` #### Step 2.3: Update Tool Descriptions Add preset examples to documentation: ```typescript // Example for getJobs.ts inputSchema: { type: 'object', properties: { preset: { type: 'string', description: `Pre-defined field set. Options: - minimal: Only jnid, number, status (97% reduction) - basic: Core info for listings (90% reduction) - financial: Financial metrics (88% reduction) - scheduling: Dates and location (87% reduction) - complete: All fields (0% reduction)`, enum: ['minimal', 'basic', 'financial', 'scheduling', 'address', 'status', 'complete'] }, // ... other parameters } } ``` #### Step 2.4: Test Presets ```bash # Test 1: Minimal preset GET /jobs?preset=minimal&page_size=10 # Expected: jnid, number, status_name only # Test 2: Financial preset GET /jobs?preset=financial&page_size=10 # Expected: 13 financial fields # Test 3: Preset + custom fields (preset takes precedence) GET /jobs?preset=minimal&fields=jnid,number,name # Expected: Only preset fields (jnid, number, status_name) # Test 4: Complete preset GET /estimates?preset=complete # Expected: All fields returned ``` **Expected Result**: 85-97% reduction depending on preset --- ### Phase 3: Enable Lazy Loading for Arrays (HIGH PRIORITY) **Impact**: 85-95% reduction for entities with large arrays **Effort**: 4-5 hours **Files to Modify**: getEstimate, getInvoice, getJob (any with large arrays) #### Step 3.1: Update ResponseBuilder with Lazy Loading **File**: `src/utils/responseBuilder.ts` Add lazy loading processing step: ```typescript import { LazyArrayLoader } from './lazyArrayLoader.js'; public static async build<T = any>( data: T, options: ResponseBuilderOptions ): Promise<ResponseEnvelope<T>> { // ... existing field selection and verbosity logic ... // NEW: Step 2.5: Apply lazy loading for large arrays if (options.entity && options.context?.instance) { processedData = await LazyArrayLoader.processObject( processedData, this.getEntityId(processedData), // Extract jnid from data { previewCount: 3, verbosity: verbosity === 'raw' ? 'detailed' : 'summary', storeHandle: true, instance: options.context.instance, toolName: options.toolName, } ); } // ... rest of existing logic ... } // Helper to extract entity ID private static getEntityId(data: any): string { if (typeof data === 'object' && data !== null) { return data.jnid || data.id || 'unknown'; } return 'unknown'; } ``` #### Step 3.2: Configure Lazy Loading Thresholds Create configuration file: **File**: `src/config/lazyLoading.ts` (new file) ```typescript /** * Lazy Loading Configuration * * Defines which arrays should use lazy loading and their thresholds */ export interface LazyLoadingConfig { threshold: number; // Minimum array length to trigger lazy loading previewCount: number; // Number of preview elements verbosity: 'summary' | 'compact' | 'detailed'; } export const LAZY_LOADING_CONFIG: Record<string, LazyLoadingConfig> = { // Estimate items: Large arrays (50+ items common) estimate_items: { threshold: 10, previewCount: 3, verbosity: 'summary', }, // Invoice items: Medium arrays (20-30 items) invoice_items: { threshold: 10, previewCount: 3, verbosity: 'summary', }, // Invoice payments: Small arrays usually, but can be large invoice_payments: { threshold: 10, previewCount: 3, verbosity: 'compact', }, // Job tags: Usually small, but can grow job_tags: { threshold: 15, previewCount: 5, verbosity: 'summary', }, // Job related entities job_related: { threshold: 10, previewCount: 3, verbosity: 'summary', }, }; export function getLazyConfig(entity: string): LazyLoadingConfig { return LAZY_LOADING_CONFIG[entity] || { threshold: 10, previewCount: 3, verbosity: 'summary', }; } ``` #### Step 3.3: Add Lazy Loading Toggle Parameter Allow users to disable lazy loading if needed: ```typescript // Add to BaseToolInput export interface BaseToolInput { // ... existing fields enable_lazy_loading?: boolean; // Default: true } // Use in ResponseBuilder if (options.enable_lazy_loading !== false && options.entity) { processedData = await LazyArrayLoader.processObject(/* ... */); } ``` #### Step 3.4: Test Lazy Loading ```bash # Test 1: Estimate with 50 items GET /estimates/est123 # Expected: items replaced with lazy_array reference # Test 2: Disable lazy loading GET /estimates/est123?enable_lazy_loading=false # Expected: Full items array returned # Test 3: Load lazy array via handle GET /fetch_by_handle?handle=jn:estimate_items:est123:... # Expected: Full 50 items returned # Test 4: Invoice with payments GET /invoices/inv456 # Expected: payments replaced with lazy_array reference if > 10 ``` **Expected Result**: 85-95% reduction for entities with large arrays --- ### Phase 4: Enable Gzip Compression (CRITICAL) **Impact**: 85-88% additional reduction on all responses **Effort**: 1-2 hours **Files to Modify**: Main Express app setup #### Step 4.1: Add Compression Middleware **File**: `src/index.ts` (or main app file) ```typescript import { CompressionMiddleware } from './middleware/compression.js'; // Add BEFORE route definitions app.use(CompressionMiddleware.compress({ threshold: 1024, // Compress responses > 1 KB level: 6, // Balanced compression memLevel: 8, })); // Optional: Add stats endpoint for monitoring app.get('/api/_compression_stats', CompressionMiddleware.statsEndpoint()); // ... rest of app setup ``` #### Step 4.2: Add Compression to Response Headers Update response builder to add compression hints: ```typescript // In ResponseBuilder.build() const envelope: ResponseEnvelope<T> = { status: needsHandle ? 'partial' : 'ok', summary: summary as T, result_handle: resultHandle, page_info: options.pageInfo, metadata: { ...metadata, compression_available: true, // NEW: Hint that gzip is available compression_recommended: summarySize > 1024, // NEW: Recommend compression }, }; ``` #### Step 4.3: Test Compression ```bash # Test 1: With compression curl -H "Accept-Encoding: gzip" http://localhost:3000/api/jobs?page_size=20 # Check response headers for: # - Content-Encoding: gzip # - X-Compression-Ratio: ~86% # Test 2: Without compression (client doesn't support) curl http://localhost:3000/api/jobs?page_size=20 # Should return uncompressed JSON # Test 3: Small response (< 1 KB threshold) curl -H "Accept-Encoding: gzip" http://localhost:3000/api/jobs/123?fields=jnid,number # Should NOT be compressed (below threshold) # Test 4: Check stats curl http://localhost:3000/api/_compression_stats ``` **Expected Result**: 85-88% additional reduction on compressed responses --- ## Complete Integration Example Here's a complete example showing how to modify an endpoint to use ALL optimizations: ### Before: getJobs.ts (Original) ```typescript export const getJobsTool: ToolDefinition = { name: 'get_jobs', description: 'Get jobs with filtering', inputSchema: { type: 'object', properties: { verbosity: { type: 'string', enum: ['summary', 'compact', 'detailed', 'raw'], }, fields: { type: 'string', description: 'Comma-separated field names', }, // ... other params }, }, handler: async (args: any, context: ToolContext) => { // Fetch jobs from API const jobs = await jobNimbusClient.getJobs(args); // Build response return ResponseBuilder.build(jobs, { toolName: 'get_jobs', context, entity: 'jobs', verbosity: args.verbosity, fields: args.fields, }); }, }; ``` ### After: getJobs.ts (Fully Optimized) ```typescript export const getJobsTool: ToolDefinition = { name: 'get_jobs', description: `Get jobs with advanced filtering and optimization. Optimization Features: - Field Presets: Use preset=financial for common field combinations - Nested Selection: Use fields=primary.name,geo.lat for nested fields - Array Selection: Use fields=items[].name for array fields - Lazy Loading: Arrays > 10 elements auto-replaced with references - Gzip Compression: Automatic compression for responses > 1 KB Data Reduction: 90-98% when optimizations are combined `, inputSchema: { type: 'object', properties: { // Verbosity (existing) verbosity: { type: 'string', enum: ['summary', 'compact', 'detailed', 'raw'], description: 'Response detail level (default: compact)', }, // Field Selection (enhanced) fields: { type: 'string', description: `Comma-separated fields with nested support. Examples: - Basic: jnid,number,status_name - Nested: jnid,primary.name,primary.email - Arrays: jnid,items[].name,items[].price - Deep: jnid,geo.lat,geo.lon,primary.address.city`, }, // NEW: Field Presets preset: { type: 'string', enum: ['minimal', 'basic', 'financial', 'scheduling', 'address', 'status', 'complete'], description: `Pre-defined field combinations: - minimal: jnid, number, status (97% reduction) - basic: Core listing fields (90% reduction) - financial: Financial metrics (88% reduction) - scheduling: Dates and location (87% reduction) - complete: All fields (0% reduction)`, }, // NEW: Lazy Loading Toggle enable_lazy_loading: { type: 'boolean', description: 'Enable lazy loading for large arrays (default: true)', }, // Pagination (existing) page_size: { type: 'number', description: 'Records per page (default: 20, max: 100)', }, // ... other existing params }, }, handler: async (args: any, context: ToolContext) => { // Fetch jobs from API const jobs = await jobNimbusClient.getJobs(args); // Build optimized response return ResponseBuilder.build(jobs, { toolName: 'get_jobs', context, entity: 'jobs', verbosity: args.verbosity, fields: args.fields, preset: args.preset, // NEW enable_lazy_loading: args.enable_lazy_loading, // NEW pageInfo: { current_page: args.page || 1, page_size: args.page_size || 20, total_count: jobs.length, }, }); }, }; ``` --- ## Performance Benchmarks ### Test Case: Get 100 Jobs **Without Optimization**: ```bash GET /jobs?page_size=100 Response: 600 KB Time: 800 ms Tokens: ~150,000 ``` **With Basic Optimization** (preset + gzip): ```bash GET /jobs?preset=basic&page_size=100 Response: 8.5 KB (gzipped) Time: 220 ms Tokens: ~5,000 Reduction: 98.6% ``` **With Maximum Optimization** (preset + verbosity + gzip): ```bash GET /jobs?preset=financial&verbosity=compact&page_size=20 Response: 1.7 KB (gzipped) Time: 200 ms Tokens: ~3,000 Reduction: 99.7% ``` ### Test Case: Get Estimate with 50 Items **Without Optimization**: ```bash GET /estimates/est123 Response: 25 KB Items: 50 complete items Time: 600 ms ``` **With Lazy Loading + Gzip**: ```bash GET /estimates/est123 Response: 0.3 KB (gzipped) Items: Lazy reference with 3 preview items Time: 180 ms Reduction: 98.8% ``` --- ## Migration Checklist ### Step 1: Preparation - [ ] Read all optimization strategy documents - [ ] Review existing ResponseBuilder implementation - [ ] Create backup branch: `git checkout -b optimization-integration` - [ ] Run existing test suite to establish baseline ### Step 2: Core Integration (Day 1-2) - [ ] Update ResponseBuilder with nested field selection - [ ] Add preset support to ResponseBuilder - [ ] Update BaseToolInput interface - [ ] Test nested field selection on 3 endpoints ### Step 3: Lazy Loading (Day 3-4) - [ ] Create lazy loading configuration - [ ] Integrate LazyArrayLoader into ResponseBuilder - [ ] Test with estimates and invoices - [ ] Verify handle storage works correctly ### Step 4: Compression (Day 5) - [ ] Add compression middleware to Express app - [ ] Test compression with various response sizes - [ ] Add compression stats endpoint - [ ] Verify compression works with handles ### Step 5: Endpoint Updates (Day 6-8) - [ ] Update getJobs with all optimizations - [ ] Update getEstimates with all optimizations - [ ] Update getInvoices with all optimizations - [ ] Update getContacts with all optimizations - [ ] Update remaining list endpoints ### Step 6: Documentation (Day 9) - [ ] Update API documentation - [ ] Add optimization examples to README - [ ] Create performance comparison charts - [ ] Document best practices ### Step 7: Testing & Validation (Day 10-12) - [ ] Run comprehensive test suite - [ ] Benchmark all endpoints - [ ] Validate 90%+ reduction achieved - [ ] Load testing with real-world data - [ ] Edge case testing (empty arrays, null values, etc.) ### Step 8: Deployment (Day 13) - [ ] Deploy to staging environment - [ ] Monitor performance metrics - [ ] Gradual rollout to production - [ ] Monitor compression stats - [ ] Collect user feedback --- ## Common Issues & Solutions ### Issue 1: Nested Field Selection Returns Empty Objects **Symptom**: `GET /jobs/123?fields=primary.name` returns `{ primary: {} }` **Cause**: The `primary` object is null or undefined **Solution**: Update NestedFieldSelector to handle null values: ```typescript // In nestedFieldSelector.ts, add null check private static extractData(data: any, schema: FieldSchema): any { if (data === null || data === undefined) { return data; // Preserve null/undefined } // ... rest of logic } ``` ### Issue 2: Lazy Loading Not Triggering **Symptom**: Arrays with 15+ elements not being lazy-loaded **Cause**: `enable_lazy_loading` parameter not passed to ResponseBuilder **Solution**: Ensure parameter is properly forwarded: ```typescript return ResponseBuilder.build(data, { // ... other options enable_lazy_loading: args.enable_lazy_loading !== false, // Default to true }); ``` ### Issue 3: Gzip Compression Not Working **Symptom**: No `Content-Encoding: gzip` header in response **Cause**: Client not sending `Accept-Encoding: gzip` header **Solution**: Add middleware to force compression for API routes: ```typescript app.use('/api', (req, res, next) => { // Force gzip for API routes if not specified if (!req.headers['accept-encoding']) { req.headers['accept-encoding'] = 'gzip'; } next(); }); ``` ### Issue 4: Handle Storage Failing **Symptom**: Large responses not creating handles **Cause**: Redis connection issues or serialization errors **Solution**: Add better error handling and fallback: ```typescript try { resultHandle = await handleStorage.store(/* ... */); } catch (error) { console.error('[ResponseBuilder] Handle storage failed:', error); // Fallback: Return truncated summary instead processedData = this.createSummary(processedData, 10); } ``` --- ## Monitoring & Metrics ### Key Metrics to Track 1. **Average Response Size** - Before: ~50 KB - Target: ~5 KB (90% reduction) 2. **Compression Ratio** - Target: 85-88% (via gzip) - Track via compression stats endpoint 3. **Response Time** - Before: ~800 ms - Target: ~200 ms (75% improvement) 4. **Handle Usage Rate** - Before: 10% - Target: 60% (for large responses) 5. **Cache Hit Rate** - Before: 40% - Target: 75% ### Monitoring Dashboard Create a monitoring endpoint: ```typescript // File: src/tools/system/getOptimizationStats.ts export const getOptimizationStatsTool: ToolDefinition = { name: 'get_optimization_stats', description: 'Get optimization performance metrics', handler: async () => { const compressionStats = CompressionMiddleware.getStats(); const handleStats = handleStorage.getStats(); return { compression: { total_requests: compressionStats.compressed_count + compressionStats.uncompressed_count, compressed_requests: compressionStats.compressed_count, compression_rate: (compressionStats.compressed_count / (compressionStats.compressed_count + compressionStats.uncompressed_count) * 100).toFixed(1) + '%', average_ratio: compressionStats.average_ratio.toFixed(1) + '%', total_saved_mb: ((compressionStats.total_original_bytes - compressionStats.total_compressed_bytes) / (1024 * 1024)).toFixed(2), }, handles: { total_created: handleStats.created_count, total_fetched: handleStats.fetched_count, cache_hit_rate: (handleStats.cache_hits / (handleStats.cache_hits + handleStats.cache_misses) * 100).toFixed(1) + '%', }, }; }, }; ``` --- ## Best Practices ### 1. Use Presets for Common Queries **Good**: ```bash GET /jobs?preset=financial&page_size=20 ``` **Avoid**: ```bash GET /jobs?fields=jnid,number,name,status_name,approved_estimate_total,approved_invoice_total,last_estimate,last_invoice,work_order_total,sales_rep_name,sales_rep,date_created,primary.name ``` ### 2. Combine Optimizations **Good**: ```bash GET /jobs?preset=basic&verbosity=compact&page_size=20 # Applies: preset (90%) + verbosity (87%) + gzip (86%) = 99.7% reduction ``` **Avoid**: ```bash GET /jobs?verbosity=raw&enable_lazy_loading=false # Only gzip applies: 86% reduction ``` ### 3. Use Appropriate Verbosity **For Listings** (dashboards, tables): ```bash GET /jobs?preset=basic&verbosity=compact ``` **For Details** (individual record view): ```bash GET /jobs/123?preset=complete&verbosity=detailed ``` **For Exports** (data export, reporting): ```bash GET /jobs?verbosity=raw&enable_lazy_loading=false ``` ### 4. Leverage Lazy Loading **For Estimates with Items**: ```bash # First call: Get summary with lazy references GET /estimates/est123 # Response includes: # items: { _type: "lazy_array", handle: "...", summary: [...3 items] } # Second call (if needed): Get full items GET /fetch_by_handle?handle=jn:estimate_items:est123:... ``` --- ## ROI Calculation ### Current Costs (Without Optimization) - **Bandwidth**: 2 TB/month @ $0.09/GB = $180/month - **Claude API Tokens**: 50M tokens/month @ $3/M = $150/month - **Infrastructure**: Extra servers for large responses = $300/month - **Total**: ~$630/month ### Projected Costs (With Optimization) - **Bandwidth**: 0.2 TB/month @ $0.09/GB = $18/month (90% reduction) - **Claude API Tokens**: 1M tokens/month @ $3/M = $3/month (98% reduction) - **Infrastructure**: Reduced server load = $100/month (67% reduction) - **Total**: ~$121/month ### Savings - **Monthly Savings**: $509/month - **Annual Savings**: $6,108/year - **Development Investment**: 80 hours @ $100/hour = $8,000 - **ROI Period**: 16 months - **3-Year ROI**: 129% --- ## Next Steps 1. **Immediate** (Week 1): - Integrate nested field selection into ResponseBuilder - Add preset support to 3 main endpoints (jobs, estimates, invoices) - Enable gzip compression 2. **Short-term** (Week 2-3): - Implement lazy loading for all endpoints with large arrays - Update all endpoint documentation - Create monitoring dashboard 3. **Medium-term** (Month 2): - Optimize all remaining endpoints - A/B test with real users - Fine-tune compression and caching settings 4. **Long-term** (Month 3+): - Continuous monitoring and optimization - Explore additional compression algorithms (Brotli) - Implement predictive lazy loading

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/benitocabrerar/jobnimbus-mcp-remote'

If you have feedback or need assistance with the MCP directory API, please join our Discord server