Files
n8n-mcp/docs/LIBRARY_USAGE.md
Romuald Członkowski c2dc7c9c51 feat: configurable MAX_SESSIONS via N8N_MCP_MAX_SESSIONS env var (v2.28.4) (#469)
* fix: n8n_test_workflow webhookId resolution and form handling (v2.28.2)

## Bug Fixes

- **webhookId Resolution**: Fixed trigger handlers using `node.id` instead of
  `node.webhookId` for building webhook URLs. This caused 404 errors when
  nodes had custom IDs.

- **Chat Trigger URL**: Fixed chat triggers using wrong URL pattern. Now
  correctly uses `/webhook/<webhookId>/chat` endpoint.

- **Form Content-Type**: Fixed form triggers failing with "Expected
  multipart/form-data" error by switching to proper multipart encoding.

## Enhancements

- **Form Field Types**: Added support for all n8n form field types (text,
  textarea, email, number, password, date, dropdown, checkbox, file, hidden)

- **Checkbox Arrays**: Automatically converts arrays to `field[]` format

- **Helpful Warnings**: Reports missing required fields with names and labels

- **Error Hints**: Provides complete field structure on failure

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

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

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: address code review issues for form trigger improvements

- Add form-data as direct dependency (was only in devDependencies)
- Add TypeScript interfaces (FormFieldValue, FormFieldOption) replacing any types
- Add FORM_FIELD_TYPES constants for type-safe switch statements
- Add isValidBase64() validation for file uploads with size limits
- Add MAX_FILE_SIZE_BYTES (10MB) constant with validation
- Update form-handler.test.ts for FormData instead of JSON
- Update trigger-detector.test.ts for chat URL /chat suffix

Conceived by Romuald Członkowski - www.aiadvisors.pl/en

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

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: configurable MAX_SESSIONS via N8N_MCP_MAX_SESSIONS env var (v2.28.3) (#468)

Make MAX_SESSIONS limit configurable for multi-tenant SaaS deployments.

- Add N8N_MCP_MAX_SESSIONS environment variable (default: 100)
- Include safety floor with Math.max(1, ...) to prevent invalid configs
- Update documentation in LIBRARY_USAGE.md, SESSION_PERSISTENCE.md
- Update CLAUDE.md and CHANGELOG.md

Fixes #468

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

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

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: add QEMU setup for multi-arch Docker builds

Add docker/setup-qemu-action@v3 before Buildx setup to enable
proper QEMU emulation for linux/arm64 builds on GitHub Actions.

Fixes CI Docker build failure with "execve: No such file or directory"

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

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Romuald Członkowski <romualdczlonkowski@MacBook-Pro-Romuald.local>
2025-12-05 11:17:51 +01:00

16 KiB

Library Usage Guide - Multi-Tenant / Hosted Deployments

This guide covers using n8n-mcp as a library dependency for building multi-tenant hosted services.

Overview

n8n-mcp can be used as a Node.js library to build multi-tenant backends that provide MCP services to multiple users or instances. The package exports all necessary components for integration into your existing services.

Installation

npm install n8n-mcp

Core Concepts

Library Mode vs CLI Mode

  • CLI Mode (default): Single-player usage via npx n8n-mcp or Docker
  • Library Mode: Multi-tenant usage by importing and using the N8NMCPEngine class

Instance Context

The InstanceContext type allows you to pass per-request configuration to the MCP engine:

interface InstanceContext {
  // Instance-specific n8n API configuration
  n8nApiUrl?: string;
  n8nApiKey?: string;
  n8nApiTimeout?: number;
  n8nApiMaxRetries?: number;

  // Instance identification
  instanceId?: string;
  sessionId?: string;

  // Extensible metadata
  metadata?: Record<string, any>;
}

Basic Example

import express from 'express';
import { N8NMCPEngine } from 'n8n-mcp';

const app = express();
const mcpEngine = new N8NMCPEngine({
  sessionTimeout: 3600000, // 1 hour
  logLevel: 'info'
});

// Handle MCP requests with per-user context
app.post('/mcp', async (req, res) => {
  const instanceContext = {
    n8nApiUrl: req.user.n8nUrl,
    n8nApiKey: req.user.n8nApiKey,
    instanceId: req.user.id
  };

  await mcpEngine.processRequest(req, res, instanceContext);
});

app.listen(3000);

Multi-Tenant Backend Example

This example shows a complete multi-tenant implementation with user authentication and instance management:

import express from 'express';
import { N8NMCPEngine, InstanceContext, validateInstanceContext } from 'n8n-mcp';

const app = express();
const mcpEngine = new N8NMCPEngine({
  sessionTimeout: 3600000, // 1 hour
  logLevel: 'info'
});

// Start MCP engine
await mcpEngine.start();

// Authentication middleware
const authenticate = async (req, res, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  // Verify token and attach user to request
  req.user = await getUserFromToken(token);
  next();
};

// Get instance configuration from database
const getInstanceConfig = async (instanceId: string, userId: string) => {
  // Your database logic here
  const instance = await db.instances.findOne({
    where: { id: instanceId, userId }
  });

  if (!instance) {
    throw new Error('Instance not found');
  }

  return {
    n8nApiUrl: instance.n8nUrl,
    n8nApiKey: await decryptApiKey(instance.encryptedApiKey),
    instanceId: instance.id
  };
};

// MCP endpoint with per-instance context
app.post('/api/instances/:instanceId/mcp', authenticate, async (req, res) => {
  try {
    // Get instance configuration
    const instance = await getInstanceConfig(req.params.instanceId, req.user.id);

    // Create instance context
    const context: InstanceContext = {
      n8nApiUrl: instance.n8nApiUrl,
      n8nApiKey: instance.n8nApiKey,
      instanceId: instance.instanceId,
      metadata: {
        userId: req.user.id,
        userAgent: req.headers['user-agent'],
        ip: req.ip
      }
    };

    // Validate context before processing
    const validation = validateInstanceContext(context);
    if (!validation.valid) {
      return res.status(400).json({
        error: 'Invalid instance configuration',
        details: validation.errors
      });
    }

    // Process request with instance context
    await mcpEngine.processRequest(req, res, context);

  } catch (error) {
    console.error('MCP request error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Health endpoint
app.get('/health', async (req, res) => {
  const health = await mcpEngine.healthCheck();
  res.status(health.status === 'healthy' ? 200 : 503).json(health);
});

// Graceful shutdown
process.on('SIGTERM', async () => {
  await mcpEngine.shutdown();
  process.exit(0);
});

app.listen(3000);

API Reference

N8NMCPEngine

Constructor

new N8NMCPEngine(options?: {
  sessionTimeout?: number;  // Session TTL in ms (default: 1800000 = 30min)
  logLevel?: 'error' | 'warn' | 'info' | 'debug';  // Default: 'info'
})

Methods

async processRequest(req, res, context?)

Process a single MCP request with optional instance context.

Parameters:

  • req: Express request object
  • res: Express response object
  • context (optional): InstanceContext with per-instance configuration

Example:

const context: InstanceContext = {
  n8nApiUrl: 'https://instance1.n8n.cloud',
  n8nApiKey: 'instance1-key',
  instanceId: 'tenant-123'
};

await engine.processRequest(req, res, context);
async healthCheck()

Get engine health status for monitoring.

Returns: EngineHealth

{
  status: 'healthy' | 'unhealthy';
  uptime: number;  // seconds
  sessionActive: boolean;
  memoryUsage: {
    used: number;
    total: number;
    unit: string;
  };
  version: string;
}

Example:

app.get('/health', async (req, res) => {
  const health = await engine.healthCheck();
  res.status(health.status === 'healthy' ? 200 : 503).json(health);
});
getSessionInfo()

Get current session information for debugging.

Returns:

{
  active: boolean;
  sessionId?: string;
  age?: number;  // milliseconds
  sessions?: {
    total: number;
    active: number;
    expired: number;
    max: number;
    sessionIds: string[];
  };
}
async start()

Start the engine (for standalone mode). Not needed when using processRequest() directly.

async shutdown()

Graceful shutdown for service lifecycle management.

Example:

process.on('SIGTERM', async () => {
  await engine.shutdown();
  process.exit(0);
});

Types

InstanceContext

Configuration for a specific user instance:

interface InstanceContext {
  n8nApiUrl?: string;
  n8nApiKey?: string;
  n8nApiTimeout?: number;
  n8nApiMaxRetries?: number;
  instanceId?: string;
  sessionId?: string;
  metadata?: Record<string, any>;
}

Validation Functions

validateInstanceContext(context: InstanceContext)

Validate and sanitize instance context.

Returns:

{
  valid: boolean;
  errors?: string[];
}

Example:

import { validateInstanceContext } from 'n8n-mcp';

const validation = validateInstanceContext(context);
if (!validation.valid) {
  console.error('Invalid context:', validation.errors);
}
isInstanceContext(obj: any)

Type guard to check if an object is a valid InstanceContext.

Example:

import { isInstanceContext } from 'n8n-mcp';

if (isInstanceContext(req.body.context)) {
  // TypeScript knows this is InstanceContext
  await engine.processRequest(req, res, req.body.context);
}

Session Management

Session Strategies

The MCP engine supports flexible session ID formats:

  • UUIDv4: Internal n8n-mcp format (default)
  • Instance-prefixed: instance-{userId}-{hash}-{uuid} for multi-tenant isolation
  • Custom formats: Any non-empty string for mcp-remote and other proxies

Session validation happens via transport lookup, not format validation. This ensures compatibility with all MCP clients.

Multi-Tenant Configuration

Set these environment variables for multi-tenant mode:

# Enable multi-tenant mode
ENABLE_MULTI_TENANT=true

# Session strategy: "instance" (default) or "shared"
MULTI_TENANT_SESSION_STRATEGY=instance

Session Strategies:

  • instance (recommended): Each tenant gets isolated sessions

    • Session ID: instance-{instanceId}-{configHash}-{uuid}
    • Better isolation and security
    • Easier debugging per tenant
  • shared: Multiple tenants share sessions with context switching

    • More efficient for high tenant count
    • Requires careful context management

Security Considerations

API Key Management

Always encrypt API keys server-side:

import { createCipheriv, createDecipheriv } from 'crypto';

// Encrypt before storing
const encryptApiKey = (apiKey: string) => {
  const cipher = createCipheriv('aes-256-gcm', encryptionKey, iv);
  return cipher.update(apiKey, 'utf8', 'hex') + cipher.final('hex');
};

// Decrypt before using
const decryptApiKey = (encrypted: string) => {
  const decipher = createDecipheriv('aes-256-gcm', encryptionKey, iv);
  return decipher.update(encrypted, 'hex', 'utf8') + decipher.final('utf8');
};

// Use decrypted key in context
const context: InstanceContext = {
  n8nApiKey: await decryptApiKey(instance.encryptedApiKey),
  // ...
};

Input Validation

Always validate instance context before processing:

import { validateInstanceContext } from 'n8n-mcp';

const validation = validateInstanceContext(context);
if (!validation.valid) {
  throw new Error(`Invalid context: ${validation.errors?.join(', ')}`);
}

Rate Limiting

Implement rate limiting per tenant:

import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  keyGenerator: (req) => req.user?.id || req.ip
});

app.post('/api/instances/:instanceId/mcp', authenticate, limiter, async (req, res) => {
  // ...
});

Error Handling

Always wrap MCP requests in try-catch blocks:

app.post('/api/instances/:instanceId/mcp', authenticate, async (req, res) => {
  try {
    const context = await getInstanceConfig(req.params.instanceId, req.user.id);
    await mcpEngine.processRequest(req, res, context);
  } catch (error) {
    console.error('MCP error:', error);

    // Don't leak internal errors to clients
    if (error.message.includes('not found')) {
      return res.status(404).json({ error: 'Instance not found' });
    }

    res.status(500).json({ error: 'Internal server error' });
  }
});

Monitoring

Health Checks

Set up periodic health checks:

setInterval(async () => {
  const health = await mcpEngine.healthCheck();

  if (health.status === 'unhealthy') {
    console.error('MCP engine unhealthy:', health);
    // Alert your monitoring system
  }

  // Log metrics
  console.log('MCP engine metrics:', {
    uptime: health.uptime,
    memory: health.memoryUsage,
    sessionActive: health.sessionActive
  });
}, 60000); // Every minute

Session Monitoring

Track active sessions:

app.get('/admin/sessions', authenticate, async (req, res) => {
  if (!req.user.isAdmin) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  const sessionInfo = mcpEngine.getSessionInfo();
  res.json(sessionInfo);
});

Testing

Unit Testing

import { N8NMCPEngine, InstanceContext } from 'n8n-mcp';

describe('MCP Engine', () => {
  let engine: N8NMCPEngine;

  beforeEach(() => {
    engine = new N8NMCPEngine({ logLevel: 'error' });
  });

  afterEach(async () => {
    await engine.shutdown();
  });

  it('should process request with context', async () => {
    const context: InstanceContext = {
      n8nApiUrl: 'https://test.n8n.io',
      n8nApiKey: 'test-key',
      instanceId: 'test-instance'
    };

    const mockReq = createMockRequest();
    const mockRes = createMockResponse();

    await engine.processRequest(mockReq, mockRes, context);

    expect(mockRes.status).toBe(200);
  });
});

Integration Testing

import request from 'supertest';
import { createApp } from './app';

describe('Multi-tenant MCP API', () => {
  let app;
  let authToken;

  beforeAll(async () => {
    app = await createApp();
    authToken = await getTestAuthToken();
  });

  it('should handle MCP request for instance', async () => {
    const response = await request(app)
      .post('/api/instances/test-instance/mcp')
      .set('Authorization', `Bearer ${authToken}`)
      .send({
        jsonrpc: '2.0',
        method: 'initialize',
        params: {
          protocolVersion: '2024-11-05',
          capabilities: {}
        },
        id: 1
      });

    expect(response.status).toBe(200);
    expect(response.body.result).toBeDefined();
  });
});

Deployment Considerations

Environment Variables

# Required for multi-tenant mode
ENABLE_MULTI_TENANT=true
MULTI_TENANT_SESSION_STRATEGY=instance

# Optional: Logging
LOG_LEVEL=info
DISABLE_CONSOLE_OUTPUT=false

# Optional: Session configuration
SESSION_TIMEOUT=1800000  # 30 minutes in milliseconds
N8N_MCP_MAX_SESSIONS=100  # Maximum concurrent sessions (default: 100)

# Optional: Performance
NODE_ENV=production

Docker Deployment

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

ENV NODE_ENV=production
ENV ENABLE_MULTI_TENANT=true
ENV LOG_LEVEL=info

EXPOSE 3000

CMD ["node", "dist/server.js"]

Kubernetes Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: n8n-mcp-backend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: n8n-mcp-backend
  template:
    metadata:
      labels:
        app: n8n-mcp-backend
    spec:
      containers:
      - name: backend
        image: your-registry/n8n-mcp-backend:latest
        ports:
        - containerPort: 3000
        env:
        - name: ENABLE_MULTI_TENANT
          value: "true"
        - name: LOG_LEVEL
          value: "info"
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 10
          periodSeconds: 30
        readinessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 10

Examples

Complete Multi-Tenant SaaS Example

For a complete implementation example, see:

Migration from Single-Player

If you're migrating from single-player (CLI/Docker) to multi-tenant:

  1. Keep backward compatibility - Use environment fallback:
const context: InstanceContext = {
  n8nApiUrl: instanceUrl || process.env.N8N_API_URL,
  n8nApiKey: instanceKey || process.env.N8N_API_KEY,
  instanceId: instanceId || 'default'
};
  1. Gradual rollout - Start with a feature flag:
const isMultiTenant = process.env.ENABLE_MULTI_TENANT === 'true';

if (isMultiTenant) {
  const context = await getInstanceConfig(req.params.instanceId);
  await engine.processRequest(req, res, context);
} else {
  // Legacy single-player mode
  await engine.processRequest(req, res);
}

Troubleshooting

Common Issues

Module Resolution Errors

If you see Cannot find module 'n8n-mcp':

# Clear node_modules and reinstall
rm -rf node_modules package-lock.json
npm install

# Verify package has types field
npm info n8n-mcp

# Check TypeScript can resolve it
npx tsc --noEmit

Session ID Validation Errors

If you see Invalid session ID format errors:

  • Ensure you're using n8n-mcp v2.18.9 or later
  • Session IDs can be any non-empty string
  • No need to generate UUIDs - use your own format

Memory Leaks

If memory usage grows over time:

// Ensure proper cleanup
process.on('SIGTERM', async () => {
  await engine.shutdown();
  process.exit(0);
});

// Monitor session count
const sessionInfo = engine.getSessionInfo();
console.log('Active sessions:', sessionInfo.sessions?.active);

Further Reading

Support