Files
n8n-mcp/docs/MCP_IMPLEMENTATION_GUIDE.md
czlonkowski 1884d5babf feat: implement AI-optimized MCP tools with 95% size reduction
- Add get_node_essentials tool for 10-20 essential properties only
- Add search_node_properties for targeted property search
- Add get_node_for_task with 14 pre-configured templates
- Add validate_node_config for comprehensive validation
- Add get_property_dependencies for visibility analysis
- Implement PropertyFilter service with curated essentials
- Implement ExampleGenerator with working examples
- Implement TaskTemplates for common workflows
- Implement ConfigValidator with security checks
- Implement PropertyDependencies for dependency analysis
- Enhance property descriptions to 100% coverage
- Add version information to essentials response
- Update documentation with new tools

Response sizes reduced from 100KB+ to <5KB for better AI agent usability.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-16 12:37:45 +02:00

12 KiB

MCP Implementation Guide - Practical Steps

Understanding the Current Architecture

Your current system already does the hard work:

n8n packages → PropertyExtractor → Complete Property Schema (JSON) → SQLite → MCP Tools

The properties are well-structured with:

  • Complete type information
  • Display options (conditional visibility)
  • Default values and descriptions
  • Options for select fields

The issue is that get_node_info returns ALL of this (200+ properties) when AI agents only need 10-20.

Step 1: Create Property Filter Service

Create src/services/property-filter.ts:

interface SimplifiedProperty {
  name: string;
  displayName: string;
  type: string;
  description: string;
  default?: any;
  options?: Array<{ value: string; label: string }>;
  required?: boolean;
  showWhen?: Record<string, any>;
}

interface EssentialConfig {
  required: string[];
  common: string[];
}

export class PropertyFilter {
  // Start with manual curation for most-used nodes
  private static ESSENTIAL_PROPERTIES: Record<string, EssentialConfig> = {
    'nodes-base.httpRequest': {
      required: ['url'],
      common: ['method', 'authentication', 'sendBody', 'contentType', 'sendHeaders']
    },
    'nodes-base.webhook': {
      required: [],
      common: ['httpMethod', 'path', 'responseMode', 'responseData', 'responseCode']
    },
    'nodes-base.set': {
      required: [],
      common: ['mode', 'assignments']
    },
    'nodes-base.if': {
      required: [],
      common: ['conditions']
    },
    'nodes-base.code': {
      required: [],
      common: ['language', 'jsCode', 'pythonCode']
    },
    'nodes-base.postgres': {
      required: [],
      common: ['operation', 'query', 'table', 'columns']
    },
    'nodes-base.openAi': {
      required: [],
      common: ['resource', 'operation', 'modelId', 'prompt']
    }
  };
  
  static getEssentials(allProperties: any[], nodeType: string): {
    required: SimplifiedProperty[];
    common: SimplifiedProperty[];
  } {
    const config = this.ESSENTIAL_PROPERTIES[nodeType];
    
    if (!config) {
      // Fallback: Take required + first 5 non-conditional properties
      return this.inferEssentials(allProperties);
    }
    
    // Extract specified properties
    const required = config.required
      .map(name => allProperties.find(p => p.name === name))
      .filter(Boolean)
      .map(p => this.simplifyProperty(p));
    
    const common = config.common
      .map(name => allProperties.find(p => p.name === name))
      .filter(Boolean)
      .map(p => this.simplifyProperty(p));
    
    return { required, common };
  }
  
  private static simplifyProperty(prop: any): SimplifiedProperty {
    const simplified: SimplifiedProperty = {
      name: prop.name,
      displayName: prop.displayName || prop.name,
      type: prop.type,
      description: prop.description || '',
      required: prop.required || false
    };
    
    // Include default if it's simple
    if (prop.default !== undefined && typeof prop.default !== 'object') {
      simplified.default = prop.default;
    }
    
    // Simplify options
    if (prop.options && Array.isArray(prop.options)) {
      simplified.options = prop.options.map((opt: any) => ({
        value: typeof opt === 'string' ? opt : (opt.value || opt.name),
        label: typeof opt === 'string' ? opt : (opt.name || opt.value)
      }));
    }
    
    // Include simple display conditions
    if (prop.displayOptions?.show && Object.keys(prop.displayOptions.show).length <= 2) {
      simplified.showWhen = prop.displayOptions.show;
    }
    
    return simplified;
  }
  
