Skip to content

Latest commit

 

History

History
519 lines (420 loc) · 13.4 KB

File metadata and controls

519 lines (420 loc) · 13.4 KB
layout default
title Chapter 4: N8nApiClient - Communicating with n8n
parent n8n-MCP Tutorial
nav_order 4

Chapter 4: N8nApiClient - Communicating with n8n

Welcome to the communication layer! In Chapter 3, we explored how the HTTP server manages MCP sessions. Now let's dive into the N8nApiClient—the sophisticated diplomat that handles all communication with n8n instances.

Think of the N8nApiClient as an expert translator and negotiator. n8n speaks its own REST API language, and the N8nApiClient not only translates MCP requests into n8n API calls but also handles negotiations, retries, error recovery, and version compatibility.

What Makes API Communication Complex?

n8n API communication isn't trivial. Consider these challenges:

  1. Version Compatibility - n8n evolves rapidly, API changes between versions
  2. Authentication - Secure API key handling and renewal
  3. Rate Limiting - n8n instances have request limits
  4. Network Reliability - Temporary outages, timeouts, connection issues
  5. Data Transformation - Converting between MCP and n8n data formats
  6. Error Handling - Meaningful error messages from cryptic HTTP responses

The N8nApiClient solves all these elegantly.

Core Architecture

The client is built around Axios with extensive customization:

interface N8nApiClientConfig {
  baseUrl: string;        // n8n instance URL
  apiKey: string;         // Authentication key
  timeout?: number;       // Request timeout (default: 30s)
  maxRetries?: number;    // Retry attempts (default: 3)
}

class N8nApiClient {
  private client: AxiosInstance;
  private maxRetries: number;
  private versionInfo: N8nVersionInfo | null = null;
}

Intelligent Version Detection

One of the client's most sophisticated features is version-aware communication:

// Automatically detects n8n version and adapts behavior
const version = await client.getVersion();
// Returns: { version: "2.2.3", major: 2, minor: 2, patch: 3 }

// Different versions may have different API behaviors
if (version.greaterThan('2.0.0')) {
  // Use new API endpoints
} else {
  // Fallback to legacy endpoints
}

This enables n8n-MCP to work across different n8n versions without manual configuration.

Request/Response Interceptors

The client uses Axios interceptors for cross-cutting concerns:

// Request interceptor - adds authentication and logging
this.client.interceptors.request.use((config) => {
  // Add API key header
  config.headers['X-N8N-API-KEY'] = this.apiKey;

  // Log outgoing requests
  logger.debug(`n8n API Request: ${config.method?.toUpperCase()} ${config.url}`);

  return config;
});

// Response interceptor - handles errors and logging
this.client.interceptors.response.use(
  (response) => {
    logger.debug(`n8n API Response: ${response.status}`);
    return response;
  },
  (error) => {
    // Transform n8n errors into meaningful messages
    const n8nError = handleN8nApiError(error);
    logN8nError(n8nError, 'n8n API Response');
    return Promise.reject(n8nError);
  }
);

Comprehensive Workflow Operations

The client provides methods for all n8n workflow operations:

CRUD Operations

// Create workflow
const workflow = await client.createWorkflow({
  name: 'Email Marketing Automation',
  nodes: [...],
  connections: {...},
  settings: {...}
});

// Read workflow
const workflow = await client.getWorkflow('workflow-id');

// Update workflow
const updated = await client.updateWorkflow('workflow-id', {
  name: 'Updated Marketing Workflow',
  nodes: modifiedNodes
});

// Delete workflow
await client.deleteWorkflow('workflow-id');

Advanced Operations

// List workflows with filtering
const workflows = await client.listWorkflows({
  limit: 50,
  offset: 0,
  tags: ['marketing', 'automation']
});

// Activate/deactivate workflows
await client.activateWorkflow('workflow-id');
await client.deactivateWorkflow('workflow-id');

Execution Management

Workflow executions are central to n8n's operation:

// Start workflow execution
const execution = await client.executeWorkflow('workflow-id', {
  data: inputData  // Optional input data
});

// Get execution status and results
const execution = await client.getExecution(executionId);

// List executions with filtering
const executions = await client.listExecutions({
  workflowId: 'workflow-id',
  limit: 10,
  status: 'success'  // success, error, running, waiting
});

