diff --git a/data/nodes.db b/data/nodes.db index 0c9fc84..e61ec2d 100644 Binary files a/data/nodes.db and b/data/nodes.db differ 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'"); + }); +});