  private static inferEssentials(properties: any[]) {
    // For unknown nodes, use heuristics
    const required = properties
      .filter(p => p.required)
      .map(p => this.simplifyProperty(p));
    
    const common = properties
      .filter(p => !p.required && !p.displayOptions) // Simple, always-visible properties
      .slice(0, 5)
      .map(p => this.simplifyProperty(p));
    
    return { required, common };
  }
}

Step 2: Create Example Generator

Create src/services/example-generator.ts:

export class ExampleGenerator {
  private static EXAMPLES: Record<string, Record<string, any>> = {
    'nodes-base.httpRequest': {
      minimal: {
        url: 'https://api.example.com/data'
      },
      getWithAuth: {
        method: 'GET',
        url: 'https://api.example.com/protected',
        authentication: 'genericCredentialType',
        genericAuthType: 'headerAuth'
      },
      postJson: {
        method: 'POST',
        url: 'https://api.example.com/create',
        sendBody: true,
        contentType: 'json',
        specifyBody: 'json',
        jsonBody: '{ "name": "Example User", "email": "user@example.com" }'
      }
    },
    'nodes-base.webhook': {
      minimal: {
        path: 'my-webhook',
        httpMethod: 'POST'
      },
      withResponse: {
        path: 'webhook-endpoint',
        httpMethod: 'POST',
        responseMode: 'lastNode',
        responseData: 'allEntries'
      }
    }
  };
  
  static getExamples(nodeType: string, essentials: any): Record<string, any> {
    // Return curated examples if available
    if (this.EXAMPLES[nodeType]) {
      return this.EXAMPLES[nodeType];
    }
    
    // Otherwise, generate minimal example
    const minimal: Record<string, any> = {};
    
    // Add required fields
    for (const prop of essentials.required) {
      minimal[prop.name] = this.getDefaultValue(prop);
    }
    
    // Add first common field with a default
    const firstCommon = essentials.common.find((p: any) => p.default !== undefined);
    if (firstCommon) {
      minimal[firstCommon.name] = firstCommon.default;
    }
    
    return { minimal };
  }
  
  private static getDefaultValue(prop: any): any {
    if (prop.default !== undefined) return prop.default;
    
    switch (prop.type) {
      case 'string':
        return prop.name === 'url' ? 'https://api.example.com' : '';
      case 'number':
        return 0;
      case 'boolean':
        return false;
      case 'options':
        return prop.options?.[0]?.value || '';
      default:
        return '';
    }
  }
}

Step 3: Implement get_node_essentials Tool

Add to src/mcp/server.ts in the tool handler switch:

case "get_node_essentials": {
  const { nodeType } = request.params.arguments as { nodeType: string };
  
  // Get node from database
  const node = await service.getNodeByType(nodeType);
  if (!node) {
    throw new Error(`Node type ${nodeType} not found`);
  }
  
  // Parse properties
  const allProperties = JSON.parse(node.properties_schema || '[]');
  
  // Get essentials
  const essentials = PropertyFilter.getEssentials(allProperties, nodeType);
  
  // Generate examples
  const examples = ExampleGenerator.getExamples(nodeType, essentials);
  
  // Parse operations
  const operations = JSON.parse(node.operations || '[]');
  
  return {
    content: [{
      type: "text",
      text: JSON.stringify({
        nodeType: node.node_type,
        displayName: node.display_name,
        description: node.description,
        category: node.category,
        requiredProperties: essentials.required,
        commonProperties: essentials.common,
        operations: operations.map((op: any) => ({
          name: op.name || op.operation,
          description: op.description
        })),
        examples,
        metadata: {
          totalProperties: allProperties.length,
          isAITool: node.is_ai_tool,
          isTrigger: node.is_trigger,
          hasCredentials: node.credentials_required ? true : false
        }
      }, null, 2)
    }]
  };
}

