From 1750fb4acfac6e174ad5a95534c01368050337cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Romuald=20Cz=C5=82onkowski?= <56956555+czlonkowski@users.noreply.github.com> Date: Sat, 4 Apr 2026 13:09:03 +0200 Subject: [PATCH] fix: credential get fallback, update type field, and code refinements (v2.47.1) (#703) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /credentials/:id is not in the n8n public API; fall back to list + filter, catching both 403 and 405 responses - Forward optional `type` field in credential update for n8n versions that require it in PATCH payload - Strip `data` field from create/update credential responses - Code simplification: typed error handling, consolidated booleans, null-coalescing in version fetch Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en Co-authored-by: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 10 ++++++++++ package-lock.json | 4 ++-- package.json | 2 +- src/mcp/handlers-n8n-manager.ts | 22 ++++++++++++++++++++-- src/services/n8n-api-client.ts | 8 +------- src/services/workflow-security-scanner.ts | 7 ++++--- 6 files changed, 38 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddeba10..47bda49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.47.1] - 2026-04-04 + +### Fixed + +- **Credential get fallback** — `n8n_manage_credentials({action: "get"})` now falls back to list + filter when `GET /credentials/:id` returns 403 Forbidden or 405 Method Not Allowed, since this endpoint is not in the n8n public API +- **Credential update accepts `type` field** — `n8n_manage_credentials({action: "update"})` now forwards the optional `type` field to the n8n API, which some n8n versions require in the PATCH payload +- **Credential response stripping** — `create` and `update` handlers now strip the `data` field from responses (defense-in-depth, matching the `get` handler pattern) + +Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en + ## [2.47.0] - 2026-04-04 ### Added diff --git a/package-lock.json b/package-lock.json index 899650c..f1fbaba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "n8n-mcp", - "version": "2.47.0", + "version": "2.47.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "n8n-mcp", - "version": "2.47.0", + "version": "2.47.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "1.28.0", diff --git a/package.json b/package.json index e4bca16..e0808c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-mcp", - "version": "2.47.0", + "version": "2.47.1", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/mcp/handlers-n8n-manager.ts b/src/mcp/handlers-n8n-manager.ts index f140d3c..f7f565c 100644 --- a/src/mcp/handlers-n8n-manager.ts +++ b/src/mcp/handlers-n8n-manager.ts @@ -2994,6 +2994,7 @@ const createCredentialSchema = z.object({ const updateCredentialSchema = z.object({ id: z.string({ required_error: 'Credential ID is required' }), name: z.string().optional(), + type: z.string().optional(), data: z.record(z.any()).optional(), }); @@ -3027,7 +3028,23 @@ export async function handleGetCredential(args: unknown, context?: InstanceConte try { const client = ensureApiConfigured(context); const { id } = getCredentialSchema.parse(args); - const credential = await client.getCredential(id); + let credential; + try { + credential = await client.getCredential(id); + } catch (getError: unknown) { + // GET /credentials/:id is not in the n8n public API — fall back to list + filter + const status = (getError as { statusCode?: number }).statusCode; + const msg = (getError as Error).message ?? ''; + const isUnsupported = status === 405 || status === 403 || msg.includes('not allowed'); + if (!isUnsupported) { + throw getError; + } + const list = await client.listCredentials(); + credential = list.data.find((c) => c.id === id); + if (!credential) { + return { success: false, error: `Credential ${id} not found` }; + } + } // Strip sensitive data field — defense in depth against future n8n versions returning decrypted values const { data: _sensitiveData, ...safeCred } = credential; return { @@ -3059,10 +3076,11 @@ export async function handleCreateCredential(args: unknown, context?: InstanceCo export async function handleUpdateCredential(args: unknown, context?: InstanceContext): Promise { try { const client = ensureApiConfigured(context); - const { id, name, data } = updateCredentialSchema.parse(args); + const { id, name, type, data } = updateCredentialSchema.parse(args); logger.info(`Updating credential: id="${id}"${name ? `, name="${name}"` : ''}`); const updatePayload: Record = {}; if (name !== undefined) updatePayload.name = name; + if (type !== undefined) updatePayload.type = type; if (data !== undefined) updatePayload.data = data; const credential = await client.updateCredential(id, updatePayload); const { data: _sensitiveData, ...safeCred } = credential; diff --git a/src/services/n8n-api-client.ts b/src/services/n8n-api-client.ts index d4c8e0a..ebc126d 100644 --- a/src/services/n8n-api-client.ts +++ b/src/services/n8n-api-client.ts @@ -135,13 +135,7 @@ export class N8nApiClient { * Internal method to fetch version once */ private async fetchVersionOnce(): Promise { - // Check if already cached globally - let version = getCachedVersion(this.baseUrl); - if (!version) { - // Fetch from server - version = await fetchN8nVersion(this.baseUrl); - } - return version; + return getCachedVersion(this.baseUrl) ?? await fetchN8nVersion(this.baseUrl); } /** diff --git a/src/services/workflow-security-scanner.ts b/src/services/workflow-security-scanner.ts index c56977c..43815fd 100644 --- a/src/services/workflow-security-scanner.ts +++ b/src/services/workflow-security-scanner.ts @@ -248,10 +248,11 @@ function checkDataRetentionSettings(workflow: WorkflowInput): AuditFinding[] { return []; } - const savesAllErrors = settings.saveDataErrorExecution === 'all'; - const savesAllSuccess = settings.saveDataSuccessExecution === 'all'; + const savesAllData = + settings.saveDataErrorExecution === 'all' && + settings.saveDataSuccessExecution === 'all'; - if (!savesAllErrors || !savesAllSuccess) { + if (!savesAllData) { return []; }