feat: integrate n8n management tools from n8n-manager-for-ai-agents (v2.6.0)

- Added 14 n8n management tools for workflow CRUD and execution management
- Integrated n8n API client with full error handling and validation
- Added conditional tool registration (only when N8N_API_URL configured)
- Complete workflow lifecycle: discover → build → validate → deploy → execute
- Updated documentation and added integration tests
- Maintains backward compatibility - existing functionality unchanged

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-06-26 11:09:09 +02:00
parent 150de3d1c2
commit 74f05e937f
13 changed files with 2409 additions and 6 deletions

View File

@@ -52,3 +52,22 @@ AUTH_TOKEN=your-secure-token-here
# Default: * (allow all origins)
# For production, set to your specific domain
# CORS_ORIGIN=https://your-client-domain.com
# =========================
# N8N API CONFIGURATION
# =========================
# Optional: Enable n8n management tools by providing API credentials
# These tools allow creating, updating, and executing workflows
# n8n instance API URL (without /api/v1 suffix)
# Example: https://your-n8n-instance.com
# N8N_API_URL=
# n8n API Key (get from Settings > API in your n8n instance)
# N8N_API_KEY=
# n8n API request timeout in milliseconds (default: 30000)
# N8N_API_TIMEOUT=30000
# Maximum number of API request retries (default: 3)
# N8N_API_MAX_RETRIES=3

View File

@@ -6,7 +6,22 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
n8n-mcp is a comprehensive documentation and knowledge server that provides AI assistants with complete access to n8n node information through the Model Context Protocol (MCP). It serves as a bridge between n8n's workflow automation platform and AI models, enabling them to understand and work with n8n nodes effectively.
## ✅ Latest Updates (v2.5.1)
## ✅ Latest Updates (v2.6.0)
### Update (v2.6.0) - n8n Management Tools Integration:
-**NEW: 14 n8n management tools** - Create, update, execute workflows via API
-**NEW: n8n_create_workflow** - Create workflows programmatically
-**NEW: n8n_update_workflow** - Update existing workflows
-**NEW: n8n_trigger_webhook_workflow** - Execute workflows via webhooks
-**NEW: n8n_list_executions** - Monitor workflow executions
-**NEW: n8n_health_check** - Check n8n instance connectivity
- ✅ Integrated n8n-manager-for-ai-agents functionality
- ✅ Optional feature - only enabled when N8N_API_URL and N8N_API_KEY configured
- ✅ Complete workflow lifecycle: discover → build → validate → deploy → execute
- ✅ Smart error handling for API limitations (activation, direct execution)
- ✅ Conditional tool registration based on configuration
## ✅ Previous Updates (v2.5.1)
### Update (v2.5.1) - AI Tool Support Enhancements:
-**NEW: get_node_as_tool_info tool** - Get specific information about using ANY node as an AI tool
@@ -195,6 +210,7 @@ npm run test:ai-workflow-validation # Test AI workflow validation
npm run test:mcp-tools # Test MCP tool enhancements
npm run test:single-session # Test single session HTTP
npm run test:template-validation # Test template validation
npm run test:n8n-manager # Test n8n management tools integration
# Workflow Validation Commands:
npm run test:workflow-validation # Test workflow validation features
@@ -321,6 +337,29 @@ The project implements MCP (Model Context Protocol) to expose n8n node documenta
- `search_templates` - **NEW** Search templates by keywords
- `get_templates_for_task` - **NEW** Get curated templates for common tasks
### n8n Management Tools (NEW v2.6.0 - Requires API Configuration)
These tools are only available when N8N_API_URL and N8N_API_KEY are configured:
#### Workflow Management
- `n8n_create_workflow` - Create new workflows with nodes and connections
- `n8n_get_workflow` - Get complete workflow by ID
- `n8n_get_workflow_details` - Get workflow with execution statistics
- `n8n_get_workflow_structure` - Get simplified workflow structure
- `n8n_get_workflow_minimal` - Get minimal workflow info
- `n8n_update_workflow` - Update existing workflows
- `n8n_delete_workflow` - Delete workflows permanently
- `n8n_list_workflows` - List workflows with filtering
#### Execution Management
- `n8n_trigger_webhook_workflow` - Trigger workflows via webhook URL
- `n8n_get_execution` - Get execution details by ID
- `n8n_list_executions` - List executions with status filtering
- `n8n_delete_execution` - Delete execution records
#### System Tools
- `n8n_health_check` - Check n8n API connectivity and features
- `n8n_list_available_tools` - List all available management tools
### Database Structure
Uses SQLite with enhanced schema:
- **nodes** table: Core node information with FTS5 indexing

View File

