UNPKG

n8n-mcp

Version:

Integration between n8n workflow automation and Model Context Protocol (MCP)

1,237 lines (1,236 loc) 119 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.getInstanceCacheStatistics = getInstanceCacheStatistics; exports.getInstanceCacheMetrics = getInstanceCacheMetrics; exports.clearInstanceCache = clearInstanceCache; exports.getN8nApiClient = getN8nApiClient; exports.tryParseJson = tryParseJson; exports.handleCreateWorkflow = handleCreateWorkflow; exports.handleGetWorkflow = handleGetWorkflow; exports.handleGetWorkflowDetails = handleGetWorkflowDetails; exports.handleGetWorkflowStructure = handleGetWorkflowStructure; exports.handleGetWorkflowMinimal = handleGetWorkflowMinimal; exports.handleGetWorkflowFiltered = handleGetWorkflowFiltered; exports.handleGetWorkflowActive = handleGetWorkflowActive; exports.handleUpdateWorkflow = handleUpdateWorkflow; exports.handleDeleteWorkflow = handleDeleteWorkflow; exports.handleListWorkflows = handleListWorkflows; exports.handleValidateWorkflow = handleValidateWorkflow; exports.handleAutofixWorkflow = handleAutofixWorkflow; exports.handleTestWorkflow = handleTestWorkflow; exports.handleGetExecution = handleGetExecution; exports.handleListExecutions = handleListExecutions; exports.handleDeleteExecution = handleDeleteExecution; exports.handleListTestRuns = handleListTestRuns; exports.handleGetTestRun = handleGetTestRun; exports.handleListTestCases = handleListTestCases; exports.handleHealthCheck = handleHealthCheck; exports.handleDiagnostic = handleDiagnostic; exports.handleWorkflowVersions = handleWorkflowVersions; exports.handleDeployTemplate = handleDeployTemplate; exports.handleTriggerWebhookWorkflow = handleTriggerWebhookWorkflow; exports.handleCreateTable = handleCreateTable; exports.handleListTables = handleListTables; exports.handleGetTable = handleGetTable; exports.handleUpdateTable = handleUpdateTable; exports.handleDeleteTable = handleDeleteTable; exports.handleGetRows = handleGetRows; exports.handleInsertRows = handleInsertRows; exports.handleUpdateRows = handleUpdateRows; exports.handleUpsertRows = handleUpsertRows; exports.handleDeleteRows = handleDeleteRows; exports.handleListCredentials = handleListCredentials; exports.handleGetCredential = handleGetCredential; exports.handleCreateCredential = handleCreateCredential; exports.handleUpdateCredential = handleUpdateCredential; exports.handleDeleteCredential = handleDeleteCredential; exports.handleGetCredentialSchema = handleGetCredentialSchema; exports.handleAuditInstance = handleAuditInstance; const crypto_1 = require("crypto"); const n8n_api_client_1 = require("../services/n8n-api-client"); const workflow_security_scanner_1 = require("../services/workflow-security-scanner"); const audit_report_builder_1 = require("../services/audit-report-builder"); const n8n_api_1 = require("../config/n8n-api"); const n8n_api_2 = require("../types/n8n-api"); const n8n_validation_1 = require("../services/n8n-validation"); const n8n_errors_1 = require("../utils/n8n-errors"); const logger_1 = require("../utils/logger"); const zod_1 = require("zod"); const workflow_validator_1 = require("../services/workflow-validator"); const enhanced_config_validator_1 = require("../services/enhanced-config-validator"); const instance_context_1 = require("../types/instance-context"); const workflow_auto_fixer_1 = require("../services/workflow-auto-fixer"); const expression_format_validator_1 = require("../services/expression-format-validator"); const workflow_versioning_service_1 = require("../services/workflow-versioning-service"); const handlers_workflow_diff_1 = require("./handlers-workflow-diff"); const telemetry_1 = require("../telemetry"); const cache_utils_1 = require("../utils/cache-utils"); const execution_processor_1 = require("../services/execution-processor"); const npm_version_checker_1 = require("../utils/npm-version-checker"); const mcp_input_normalizer_1 = require("../utils/mcp-input-normalizer"); let defaultApiClient = null; let lastDefaultConfigUrl = null; const cacheMutex = new cache_utils_1.CacheMutex(); const instanceClients = (0, cache_utils_1.createInstanceCache)((client, key) => { logger_1.logger.debug('Evicting API client from cache', { cacheKey: key.substring(0, 8) + '...' }); }); function getInstanceCacheStatistics() { return (0, cache_utils_1.getCacheStatistics)(); } function getInstanceCacheMetrics() { return cache_utils_1.cacheMetrics.getMetrics(); } function clearInstanceCache() { instanceClients.clear(); cache_utils_1.cacheMetrics.recordClear(); cache_utils_1.cacheMetrics.updateSize(0, instanceClients.max); } function getN8nApiClient(context) { if (context?.n8nApiUrl && context?.n8nApiKey) { const validation = (0, instance_context_1.validateInstanceContext)(context); if (!validation.valid) { logger_1.logger.warn('Invalid instance context provided', { instanceId: context.instanceId, errors: validation.errors }); return null; } const cacheKey = (0, cache_utils_1.createCacheKey)(`${context.n8nApiUrl}:${context.n8nApiKey}:${context.instanceId || ''}`); if (instanceClients.has(cacheKey)) { cache_utils_1.cacheMetrics.recordHit(); return instanceClients.get(cacheKey) || null; } cache_utils_1.cacheMetrics.recordMiss(); if (cacheMutex.isLocked(cacheKey)) { const waitTime = 100; const start = Date.now(); while (cacheMutex.isLocked(cacheKey) && (Date.now() - start) < 1000) { } if (instanceClients.has(cacheKey)) { cache_utils_1.cacheMetrics.recordHit(); return instanceClients.get(cacheKey) || null; } } const config = (0, n8n_api_1.getN8nApiConfigFromContext)(context); if (config) { logger_1.logger.info('Creating instance-specific n8n API client', { url: config.baseUrl.replace(/^(https?:\/\/[^\/]+).*/, '$1'), instanceId: context.instanceId, cacheKey: cacheKey.substring(0, 8) + '...' }); const client = new n8n_api_client_1.N8nApiClient(config); instanceClients.set(cacheKey, client); cache_utils_1.cacheMetrics.recordSet(); cache_utils_1.cacheMetrics.updateSize(instanceClients.size, instanceClients.max); return client; } return null; } if (process.env.ENABLE_MULTI_TENANT === 'true') { logger_1.logger.warn('Refusing env-credential fallback in multi-tenant mode'); return null; } logger_1.logger.info('Falling back to environment configuration for n8n API client'); const config = (0, n8n_api_1.getN8nApiConfig)(); if (!config) { if (defaultApiClient) { logger_1.logger.info('n8n API configuration removed, clearing default client'); defaultApiClient = null; lastDefaultConfigUrl = null; } return null; } if (!defaultApiClient || lastDefaultConfigUrl !== config.baseUrl) { logger_1.logger.info('n8n API client initialized from environment', { url: config.baseUrl }); defaultApiClient = new n8n_api_client_1.N8nApiClient(config); lastDefaultConfigUrl = config.baseUrl; } return defaultApiClient; } function ensureApiConfigured(context) { const client = getN8nApiClient(context); if (!client) { if (context?.instanceId) { throw new Error(`n8n API not configured for instance ${context.instanceId}. Please provide n8nApiUrl and n8nApiKey in the instance context.`); } throw new Error('n8n API not configured. Please set N8N_API_URL and N8N_API_KEY environment variables.'); } return client; } function resolveN8nApiConfigForResponse(context) { const fromContext = context ? (0, n8n_api_1.getN8nApiConfigFromContext)(context) : null; if (fromContext) { return fromContext; } if (process.env.ENABLE_MULTI_TENANT === 'true') { return null; } return (0, n8n_api_1.getN8nApiConfig)(); } function tryParseJson(val) { if (typeof val !== 'string') return val; try { return JSON.parse(val); } catch { return val; } } function stripActiveVersion(workflow) { const { activeVersion, ...rest } = workflow; return rest; } const emptyToUndefined = (v) => typeof v === 'string' && v.trim() === '' ? undefined : v; const optionalEmptyAware = (schema) => zod_1.z.preprocess(emptyToUndefined, schema.optional()); const createWorkflowSchema = zod_1.z.object({ name: zod_1.z.string(), nodes: zod_1.z.preprocess(mcp_input_normalizer_1.normalizeMcpWorkflowNodes, zod_1.z.array(zod_1.z.any())), connections: zod_1.z.preprocess(mcp_input_normalizer_1.normalizeMcpWorkflowConnections, zod_1.z.record(zod_1.z.string(), zod_1.z.any())), settings: zod_1.z.preprocess(mcp_input_normalizer_1.normalizeMcpJsonValue, zod_1.z.object({ executionOrder: zod_1.z.enum(['v0', 'v1']).optional(), timezone: zod_1.z.string().optional(), saveDataErrorExecution: zod_1.z.enum(['all', 'none']).optional(), saveDataSuccessExecution: zod_1.z.enum(['all', 'none']).optional(), saveManualExecutions: zod_1.z.boolean().optional(), saveExecutionProgress: zod_1.z.boolean().optional(), executionTimeout: zod_1.z.number().optional(), errorWorkflow: zod_1.z.string().optional(), })).optional(), projectId: zod_1.z.string().optional(), }); const updateWorkflowSchema = zod_1.z.object({ id: zod_1.z.string(), name: zod_1.z.string().optional(), nodes: zod_1.z.preprocess(mcp_input_normalizer_1.normalizeMcpWorkflowNodes, zod_1.z.array(zod_1.z.any())).optional(), connections: zod_1.z.preprocess(mcp_input_normalizer_1.normalizeMcpWorkflowConnections, zod_1.z.record(zod_1.z.string(), zod_1.z.any())).optional(), settings: zod_1.z.preprocess(mcp_input_normalizer_1.normalizeMcpJsonValue, zod_1.z.any()).optional(), createBackup: zod_1.z.boolean().optional(), intent: zod_1.z.string().optional(), }); const listWorkflowsSchema = zod_1.z.object({ limit: zod_1.z.number().min(1).max(100).optional(), cursor: optionalEmptyAware(zod_1.z.string()), active: zod_1.z.boolean().optional(), tags: zod_1.z.preprocess(mcp_input_normalizer_1.normalizeMcpJsonValue, zod_1.z.array(zod_1.z.string())).optional(), projectId: optionalEmptyAware(zod_1.z.string()), excludePinnedData: zod_1.z.boolean().optional(), }); const validateWorkflowSchema = zod_1.z.object({ id: zod_1.z.string(), options: zod_1.z.object({ validateNodes: zod_1.z.boolean().optional(), validateConnections: zod_1.z.boolean().optional(), validateExpressions: zod_1.z.boolean().optional(), profile: zod_1.z.enum(['minimal', 'runtime', 'ai-friendly', 'strict']).optional(), }).optional(), }); const autofixWorkflowSchema = zod_1.z.object({ id: zod_1.z.string(), applyFixes: zod_1.z.boolean().optional().default(false), fixTypes: zod_1.z.array(zod_1.z.enum([ 'expression-format', 'typeversion-correction', 'error-output-config', 'node-type-correction', 'webhook-missing-path', 'typeversion-upgrade', 'version-migration', 'tool-variant-correction', 'connection-numeric-keys', 'connection-invalid-type', 'connection-id-to-name', 'connection-duplicate-removal', 'connection-input-index' ])).optional(), confidenceThreshold: zod_1.z.enum(['high', 'medium', 'low']).optional().default('medium'), maxFixes: zod_1.z.number().optional().default(50) }); const testWorkflowSchema = zod_1.z.object({ workflowId: zod_1.z.string(), triggerType: optionalEmptyAware(zod_1.z.enum(['webhook', 'form', 'chat'])), httpMethod: optionalEmptyAware(zod_1.z.enum(['GET', 'POST', 'PUT', 'DELETE'])), webhookPath: optionalEmptyAware(zod_1.z.string()), message: optionalEmptyAware(zod_1.z.string()), sessionId: optionalEmptyAware(zod_1.z.string()), data: zod_1.z.record(zod_1.z.unknown()).optional(), headers: zod_1.z.record(zod_1.z.string()).optional(), timeout: zod_1.z.number().optional(), waitForResponse: zod_1.z.boolean().optional(), }); const listExecutionsSchema = zod_1.z.object({ limit: zod_1.z.number().min(1).max(100).optional(), cursor: optionalEmptyAware(zod_1.z.string()), workflowId: optionalEmptyAware(zod_1.z.string()), projectId: optionalEmptyAware(zod_1.z.string()), status: optionalEmptyAware(zod_1.z.enum(['success', 'error', 'waiting'])), includeData: zod_1.z.boolean().optional(), }); const listTestRunsSchema = zod_1.z.object({ workflowId: zod_1.z.string(), status: optionalEmptyAware(zod_1.z.enum(['new', 'running', 'completed', 'error', 'cancelled'])), limit: zod_1.z.number().min(1).max(250).optional(), cursor: optionalEmptyAware(zod_1.z.string()), }); const getTestRunSchema = zod_1.z.object({ workflowId: zod_1.z.string(), runId: zod_1.z.string(), }); const listTestCasesSchema = zod_1.z.object({ workflowId: zod_1.z.string(), runId: zod_1.z.string(), limit: zod_1.z.number().min(1).max(250).optional(), cursor: optionalEmptyAware(zod_1.z.string()), }); const workflowVersionsSchema = zod_1.z.object({ mode: zod_1.z.enum(['list', 'get', 'rollback', 'delete', 'prune']), workflowId: zod_1.z.string().optional(), versionId: zod_1.z.number().optional(), limit: zod_1.z.number().default(10).optional(), validateBefore: zod_1.z.boolean().default(true).optional(), deleteAll: zod_1.z.boolean().default(false).optional(), maxVersions: zod_1.z.number().default(10).optional(), }); async function handleCreateWorkflow(args, context) { try { const client = ensureApiConfigured(context); const input = createWorkflowSchema.parse(args); const shortFormErrors = []; input.nodes?.forEach((node, index) => { if (node.type?.startsWith('nodes-base.') || node.type?.startsWith('nodes-langchain.')) { const fullForm = node.type.startsWith('nodes-base.') ? node.type.replace('nodes-base.', 'n8n-nodes-base.') : node.type.replace('nodes-langchain.', '@n8n/n8n-nodes-langchain.'); shortFormErrors.push(`Node ${index} ("${node.name}") uses SHORT form "${node.type}". ` + `The n8n API requires FULL form. Change to "${fullForm}"`); } }); if (shortFormErrors.length > 0) { telemetry_1.telemetry.trackWorkflowCreation(input, false); return { success: false, error: 'Node type format error: n8n API requires FULL form node types', details: { errors: shortFormErrors, hint: 'Use n8n-nodes-base.* instead of nodes-base.* for standard nodes' } }; } const errors = (0, n8n_validation_1.validateWorkflowStructure)(input); if (errors.length > 0) { telemetry_1.telemetry.trackWorkflowCreation(input, false); return { success: false, error: 'Workflow validation failed', details: { errors } }; } const workflow = await client.createWorkflow(input); if (!workflow || !workflow.id) { return { success: false, error: 'Workflow creation failed: n8n API returned an empty or invalid response. Verify your N8N_API_URL points to the correct /api/v1 endpoint and that the n8n instance supports workflow creation.', details: { response: workflow ? { keys: Object.keys(workflow) } : null } }; } telemetry_1.telemetry.trackWorkflowCreation(workflow, true); return { success: true, data: { id: workflow.id, name: workflow.name, active: workflow.active, nodeCount: workflow.nodes?.length || 0 }, message: `Workflow "${workflow.name}" created successfully with ID: ${workflow.id}. Use n8n_get_workflow with mode 'structure' to verify current state.` }; } catch (error) { if (error instanceof zod_1.z.ZodError) { return { success: false, error: 'Invalid input', details: { errors: error.errors } }; } if (error instanceof n8n_errors_1.N8nApiError) { return { success: false, error: (0, n8n_errors_1.getUserFriendlyErrorMessage)(error), code: error.code, details: error.details }; } return { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' }; } } async function handleGetWorkflow(args, context) { try { const client = ensureApiConfigured(context); const { id } = zod_1.z.object({ id: zod_1.z.string() }).parse(args); const workflow = await client.getWorkflow(id); return { success: true, data: stripActiveVersion(workflow) }; } catch (error) { if (error instanceof zod_1.z.ZodError) { return { success: false, error: 'Invalid input', details: { errors: error.errors } }; } if (error instanceof n8n_errors_1.N8nApiError) { return { success: false, error: (0, n8n_errors_1.getUserFriendlyErrorMessage)(error), code: error.code }; } return { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' }; } } async function handleGetWorkflowDetails(args, context) { try { const client = ensureApiConfigured(context); const { id } = zod_1.z.object({ id: zod_1.z.string() }).parse(args); const workflow = await client.getWorkflow(id); const executions = await client.listExecutions({ workflowId: id, limit: 10 }); const stats = { totalExecutions: executions.data.length, successCount: executions.data.filter(e => e.status === n8n_api_2.ExecutionStatus.SUCCESS).length, errorCount: executions.data.filter(e => e.status === n8n_api_2.ExecutionStatus.ERROR).length, lastExecutionTime: executions.data[0]?.startedAt || null }; return { success: true, data: { workflow: stripActiveVersion(workflow), executionStats: stats, hasWebhookTrigger: (0, n8n_validation_1.hasWebhookTrigger)(workflow), webhookPath: (0, n8n_validation_1.getWebhookUrl)(workflow) } }; } catch (error) { if (error instanceof zod_1.z.ZodError) { return { success: false, error: 'Invalid input', details: { errors: error.errors } }; } if (error instanceof n8n_errors_1.N8nApiError) { return { success: false, error: (0, n8n_errors_1.getUserFriendlyErrorMessage)(error), code: error.code }; } return { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' }; } } async function handleGetWorkflowStructure(args, context) { try { const client = ensureApiConfigured(context); const { id } = zod_1.z.object({ id: zod_1.z.string() }).parse(args); const workflow = await client.getWorkflow(id); 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, isArchived: workflow.isArchived, nodes: simplifiedNodes, connections: workflow.connections, nodeCount: workflow.nodes.length, connectionCount: Object.keys(workflow.connections).length } }; } catch (error) { if (error instanceof zod_1.z.ZodError) { return { success: false, error: 'Invalid input', details: { errors: error.errors } }; } if (error instanceof n8n_errors_1.N8nApiError) { return { success: false, error: (0, n8n_errors_1.getUserFriendlyErrorMessage)(error), code: error.code }; } return { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' }; } } async function handleGetWorkflowMinimal(args, context) { try { const client = ensureApiConfigured(context); const { id } = zod_1.z.object({ id: zod_1.z.string() }).parse(args); const workflow = await client.getWorkflow(id); return { success: true, data: { id: workflow.id, name: workflow.name, active: workflow.active, isArchived: workflow.isArchived, tags: workflow.tags || [], createdAt: workflow.createdAt, updatedAt: workflow.updatedAt } }; } catch (error) { if (error instanceof zod_1.z.ZodError) { return { success: false, error: 'Invalid input', details: { errors: error.errors } }; } if (error instanceof n8n_errors_1.N8nApiError) { return { success: false, error: (0, n8n_errors_1.getUserFriendlyErrorMessage)(error), code: error.code }; } return { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' }; } } async function handleGetWorkflowFiltered(args, context) { try { const client = ensureApiConfigured(context); const { id, nodeNames } = zod_1.z.object({ id: zod_1.z.string(), nodeNames: zod_1.z.array(zod_1.z.string()).min(1) }).parse(args); const workflow = await client.getWorkflow(id); const requested = new Set(nodeNames); const matchedNodes = workflow.nodes.filter(node => requested.has(node.name) || requested.has(node.id)); const matchedKeys = new Set(matchedNodes.flatMap(node => [node.name, node.id])); const notFound = nodeNames.filter(key => !matchedKeys.has(key)); return { success: true, data: { id: workflow.id, name: workflow.name, active: workflow.active, isArchived: workflow.isArchived, nodes: matchedNodes, nodeCount: workflow.nodes.length, returnedCount: matchedNodes.length, ...(notFound.length > 0 ? { notFound } : {}) } }; } catch (error) { if (error instanceof zod_1.z.ZodError) { return { success: false, error: 'Invalid input', details: { errors: error.errors } }; } if (error instanceof n8n_errors_1.N8nApiError) { return { success: false, error: (0, n8n_errors_1.getUserFriendlyErrorMessage)(error), code: error.code }; } return { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' }; } } async function handleGetWorkflowActive(args, context) { try { const client = ensureApiConfigured(context); const { id } = zod_1.z.object({ id: zod_1.z.string() }).parse(args); const workflow = await client.getWorkflow(id); const activeVersion = workflow.activeVersion; const baseMeta = { id: workflow.id, name: workflow.name, active: workflow.active, isArchived: workflow.isArchived, tags: workflow.tags || [], settings: workflow.settings, createdAt: workflow.createdAt, updatedAt: workflow.updatedAt, }; if (workflow.activeVersionId && activeVersion) { return { success: true, data: { ...baseMeta, activeVersionId: workflow.activeVersionId, versionCreatedAt: activeVersion.createdAt ?? null, versionName: activeVersion.name ?? null, nodes: activeVersion.nodes, connections: activeVersion.connections, } }; } if (workflow.active === true) { return { success: true, data: { ...baseMeta, activeVersionId: null, versionCreatedAt: null, versionName: null, nodes: workflow.nodes, connections: workflow.connections, } }; } return { success: false, error: 'No published version. Workflow is inactive and has never been activated. Use mode="full" to see the draft.', code: 'NO_ACTIVE_VERSION' }; } catch (error) { if (error instanceof zod_1.z.ZodError) { return { success: false, error: 'Invalid input', details: { errors: error.errors } }; } if (error instanceof n8n_errors_1.N8nApiError) { return { success: false, error: (0, n8n_errors_1.getUserFriendlyErrorMessage)(error), code: error.code }; } return { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' }; } } async function handleUpdateWorkflow(args, repository, context) { const startTime = Date.now(); const sessionId = `mutation_${Date.now()}_${(0, crypto_1.randomUUID)()}`; let workflowBefore = null; let userIntent = 'Full workflow update'; try { const client = ensureApiConfigured(context); const input = updateWorkflowSchema.parse(args); const { id, createBackup, intent, ...updateData } = input; userIntent = intent || 'Full workflow update'; const current = await client.getWorkflow(id); workflowBefore = JSON.parse(JSON.stringify(current)); if (updateData.nodes && current.nodes) { const currentById = new Map(); const currentByName = new Map(); for (const node of current.nodes) { if (node.id) currentById.set(node.id, node); currentByName.set(node.name, node); } for (const node of updateData.nodes) { const hasCredentials = node.credentials && typeof node.credentials === 'object' && Object.keys(node.credentials).length > 0; if (!hasCredentials) { const match = (node.id && currentById.get(node.id)) || currentByName.get(node.name); if (match?.credentials) { node.credentials = match.credentials; } } } } const { settings: settingsUpdate, ...nonSettingsUpdate } = updateData; const fullWorkflow = { ...current, ...nonSettingsUpdate }; if (settingsUpdate && typeof settingsUpdate === 'object') { fullWorkflow.settings = { ...(current.settings ?? {}), ...settingsUpdate, }; } if (updateData.nodes || updateData.connections) { if (createBackup !== false) { try { const versioningService = new workflow_versioning_service_1.WorkflowVersioningService(repository, client, (0, instance_context_1.getInstanceScopeId)(context)); const backupResult = await versioningService.createBackup(id, current, { trigger: 'full_update' }); logger_1.logger.info('Workflow backup created', { workflowId: id, versionId: backupResult.versionId, versionNumber: backupResult.versionNumber, pruned: backupResult.pruned }); } catch (error) { logger_1.logger.warn('Failed to create workflow backup', { workflowId: id, error: error.message }); } } const errors = (0, n8n_validation_1.validateWorkflowStructure)(fullWorkflow); if (errors.length > 0) { return { success: false, error: 'Workflow validation failed', details: { errors } }; } } const workflow = await client.updateWorkflow(id, fullWorkflow); if (workflowBefore) { void trackWorkflowMutationForFullUpdate({ sessionId, toolName: 'n8n_update_full_workflow', userIntent, operations: [], workflowBefore, workflowAfter: workflow, mutationSuccess: true, durationMs: Date.now() - startTime, }).catch(err => { logger_1.logger.warn('Failed to track mutation telemetry:', err); }); } return { success: true, data: { id: workflow.id, name: workflow.name, active: workflow.active, nodeCount: workflow.nodes?.length || 0 }, message: `Workflow "${workflow.name}" updated successfully. Use n8n_get_workflow with mode 'structure' to verify current state.` }; } catch (error) { if (workflowBefore) { void trackWorkflowMutationForFullUpdate({ sessionId, toolName: 'n8n_update_full_workflow', userIntent, operations: [], workflowBefore, workflowAfter: workflowBefore, mutationSuccess: false, mutationError: error instanceof Error ? error.message : 'Unknown error', durationMs: Date.now() - startTime, }).catch(err => { logger_1.logger.warn('Failed to track mutation telemetry for failed operation:', err); }); } if (error instanceof zod_1.z.ZodError) { return { success: false, error: 'Invalid input', details: { errors: error.errors } }; } if (error instanceof n8n_errors_1.N8nApiError) { return { success: false, error: (0, n8n_errors_1.getUserFriendlyErrorMessage)(error), code: error.code, details: error.details }; } return { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' }; } } async function trackWorkflowMutationForFullUpdate(data) { try { const { telemetry } = await Promise.resolve().then(() => __importStar(require('../telemetry/telemetry-manager.js'))); await telemetry.trackWorkflowMutation(data); } catch (error) { logger_1.logger.debug('Telemetry tracking failed:', error); } } async function handleDeleteWorkflow(args, context) { try { const client = ensureApiConfigured(context); const { id } = zod_1.z.object({ id: zod_1.z.string() }).parse(args); const deleted = await client.deleteWorkflow(id); return { success: true, data: { id: deleted?.id || id, name: deleted?.name, deleted: true }, message: `Workflow "${deleted?.name || id}" deleted successfully.` }; } catch (error) { if (error instanceof zod_1.z.ZodError) { return { success: false, error: 'Invalid input', details: { errors: error.errors } }; } if (error instanceof n8n_errors_1.N8nApiError) { return { success: false, error: (0, n8n_errors_1.getUserFriendlyErrorMessage)(error), code: error.code }; } return { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' }; } } async function handleListWorkflows(args, context) { try { const client = ensureApiConfigured(context); const input = listWorkflowsSchema.parse(args || {}); const tagsParam = input.tags && input.tags.length > 0 ? input.tags.join(',') : undefined; const response = await client.listWorkflows({ limit: input.limit || 100, cursor: input.cursor, active: input.active, tags: tagsParam, projectId: input.projectId, excludePinnedData: input.excludePinnedData ?? true }); const minimalWorkflows = response.data.map(workflow => ({ id: workflow.id, name: workflow.name, active: workflow.active, isArchived: workflow.isArchived, createdAt: workflow.createdAt, updatedAt: workflow.updatedAt, tags: workflow.tags || [], nodeCount: workflow.nodes?.length || 0 })); return { success: true, data: { workflows: minimalWorkflows, returned: minimalWorkflows.length, nextCursor: response.nextCursor, hasMore: !!response.nextCursor, ...(response.nextCursor ? { _note: "More workflows available. Use cursor to get next page." } : {}) } }; } catch (error) { if (error instanceof zod_1.z.ZodError) { return { success: false, error: 'Invalid input', details: { errors: error.errors } }; } if (error instanceof n8n_errors_1.N8nApiError) { return { success: false, error: (0, n8n_errors_1.getUserFriendlyErrorMessage)(error), code: error.code }; } return { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' }; } } async function handleValidateWorkflow(args, repository, context) { try { const client = ensureApiConfigured(context); const input = validateWorkflowSchema.parse(args); const workflowResponse = await handleGetWorkflow({ id: input.id }, context); if (!workflowResponse.success) { return workflowResponse; } const workflow = workflowResponse.data; const validator = new workflow_validator_1.WorkflowValidator(repository, enhanced_config_validator_1.EnhancedConfigValidator); const validationResult = await validator.validateWorkflow(workflow, input.options); const response = { valid: validationResult.valid, workflowId: workflow.id, workflowName: workflow.name, summary: { totalNodes: validationResult.statistics.totalNodes, enabledNodes: validationResult.statistics.enabledNodes, triggerNodes: validationResult.statistics.triggerNodes, validConnections: validationResult.statistics.validConnections, invalidConnections: validationResult.statistics.invalidConnections, expressionsValidated: validationResult.statistics.expressionsValidated, errorCount: validationResult.errors.length, warningCount: validationResult.warnings.length } }; if (validationResult.errors.length > 0) { response.errors = validationResult.errors.map(e => ({ node: e.nodeName || 'workflow', nodeName: e.nodeName, message: e.message, details: e.details })); } if (validationResult.warnings.length > 0) { response.warnings = validationResult.warnings.map(w => ({ node: w.nodeName || 'workflow', nodeName: w.nodeName, message: w.message, details: w.details })); } if (validationResult.suggestions.length > 0) { response.suggestions = validationResult.suggestions; } if (validationResult.valid) { telemetry_1.telemetry.trackWorkflowCreation(workflow, true); } return { success: true, data: response }; } catch (error) { if (error instanceof zod_1.z.ZodError) { return { success: false, error: 'Invalid input', details: { errors: error.errors } }; } if (error instanceof n8n_errors_1.N8nApiError) { return { success: false, error: (0, n8n_errors_1.getUserFriendlyErrorMessage)(error), code: error.code }; } return { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' }; } } async function handleAutofixWorkflow(args, repository, context) { try { const client = ensureApiConfigured(context); const input = autofixWorkflowSchema.parse(args); const workflowResponse = await handleGetWorkflow({ id: input.id }, context); if (!workflowResponse.success) { return workflowResponse; } const workflow = workflowResponse.data; const validator = new workflow_validator_1.WorkflowValidator(repository, enhanced_config_validator_1.EnhancedConfigValidator); const validationResult = await validator.validateWorkflow(workflow, { validateNodes: true, validateConnections: true, validateExpressions: true, profile: 'ai-friendly' }); const allFormatIssues = []; for (const node of workflow.nodes) { const formatContext = { nodeType: node.type, nodeName: node.name, nodeId: node.id }; const nodeFormatIssues = expression_format_validator_1.ExpressionFormatValidator.validateNodeParameters(node.parameters, formatContext); const enrichedIssues = nodeFormatIssues.map(issue => ({ ...issue, nodeName: node.name, nodeId: node.id })); allFormatIssues.push(...enrichedIssues); } const autoFixer = new workflow_auto_fixer_1.WorkflowAutoFixer(repository); const fixResult = await autoFixer.generateFixes(workflow, validationResult, allFormatIssues, { applyFixes: input.applyFixes, fixTypes: input.fixTypes, confidenceThreshold: input.confidenceThreshold, maxFixes: input.maxFixes }); if (fixResult.fixes.length === 0) { return { success: true, data: { workflowId: workflow.id, workflowName: workflow.name, message: 'No automatic fixes available for this workflow', validationSummary: { errors: validationResult.errors.length, warnings: validationResult.warnings.length } } }; } if (!input.applyFixes) { return { success: true, data: { workflowId: workflow.id, workflowName: workflow.name, preview: true, fixesAvailable: fixResult.fixes.length, fixes: fixResult.fixes, summary: fixResult.summary, stats: fixResult.stats, message: `${fixResult.fixes.length} fixes available. Set applyFixes=true to apply them.` } }; } if (fixResult.operations.length > 0) { const updateResult = await (0, handlers_workflow_diff_1.handleUpdatePartialWorkflow)({ id: workflow.id, operations: fixResult.operations, createBackup: true }, repository, context); if (!updateResult.success) { return { success: false, error: 'Failed to apply fixes', details: { fixes: fixResult.fixes, updateError: updateResult.error } }; } return { success: true, data: { workflowId: workflow.id, workflowName: workflow.name, fixesApplied: fixResult.fixes.length, fixes: fixResult.fixes, summary: fixResult.summary, stats: fixResult.stats, message: `Successfully applied ${fixResult.fixes.length} fixes to workflow "${workflow.name}"` } }; } return { success: true, data: { workflowId: workflow.id, workflowName: workflow.name, message: 'No fixes needed' } }; } catch (error) { if (error instanceof zod_1.z.ZodError) { return { success: false, error: 'Invalid input', details: { errors: error.errors } }; } if (error instanceof n8n_errors_1.N8nApiError) { return { success: false, error: (0, n8n_errors_1.getUserFriendlyErrorMessage)(error), code: error.code }; } return { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' }; } } async function handleTestWorkflow(args, context) { try { const client = ensureApiConfigured(context); const input = testWorkflowSchema.parse(args); const { detectTriggerFromWorkflow, ensureRegistryInitialized, TriggerRegistry, } = await Promise.resolve().then(() => __importStar(require('../triggers'))); await ensureRegistryInitialized(); const workflow = await client.getWorkflow(input.workflowId); let triggerType = input.triggerType; let triggerInfo; const detection = detectTriggerFromWorkflow(workflow); if (!triggerType) { if (detection.detected && detection.trigger) { triggerType = detection.trigger.type; triggerInfo = detection.trigger; } else { return { success: false, error: 'Workflow cannot be triggered externally', details: { workflowId: input.workflowId, reason: detection.reason, hint: 'Only workflows with webhook, form, or chat triggers can be executed via the API. Add one of these trigger nodes to your workflow.', }, }; } } else { if (detection.detected && detection.trigger?.type === triggerType) { triggerInfo = detection.trigger; } else if (!detection.detected || detection.trigger?.type !== triggerType) { return { success: false, error: `Workflow does not have a ${triggerType} trigger`, details: { workflowId: input.workflowId, requestedTrigger: triggerType, detectedTrigger: detection.trigger?.type || 'none', hint: detection.detected ? `Workflow has a ${detection.trigger?.type} trigger. Either use that type or omit triggerType for auto-detection.` : 'Workflow has no externally-triggerable triggers (webhook, form, or chat).', }, }; } } const handler = TriggerRegistry.getHandler(triggerType, client, context); if (!handler) { return { success: false, error: `No handler registered for trigger type: ${triggerType}`, details: { supportedTypes: TriggerRegistry.getRegisteredTypes(), }, }; } if (handler.capabilities.requiresActiveWorkflow && !workflow.active) { return { success: false, error: 'Workflow must be active to trigger via this method', details: { workflowId: input.workflowId, triggerType, hint: 'Activate the workflow in n8n using n8n_update_partial_workflow with [{type: "activateWorkflow"}]', }, }; } if (triggerType === 'chat' && !input.message) { return { success: false, error: 'Chat trigger requires a message parameter', details: { hint: 'Provide message="your message" for chat triggers', }, }; } const triggerInput = { workflowId: input.workflowId, triggerType, httpMethod: input.httpMethod, webhookPath: input.webhookPath, message: input.message || '', sessionId: input.sessionId, data: input.data, formData: input.data, headers: input.headers, timeout: input.timeout, waitForResponse: input.waitForResponse, }; const response = await handler.execute(triggerInput, workflow, triggerInfo); return { success: response.success, data: response.data, message: response.success ? `Workflow triggered successfully via ${triggerType}` : response.error, executionId: response.executionId, workflowId: input.workflowId, details: { triggerType, metadata: response.metadata, ...(response.details || {}), }, }; } catch (error) { if (error instanceof zod_1.z.ZodError) { return { success: false, error: 'Invalid input', details: { errors: error.errors }, }; }