From ca20586eda6c13a2eaf9003faf06b79afb84f3c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Romuald=20Cz=C5=82onkowski?= <56956555+czlonkowski@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:40:17 +0200 Subject: [PATCH] fix: use stdio-wrapper as bin entry and preserve credentials on workflow update (v2.45.1) (#695) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch the npm bin entry from index.js to stdio-wrapper.js to prevent INFO-level logs from corrupting the JSON-RPC stdio transport. Also update both publish scripts so the fix persists across releases. Fixes #693. Preserve node credentials during full workflow updates. AI-generated node updates typically omit credential references, causing the n8n API to reject the PUT. The update handler now merges credentials from the current server-side workflow when user-provided nodes lack them. Fixes #689. Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en Co-authored-by: Claude Opus 4.6 (1M context) --- .claude/agents/n8n-mcp-tester.md | 5 +- CHANGELOG.md | 13 ++ package.json | 4 +- package.runtime.json | 2 +- scripts/publish-npm-quick.sh | 2 +- scripts/publish-npm.sh | 2 +- src/mcp/handlers-n8n-manager.ts | 22 +++ tests/unit/mcp/handlers-n8n-manager.test.ts | 144 ++++++++++++++++++++ 8 files changed, 186 insertions(+), 8 deletions(-) diff --git a/.claude/agents/n8n-mcp-tester.md b/.claude/agents/n8n-mcp-tester.md index 1ad13ed..f0f82b6 100644 --- a/.claude/agents/n8n-mcp-tester.md +++ b/.claude/agents/n8n-mcp-tester.md @@ -1,10 +1,9 @@ --- name: n8n-mcp-tester -description: Use this agent when you need to test n8n-mcp functionality after implementing fixes or new features. This agent should be invoked proactively after code changes to validate that the MCP tools work correctly with real n8n workflows. The agent tests the actual MCP server tools, not just unit tests. Examples:\n\n\nContext: Developer has just fixed a validation bug in the n8n-mcp server\nuser: "I've fixed the validation issue for HTTP Request nodes"\nassistant: "I've completed the fix. Now let me build the project and then test it."\n\nassistant: "Project built successfully. Please reload the MCP server in Claude Desktop to apply the changes."\nuser: "Reloaded"\nassistant: "Great! Now let me invoke the n8n-mcp-tester agent to verify the fix works correctly."\n\nSince a fix has been implemented and the MCP server reloaded, use the n8n-mcp-tester agent to validate the fix works in real-world usage.\n\n\n\n\nContext: Developer has added a new MCP tool for workflow validation\nuser: "I've implemented the new workflow validation tool"\nassistant: "Implementation complete. Let me build and prepare for testing."\n\nassistant: "Build successful. Please reload the MCP server to load the new tool."\nuser: "Done"\nassistant: "Perfect! I'll now use the n8n-mcp-tester agent to test the new workflow validation tool."\n\nAfter implementing new MCP functionality and reloading the server, invoke n8n-mcp-tester to verify it works correctly.\n\n -tools: Glob, Grep, Read, WebFetch, TodoWrite, WebSearch, mcp__supabase__create_branch, mcp__supabase__list_branches, mcp__supabase__delete_branch, mcp__supabase__merge_branch, mcp__supabase__reset_branch, mcp__supabase__rebase_branch, mcp__supabase__list_tables, mcp__supabase__list_extensions, mcp__supabase__list_migrations, mcp__supabase__apply_migration, mcp__supabase__execute_sql, mcp__supabase__get_logs, mcp__supabase__get_advisors, mcp__supabase__get_project_url, mcp__supabase__generate_typescript_types, mcp__supabase__search_docs, mcp__supabase__list_edge_functions, mcp__supabase__deploy_edge_function, mcp__n8n-mcp__tools_documentation, mcp__n8n-mcp__search_nodes, mcp__n8n-mcp__get_template, mcp__n8n-mcp__search_templates, mcp__n8n-mcp__validate_workflow, mcp__n8n-mcp__n8n_create_workflow, mcp__n8n-mcp__n8n_get_workflow, mcp__n8n-mcp__n8n_update_full_workflow, mcp__n8n-mcp__n8n_update_partial_workflow, mcp__n8n-mcp__n8n_delete_workflow, mcp__n8n-mcp__n8n_list_workflows, mcp__n8n-mcp__n8n_validate_workflow, mcp__n8n-mcp__n8n_trigger_webhook_workflow, mcp__n8n-mcp__n8n_health_check, mcp__brightdata-mcp__search_engine, mcp__brightdata-mcp__scrape_as_markdown, mcp__brightdata-mcp__search_engine_batch, mcp__brightdata-mcp__scrape_batch, mcp__supabase__get_publishable_keys, mcp__supabase__get_edge_function, mcp__n8n-mcp__get_node, mcp__n8n-mcp__validate_node, mcp__n8n-mcp__n8n_autofix_workflow, mcp__n8n-mcp__n8n_executions, mcp__n8n-mcp__n8n_workflow_versions, mcp__n8n-mcp__n8n_deploy_template, mcp__ide__getDiagnostics, mcp__ide__executeCode +description: "Use this agent when you need to test n8n-mcp functionality after implementing fixes or new features. This agent should be invoked proactively after code changes to validate that the MCP tools work correctly with real n8n workflows. The agent tests the actual MCP server tools, not just unit tests. Examples:\\n\\n\\nContext: Developer has just fixed a validation bug in the n8n-mcp server\\nuser: \"I've fixed the validation issue for HTTP Request nodes\"\\nassistant: \"I've completed the fix. Now let me build the project and then test it.\"\\n\\nassistant: \"Project built successfully. Please reload the MCP server in Claude Desktop to apply the changes.\"\\nuser: \"Reloaded\"\\nassistant: \"Great! Now let me invoke the n8n-mcp-tester agent to verify the fix works correctly.\"\\n\\nSince a fix has been implemented and the MCP server reloaded, use the n8n-mcp-tester agent to validate the fix works in real-world usage.\\n\\n\\n\\n\\nContext: Developer has added a new MCP tool for workflow validation\\nuser: \"I've implemented the new workflow validation tool\"\\nassistant: \"Implementation complete. Let me build and prepare for testing.\"\\n\\nassistant: \"Build successful. Please reload the MCP server to load the new tool.\"\\nuser: \"Done\"\\nassistant: \"Perfect! I'll now use the n8n-mcp-tester agent to test the new workflow validation tool.\"\\n\\nAfter implementing new MCP functionality and reloading the server, invoke n8n-mcp-tester to verify it works correctly.\\n\\n" +tools: "Glob, Grep, Read, WebFetch, WebSearch, ListMcpResourcesTool, ReadMcpResourceTool, Bash, mcp__context7__query-docs, mcp__context7__resolve-library-id, mcp__n8n-mcp-testing__get_node, mcp__n8n-mcp-testing__get_template, mcp__n8n-mcp-testing__n8n_autofix_workflow, mcp__n8n-mcp-testing__n8n_create_workflow, mcp__n8n-mcp-testing__n8n_delete_workflow, mcp__n8n-mcp-testing__n8n_deploy_template, mcp__n8n-mcp-testing__n8n_executions, mcp__n8n-mcp-testing__n8n_generate_workflow, mcp__n8n-mcp-testing__n8n_get_workflow, mcp__n8n-mcp-testing__n8n_health_check, mcp__n8n-mcp-testing__n8n_list_workflows, mcp__n8n-mcp-testing__n8n_manage_datatable, mcp__n8n-mcp-testing__n8n_test_workflow, mcp__n8n-mcp-testing__n8n_update_full_workflow, mcp__n8n-mcp-testing__n8n_update_partial_workflow, mcp__n8n-mcp-testing__n8n_validate_workflow, mcp__n8n-mcp-testing__n8n_workflow_versions, mcp__n8n-mcp-testing__search_nodes, mcp__n8n-mcp-testing__search_templates, mcp__n8n-mcp-testing__tools_documentation, mcp__n8n-mcp-testing__validate_node, mcp__n8n-mcp-testing__validate_workflow, mcp__plugin_postgres-best-practices_supabase__authenticate, mcp__supabase-telemetry__apply_migration, mcp__supabase-telemetry__create_branch, mcp__supabase-telemetry__delete_branch, mcp__supabase-telemetry__deploy_edge_function, mcp__supabase-telemetry__execute_sql, mcp__supabase-telemetry__generate_typescript_types, mcp__supabase-telemetry__get_advisors, mcp__supabase-telemetry__get_edge_function, mcp__supabase-telemetry__get_logs, mcp__supabase-telemetry__get_project_url, mcp__supabase-telemetry__get_publishable_keys, mcp__supabase-telemetry__list_branches, mcp__supabase-telemetry__list_edge_functions, mcp__supabase-telemetry__list_extensions, mcp__supabase-telemetry__list_migrations, mcp__supabase-telemetry__list_tables, mcp__supabase-telemetry__merge_branch, mcp__supabase-telemetry__rebase_branch, mcp__supabase-telemetry__reset_branch, mcp__supabase-telemetry__search_docs" model: sonnet --- - You are n8n-mcp-tester, a specialized testing agent for the n8n Model Context Protocol (MCP) server. You validate that MCP tools and functionality work correctly in real-world scenarios after fixes or new features are implemented. ## Your Core Responsibilities diff --git a/CHANGELOG.md b/CHANGELOG.md index a7109b9..2542061 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.45.1] - 2026-04-02 + +### Fixed + +- **Use stdio-wrapper.js as default bin entry point** — the previous entry point (`index.js`) wrote INFO-level logs to stdout, corrupting JSON-RPC MCP transport for stdio-mode users (Fixes #693, Related: #555, #628) +- **Preserve node credentials during full workflow updates** — `n8n_update_full_workflow` now carries forward existing credential references from the server when user-provided nodes omit them, preventing "missing credentials" errors on PUT (Fixes #689) + +### Changed + +- **Updated publish scripts** to use `stdio-wrapper.js` as the npm bin entry point, ensuring the fix persists across releases + +Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en + ## [2.45.0] - 2026-04-01 ### Changed diff --git a/package.json b/package.json index fc50379..190103e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-mcp", - "version": "2.45.0", + "version": "2.45.1", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -12,7 +12,7 @@ } }, "bin": { - "n8n-mcp": "./dist/mcp/index.js" + "n8n-mcp": "./dist/mcp/stdio-wrapper.js" }, "scripts": { "build": "tsc -p tsconfig.build.json", diff --git a/package.runtime.json b/package.runtime.json index 5f34d23..3c5b340 100644 --- a/package.runtime.json +++ b/package.runtime.json @@ -1,6 +1,6 @@ { "name": "n8n-mcp-runtime", - "version": "2.45.0", + "version": "2.45.1", "description": "n8n MCP Server Runtime Dependencies Only", "private": true, "dependencies": { diff --git a/scripts/publish-npm-quick.sh b/scripts/publish-npm-quick.sh index 6969722..bc80831 100755 --- a/scripts/publish-npm-quick.sh +++ b/scripts/publish-npm-quick.sh @@ -35,7 +35,7 @@ node -e " const pkg = require('./package.json'); pkg.name = 'n8n-mcp'; pkg.description = 'Integration between n8n workflow automation and Model Context Protocol (MCP)'; -pkg.bin = { 'n8n-mcp': './dist/mcp/index.js' }; +pkg.bin = { 'n8n-mcp': './dist/mcp/stdio-wrapper.js' }; pkg.repository = { type: 'git', url: 'git+https://github.com/czlonkowski/n8n-mcp.git' }; pkg.keywords = ['n8n', 'mcp', 'model-context-protocol', 'ai', 'workflow', 'automation']; pkg.author = 'Romuald Czlonkowski @ www.aiadvisors.pl/en'; diff --git a/scripts/publish-npm.sh b/scripts/publish-npm.sh index f1e2321..18d747b 100755 --- a/scripts/publish-npm.sh +++ b/scripts/publish-npm.sh @@ -68,7 +68,7 @@ pkg.exports = { import: './dist/index.js' } }; -pkg.bin = { 'n8n-mcp': './dist/mcp/index.js' }; +pkg.bin = { 'n8n-mcp': './dist/mcp/stdio-wrapper.js' }; pkg.repository = { type: 'git', url: 'git+https://github.com/czlonkowski/n8n-mcp.git' }; pkg.keywords = ['n8n', 'mcp', 'model-context-protocol', 'ai', 'workflow', 'automation']; pkg.author = 'Romuald Czlonkowski @ www.aiadvisors.pl/en'; diff --git a/src/mcp/handlers-n8n-manager.ts b/src/mcp/handlers-n8n-manager.ts index 5075e97..0fdbcbe 100644 --- a/src/mcp/handlers-n8n-manager.ts +++ b/src/mcp/handlers-n8n-manager.ts @@ -776,6 +776,28 @@ export async function handleUpdateWorkflow( const current = await client.getWorkflow(id); workflowBefore = JSON.parse(JSON.stringify(current)); + // Preserve credentials from current workflow for nodes that don't specify them. + // AI-generated node updates typically omit credential references because they + // aren't included in the context provided to the AI. Without this merge, the + // n8n API rejects the PUT with missing credentials. + 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 as any[]) { + 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; + } + } + } + } + // Create backup before modifying workflow (default: true) if (createBackup !== false) { try { diff --git a/tests/unit/mcp/handlers-n8n-manager.test.ts b/tests/unit/mcp/handlers-n8n-manager.test.ts index 54dbe0f..df38aa7 100644 --- a/tests/unit/mcp/handlers-n8n-manager.test.ts +++ b/tests/unit/mcp/handlers-n8n-manager.test.ts @@ -16,6 +16,12 @@ import { ExecutionStatus } from '@/types/n8n-api'; vi.mock('@/services/n8n-api-client'); vi.mock('@/services/workflow-validator'); vi.mock('@/database/node-repository'); +vi.mock('@/services/workflow-versioning-service', () => ({ + WorkflowVersioningService: vi.fn().mockImplementation(() => ({ + createBackup: vi.fn().mockResolvedValue({ versionId: 'v1', versionNumber: 1, pruned: 0 }), + getVersions: vi.fn().mockResolvedValue([]), + })), +})); vi.mock('@/config/n8n-api', () => ({ getN8nApiConfig: vi.fn() })); @@ -1343,4 +1349,142 @@ describe('handlers-n8n-manager', () => { expect(result.error).toMatch(/mode:\s*'preview'/); }); }); + + describe('handleUpdateWorkflow - credential preservation', () => { + function mockCurrentWorkflow(nodes: any[]): void { + const workflow = createTestWorkflow({ id: 'wf-1', active: false, nodes }); + mockApiClient.getWorkflow.mockResolvedValue(workflow); + mockApiClient.updateWorkflow.mockResolvedValue({ ...workflow, updatedAt: '2024-01-02' }); + } + + function getSentNodes(): any[] { + return mockApiClient.updateWorkflow.mock.calls[0][1].nodes; + } + + it('should preserve credentials from current workflow when update nodes omit them', async () => { + mockCurrentWorkflow([ + { + id: 'node-1', name: 'Postgres', type: 'n8n-nodes-base.postgres', + typeVersion: 2, position: [100, 100], + parameters: { operation: 'executeQuery', query: 'SELECT 1' }, + credentials: { postgresApi: { id: 'cred-123', name: 'My Postgres' } }, + }, + { + id: 'node-2', name: 'HTTP Request', type: 'n8n-nodes-base.httpRequest', + typeVersion: 4, position: [300, 100], + parameters: { url: 'https://example.com' }, + credentials: { httpBasicAuth: { id: 'cred-456', name: 'Basic Auth' } }, + }, + { + id: 'node-3', name: 'Set', type: 'n8n-nodes-base.set', + typeVersion: 3, position: [500, 100], parameters: {}, + }, + ]); + + await handlers.handleUpdateWorkflow( + { + id: 'wf-1', + nodes: [ + { + id: 'node-1', name: 'Postgres', type: 'n8n-nodes-base.postgres', + typeVersion: 2, position: [100, 100], + parameters: { operation: 'executeQuery', query: 'SELECT * FROM users' }, + }, + { + id: 'node-2', name: 'HTTP Request', type: 'n8n-nodes-base.httpRequest', + typeVersion: 4, position: [300, 100], + parameters: { url: 'https://example.com/v2' }, + }, + { + id: 'node-3', name: 'Set', type: 'n8n-nodes-base.set', + typeVersion: 3, position: [500, 100], parameters: { mode: 'manual' }, + }, + ], + connections: {}, + }, + mockRepository, + ); + + const sentNodes = getSentNodes(); + expect(sentNodes[0].credentials).toEqual({ postgresApi: { id: 'cred-123', name: 'My Postgres' } }); + expect(sentNodes[1].credentials).toEqual({ httpBasicAuth: { id: 'cred-456', name: 'Basic Auth' } }); + expect(sentNodes[2].credentials).toBeUndefined(); + }); + + it('should not overwrite user-provided credentials', async () => { + mockCurrentWorkflow([ + { + id: 'node-1', name: 'Postgres', type: 'n8n-nodes-base.postgres', + typeVersion: 2, position: [100, 100], parameters: {}, + credentials: { postgresApi: { id: 'cred-old', name: 'Old Postgres' } }, + }, + ]); + + await handlers.handleUpdateWorkflow( + { + id: 'wf-1', + nodes: [ + { + id: 'node-1', name: 'Postgres', type: 'n8n-nodes-base.postgres', + typeVersion: 2, position: [100, 100], parameters: {}, + credentials: { postgresApi: { id: 'cred-new', name: 'New Postgres' } }, + }, + ], + connections: {}, + }, + mockRepository, + ); + + const sentNodes = getSentNodes(); + expect(sentNodes[0].credentials).toEqual({ postgresApi: { id: 'cred-new', name: 'New Postgres' } }); + }); + + it('should match nodes by name when ids differ', async () => { + mockCurrentWorkflow([ + { + id: 'server-id-1', name: 'Gmail', type: 'n8n-nodes-base.gmail', + typeVersion: 2, position: [100, 100], parameters: {}, + credentials: { gmailOAuth2: { id: 'cred-gmail', name: 'Gmail' } }, + }, + ]); + + await handlers.handleUpdateWorkflow( + { + id: 'wf-1', + nodes: [ + { + id: 'client-id-different', name: 'Gmail', type: 'n8n-nodes-base.gmail', + typeVersion: 2, position: [100, 100], + parameters: { resource: 'message' }, + }, + ], + connections: {}, + }, + mockRepository, + ); + + const sentNodes = getSentNodes(); + expect(sentNodes[0].credentials).toEqual({ gmailOAuth2: { id: 'cred-gmail', name: 'Gmail' } }); + }); + + it('should treat empty credentials object as missing and carry forward', async () => { + mockCurrentWorkflow([ + { id: 'node-1', name: 'Postgres', type: 'n8n-nodes-base.postgres', typeVersion: 2, position: [100, 100], parameters: {}, credentials: { postgresApi: { id: 'cred-123', name: 'My Postgres' } } }, + ]); + + await handlers.handleUpdateWorkflow( + { + id: 'wf-1', + nodes: [ + { id: 'node-1', name: 'Postgres', type: 'n8n-nodes-base.postgres', typeVersion: 2, position: [100, 100], parameters: {}, credentials: {} }, + ], + connections: {}, + }, + mockRepository, + ); + + const sentNodes = getSentNodes(); + expect(sentNodes[0].credentials).toEqual({ postgresApi: { id: 'cred-123', name: 'My Postgres' } }); + }); + }); }); \ No newline at end of file