// Delete old executions
await client.deleteExecution(executionId);

Credential Management

Secure handling of n8n credentials:

// List available credentials
const credentials = await client.listCredentials();

// Get specific credential
const credential = await client.getCredential('credential-id');

// Create new credential
const newCredential = await client.createCredential({
  name: 'Gmail Integration',
  type: 'gmailOAuth2',
  data: {
    clientId: '...',
    clientSecret: '...'
  }
});

Tag Management

n8n uses tags for organization:

// List all tags
const tags = await client.listTags();

// Create new tag
const tag = await client.createTag({
  name: 'Marketing',
  color: '#FF6B6B'
});

Robust Error Handling

The client implements sophisticated error handling:

Custom Error Types

class N8nApiError extends Error {
  statusCode: number;
  errorCode: string;
  details?: any;

  constructor(message: string, statusCode: number, errorCode: string, details?: any) {
    super(message);
    this.statusCode = statusCode;
    this.errorCode = errorCode;
    this.details = details;
  }
}

// Usage
try {
  await client.createWorkflow(workflowData);
} catch (error) {
  if (error instanceof N8nApiError) {
    switch (error.errorCode) {
      case 'WORKFLOW_EXISTS':
        // Handle duplicate workflow
        break;
      case 'INVALID_CREDENTIALS':
        // Handle auth failure
        break;
      default:
        // Handle other errors
    }
  }
}

Error Transformation

function handleN8nApiError(error: any): N8nApiError {
  if (error.response) {
    // Server responded with error status
    const { status, data } = error.response;

    switch (status) {
      case 400:
        return new N8nApiError('Invalid request', 400, 'INVALID_REQUEST', data);
      case 401:
        return new N8nApiError('Authentication failed', 401, 'AUTH_FAILED');
      case 403:
        return new N8nApiError('Access denied', 403, 'ACCESS_DENIED');
      case 404:
        return new N8nApiError('Resource not found', 404, 'NOT_FOUND');
      case 429:
        return new N8nApiError('Rate limit exceeded', 429, 'RATE_LIMIT');
      default:
        return new N8nApiError('Server error', status, 'SERVER_ERROR', data);
    }
  } else if (error.request) {
    // Network error
    return new N8nApiError('Network error', 0, 'NETWORK_ERROR', error.message);
  } else {
    // Other error
    return new N8nApiError('Request error', 0, 'REQUEST_ERROR', error.message);
  }
}

Retry Logic and Resilience

Network issues are handled gracefully:

// Automatic retry with exponential backoff
const response = await this.executeWithRetry(async () => {
  return this.client.get('/workflows');
});

private async executeWithRetry<T>(operation: () => Promise<T>): Promise<T> {
  let lastError: Error;

  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error;

      // Don't retry on client errors (4xx)
      if (error.statusCode >= 400 && error.statusCode < 500) {
        throw error;
      }

      // Don't retry on last attempt
      if (attempt === this.maxRetries) {
        throw error;
      }

      // Exponential backoff: 1s, 2s, 4s, 8s...
      const delay = Math.pow(2, attempt) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw lastError!;
}

Data Transformation

The client handles complex data transformations between MCP and n8n formats:

Workflow Data Cleaning

// n8n requires specific workflow format
function cleanWorkflowForCreate(workflow: any) {
  return {
    ...workflow,
    // Remove client-side fields
    id: undefined,
    createdAt: undefined,
    updatedAt: undefined,

    // Ensure required fields
    name: workflow.name || 'Untitled Workflow',
    nodes: workflow.nodes || [],
    connections: workflow.connections || {},

    // Version-specific transformations
    ...(version.greaterThan('2.0.0') ? {
      settings: {
        ...workflow.settings,
        executionOrder: workflow.settings?.executionOrder || 'v1'
      }
    } : {})
  };
}

Settings Compatibility

// Handle version-specific settings
function cleanSettingsForVersion(settings: any, version: N8nVersionInfo) {
  if (version.greaterThan('2.1.0')) {
    // New version supports additional settings
    return {
      ...settings,
      timezone: settings.timezone || 'UTC',
      saveExecutionProgress: settings.saveExecutionProgress ?? true
    };
  } else {
    // Legacy version - remove unsupported settings
    const { timezone, saveExecutionProgress, ...legacySettings } = settings;
    return legacySettings;
  }
}