Step 4: Add Tool Definition

In src/mcp/server.ts, add to the tools array:

{
  name: "get_node_essentials",
  description: "Get only essential properties for a node (10-20 most important properties instead of 200+). Perfect for quick configuration.",
  inputSchema: {
    type: "object",
    properties: {
      nodeType: {
        type: "string",
        description: "The node type (e.g., 'nodes-base.httpRequest')"
      }
    },
    required: ["nodeType"]
  }
}

Step 5: Test Implementation

Create scripts/test-essentials.ts:

#!/usr/bin/env node
import { NodeDocumentationService } from '../src/services/node-documentation-service';
import { PropertyFilter } from '../src/services/property-filter';
import { ExampleGenerator } from '../src/services/example-generator';

async function testEssentials() {
  const service = new NodeDocumentationService();
  await service.initialize();
  
  const nodeTypes = [
    'nodes-base.httpRequest',
    'nodes-base.webhook', 
    'nodes-base.set',
    'nodes-base.code'
  ];
  
  for (const nodeType of nodeTypes) {
    console.log(`\n=== Testing ${nodeType} ===`);
    
    const node = await service.getNodeByType(nodeType);
    if (!node) continue;
    
    const allProperties = JSON.parse(node.properties_schema || '[]');
    const essentials = PropertyFilter.getEssentials(allProperties, nodeType);
    const examples = ExampleGenerator.getExamples(nodeType, essentials);
    
    console.log(`Total properties: ${allProperties.length}`);
    console.log(`Essential properties: ${essentials.required.length + essentials.common.length}`);
    console.log(`Size reduction: ${Math.round((1 - (essentials.required.length + essentials.common.length) / allProperties.length) * 100)}%`);
    
    console.log('\nRequired:', essentials.required.map(p => p.name).join(', ') || 'None');
    console.log('Common:', essentials.common.map(p => p.name).join(', '));
    console.log('Examples:', Object.keys(examples).join(', '));
    
    // Compare response sizes
    const fullSize = JSON.stringify(allProperties).length;
    const essentialSize = JSON.stringify({ ...essentials, examples }).length;
    console.log(`\nResponse size: ${(fullSize / 1024).toFixed(1)}KB → ${(essentialSize / 1024).toFixed(1)}KB`);
  }
  
  await service.close();
}

testEssentials().catch(console.error);

Step 6: Iterate Based on Testing

After testing, refine the essential property lists by:

  1. Analyzing actual usage: Which properties do users set most often?
  2. AI agent feedback: Which properties cause confusion?
  3. Workflow analysis: What are common patterns?

Next Tools to Implement

search_node_properties (Week 1)

case "search_node_properties": {
  const { nodeType, query } = request.params.arguments;
  const allProperties = JSON.parse(node.properties_schema || '[]');
  
  // Flatten nested properties and search
  const flattened = PropertyFlattener.flatten(allProperties);
  const matches = flattened.filter(p => 
    p.name.toLowerCase().includes(query.toLowerCase()) ||
    p.displayName?.toLowerCase().includes(query.toLowerCase()) ||
    p.description?.toLowerCase().includes(query.toLowerCase())
  );
  
  return { matches: matches.slice(0, 20) };
}

validate_node_config (Week 2)

case "validate_node_config": {
  const { nodeType, config } = request.params.arguments;
  // Use existing properties and displayOptions to validate
}

get_node_for_task (Week 2)

case "get_node_for_task": {
  const { task } = request.params.arguments;
  // Return pre-configured templates
}

Measuring Success

Track these metrics:

  1. Response size reduction (target: >90%)
  2. Time to configure a node (target: <1 minute)
  3. AI agent success rate (target: >90%)
  4. Number of tool calls needed (target: 2-3)

Key Insight

Your existing system is already excellent at extracting properties. The solution isn't to rebuild it, but to add intelligent filtering on top. This approach:

  • Delivers immediate value
  • Requires minimal changes
  • Preserves all existing functionality
  • Can be iteratively improved