diff --git a/docs/LIBRARY_USAGE.md b/docs/LIBRARY_USAGE.md new file mode 100644 index 0000000..f3fbebd --- /dev/null +++ b/docs/LIBRARY_USAGE.md @@ -0,0 +1,724 @@ +# 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 + +```bash +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: + +```typescript +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; +} +``` + +## Basic Example + +```typescript +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: + +```typescript +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 + +```typescript +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:** +```typescript +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` +```typescript +{ + status: 'healthy' | 'unhealthy'; + uptime: number; // seconds + sessionActive: boolean; + memoryUsage: { + used: number; + total: number; + unit: string; + }; + version: string; +} +``` + +**Example:** +```typescript +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:** +```typescript +{ + 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:** +```typescript +process.on('SIGTERM', async () => { + await engine.shutdown(); + process.exit(0); +}); +``` + +### Types + +#### InstanceContext + +Configuration for a specific user instance: + +```typescript +interface InstanceContext { + n8nApiUrl?: string; + n8nApiKey?: string; + n8nApiTimeout?: number; + n8nApiMaxRetries?: number; + instanceId?: string; + sessionId?: string; + metadata?: Record; +} +``` + +#### Validation Functions + +##### `validateInstanceContext(context: InstanceContext)` + +Validate and sanitize instance context. + +**Returns:** +```typescript +{ + valid: boolean; + errors?: string[]; +} +``` + +**Example:** +```typescript +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:** +```typescript +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: + +```bash +# 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: + +```typescript +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: + +```typescript +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: + +```typescript +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: + +```typescript +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: + +```typescript +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: + +```typescript +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 + +```typescript +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 + +```typescript +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 + +```bash +# 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 +MAX_SESSIONS=100 + +# Optional: Performance +NODE_ENV=production +``` + +### Docker Deployment + +```dockerfile +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 + +```yaml +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: +- [n8n-mcp-backend](https://github.com/czlonkowski/n8n-mcp-backend) - Full hosted service implementation + +### Migration from Single-Player + +If you're migrating from single-player (CLI/Docker) to multi-tenant: + +1. **Keep backward compatibility** - Use environment fallback: +```typescript +const context: InstanceContext = { + n8nApiUrl: instanceUrl || process.env.N8N_API_URL, + n8nApiKey: instanceKey || process.env.N8N_API_KEY, + instanceId: instanceId || 'default' +}; +``` + +2. **Gradual rollout** - Start with a feature flag: +```typescript +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'`: + +```bash +# 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: + +```typescript +// 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 + +- [MCP Protocol Specification](https://modelcontextprotocol.io/docs) +- [n8n API Documentation](https://docs.n8n.io/api/) +- [Express.js Guide](https://expressjs.com/en/guide/routing.html) +- [n8n-mcp Main README](../README.md) + +## Support + +- **Issues**: [GitHub Issues](https://github.com/czlonkowski/n8n-mcp/issues) +- **Discussions**: [GitHub Discussions](https://github.com/czlonkowski/n8n-mcp/discussions) +- **Security**: For security issues, see [SECURITY.md](../SECURITY.md) diff --git a/package.json b/package.json index b33915e..fb2c9e0 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,16 @@ { "name": "n8n-mcp", - "version": "2.18.8", + "version": "2.18.9", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "require": "./dist/index.js", + "import": "./dist/index.js" + } + }, "bin": { "n8n-mcp": "./dist/mcp/index.js" }, diff --git a/src/http-server-single-session.ts b/src/http-server-single-session.ts index 3c1a0d3..17716d1 100644 --- a/src/http-server-single-session.ts +++ b/src/http-server-single-session.ts @@ -188,11 +188,22 @@ export class SingleSessionHTTPServer { /** * Validate session ID format + * + * Accepts any non-empty string to support various MCP clients: + * - UUIDv4 (internal n8n-mcp format) + * - instance-{userId}-{hash}-{uuid} (multi-tenant format) + * - Custom formats from mcp-remote and other proxies + * + * Security: Session validation happens via lookup in this.transports, + * not format validation. This ensures compatibility with all MCP clients. + * + * @param sessionId - Session identifier from MCP client + * @returns true if valid, false otherwise */ private isValidSessionId(sessionId: string): boolean { - // UUID v4 format validation - const uuidv4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - return uuidv4Regex.test(sessionId); + // Accept any non-empty string as session ID + // This ensures compatibility with all MCP clients and proxies + return Boolean(sessionId && sessionId.length > 0); } /** diff --git a/src/index.ts b/src/index.ts index 572edc4..b5c1005 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,22 @@ export { SingleSessionHTTPServer } from './http-server-single-session'; export { ConsoleManager } from './utils/console-manager'; export { N8NDocumentationMCPServer } from './mcp/server'; +// Type exports for multi-tenant and library usage +export type { + InstanceContext +} from './types/instance-context'; +export { + validateInstanceContext, + isInstanceContext +} from './types/instance-context'; + +// Re-export MCP SDK types for convenience +export type { + Tool, + CallToolResult, + ListToolsResult +} from '@modelcontextprotocol/sdk/types.js'; + // Default export for convenience import N8NMCPEngine from './mcp-engine'; export default N8NMCPEngine;