@@ -104,6 +104,9 @@ Add to Claude Desktop config:
"-e", "MCP_MODE=stdio",
"-e", "LOG_LEVEL=error",
"-e", "DISABLE_CONSOLE_OUTPUT=true",
// Optional: Enable n8n management tools
// "-e", "N8N_API_URL=https://your-n8n-instance.com",
// "-e", "N8N_API_KEY=your-api-key",
"ghcr.io/czlonkowski/n8n-mcp:latest"
]
}
@@ -191,6 +194,29 @@ Once connected, Claude can use these powerful tools:
- **`get_node_documentation`** - Get parsed documentation from n8n-docs
- **`get_database_statistics`** - View database metrics and coverage
### n8n Management Tools (NEW! Requires API Configuration)
These tools allow you to manage n8n workflows directly. Configure with `N8N_API_URL` and `N8N_API_KEY`.
#### Workflow Management
- **`n8n_create_workflow`** - Create new workflows with nodes and connections
- **`n8n_get_workflow`** - Get complete workflow by ID
- **`n8n_get_workflow_details`** - Get workflow with execution statistics
- **`n8n_get_workflow_structure`** - Get simplified workflow structure
- **`n8n_get_workflow_minimal`** - Get minimal workflow info (ID, name, active status)
- **`n8n_update_workflow`** - Update existing workflows
- **`n8n_delete_workflow`** - Delete workflows permanently
- **`n8n_list_workflows`** - List workflows with filtering and pagination
#### Execution Management
- **`n8n_trigger_webhook_workflow`** - Trigger workflows via webhook URL
- **`n8n_get_execution`** - Get execution details by ID
- **`n8n_list_executions`** - List executions with status filtering
- **`n8n_delete_execution`** - Delete execution records
#### System Tools
- **`n8n_health_check`** - Check n8n API connectivity and features
- **`n8n_list_available_tools`** - List all available management tools
### Example Usage
```typescript
@@ -237,7 +263,10 @@ If you prefer running locally:
"NODE_ENV": "production",
"LOG_LEVEL": "error",
"MCP_MODE": "stdio",
"DISABLE_CONSOLE_OUTPUT": "true"
"DISABLE_CONSOLE_OUTPUT": "true",
// Optional: Enable n8n management tools
// "N8N_API_URL": "https://your-n8n-instance.com",
// "N8N_API_KEY": "your-api-key"
}
}
}

View File

@@ -31,6 +31,7 @@
"test:enhanced-validation": "node dist/scripts/test-enhanced-validation.js",
"test:ai-workflow-validation": "node dist/scripts/test-ai-workflow-validation.js",
"test:mcp-tools": "node dist/scripts/test-mcp-tools.js",
"test:n8n-manager": "node dist/scripts/test-n8n-manager-integration.js",
"db:rebuild": "node dist/scripts/rebuild-database.js",
"db:init": "node -e \"new (require('./dist/services/sqlite-storage-service').SQLiteStorageService)(); console.log('Database initialized')\"",
"docs:rebuild": "ts-node src/scripts/rebuild-database.ts"

56
src/config/n8n-api.ts Normal file
View File

@@ -0,0 +1,56 @@
import { z } from 'zod';
import dotenv from 'dotenv';
import { logger } from '../utils/logger';
// Load environment variables
dotenv.config();
// n8n API configuration schema
const n8nApiConfigSchema = z.object({
N8N_API_URL: z.string().url().optional(),
N8N_API_KEY: z.string().min(1).optional(),
N8N_API_TIMEOUT: z.coerce.number().positive().default(30000),
N8N_API_MAX_RETRIES: z.coerce.number().positive().default(3),
});
// Parse and validate n8n API configuration
export function loadN8nApiConfig() {
const result = n8nApiConfigSchema.safeParse(process.env);
if (!result.success) {
logger.warn('n8n API configuration validation failed:', result.error.format());
return null;
}
const config = result.data;
// Check if both URL and API key are provided
if (!config.N8N_API_URL || !config.N8N_API_KEY) {
logger.info('n8n API not configured. Management tools will be disabled.');
return null;
}
logger.info('n8n API configured successfully', {
url: config.N8N_API_URL,
timeout: config.N8N_API_TIMEOUT,
maxRetries: config.N8N_API_MAX_RETRIES,
});
return {
baseUrl: config.N8N_API_URL,
apiKey: config.N8N_API_KEY,
timeout: config.N8N_API_TIMEOUT,
maxRetries: config.N8N_API_MAX_RETRIES,
};
}
// Export the configuration (null if not configured)
export const n8nApiConfig = loadN8nApiConfig();
// Helper to check if n8n API is configured
export function isN8nApiConfigured(): boolean {
return n8nApiConfig !== null;
}
// Type export
export type N8nApiConfig = NonNullable<ReturnType<typeof loadN8nApiConfig>>;

View File

@@ -0,0 +1,732 @@
import { N8nApiClient } from '../services/n8n-api-client';
import { n8nApiConfig } from '../config/n8n-api';
import {
Workflow,
WorkflowNode,
WorkflowConnection,
ExecutionStatus,
WebhookRequest,
McpToolResponse
} from '../types/n8n-api';
import {
validateWorkflowStructure,
hasWebhookTrigger,
getWebhookUrl
} from '../services/n8n-validation';
import {
N8nApiError,
N8nNotFoundError,
getUserFriendlyErrorMessage
} from '../utils/n8n-errors';
import { logger } from '../utils/logger';
import { z } from 'zod';
// Singleton n8n API client instance
let apiClient: N8nApiClient | null = null;
// Get or create API client
export function getN8nApiClient(): N8nApiClient | null {
if (!n8nApiConfig) {
return null;
}
if (!apiClient) {
apiClient = new N8nApiClient(n8nApiConfig);
}
return apiClient;
}
// Helper to ensure API is configured
function ensureApiConfigured(): N8nApiClient {
const client = getN8nApiClient();
if (!client) {
throw new Error('n8n API not configured. Please set N8N_API_URL and N8N_API_KEY environment variables.');
}
return client;
}
// Zod schemas for input validation
const createWorkflowSchema = z.object({
name: z.string(),
nodes: z.array(z.any()),
connections: z.record(z.any()),
settings: z.object({
executionOrder: z.enum(['v0', 'v1']).optional(),
timezone: z.string().optional(),
saveDataErrorExecution: z.enum(['all', 'none']).optional(),
saveDataSuccessExecution: z.enum(['all', 'none']).optional(),
saveManualExecutions: z.boolean().optional(),
saveExecutionProgress: z.boolean().optional(),
executionTimeout: z.number().optional(),
errorWorkflow: z.string().optional(),
}).optional(),
});
const updateWorkflowSchema = z.object({
id: z.string(),
name: z.string().optional(),
nodes: z.array(z.any()).optional(),
connections: z.record(z.any()).optional(),
settings: z.any().optional(),
});
const listWorkflowsSchema = z.object({
limit: z.number().min(1).max(100).optional(),
cursor: z.string().optional(),
active: z.boolean().optional(),
tags: z.array(z.string()).optional(),
projectId: z.string().optional(),
excludePinnedData: z.boolean().optional(),
});
const triggerWebhookSchema = z.object({
webhookUrl: z.string().url(),
httpMethod: z.enum(['GET', 'POST', 'PUT', 'DELETE']).optional(),
data: z.record(z.unknown()).optional(),
headers: z.record(z.string()).optional(),
waitForResponse: z.boolean().optional(),
});
const listExecutionsSchema = z.object({
limit: z.number().min(1).max(100).optional(),
cursor: z.string().optional(),
workflowId: z.string().optional(),
projectId: z.string().optional(),
status: z.enum(['success', 'error', 'waiting']).optional(),
includeData: z.boolean().optional(),
});
// Workflow Management Handlers
export async function handleCreateWorkflow(args: unknown): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured();
const input = createWorkflowSchema.parse(args);
// Validate workflow structure
const errors = validateWorkflowStructure(input);
if (errors.length > 0) {
return {
success: false,
error: 'Workflow validation failed',
details: { errors }
};
}
// Create workflow
const workflow = await client.createWorkflow(input);
return {
success: true,
data: workflow,
message: `Workflow "${workflow.name}" created successfully with ID: ${workflow.id}`
};
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
error: 'Invalid input',
details: { errors: error.errors }
};
}
if (error instanceof N8nApiError) {
return {
success: false,
error: getUserFriendlyErrorMessage(error),
code: error.code,
details: error.details as Record<string, unknown> | undefined
};
}
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred'
};
}
}
export async function handleGetWorkflow(args: unknown): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured();
const { id } = z.object({ id: z.string() }).parse(args);
const workflow = await client.getWorkflow(id);
return {
success: true,
data: workflow
};
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
error: 'Invalid input',
details: { errors: error.errors }
};
}
if (error instanceof N8nApiError) {
return {
success: false,
error: getUserFriendlyErrorMessage(error),
code: error.code
};
}
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred'
};
}
}
export async function handleGetWorkflowDetails(args: unknown): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured();
const { id } = z.object({ id: z.string() }).parse(args);
const workflow = await client.getWorkflow(id);
// Get recent executions for this workflow
const executions = await client.listExecutions({
workflowId: id,
limit: 10
});
// Calculate execution statistics
const stats = {
totalExecutions: executions.data.length,
successCount: executions.data.filter(e => e.status === ExecutionStatus.SUCCESS).length,
errorCount: executions.data.filter(e => e.status === ExecutionStatus.ERROR).length,
lastExecutionTime: executions.data[0]?.startedAt || null
};
return {
success: true,
data: {
workflow,
executionStats: stats,
hasWebhookTrigger: hasWebhookTrigger(workflow),
webhookPath: getWebhookUrl(workflow)
}
};
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
error: 'Invalid input',
details: { errors: error.errors }
};
}
if (error instanceof N8nApiError) {
return {
success: false,
error: getUserFriendlyErrorMessage(error),
code: error.code
};
}
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred'
};
}
}
export async function handleGetWorkflowStructure(args: unknown): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured();
const { id } = z.object({ id: z.string() }).parse(args);
const workflow = await client.getWorkflow(id);
// Simplify nodes to just essential structure
const simplifiedNodes = workflow.nodes.map(node => ({
id: node.id,
name: node.name,
type: node.type,
position: node.position,
disabled: node.disabled || false
}));
return {
success: true,
data: {
id: workflow.id,
name: workflow.name,
active: workflow.active,
nodes: simplifiedNodes,
connections: workflow.connections,
nodeCount: workflow.nodes.length,
connectionCount: Object.keys(workflow.connections).length
}
};
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
error: 'Invalid input',
details: { errors: error.errors }
};
}
if (error instanceof N8nApiError) {
return {
success: false,
error: getUserFriendlyErrorMessage(error),
code: error.code
};
}
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred'
};
}
}
export async function handleGetWorkflowMinimal(args: unknown): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured();
const { id } = z.object({ id: z.string() }).parse(args);
const workflow = await client.getWorkflow(id);
return {
success: true,
data: {
id: workflow.id,
name: workflow.name,
active: workflow.active,
tags: workflow.tags || [],
createdAt: workflow.createdAt,
updatedAt: workflow.updatedAt
}
};
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
error: 'Invalid input',
details: { errors: error.errors }
};
}
if (error instanceof N8nApiError) {
return {
success: false,
error: getUserFriendlyErrorMessage(error),
code: error.code
};
}
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred'
};
}
}
export async function handleUpdateWorkflow(args: unknown): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured();
const input = updateWorkflowSchema.parse(args);
const { id, ...updateData } = input;
// If nodes/connections are being updated, validate the structure
if (updateData.nodes || updateData.connections) {
// Fetch current workflow if only partial update
let fullWorkflow = updateData as Partial<Workflow>;
if (!updateData.nodes || !updateData.connections) {
const current = await client.getWorkflow(id);
fullWorkflow = {
...current,
...updateData
};
}
const errors = validateWorkflowStructure(fullWorkflow);
if (errors.length > 0) {
return {
success: false,
error: 'Workflow validation failed',
details: { errors }
};
}
}
// Update workflow
const workflow = await client.updateWorkflow(id, updateData);
return {
success: true,
data: workflow,
message: `Workflow "${workflow.name}" updated successfully`
};
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
error: 'Invalid input',
details: { errors: error.errors }
};
}
if (error instanceof N8nApiError) {
return {
success: false,
error: getUserFriendlyErrorMessage(error),
code: error.code,
details: error.details as Record<string, unknown> | undefined
};
}
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred'
};
}
}
export async function handleDeleteWorkflow(args: unknown): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured();
const { id } = z.object({ id: z.string() }).parse(args);
await client.deleteWorkflow(id);
return {
success: true,
message: `Workflow ${id} deleted successfully`
};
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
error: 'Invalid input',
details: { errors: error.errors }
};
}
if (error instanceof N8nApiError) {
return {
success: false,
error: getUserFriendlyErrorMessage(error),
code: error.code
};
}
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred'
};
}
}
export async function handleListWorkflows(args: unknown): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured();
const input = listWorkflowsSchema.parse(args || {});
const response = await client.listWorkflows({
limit: input.limit || 100,
cursor: input.cursor,
active: input.active,
tags: input.tags,
projectId: input.projectId,
excludePinnedData: input.excludePinnedData ?? true
});
return {
success: true,
data: {
workflows: response.data,
nextCursor: response.nextCursor,
total: response.data.length
}
};
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
error: 'Invalid input',
details: { errors: error.errors }
};
}
if (error instanceof N8nApiError) {
return {
success: false,
error: getUserFriendlyErrorMessage(error),
code: error.code
};
}
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred'
};
}
}
// Execution Management Handlers
export async function handleTriggerWebhookWorkflow(args: unknown): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured();
const input = triggerWebhookSchema.parse(args);
const webhookRequest: WebhookRequest = {
webhookUrl: input.webhookUrl,
httpMethod: input.httpMethod || 'POST',
data: input.data,
headers: input.headers,
waitForResponse: input.waitForResponse ?? true
};
const response = await client.triggerWebhook(webhookRequest);
return {
success: true,
data: response,
message: 'Webhook triggered successfully'
};
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
error: 'Invalid input',
details: { errors: error.errors }
};
}
if (error instanceof N8nApiError) {
return {
success: false,
error: getUserFriendlyErrorMessage(error),
code: error.code,
details: error.details as Record<string, unknown> | undefined
};
}
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred'
};
}
}
export async function handleGetExecution(args: unknown): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured();
const { id, includeData } = z.object({
id: z.string(),
includeData: z.boolean().optional()
}).parse(args);
const execution = await client.getExecution(id, includeData || false);
return {
success: true,
data: execution
};
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
error: 'Invalid input',
details: { errors: error.errors }
};
}
if (error instanceof N8nApiError) {
return {
success: false,
error: getUserFriendlyErrorMessage(error),
code: error.code
};
}
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred'
};
}
}
export async function handleListExecutions(args: unknown): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured();
const input = listExecutionsSchema.parse(args || {});
const response = await client.listExecutions({
limit: input.limit || 100,
cursor: input.cursor,
workflowId: input.workflowId,
projectId: input.projectId,
status: input.status as ExecutionStatus | undefined,
includeData: input.includeData || false
});
return {
success: true,
data: {
executions: response.data,
nextCursor: response.nextCursor,
total: response.data.length
}
};
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
error: 'Invalid input',
details: { errors: error.errors }
};
}
if (error instanceof N8nApiError) {
return {
success: false,
error: getUserFriendlyErrorMessage(error),
code: error.code
};
}
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred'
};
}
}
export async function handleDeleteExecution(args: unknown): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured();
const { id } = z.object({ id: z.string() }).parse(args);
await client.deleteExecution(id);
return {
success: true,
message: `Execution ${id} deleted successfully`
};
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
error: 'Invalid input',
details: { errors: error.errors }
};
}
if (error instanceof N8nApiError) {
return {
success: false,
error: getUserFriendlyErrorMessage(error),
code: error.code
};
}
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred'
};
}
}
// System Tools Handlers
export async function handleHealthCheck(): Promise<McpToolResponse> {
try {
const client = ensureApiConfigured();
const health = await client.healthCheck();
return {
success: true,
data: {
status: health.status,
instanceId: health.instanceId,
n8nVersion: health.n8nVersion,
features: health.features,
apiUrl: n8nApiConfig?.baseUrl
}
};
} catch (error) {
if (error instanceof N8nApiError) {
return {
success: false,
error: getUserFriendlyErrorMessage(error),
code: error.code,
details: {
apiUrl: n8nApiConfig?.baseUrl,
hint: 'Check if n8n is running and API is enabled'
}
};
}
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred'
};
}
}
export async function handleListAvailableTools(): Promise<McpToolResponse> {
const tools = [
{
category: 'Workflow Management',
tools: [
{ name: 'n8n_create_workflow', description: 'Create new workflows' },
{ name: 'n8n_get_workflow', description: 'Get workflow by ID' },
{ name: 'n8n_get_workflow_details', description: 'Get detailed workflow info with stats' },
{ name: 'n8n_get_workflow_structure', description: 'Get simplified workflow structure' },
{ name: 'n8n_get_workflow_minimal', description: 'Get minimal workflow info' },
{ name: 'n8n_update_workflow', description: 'Update existing workflows' },
{ name: 'n8n_delete_workflow', description: 'Delete workflows' },
{ name: 'n8n_list_workflows', description: 'List workflows with filters' }
]
},
{
category: 'Execution Management',
tools: [
{ name: 'n8n_trigger_webhook_workflow', description: 'Trigger workflows via webhook' },
{ name: 'n8n_get_execution', description: 'Get execution details' },
{ name: 'n8n_list_executions', description: 'List executions with filters' },
{ name: 'n8n_delete_execution', description: 'Delete execution records' }
]
},
{
category: 'System',
tools: [
{ name: 'n8n_health_check', description: 'Check API connectivity' },
{ name: 'n8n_list_available_tools', description: 'List all available tools' }
]
}
];
const apiConfigured = n8nApiConfig !== null;
return {
success: true,
data: {
tools,
apiConfigured,
configuration: apiConfigured ? {
apiUrl: n8nApiConfig!.baseUrl,
timeout: n8nApiConfig!.timeout,
maxRetries: n8nApiConfig!.maxRetries
} : null,
limitations: [
'Cannot activate/deactivate workflows via API',
'Cannot execute workflows directly (must use webhooks)',
'Cannot stop running executions',
'Tags and credentials have limited API support'
]
}
};
}

View File

@@ -8,6 +8,7 @@ import {
import { existsSync } from 'fs';
import path from 'path';
import { n8nDocumentationToolsFinal } from './tools-update';
import { n8nManagementTools } from './tools-n8n-manager';
import { logger } from '../utils/logger';
import { NodeRepository } from '../database/node-repository';
import { DatabaseAdapter, createDatabaseAdapter } from '../database/database-adapter';
@@ -20,6 +21,8 @@ import { PropertyDependencies } from '../services/property-dependencies';
import { SimpleCache } from '../utils/simple-cache';
import { TemplateService } from '../templates/template-service';
import { WorkflowValidator } from '../services/workflow-validator';
import { isN8nApiConfigured } from '../config/n8n-api';
import * as n8nHandlers from './handlers-n8n-manager';
interface NodeRow {
node_type: string;
@@ -130,9 +133,19 @@ export class N8NDocumentationMCPServer {
});
// Handle tool listing
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: n8nDocumentationToolsFinal,
}));
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
// Combine documentation tools with management tools if API is configured
const tools = [...n8nDocumentationToolsFinal];
if (isN8nApiConfigured()) {
tools.push(...n8nManagementTools);
logger.info('n8n management tools enabled');
} else {
logger.info('n8n management tools disabled (API not configured)');
}
return { tools };
});
// Handle tool execution
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -211,6 +224,37 @@ export class N8NDocumentationMCPServer {
return this.validateWorkflowConnections(args.workflow);
case 'validate_workflow_expressions':
return this.validateWorkflowExpressions(args.workflow);
// n8n Management Tools (if API is configured)
case 'n8n_create_workflow':
return n8nHandlers.handleCreateWorkflow(args);
case 'n8n_get_workflow':
return n8nHandlers.handleGetWorkflow(args);
case 'n8n_get_workflow_details':
return n8nHandlers.handleGetWorkflowDetails(args);
case 'n8n_get_workflow_structure':
return n8nHandlers.handleGetWorkflowStructure(args);
case 'n8n_get_workflow_minimal':
return n8nHandlers.handleGetWorkflowMinimal(args);
case 'n8n_update_workflow':
return n8nHandlers.handleUpdateWorkflow(args);
case 'n8n_delete_workflow':
return n8nHandlers.handleDeleteWorkflow(args);
case 'n8n_list_workflows':
return n8nHandlers.handleListWorkflows(args);
case 'n8n_trigger_webhook_workflow':
return n8nHandlers.handleTriggerWebhookWorkflow(args);
case 'n8n_get_execution':
return n8nHandlers.handleGetExecution(args);
case 'n8n_list_executions':
return n8nHandlers.handleListExecutions(args);
case 'n8n_delete_execution':
return n8nHandlers.handleDeleteExecution(args);
case 'n8n_health_check':
return n8nHandlers.handleHealthCheck();
case 'n8n_list_available_tools':
return n8nHandlers.handleListAvailableTools();
default:
throw new Error(`Unknown tool: ${name}`);
}

View File

@@ -0,0 +1,322 @@
import { ToolDefinition } from '../types';
/**
* n8n Management Tools
*
* These tools enable AI agents to manage n8n workflows through the n8n API.
* They require N8N_API_URL and N8N_API_KEY to be configured.
*/
export const n8nManagementTools: ToolDefinition[] = [
// Workflow Management Tools
{
name: 'n8n_create_workflow',
description: `Create a new workflow in n8n. Requires workflow name, nodes array, and connections object. The workflow will be created in inactive state and must be manually activated in the UI. Returns the created workflow with its ID.`,
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Workflow name (required)'
},
nodes: {
type: 'array',
description: 'Array of workflow nodes. Each node must have: id, name, type, typeVersion, position, and parameters',
items: {
type: 'object',
required: ['id', 'name', 'type', 'typeVersion', 'position', 'parameters'],
properties: {
id: { type: 'string' },
name: { type: 'string' },
type: { type: 'string' },
typeVersion: { type: 'number' },
position: {
type: 'array',
items: { type: 'number' },
minItems: 2,
maxItems: 2
},
parameters: { type: 'object' },
credentials: { type: 'object' },
disabled: { type: 'boolean' },
notes: { type: 'string' },
continueOnFail: { type: 'boolean' },
retryOnFail: { type: 'boolean' },
maxTries: { type: 'number' },
waitBetweenTries: { type: 'number' }
}
}
},
connections: {
type: 'object',
description: 'Workflow connections object. Keys are source node IDs, values define output connections'
},
settings: {
type: 'object',
description: 'Optional workflow settings (execution order, timezone, error handling)',
properties: {
executionOrder: { type: 'string', enum: ['v0', 'v1'] },
timezone: { type: 'string' },
saveDataErrorExecution: { type: 'string', enum: ['all', 'none'] },
saveDataSuccessExecution: { type: 'string', enum: ['all', 'none'] },
saveManualExecutions: { type: 'boolean' },
saveExecutionProgress: { type: 'boolean' },
executionTimeout: { type: 'number' },
errorWorkflow: { type: 'string' }
}
}
},
required: ['name', 'nodes', 'connections']
}
},
{
name: 'n8n_get_workflow',
description: `Get a workflow by ID. Returns the complete workflow including nodes, connections, and settings.`,
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Workflow ID'
}
},
required: ['id']
}
},
{
name: 'n8n_get_workflow_details',
description: `Get detailed workflow information including metadata, version, and execution statistics. More comprehensive than get_workflow.`,
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Workflow ID'
}
},
required: ['id']
}
},
{
name: 'n8n_get_workflow_structure',
description: `Get simplified workflow structure showing only nodes and their connections. Useful for understanding workflow flow without parameter details.`,
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Workflow ID'
}
},
required: ['id']
}
},
{
name: 'n8n_get_workflow_minimal',
description: `Get minimal workflow information (ID, name, active status, tags). Fast and lightweight for listing purposes.`,
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Workflow ID'
}
},
required: ['id']
}
},
{
name: 'n8n_update_workflow',
description: `Update an existing workflow. Requires the full nodes array when modifying nodes/connections. Cannot activate workflows via API - use UI instead.`,
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Workflow ID to update'
},
name: {
type: 'string',
description: 'New workflow name'
},
nodes: {
type: 'array',
description: 'Complete array of workflow nodes (required if modifying workflow structure)'
},
connections: {
type: 'object',
description: 'Complete connections object (required if modifying workflow structure)'
},
settings: {
type: 'object',
description: 'Workflow settings to update'
}
},
required: ['id']
}
},
{
name: 'n8n_delete_workflow',
description: `Permanently delete a workflow. This action cannot be undone.`,
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Workflow ID to delete'
}
},
required: ['id']
}
},
{
name: 'n8n_list_workflows',
description: `List workflows with optional filters. Supports pagination via cursor.`,
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Number of workflows to return (1-100, default: 100)'
},
cursor: {
type: 'string',
description: 'Pagination cursor from previous response'
},
active: {
type: 'boolean',
description: 'Filter by active status'
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Filter by tags (exact match)'
},
projectId: {
type: 'string',
description: 'Filter by project ID (enterprise feature)'
},
excludePinnedData: {
type: 'boolean',
description: 'Exclude pinned data from response (default: true)'
}
}
}
},
// Execution Management Tools
{
name: 'n8n_trigger_webhook_workflow',
description: `Trigger a workflow via webhook. Workflow must be ACTIVE and have a Webhook trigger node. HTTP method must match webhook configuration.`,
inputSchema: {
type: 'object',
properties: {
webhookUrl: {
type: 'string',
description: 'Full webhook URL from n8n workflow (e.g., https://n8n.example.com/webhook/abc-def-ghi)'
},
httpMethod: {
type: 'string',
enum: ['GET', 'POST', 'PUT', 'DELETE'],
description: 'HTTP method (must match webhook configuration, often GET)'
},
data: {
type: 'object',
description: 'Data to send with the webhook request'
},
headers: {
type: 'object',
description: 'Additional HTTP headers'
},
waitForResponse: {
type: 'boolean',
description: 'Wait for workflow completion (default: true)'
}
},
required: ['webhookUrl']
}
},
{
name: 'n8n_get_execution',
description: `Get details of a specific execution by ID.`,
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Execution ID'
},
includeData: {
type: 'boolean',
description: 'Include full execution data (default: false)'
}
},
required: ['id']
}
},
{
name: 'n8n_list_executions',
description: `List workflow executions with optional filters. Supports pagination.`,
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Number of executions to return (1-100, default: 100)'
},
cursor: {
type: 'string',
description: 'Pagination cursor from previous response'
},
workflowId: {
type: 'string',
description: 'Filter by workflow ID'
},
projectId: {
type: 'string',
description: 'Filter by project ID (enterprise feature)'
},
status: {
type: 'string',
enum: ['success', 'error', 'waiting'],
description: 'Filter by execution status'
},
includeData: {
type: 'boolean',
description: 'Include execution data (default: false)'
}
}
}
},
{
name: 'n8n_delete_execution',
description: `Delete an execution record. This only removes the execution history, not any data processed.`,
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Execution ID to delete'
}
},
required: ['id']
}
},
// System Tools
{
name: 'n8n_health_check',
description: `Check n8n instance health and API connectivity. Returns status and available features.`,
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'n8n_list_available_tools',
description: `List all available n8n management tools and their capabilities. Useful for understanding what operations are possible.`,
inputSchema: {
type: 'object',
properties: {}
}
}
];

View File

@@ -0,0 +1,147 @@
#!/usr/bin/env node
import { config } from 'dotenv';
import { logger } from '../utils/logger';
import { isN8nApiConfigured, n8nApiConfig } from '../config/n8n-api';
import { getN8nApiClient } from '../mcp/handlers-n8n-manager';
import { N8nApiClient } from '../services/n8n-api-client';
import { Workflow, ExecutionStatus } from '../types/n8n-api';
// Load environment variables
config();
async function testN8nManagerIntegration() {
logger.info('Testing n8n Manager Integration...');
// Check if API is configured
if (!isN8nApiConfigured()) {
logger.warn('n8n API not configured. Set N8N_API_URL and N8N_API_KEY to test.');
logger.info('Example:');
logger.info(' N8N_API_URL=https://your-n8n.com N8N_API_KEY=your-key npm run test:n8n-manager');
return;
}
logger.info('n8n API Configuration:', {
url: n8nApiConfig!.baseUrl,
timeout: n8nApiConfig!.timeout,
maxRetries: n8nApiConfig!.maxRetries
});
const client = getN8nApiClient();
if (!client) {
logger.error('Failed to create n8n API client');
return;
}
try {
// Test 1: Health Check
logger.info('\n=== Test 1: Health Check ===');
const health = await client.healthCheck();
logger.info('Health check passed:', health);
// Test 2: List Workflows
logger.info('\n=== Test 2: List Workflows ===');
const workflows = await client.listWorkflows({ limit: 5 });
logger.info(`Found ${workflows.data.length} workflows`);
workflows.data.forEach(wf => {
logger.info(`- ${wf.name} (ID: ${wf.id}, Active: ${wf.active})`);
});
// Test 3: Create a Test Workflow
logger.info('\n=== Test 3: Create Test Workflow ===');
const testWorkflow: Partial<Workflow> = {
name: `Test Workflow - MCP Integration ${Date.now()}`,
nodes: [
{
id: '1',
name: 'Start',
type: 'n8n-nodes-base.start',
typeVersion: 1,
position: [250, 300],
parameters: {}
},
{
id: '2',
name: 'Set',
type: 'n8n-nodes-base.set',
typeVersion: 1,
position: [450, 300],
parameters: {
values: {
string: [
{
name: 'message',
value: 'Hello from MCP!'
}
]
}
}
}
],
connections: {
'1': {
main: [[{ node: '2', type: 'main', index: 0 }]]
}
},
settings: {
executionOrder: 'v1',
saveDataErrorExecution: 'all',
saveDataSuccessExecution: 'all',
saveManualExecutions: true,
saveExecutionProgress: true
}
};
const createdWorkflow = await client.createWorkflow(testWorkflow);
logger.info('Created workflow:', {
id: createdWorkflow.id,
name: createdWorkflow.name,
active: createdWorkflow.active
});
// Test 4: Get Workflow Details
logger.info('\n=== Test 4: Get Workflow Details ===');
const workflowDetails = await client.getWorkflow(createdWorkflow.id!);
logger.info('Retrieved workflow:', {
id: workflowDetails.id,
name: workflowDetails.name,
nodeCount: workflowDetails.nodes.length
});
// Test 5: Update Workflow
logger.info('\n=== Test 5: Update Workflow ===');
// n8n API requires full workflow structure for updates
const updatedWorkflow = await client.updateWorkflow(createdWorkflow.id!, {
name: `${createdWorkflow.name} - Updated`,
nodes: workflowDetails.nodes,
connections: workflowDetails.connections,
settings: workflowDetails.settings
});
logger.info('Updated workflow name:', updatedWorkflow.name);
// Test 6: List Executions
logger.info('\n=== Test 6: List Recent Executions ===');
const executions = await client.listExecutions({ limit: 5 });
logger.info(`Found ${executions.data.length} recent executions`);
executions.data.forEach(exec => {
logger.info(`- Workflow: ${exec.workflowName || exec.workflowId}, Status: ${exec.status}, Started: ${exec.startedAt}`);
});
// Test 7: Cleanup - Delete Test Workflow
logger.info('\n=== Test 7: Cleanup ===');
await client.deleteWorkflow(createdWorkflow.id!);
logger.info('Deleted test workflow');
logger.info('\n✅ All tests passed successfully!');
} catch (error) {
logger.error('Test failed:', error);
process.exit(1);
}
}
// Run tests
testN8nManagerIntegration().catch(error => {
logger.error('Unhandled error:', error);
process.exit(1);
});

View File

@@ -0,0 +1,390 @@
import axios, { AxiosInstance, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios';
import { logger } from '../utils/logger';
import {
Workflow,
WorkflowListParams,
WorkflowListResponse,
Execution,
ExecutionListParams,
ExecutionListResponse,
Credential,
CredentialListParams,
CredentialListResponse,
Tag,
TagListParams,
TagListResponse,
HealthCheckResponse,
Variable,
WebhookRequest,
WorkflowExport,
WorkflowImport,
SourceControlStatus,
SourceControlPullResult,
SourceControlPushResult,
} from '../types/n8n-api';
import { handleN8nApiError, logN8nError } from '../utils/n8n-errors';
import { cleanWorkflowForCreate, cleanWorkflowForUpdate } from './n8n-validation';
export interface N8nApiClientConfig {
baseUrl: string;
apiKey: string;
timeout?: number;
maxRetries?: number;
}
export class N8nApiClient {
private client: AxiosInstance;
private maxRetries: number;
constructor(config: N8nApiClientConfig) {
const { baseUrl, apiKey, timeout = 30000, maxRetries = 3 } = config;
this.maxRetries = maxRetries;
// Ensure baseUrl ends with /api/v1
const apiUrl = baseUrl.endsWith('/api/v1')
? baseUrl
: `${baseUrl.replace(/\/$/, '')}/api/v1`;
this.client = axios.create({
baseURL: apiUrl,
timeout,
headers: {
'X-N8N-API-KEY': apiKey,
'Content-Type': 'application/json',
},
});
// Request interceptor for logging
this.client.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
logger.debug(`n8n API Request: ${config.method?.toUpperCase()} ${config.url}`, {
params: config.params,
data: config.data,
});
return config;
},
(error: unknown) => {
logger.error('n8n API Request Error:', error);
return Promise.reject(error);
}
);
// Response interceptor for logging
this.client.interceptors.response.use(
(response: any) => {
logger.debug(`n8n API Response: ${response.status} ${response.config.url}`);
return response;
},
(error: unknown) => {
const n8nError = handleN8nApiError(error);
logN8nError(n8nError, 'n8n API Response');
return Promise.reject(n8nError);
}
);
}
// Health check to verify API connectivity
async healthCheck(): Promise<HealthCheckResponse> {
try {
// First try the health endpoint
const response = await this.client.get('/health');
return response.data;
} catch (error) {
// If health endpoint doesn't exist, try listing workflows with limit 1
// This is a fallback for older n8n versions
try {
await this.client.get('/workflows', { params: { limit: 1 } });
return {
status: 'ok',
features: {} // We can't determine features without proper health endpoint
};
} catch (fallbackError) {
throw handleN8nApiError(fallbackError);
}
}
}
// Workflow Management
async createWorkflow(workflow: Partial<Workflow>): Promise<Workflow> {
try {
const cleanedWorkflow = cleanWorkflowForCreate(workflow);
const response = await this.client.post('/workflows', cleanedWorkflow);
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async getWorkflow(id: string): Promise<Workflow> {
try {
const response = await this.client.get(`/workflows/${id}`);
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async updateWorkflow(id: string, workflow: Partial<Workflow>): Promise<Workflow> {
try {
// First, try PUT method (newer n8n versions)
const cleanedWorkflow = cleanWorkflowForUpdate(workflow as Workflow);
try {
const response = await this.client.put(`/workflows/${id}`, cleanedWorkflow);
return response.data;
} catch (putError: any) {
// If PUT fails with 405 (Method Not Allowed), try PATCH
if (putError.response?.status === 405) {
logger.debug('PUT method not supported, falling back to PATCH');
const response = await this.client.patch(`/workflows/${id}`, cleanedWorkflow);
return response.data;
}
throw putError;
}
} catch (error) {
throw handleN8nApiError(error);
}
}
async deleteWorkflow(id: string): Promise<void> {
try {
await this.client.delete(`/workflows/${id}`);
} catch (error) {
throw handleN8nApiError(error);
}
}
async listWorkflows(params: WorkflowListParams = {}): Promise<WorkflowListResponse> {
try {
const response = await this.client.get('/workflows', { params });
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
// Execution Management
async getExecution(id: string, includeData = false): Promise<Execution> {
try {
const response = await this.client.get(`/executions/${id}`, {
params: { includeData },
});
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async listExecutions(params: ExecutionListParams = {}): Promise<ExecutionListResponse> {
try {
const response = await this.client.get('/executions', { params });
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async deleteExecution(id: string): Promise<void> {
try {
await this.client.delete(`/executions/${id}`);
} catch (error) {
throw handleN8nApiError(error);
}
}
// Webhook Execution
async triggerWebhook(request: WebhookRequest): Promise<any> {
try {
const { webhookUrl, httpMethod, data, headers, waitForResponse = true } = request;
// Extract path from webhook URL
const url = new URL(webhookUrl);
const webhookPath = url.pathname;
// Make request directly to webhook endpoint
const config: AxiosRequestConfig = {
method: httpMethod,
url: webhookPath,
headers: {
...headers,
// Don't override API key header for webhook endpoints
'X-N8N-API-KEY': undefined,
},
data: httpMethod !== 'GET' ? data : undefined,
params: httpMethod === 'GET' ? data : undefined,
// Webhooks might take longer
timeout: waitForResponse ? 120000 : 30000,
};
// Create a new axios instance for webhook requests to avoid API interceptors
const webhookClient = axios.create({
baseURL: new URL('/', webhookUrl).toString(),
validateStatus: (status) => status < 500, // Don't throw on 4xx
});
const response = await webhookClient.request(config);
return {
status: response.status,
statusText: response.statusText,
data: response.data,
headers: response.headers,
};
} catch (error) {
throw handleN8nApiError(error);
}
}
// Credential Management
async listCredentials(params: CredentialListParams = {}): Promise<CredentialListResponse> {
try {
const response = await this.client.get('/credentials', { params });
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async getCredential(id: string): Promise<Credential> {
try {
const response = await this.client.get(`/credentials/${id}`);
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async createCredential(credential: Partial<Credential>): Promise<Credential> {
try {
const response = await this.client.post('/credentials', credential);
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async updateCredential(id: string, credential: Partial<Credential>): Promise<Credential> {
try {
const response = await this.client.patch(`/credentials/${id}`, credential);
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async deleteCredential(id: string): Promise<void> {
try {
await this.client.delete(`/credentials/${id}`);
} catch (error) {
throw handleN8nApiError(error);
}
}
// Tag Management
async listTags(params: TagListParams = {}): Promise<TagListResponse> {
try {
const response = await this.client.get('/tags', { params });
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async createTag(tag: Partial<Tag>): Promise<Tag> {
try {
const response = await this.client.post('/tags', tag);
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async updateTag(id: string, tag: Partial<Tag>): Promise<Tag> {
try {
const response = await this.client.patch(`/tags/${id}`, tag);
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async deleteTag(id: string): Promise<void> {
try {
await this.client.delete(`/tags/${id}`);
} catch (error) {
throw handleN8nApiError(error);
}
}
// Source Control Management (Enterprise feature)
async getSourceControlStatus(): Promise<SourceControlStatus> {
try {
const response = await this.client.get('/source-control/status');
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async pullSourceControl(force = false): Promise<SourceControlPullResult> {
try {
const response = await this.client.post('/source-control/pull', { force });
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async pushSourceControl(
message: string,
fileNames?: string[]
): Promise<SourceControlPushResult> {
try {
const response = await this.client.post('/source-control/push', {
message,
fileNames,
});
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
// Variable Management (via Source Control API)
async getVariables(): Promise<Variable[]> {
try {
const response = await this.client.get('/variables');
return response.data.data || [];
} catch (error) {
// Variables might not be available in all n8n versions
logger.warn('Variables API not available, returning empty array');
return [];
}
}
async createVariable(variable: Partial<Variable>): Promise<Variable> {
try {
const response = await this.client.post('/variables', variable);
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async updateVariable(id: string, variable: Partial<Variable>): Promise<Variable> {
try {
const response = await this.client.patch(`/variables/${id}`, variable);
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async deleteVariable(id: string): Promise<void> {
try {
await this.client.delete(`/variables/${id}`);
} catch (error) {
throw handleN8nApiError(error);
}
}
}

View File

@@ -0,0 +1,207 @@
import { z } from 'zod';
import { WorkflowNode, WorkflowConnection, Workflow } from '../types/n8n-api';
// Zod schemas for n8n API validation
export const workflowNodeSchema = z.object({
id: z.string(),
name: z.string(),
type: z.string(),
typeVersion: z.number(),
position: z.tuple([z.number(), z.number()]),
parameters: z.record(z.unknown()),
credentials: z.record(z.string()).optional(),
disabled: z.boolean().optional(),
notes: z.string().optional(),
notesInFlow: z.boolean().optional(),
continueOnFail: z.boolean().optional(),
retryOnFail: z.boolean().optional(),
maxTries: z.number().optional(),
waitBetweenTries: z.number().optional(),
alwaysOutputData: z.boolean().optional(),
executeOnce: z.boolean().optional(),
});
export const workflowConnectionSchema = z.record(
z.object({
main: z.array(
z.array(
z.object({
node: z.string(),
type: z.string(),
index: z.number(),
})
)
),
})
);
export const workflowSettingsSchema = z.object({
executionOrder: z.enum(['v0', 'v1']).default('v1'),
timezone: z.string().optional(),
saveDataErrorExecution: z.enum(['all', 'none']).default('all'),
saveDataSuccessExecution: z.enum(['all', 'none']).default('all'),
saveManualExecutions: z.boolean().default(true),
saveExecutionProgress: z.boolean().default(true),
executionTimeout: z.number().optional(),
errorWorkflow: z.string().optional(),
});
// Default settings for workflow creation
export const defaultWorkflowSettings = {
executionOrder: 'v1' as const,
saveDataErrorExecution: 'all' as const,
saveDataSuccessExecution: 'all' as const,
saveManualExecutions: true,
saveExecutionProgress: true,
};
// Validation functions
export function validateWorkflowNode(node: unknown): WorkflowNode {
return workflowNodeSchema.parse(node);
}
export function validateWorkflowConnections(connections: unknown): WorkflowConnection {
return workflowConnectionSchema.parse(connections);
}
export function validateWorkflowSettings(settings: unknown): z.infer<typeof workflowSettingsSchema> {
return workflowSettingsSchema.parse(settings);
}
// Clean workflow data for API operations
export function cleanWorkflowForCreate(workflow: Partial<Workflow>): Partial<Workflow> {
const {
// Remove read-only fields
id,
createdAt,
updatedAt,
versionId,
meta,
// Remove fields that cause API errors during creation
active,
tags,
// Keep everything else
...cleanedWorkflow
} = workflow;
// Ensure settings are present with defaults
if (!cleanedWorkflow.settings) {
cleanedWorkflow.settings = defaultWorkflowSettings;
}
return cleanedWorkflow;
}
export function cleanWorkflowForUpdate(workflow: Workflow): Partial<Workflow> {
const {
// Remove read-only/computed fields
id,
createdAt,
updatedAt,
versionId,
meta,
staticData,
// Remove fields that cause API errors
pinData,
tags,
// Keep everything else
...cleanedWorkflow
} = workflow as any;
// Ensure settings are present
if (!cleanedWorkflow.settings) {
cleanedWorkflow.settings = defaultWorkflowSettings;
}
return cleanedWorkflow;
}
// Validate workflow structure
export function validateWorkflowStructure(workflow: Partial<Workflow>): string[] {
const errors: string[] = [];
// Check required fields
if (!workflow.name) {
errors.push('Workflow name is required');
}
if (!workflow.nodes || workflow.nodes.length === 0) {
errors.push('Workflow must have at least one node');
}
if (!workflow.connections) {
errors.push('Workflow connections are required');
}
// Validate nodes
if (workflow.nodes) {
workflow.nodes.forEach((node, index) => {
try {
validateWorkflowNode(node);
} catch (error) {
errors.push(`Invalid node at index ${index}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
});
}
// Validate connections
if (workflow.connections) {
try {
validateWorkflowConnections(workflow.connections);
} catch (error) {
errors.push(`Invalid connections: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Validate that all connection references exist
if (workflow.nodes && workflow.connections) {
const nodeIds = new Set(workflow.nodes.map(node => node.id));
Object.entries(workflow.connections).forEach(([sourceId, connection]) => {
if (!nodeIds.has(sourceId)) {
errors.push(`Connection references non-existent source node: ${sourceId}`);
}
connection.main.forEach((outputs, outputIndex) => {
outputs.forEach((target, targetIndex) => {
if (!nodeIds.has(target.node)) {
errors.push(`Connection references non-existent target node: ${target.node} (from ${sourceId}[${outputIndex}][${targetIndex}])`);
}
});
});
});
}
return errors;
}
// Check if workflow has webhook trigger
export function hasWebhookTrigger(workflow: Workflow): boolean {
return workflow.nodes.some(node =>
node.type === 'n8n-nodes-base.webhook' ||
node.type === 'n8n-nodes-base.webhookTrigger'
);
}
// Get webhook URL from workflow
export function getWebhookUrl(workflow: Workflow): string | null {
const webhookNode = workflow.nodes.find(node =>
node.type === 'n8n-nodes-base.webhook' ||
node.type === 'n8n-nodes-base.webhookTrigger'
);
if (!webhookNode || !webhookNode.parameters) {
return null;
}
// Check for path parameter
const path = webhookNode.parameters.path as string | undefined;
if (!path) {
return null;
}
// Note: We can't construct the full URL without knowing the n8n instance URL
// The caller will need to prepend the base URL
return path;
}

281
src/types/n8n-api.ts Normal file
View File

@@ -0,0 +1,281 @@
// n8n API Types - Ported from n8n-manager-for-ai-agents
// These types define the structure of n8n API requests and responses
// Workflow Node Types
export interface WorkflowNode {
id: string;
name: string;
type: string;
typeVersion: number;
position: [number, number];
parameters: Record<string, unknown>;
credentials?: Record<string, string>;
disabled?: boolean;
notes?: string;
notesInFlow?: boolean;
continueOnFail?: boolean;
retryOnFail?: boolean;
maxTries?: number;
waitBetweenTries?: number;
alwaysOutputData?: boolean;
executeOnce?: boolean;
}
export interface WorkflowConnection {
[sourceNodeId: string]: {
main: Array<Array<{
node: string;
type: string;
index: number;
}>>;
};
}
export interface WorkflowSettings {
executionOrder?: 'v0' | 'v1';
timezone?: string;
saveDataErrorExecution?: 'all' | 'none';
saveDataSuccessExecution?: 'all' | 'none';
saveManualExecutions?: boolean;
saveExecutionProgress?: boolean;
executionTimeout?: number;
errorWorkflow?: string;
}
export interface Workflow {
id?: string;
name: string;
nodes: WorkflowNode[];
connections: WorkflowConnection;
active?: boolean; // Optional for creation as it's read-only
settings?: WorkflowSettings;
staticData?: Record<string, unknown>;
tags?: string[];
updatedAt?: string;
createdAt?: string;
versionId?: string;
meta?: {
instanceId?: string;
};
}
// Execution Types
export enum ExecutionStatus {
SUCCESS = 'success',
ERROR = 'error',
WAITING = 'waiting',
// Note: 'running' status is not returned by the API
}
export interface ExecutionSummary {
id: string;
finished: boolean;
mode: string;
retryOf?: string;
retrySuccessId?: string;
status: ExecutionStatus;
startedAt: string;
stoppedAt?: string;
workflowId: string;
workflowName?: string;
waitTill?: string;
}
export interface ExecutionData {
startData?: Record<string, unknown>;
resultData: {
runData: Record<string, unknown>;
lastNodeExecuted?: string;
error?: Record<string, unknown>;
};
executionData?: Record<string, unknown>;
}
export interface Execution extends ExecutionSummary {
data?: ExecutionData;
}
// Credential Types
export interface Credential {
id?: string;
name: string;
type: string;
data?: Record<string, unknown>;
nodesAccess?: Array<{
nodeType: string;
date?: string;
}>;
createdAt?: string;
updatedAt?: string;
}
// Tag Types
export interface Tag {
id?: string;
name: string;
workflowIds?: string[];
createdAt?: string;
updatedAt?: string;
}
// Variable Types
export interface Variable {
id?: string;
key: string;
value: string;
type?: 'string';
}
// Import/Export Types
export interface WorkflowExport {
id: string;
name: string;
active: boolean;
createdAt: string;
updatedAt: string;
nodes: WorkflowNode[];
connections: WorkflowConnection;
settings?: WorkflowSettings;
staticData?: Record<string, unknown>;
tags?: string[];
pinData?: Record<string, unknown>;
versionId?: string;
meta?: Record<string, unknown>;
}
export interface WorkflowImport {
name: string;
nodes: WorkflowNode[];
connections: WorkflowConnection;
settings?: WorkflowSettings;
staticData?: Record<string, unknown>;
tags?: string[];
pinData?: Record<string, unknown>;
}
// Source Control Types
export interface SourceControlStatus {
ahead: number;
behind: number;
conflicted: string[];
created: string[];
current: string;
deleted: string[];
detached: boolean;
files: Array<{
path: string;
status: string;
}>;
modified: string[];
notAdded: string[];
renamed: Array<{
from: string;
to: string;
}>;
staged: string[];
tracking: string;
}
export interface SourceControlPullResult {
conflicts: string[];
files: Array<{
path: string;
status: string;
}>;
mergeConflicts: boolean;
pullResult: 'success' | 'conflict' | 'error';
}
export interface SourceControlPushResult {
ahead: number;
conflicts: string[];
files: Array<{
path: string;
status: string;
}>;
pushResult: 'success' | 'conflict' | 'error';
}
// Health Check Types
export interface HealthCheckResponse {
status: 'ok' | 'error';
instanceId?: string;
n8nVersion?: string;
features?: {
sourceControl?: boolean;
externalHooks?: boolean;
workers?: boolean;
[key: string]: boolean | undefined;
};
}
// Request Parameter Types
export interface WorkflowListParams {
limit?: number;
cursor?: string;
active?: boolean;
tags?: string[] | null;
projectId?: string;
excludePinnedData?: boolean;
instance?: string;
}
export interface WorkflowListResponse {
data: Workflow[];
nextCursor?: string | null;
}
export interface ExecutionListParams {
limit?: number;
cursor?: string;
workflowId?: string;
projectId?: string;
status?: ExecutionStatus;
includeData?: boolean;
}
export interface ExecutionListResponse {
data: Execution[];
nextCursor?: string | null;
}
export interface CredentialListParams {
limit?: number;
cursor?: string;
filter?: Record<string, unknown>;
}
export interface CredentialListResponse {
data: Credential[];
nextCursor?: string | null;
}
export interface TagListParams {
limit?: number;
cursor?: string;
withUsageCount?: boolean;
}
export interface TagListResponse {
data: Tag[];
nextCursor?: string | null;
}
// Webhook Request Type
export interface WebhookRequest {
webhookUrl: string;
httpMethod: 'GET' | 'POST' | 'PUT' | 'DELETE';
data?: Record<string, unknown>;
headers?: Record<string, string>;
waitForResponse?: boolean;
}
// MCP Tool Response Type
export interface McpToolResponse {
success: boolean;
data?: unknown;
error?: string;
message?: string;
code?: string;
details?: Record<string, unknown>;
}

136
src/utils/n8n-errors.ts Normal file
View File

@@ -0,0 +1,136 @@
import { logger } from './logger';
// Custom error classes for n8n API operations
export class N8nApiError extends Error {
constructor(
message: string,
public statusCode?: number,
public code?: string,
public details?: unknown
) {
super(message);
this.name = 'N8nApiError';
}
}
export class N8nAuthenticationError extends N8nApiError {
constructor(message = 'Authentication failed') {
super(message, 401, 'AUTHENTICATION_ERROR');
this.name = 'N8nAuthenticationError';
}
}
export class N8nNotFoundError extends N8nApiError {
constructor(resource: string, id?: string) {
const message = id ? `${resource} with ID ${id} not found` : `${resource} not found`;
super(message, 404, 'NOT_FOUND');
this.name = 'N8nNotFoundError';
}
}
export class N8nValidationError extends N8nApiError {
constructor(message: string, details?: unknown) {
super(message, 400, 'VALIDATION_ERROR', details);
this.name = 'N8nValidationError';
}
}
export class N8nRateLimitError extends N8nApiError {
constructor(retryAfter?: number) {
const message = retryAfter
? `Rate limit exceeded. Retry after ${retryAfter} seconds`
: 'Rate limit exceeded';
super(message, 429, 'RATE_LIMIT_ERROR', { retryAfter });
this.name = 'N8nRateLimitError';
}
}
export class N8nServerError extends N8nApiError {
constructor(message = 'Internal server error', statusCode = 500) {
super(message, statusCode, 'SERVER_ERROR');
this.name = 'N8nServerError';
}
}
// Error handling utility
export function handleN8nApiError(error: unknown): N8nApiError {
if (error instanceof N8nApiError) {
return error;
}
if (error instanceof Error) {
// Check if it's an Axios error
const axiosError = error as any;
if (axiosError.response) {
const { status, data } = axiosError.response;
const message = data?.message || axiosError.message;
switch (status) {
case 401:
return new N8nAuthenticationError(message);
case 404:
return new N8nNotFoundError('Resource', message);
case 400:
return new N8nValidationError(message, data);
case 429:
const retryAfter = axiosError.response.headers['retry-after'];
return new N8nRateLimitError(retryAfter ? parseInt(retryAfter) : undefined);
default:
if (status >= 500) {
return new N8nServerError(message, status);
}
return new N8nApiError(message, status, 'API_ERROR', data);
}
} else if (axiosError.request) {
// Request was made but no response received
return new N8nApiError('No response from n8n server', undefined, 'NO_RESPONSE');
} else {
// Something happened in setting up the request
return new N8nApiError(axiosError.message, undefined, 'REQUEST_ERROR');
}
}
// Unknown error type
return new N8nApiError('Unknown error occurred', undefined, 'UNKNOWN_ERROR', error);
}
// Utility to extract user-friendly error messages
export function getUserFriendlyErrorMessage(error: N8nApiError): string {
switch (error.code) {
case 'AUTHENTICATION_ERROR':
return 'Failed to authenticate with n8n. Please check your API key.';
case 'NOT_FOUND':
return error.message;
case 'VALIDATION_ERROR':
return `Invalid request: ${error.message}`;
case 'RATE_LIMIT_ERROR':
return 'Too many requests. Please wait a moment and try again.';
case 'NO_RESPONSE':
return 'Unable to connect to n8n. Please check the server URL and ensure n8n is running.';
case 'SERVER_ERROR':
return 'n8n server error. Please try again later or contact support.';
default:
return error.message || 'An unexpected error occurred';
}
}
// Log error with appropriate level
export function logN8nError(error: N8nApiError, context?: string): void {
const errorInfo = {
name: error.name,
message: error.message,
code: error.code,
statusCode: error.statusCode,
details: error.details,
context,
};
if (error.statusCode && error.statusCode >= 500) {
logger.error('n8n API server error', errorInfo);
} else if (error.statusCode && error.statusCode >= 400) {
logger.warn('n8n API client error', errorInfo);
} else {
logger.error('n8n API error', errorInfo);
}
}