From 64b9cf47a74f29e4dbaa0b27ca89c94533dd3328 Mon Sep 17 00:00:00 2001 From: czlonkowski <56956555+czlonkowski@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:57:29 +0200 Subject: [PATCH] feat: enhance webhook error messages with execution guidance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace generic "Please try again later or contact support" error messages with actionable guidance that directs users to use n8n_get_execution with mode='preview' for efficient debugging. ## Changes ### Core Functionality - Add formatExecutionError() to create execution-specific error messages - Add formatNoExecutionError() for cases without execution context - Update handleTriggerWebhookWorkflow to extract execution/workflow IDs from errors - Modify getUserFriendlyErrorMessage to avoid generic SERVER_ERROR message ### Type Updates - Add executionId and workflowId optional fields to McpToolResponse - Add errorHandling optional field to ToolDocumentation.full ### Error Message Format **With Execution ID:** "Workflow {workflowId} execution {executionId} failed. Use n8n_get_execution({id: '{executionId}', mode: 'preview'}) to investigate the error." **Without Execution ID:** "Workflow failed to execute. Use n8n_list_executions to find recent executions, then n8n_get_execution with mode='preview' to investigate." ### Testing - Add comprehensive tests in tests/unit/utils/n8n-errors.test.ts (20 tests) - Add 10 new tests for handleTriggerWebhookWorkflow in handlers-n8n-manager.test.ts - Update existing health check test to expect new error message format - All tests passing (52 total tests) ### Documentation - Update n8n-trigger-webhook-workflow tool documentation with errorHandling section - Document why mode='preview' is recommended (fast, efficient, safe) - Add example error responses and investigation workflow ## Why mode='preview'? - Fast: <50ms response time - Efficient: ~500 tokens (vs 50K+ for full mode) - Safe: No timeout or token limit risks - Informative: Shows structure, counts, and error details ## Breaking Changes None - backward compatible improvement to error messages only. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- data/nodes.db | Bin 60383232 -> 60383232 bytes package.runtime.json | 2 +- src/mcp/handlers-n8n-manager.ts | 41 +++- src/mcp/tool-docs/types.ts | 1 + .../n8n-trigger-webhook-workflow.ts | 44 ++++- src/types/n8n-api.ts | 2 + src/utils/n8n-errors.ts | 23 ++- tests/unit/mcp/handlers-n8n-manager.test.ts | 177 +++++++++++++++++- tests/unit/utils/n8n-errors.test.ts | 171 +++++++++++++++++ 9 files changed, 450 insertions(+), 11 deletions(-) create mode 100644 tests/unit/utils/n8n-errors.test.ts diff --git a/data/nodes.db b/data/nodes.db index 0c9fc849eb7c0af82b8d5b6bfc900a05ceb439e7..e61ec2dbddcb1daa64430c5b00afb36bbed36e5d 100644 GIT binary patch delta 4850 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 diff --git a/package.runtime.json b/package.runtime.json index 623b77f..bcec423 100644 --- a/package.runtime.json +++ b/package.runtime.json @@ -1,6 +1,6 @@ { "name": "n8n-mcp-runtime", - "version": "2.14.4", + "version": "2.14.5", "description": "n8n MCP Server Runtime Dependencies Only", "private": true, "dependencies": { diff --git a/src/mcp/handlers-n8n-manager.ts b/src/mcp/handlers-n8n-manager.ts index 20a5121..48b4202 100644 --- a/src/mcp/handlers-n8n-manager.ts +++ b/src/mcp/handlers-n8n-manager.ts @@ -18,7 +18,9 @@ import { import { N8nApiError, N8nNotFoundError, - getUserFriendlyErrorMessage + getUserFriendlyErrorMessage, + formatExecutionError, + formatNoExecutionError } from '../utils/n8n-errors'; import { logger } from '../utils/logger'; import { z } from 'zod'; @@ -942,7 +944,7 @@ export async function handleTriggerWebhookWorkflow(args: unknown, context?: Inst try { const client = ensureApiConfigured(context); const input = triggerWebhookSchema.parse(args); - + const webhookRequest: WebhookRequest = { webhookUrl: input.webhookUrl, httpMethod: input.httpMethod || 'POST', @@ -950,9 +952,9 @@ export async function handleTriggerWebhookWorkflow(args: unknown, context?: Inst headers: input.headers, waitForResponse: input.waitForResponse ?? true }; - + const response = await client.triggerWebhook(webhookRequest); - + return { success: true, data: response, @@ -966,8 +968,35 @@ export async function handleTriggerWebhookWorkflow(args: unknown, context?: Inst details: { errors: error.errors } }; } - + if (error instanceof N8nApiError) { + // Try to extract execution context from error response + const errorData = error.details as any; + const executionId = errorData?.executionId || errorData?.id || errorData?.execution?.id; + const workflowId = errorData?.workflowId || errorData?.workflow?.id; + + // If we have execution ID, provide specific guidance with n8n_get_execution + if (executionId) { + return { + success: false, + error: formatExecutionError(executionId, workflowId), + code: error.code, + executionId, + workflowId: workflowId || undefined + }; + } + + // No execution ID available - workflow likely didn't start + // Provide guidance to check recent executions + if (error.code === 'SERVER_ERROR' || error.statusCode && error.statusCode >= 500) { + return { + success: false, + error: formatNoExecutionError(), + code: error.code + }; + } + + // For other errors (auth, validation, etc), use standard message return { success: false, error: getUserFriendlyErrorMessage(error), @@ -975,7 +1004,7 @@ export async function handleTriggerWebhookWorkflow(args: unknown, context?: Inst details: error.details as Record | undefined }; } - + return { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' diff --git a/src/mcp/tool-docs/types.ts b/src/mcp/tool-docs/types.ts index 9ddc9d2..0c5da3e 100644 --- a/src/mcp/tool-docs/types.ts +++ b/src/mcp/tool-docs/types.ts @@ -22,6 +22,7 @@ export interface ToolDocumentation { examples: string[]; useCases: string[]; performance: string; + errorHandling?: string; // Optional: Documentation on error handling and debugging bestPractices: string[]; pitfalls: string[]; modeComparison?: string; // Optional: Comparison of different modes for tools with multiple modes diff --git a/src/mcp/tool-docs/workflow_management/n8n-trigger-webhook-workflow.ts b/src/mcp/tool-docs/workflow_management/n8n-trigger-webhook-workflow.ts index 772b255..decf7e0 100644 --- a/src/mcp/tool-docs/workflow_management/n8n-trigger-webhook-workflow.ts +++ b/src/mcp/tool-docs/workflow_management/n8n-trigger-webhook-workflow.ts @@ -59,19 +59,59 @@ export const n8nTriggerWebhookWorkflowDoc: ToolDocumentation = { 'Implement event-driven architectures with n8n' ], performance: `Performance varies based on workflow complexity and waitForResponse setting. Synchronous calls (waitForResponse: true) block until workflow completes. For long-running workflows, use async mode (waitForResponse: false) and monitor execution separately.`, + errorHandling: `**Enhanced Error Messages with Execution Guidance** + +When a webhook trigger fails, the error response now includes specific guidance to help debug the issue: + +**Error with Execution ID** (workflow started but failed): +- Format: "Workflow {workflowId} execution {executionId} failed. Use n8n_get_execution({id: '{executionId}', mode: 'preview'}) to investigate the error." +- Response includes: executionId and workflowId fields for direct access +- Recommended action: Use n8n_get_execution with mode='preview' for fast, efficient error inspection + +**Error without Execution ID** (workflow didn't start): +- Format: "Workflow failed to execute. Use n8n_list_executions to find recent executions, then n8n_get_execution with mode='preview' to investigate." +- Recommended action: Check recent executions with n8n_list_executions + +**Why mode='preview'?** +- Fast: <50ms response time +- Efficient: ~500 tokens (vs 50K+ for full mode) +- Safe: No timeout or token limit risks +- Informative: Shows structure, counts, and error details +- Provides recommendations for fetching more data if needed + +**Example Error Responses**: +\`\`\`json +{ + "success": false, + "error": "Workflow wf_123 execution exec_456 failed. Use n8n_get_execution({id: 'exec_456', mode: 'preview'}) to investigate the error.", + "executionId": "exec_456", + "workflowId": "wf_123", + "code": "SERVER_ERROR" +} +\`\`\` + +**Investigation Workflow**: +1. Trigger returns error with execution ID +2. Call n8n_get_execution({id: executionId, mode: 'preview'}) to see structure and error +3. Based on preview recommendation, fetch more data if needed +4. Fix issues in workflow and retry`, bestPractices: [ 'Always verify workflow is active before attempting webhook triggers', 'Match HTTP method exactly with webhook node configuration', 'Use async mode (waitForResponse: false) for long-running workflows', 'Include authentication headers when webhook requires them', - 'Test webhook URL manually first to ensure it works' + 'Test webhook URL manually first to ensure it works', + 'When errors occur, use n8n_get_execution with mode="preview" first for efficient debugging', + 'Store execution IDs from error responses for later investigation' ], pitfalls: [ 'Workflow must be ACTIVE - inactive workflows cannot be triggered', 'HTTP method mismatch returns 404 even if URL is correct', 'Webhook node must be the trigger node in the workflow', 'Timeout errors occur with long workflows in sync mode', - 'Data format must match webhook node expectations' + 'Data format must match webhook node expectations', + 'Error messages always include n8n_get_execution guidance - follow the suggested steps for efficient debugging', + 'Execution IDs in error responses are crucial for debugging - always check for and use them' ], relatedTools: ['n8n_get_execution', 'n8n_list_executions', 'n8n_get_workflow', 'n8n_create_workflow'] } diff --git a/src/types/n8n-api.ts b/src/types/n8n-api.ts index 328e6e5..bde1d42 100644 --- a/src/types/n8n-api.ts +++ b/src/types/n8n-api.ts @@ -290,6 +290,8 @@ export interface McpToolResponse { message?: string; code?: string; details?: Record; + executionId?: string; + workflowId?: string; } // Execution Filtering Types diff --git a/src/utils/n8n-errors.ts b/src/utils/n8n-errors.ts index 7b6a27d..d8a7c5b 100644 --- a/src/utils/n8n-errors.ts +++ b/src/utils/n8n-errors.ts @@ -95,6 +95,25 @@ export function handleN8nApiError(error: unknown): N8nApiError { return new N8nApiError('Unknown error occurred', undefined, 'UNKNOWN_ERROR', error); } +/** + * Format execution error message with guidance to use n8n_get_execution + * @param executionId - The execution ID from the failed execution + * @param workflowId - Optional workflow ID + * @returns Formatted error message with n8n_get_execution guidance + */ +export function formatExecutionError(executionId: string, workflowId?: string): string { + const workflowPrefix = workflowId ? `Workflow ${workflowId} execution ` : 'Execution '; + return `${workflowPrefix}${executionId} failed. Use n8n_get_execution({id: '${executionId}', mode: 'preview'}) to investigate the error.`; +} + +/** + * Format error message when no execution ID is available + * @returns Generic guidance to check executions + */ +export function formatNoExecutionError(): string { + return "Workflow failed to execute. Use n8n_list_executions to find recent executions, then n8n_get_execution with mode='preview' to investigate."; +} + // Utility to extract user-friendly error messages export function getUserFriendlyErrorMessage(error: N8nApiError): string { switch (error.code) { @@ -109,7 +128,9 @@ export function getUserFriendlyErrorMessage(error: N8nApiError): string { 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.'; + // For server errors, we should not show generic message + // Callers should check for execution context and use formatExecutionError instead + return error.message || 'n8n server error occurred'; default: return error.message || 'An unexpected error occurred'; } diff --git a/tests/unit/mcp/handlers-n8n-manager.test.ts b/tests/unit/mcp/handlers-n8n-manager.test.ts index 78db946..ab96a84 100644 --- a/tests/unit/mcp/handlers-n8n-manager.test.ts +++ b/tests/unit/mcp/handlers-n8n-manager.test.ts @@ -542,7 +542,7 @@ describe('handlers-n8n-manager', () => { expect(result).toEqual({ success: false, - error: 'n8n server error. Please try again later or contact support.', + error: 'Service unavailable', code: 'SERVER_ERROR', details: { apiUrl: 'https://n8n.test.com', @@ -642,4 +642,179 @@ describe('handlers-n8n-manager', () => { }); }); }); + + describe('handleTriggerWebhookWorkflow', () => { + it('should trigger webhook successfully', async () => { + const webhookResponse = { + status: 200, + statusText: 'OK', + data: { result: 'success' }, + headers: {} + }; + + mockApiClient.triggerWebhook.mockResolvedValue(webhookResponse); + + const result = await handlers.handleTriggerWebhookWorkflow({ + webhookUrl: 'https://n8n.test.com/webhook/test-123', + httpMethod: 'POST', + data: { test: 'data' } + }); + + expect(result).toEqual({ + success: true, + data: webhookResponse, + message: 'Webhook triggered successfully' + }); + }); + + it('should extract execution ID from webhook error response', async () => { + const apiError = new N8nServerError('Workflow execution failed'); + apiError.details = { + executionId: 'exec_abc123', + workflowId: 'wf_xyz789' + }; + + mockApiClient.triggerWebhook.mockRejectedValue(apiError); + + const result = await handlers.handleTriggerWebhookWorkflow({ + webhookUrl: 'https://n8n.test.com/webhook/test-123', + httpMethod: 'POST' + }); + + expect(result.success).toBe(false); + expect(result.error).toContain('Workflow wf_xyz789 execution exec_abc123 failed'); + expect(result.error).toContain('n8n_get_execution'); + expect(result.error).toContain("mode: 'preview'"); + expect(result.executionId).toBe('exec_abc123'); + expect(result.workflowId).toBe('wf_xyz789'); + }); + + it('should extract execution ID without workflow ID', async () => { + const apiError = new N8nServerError('Execution failed'); + apiError.details = { + executionId: 'exec_only_123' + }; + + mockApiClient.triggerWebhook.mockRejectedValue(apiError); + + const result = await handlers.handleTriggerWebhookWorkflow({ + webhookUrl: 'https://n8n.test.com/webhook/test-123', + httpMethod: 'GET' + }); + + expect(result.success).toBe(false); + expect(result.error).toContain('Execution exec_only_123 failed'); + expect(result.error).toContain('n8n_get_execution'); + expect(result.error).toContain("mode: 'preview'"); + expect(result.executionId).toBe('exec_only_123'); + expect(result.workflowId).toBeUndefined(); + }); + + it('should handle execution ID as "id" field', async () => { + const apiError = new N8nServerError('Error'); + apiError.details = { + id: 'exec_from_id_field', + workflowId: 'wf_test' + }; + + mockApiClient.triggerWebhook.mockRejectedValue(apiError); + + const result = await handlers.handleTriggerWebhookWorkflow({ + webhookUrl: 'https://n8n.test.com/webhook/test', + httpMethod: 'POST' + }); + + expect(result.error).toContain('exec_from_id_field'); + expect(result.executionId).toBe('exec_from_id_field'); + }); + + it('should provide generic guidance when no execution ID is available', async () => { + const apiError = new N8nServerError('Server error without execution context'); + apiError.details = {}; // No execution ID + + mockApiClient.triggerWebhook.mockRejectedValue(apiError); + + const result = await handlers.handleTriggerWebhookWorkflow({ + webhookUrl: 'https://n8n.test.com/webhook/test', + httpMethod: 'POST' + }); + + expect(result.success).toBe(false); + expect(result.error).toContain('Workflow failed to execute'); + expect(result.error).toContain('n8n_list_executions'); + expect(result.error).toContain('n8n_get_execution'); + expect(result.error).toContain("mode='preview'"); + expect(result.executionId).toBeUndefined(); + }); + + it('should use standard error message for authentication errors', async () => { + const authError = new N8nAuthenticationError('Invalid API key'); + mockApiClient.triggerWebhook.mockRejectedValue(authError); + + const result = await handlers.handleTriggerWebhookWorkflow({ + webhookUrl: 'https://n8n.test.com/webhook/test', + httpMethod: 'POST' + }); + + expect(result).toEqual({ + success: false, + error: 'Failed to authenticate with n8n. Please check your API key.', + code: 'AUTHENTICATION_ERROR', + details: undefined + }); + }); + + it('should use standard error message for validation errors', async () => { + const validationError = new N8nValidationError('Invalid webhook URL'); + mockApiClient.triggerWebhook.mockRejectedValue(validationError); + + const result = await handlers.handleTriggerWebhookWorkflow({ + webhookUrl: 'https://n8n.test.com/webhook/test', + httpMethod: 'POST' + }); + + expect(result.error).toBe('Invalid request: Invalid webhook URL'); + expect(result.code).toBe('VALIDATION_ERROR'); + }); + + it('should handle invalid input with Zod validation error', async () => { + const result = await handlers.handleTriggerWebhookWorkflow({ + webhookUrl: 'not-a-url', + httpMethod: 'INVALID_METHOD' + }); + + expect(result.success).toBe(false); + expect(result.error).toBe('Invalid input'); + expect(result.details).toHaveProperty('errors'); + }); + + it('should not include "contact support" in error messages', async () => { + const apiError = new N8nServerError('Test error'); + apiError.details = { executionId: 'test_exec' }; + + mockApiClient.triggerWebhook.mockRejectedValue(apiError); + + const result = await handlers.handleTriggerWebhookWorkflow({ + webhookUrl: 'https://n8n.test.com/webhook/test', + httpMethod: 'POST' + }); + + expect(result.error?.toLowerCase()).not.toContain('contact support'); + expect(result.error?.toLowerCase()).not.toContain('try again later'); + }); + + it('should always recommend preview mode in error messages', async () => { + const apiError = new N8nServerError('Error'); + apiError.details = { executionId: 'test_123' }; + + mockApiClient.triggerWebhook.mockRejectedValue(apiError); + + const result = await handlers.handleTriggerWebhookWorkflow({ + webhookUrl: 'https://n8n.test.com/webhook/test', + httpMethod: 'POST' + }); + + expect(result.error).toMatch(/mode:\s*'preview'/); + }); + }); }); \ No newline at end of file diff --git a/tests/unit/utils/n8n-errors.test.ts b/tests/unit/utils/n8n-errors.test.ts new file mode 100644 index 0000000..0d2f12f --- /dev/null +++ b/tests/unit/utils/n8n-errors.test.ts @@ -0,0 +1,171 @@ +import { describe, it, expect } from 'vitest'; +import { + formatExecutionError, + formatNoExecutionError, + getUserFriendlyErrorMessage, + N8nApiError, + N8nAuthenticationError, + N8nNotFoundError, + N8nValidationError, + N8nRateLimitError, + N8nServerError +} from '../../../src/utils/n8n-errors'; + +describe('formatExecutionError', () => { + it('should format error with both execution ID and workflow ID', () => { + const result = formatExecutionError('exec_12345', 'wf_abc'); + + expect(result).toBe("Workflow wf_abc execution exec_12345 failed. Use n8n_get_execution({id: 'exec_12345', mode: 'preview'}) to investigate the error."); + expect(result).toContain('mode: \'preview\''); + expect(result).toContain('exec_12345'); + expect(result).toContain('wf_abc'); + }); + + it('should format error with only execution ID', () => { + const result = formatExecutionError('exec_67890'); + + expect(result).toBe("Execution exec_67890 failed. Use n8n_get_execution({id: 'exec_67890', mode: 'preview'}) to investigate the error."); + expect(result).toContain('mode: \'preview\''); + expect(result).toContain('exec_67890'); + expect(result).not.toContain('Workflow'); + }); + + it('should include preview mode guidance', () => { + const result = formatExecutionError('test_id'); + + expect(result).toMatch(/mode:\s*'preview'/); + }); + + it('should format with undefined workflow ID (treated as missing)', () => { + const result = formatExecutionError('exec_123', undefined); + + expect(result).toBe("Execution exec_123 failed. Use n8n_get_execution({id: 'exec_123', mode: 'preview'}) to investigate the error."); + }); + + it('should properly escape execution ID in suggestion', () => { + const result = formatExecutionError('exec-with-special_chars.123'); + + expect(result).toContain("id: 'exec-with-special_chars.123'"); + }); +}); + +describe('formatNoExecutionError', () => { + it('should provide guidance to check recent executions', () => { + const result = formatNoExecutionError(); + + expect(result).toBe("Workflow failed to execute. Use n8n_list_executions to find recent executions, then n8n_get_execution with mode='preview' to investigate."); + expect(result).toContain('n8n_list_executions'); + expect(result).toContain('n8n_get_execution'); + expect(result).toContain("mode='preview'"); + }); + + it('should include preview mode in guidance', () => { + const result = formatNoExecutionError(); + + expect(result).toMatch(/mode\s*=\s*'preview'/); + }); +}); + +describe('getUserFriendlyErrorMessage', () => { + it('should handle authentication error', () => { + const error = new N8nAuthenticationError('Invalid API key'); + const message = getUserFriendlyErrorMessage(error); + + expect(message).toBe('Failed to authenticate with n8n. Please check your API key.'); + }); + + it('should handle not found error', () => { + const error = new N8nNotFoundError('Workflow', '123'); + const message = getUserFriendlyErrorMessage(error); + + expect(message).toContain('not found'); + }); + + it('should handle validation error', () => { + const error = new N8nValidationError('Missing required field'); + const message = getUserFriendlyErrorMessage(error); + + expect(message).toBe('Invalid request: Missing required field'); + }); + + it('should handle rate limit error', () => { + const error = new N8nRateLimitError(60); + const message = getUserFriendlyErrorMessage(error); + + expect(message).toBe('Too many requests. Please wait a moment and try again.'); + }); + + it('should handle server error with custom message', () => { + const error = new N8nServerError('Database connection failed', 503); + const message = getUserFriendlyErrorMessage(error); + + expect(message).toBe('Database connection failed'); + }); + + it('should handle server error without message', () => { + const error = new N8nApiError('', 500, 'SERVER_ERROR'); + const message = getUserFriendlyErrorMessage(error); + + expect(message).toBe('n8n server error occurred'); + }); + + it('should handle no response error', () => { + const error = new N8nApiError('Network error', undefined, 'NO_RESPONSE'); + const message = getUserFriendlyErrorMessage(error); + + expect(message).toBe('Unable to connect to n8n. Please check the server URL and ensure n8n is running.'); + }); + + it('should handle unknown error with message', () => { + const error = new N8nApiError('Custom error message'); + const message = getUserFriendlyErrorMessage(error); + + expect(message).toBe('Custom error message'); + }); + + it('should handle unknown error without message', () => { + const error = new N8nApiError(''); + const message = getUserFriendlyErrorMessage(error); + + expect(message).toBe('An unexpected error occurred'); + }); +}); + +describe('Error message integration', () => { + it('should use formatExecutionError for webhook failures with execution ID', () => { + const executionId = 'exec_webhook_123'; + const workflowId = 'wf_webhook_abc'; + const message = formatExecutionError(executionId, workflowId); + + expect(message).toContain('Workflow wf_webhook_abc execution exec_webhook_123 failed'); + expect(message).toContain('n8n_get_execution'); + expect(message).toContain("mode: 'preview'"); + }); + + it('should use formatNoExecutionError for server errors without execution context', () => { + const message = formatNoExecutionError(); + + expect(message).toContain('Workflow failed to execute'); + expect(message).toContain('n8n_list_executions'); + expect(message).toContain('n8n_get_execution'); + }); + + it('should not include "contact support" in any error message', () => { + const executionMessage = formatExecutionError('test'); + const noExecutionMessage = formatNoExecutionError(); + const serverError = new N8nServerError(); + const serverErrorMessage = getUserFriendlyErrorMessage(serverError); + + expect(executionMessage.toLowerCase()).not.toContain('contact support'); + expect(noExecutionMessage.toLowerCase()).not.toContain('contact support'); + expect(serverErrorMessage.toLowerCase()).not.toContain('contact support'); + }); + + it('should always guide users to use preview mode first', () => { + const executionMessage = formatExecutionError('test'); + const noExecutionMessage = formatNoExecutionError(); + + expect(executionMessage).toContain("mode: 'preview'"); + expect(noExecutionMessage).toContain("mode='preview'"); + }); +});