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

@@ -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'
]
}
};
}