Connection Pooling and Performance

For high-performance deployments:

// Configure connection pooling
const clientConfig = {
  baseURL: n8nApiUrl,
  timeout: 30000,
  maxSockets: 10,           // Connection pool size
  keepAlive: true,          // Persistent connections
  maxFreeSockets: 5,        // Keep some connections idle
  freeSocketTimeout: 30000  // Close idle connections after 30s
};

Monitoring and Observability

Built-in monitoring capabilities:

// Request metrics
class N8nApiClient {
  private metrics = {
    requestsTotal: 0,
    requestsByEndpoint: new Map<string, number>(),
    errorsByType: new Map<string, number>(),
    responseTimeHistogram: new Histogram()
  };

  private recordRequest(endpoint: string, duration: number, success: boolean) {
    this.metrics.requestsTotal++;
    this.metrics.requestsByEndpoint.set(
      endpoint,
      (this.metrics.requestsByEndpoint.get(endpoint) || 0) + 1
    );

    if (!success) {
      // Record error type
    }

    this.metrics.responseTimeHistogram.observe(duration);
  }
}

Testing and Mocking

The client is designed for easy testing:

// Mock for unit tests
const mockClient = {
  createWorkflow: jest.fn().mockResolvedValue(mockWorkflow),
  getWorkflow: jest.fn().mockResolvedValue(mockWorkflow),
  listWorkflows: jest.fn().mockResolvedValue([mockWorkflow])
};

// Integration tests with real n8n instance
describe('N8nApiClient Integration', () => {
  let client: N8nApiClient;

  beforeAll(() => {
    client = new N8nApiClient({
      baseUrl: process.env.N8N_TEST_URL!,
      apiKey: process.env.N8N_TEST_API_KEY!
    });
  });

  it('should create and retrieve workflow', async () => {
    const workflow = await client.createWorkflow(testWorkflowData);
    expect(workflow.id).toBeDefined();

    const retrieved = await client.getWorkflow(workflow.id);
    expect(retrieved.name).toBe(testWorkflowData.name);
  });
});

Usage Patterns

Basic Usage

const client = new N8nApiClient({
  baseUrl: 'https://my-n8n-instance.com',
  apiKey: 'my-api-key'
});

const workflows = await client.listWorkflows();

With Instance Context

// For multi-tenant applications
function createClientForInstance(context: InstanceContext): N8nApiClient {
  return new N8nApiClient({
    baseUrl: context.n8nApiUrl,
    apiKey: context.n8nApiKey,
    timeout: context.n8nApiTimeout || 30000,
    maxRetries: context.n8nApiMaxRetries || 3
  });
}

Error Recovery

async function createWorkflowWithRetry(client: N8nApiClient, data: any) {
  try {
    return await client.createWorkflow(data);
  } catch (error) {
    if (error.errorCode === 'WORKFLOW_EXISTS') {
      // Generate unique name and retry
      const uniqueData = {
        ...data,
        name: `${data.name} (${Date.now()})`
      };
      return await client.createWorkflow(uniqueData);
    }
    throw error;
  }
}

Performance Benchmarks

Typical performance characteristics:

  • Connection Time: 50-200ms (depends on network)
  • Simple Operations: 100-300ms (list workflows, get workflow)
  • Complex Operations: 500-2000ms (create/update workflows)
  • Concurrent Requests: Handles 50+ simultaneous operations
  • Memory Usage: ~5-10MB per client instance

Security Best Practices

The client implements security best practices:

// Secure API key handling
class N8nApiClient {
  private apiKey: string;

  constructor(config: N8nApiClientConfig) {
    // Validate API key format
    if (!config.apiKey || config.apiKey.length < 10) {
      throw new Error('Invalid API key format');
    }

    this.apiKey = config.apiKey;

    // Configure secure headers
    this.client.defaults.headers.common = {
      'X-N8N-API-KEY': this.apiKey,
      'User-Agent': 'n8n-mcp/2.33.0',
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    };
  }
}

Congratulations! You now understand how the N8nApiClient serves as the sophisticated communication layer between n8n-MCP and n8n instances, handling all the complexities of API communication gracefully.

In the next chapter, we'll explore the data storage layer that powers fast lookups of n8n's extensive node library.