diff --git a/CHANGELOG.md b/CHANGELOG.md index 4472fcd..04ec782 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.41.3] - 2026-03-27 + +### Fixed + +- **Session timeout default too low** (Issue #626): Raised `SESSION_TIMEOUT_MINUTES` default from 5 to 30 minutes. The 5-minute default caused sessions to expire mid-operation during complex multi-step workflows (validate → get structure → patch → validate), forcing users to retry. Configurable via environment variable. + +- **Operations array received as string from VS Code** (Issue #600): Added `z.preprocess` JSON string parsing to the `operations` parameter in `n8n_update_partial_workflow`. The VS Code MCP extension serializes arrays as JSON strings — the Zod schema now transparently parses them before validation. + +- **`undefined` values rejected in MCP tool calls from VS Code** (Issue #611): Strip explicit `undefined` values from tool arguments before Zod validation. VS Code sends `undefined` as a value which Zod's `.optional()` rejects (it expects the field to be missing, not present-but-undefined). + +Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en + ## [2.41.2] - 2026-03-27 ### Fixed diff --git a/package-lock.json b/package-lock.json index 8edc6b6..a5cb4b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "n8n-mcp", - "version": "2.41.2", + "version": "2.41.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "n8n-mcp", - "version": "2.41.2", + "version": "2.41.3", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "1.28.0", diff --git a/package.json b/package.json index 6489cc5..bb15947 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-mcp", - "version": "2.41.2", + "version": "2.41.3", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/http-server-single-session.ts b/src/http-server-single-session.ts index effd3bb..c3ed75d 100644 --- a/src/http-server-single-session.ts +++ b/src/http-server-single-session.ts @@ -107,11 +107,10 @@ export class SingleSessionHTTPServer { private session: Session | null = null; // Keep for SSE compatibility private consoleManager = new ConsoleManager(); private expressServer: any; - // Session timeout reduced from 30 minutes to 5 minutes for faster cleanup - // Configurable via SESSION_TIMEOUT_MINUTES environment variable - // This prevents memory buildup from stale sessions + // Session timeout — configurable via SESSION_TIMEOUT_MINUTES environment variable + // Default 30 minutes: balances memory cleanup with real editing sessions (#626) private sessionTimeout = parseInt( - process.env.SESSION_TIMEOUT_MINUTES || '5', 10 + process.env.SESSION_TIMEOUT_MINUTES || '30', 10 ) * 60 * 1000; private authToken: string | null = null; private cleanupTimer: NodeJS.Timeout | null = null; diff --git a/src/mcp/handlers-n8n-manager.ts b/src/mcp/handlers-n8n-manager.ts index b0761f1..5075e97 100644 --- a/src/mcp/handlers-n8n-manager.ts +++ b/src/mcp/handlers-n8n-manager.ts @@ -2731,7 +2731,7 @@ const updateTableSchema = tableIdSchema.extend({ // MCP transports may serialize JSON objects/arrays as strings. // Parse them back, but return the original value on failure so Zod reports a proper type error. -function tryParseJson(val: unknown): unknown { +export function tryParseJson(val: unknown): unknown { if (typeof val !== 'string') return val; try { return JSON.parse(val); } catch { return val; } } diff --git a/src/mcp/handlers-workflow-diff.ts b/src/mcp/handlers-workflow-diff.ts index 468531f..d5a8dc6 100644 --- a/src/mcp/handlers-workflow-diff.ts +++ b/src/mcp/handlers-workflow-diff.ts @@ -7,7 +7,7 @@ import { z } from 'zod'; import { McpToolResponse } from '../types/n8n-api'; import { WorkflowDiffRequest, WorkflowDiffOperation, WorkflowDiffValidationError } from '../types/workflow-diff'; import { WorkflowDiffEngine } from '../services/workflow-diff-engine'; -import { getN8nApiClient } from './handlers-n8n-manager'; +import { getN8nApiClient, tryParseJson } from './handlers-n8n-manager'; import { N8nApiError, getUserFriendlyErrorMessage } from '../utils/n8n-errors'; import { logger } from '../utils/logger'; import { InstanceContext } from '../types/instance-context'; @@ -39,7 +39,7 @@ const NODE_TARGETING_OPERATIONS = new Set([ // Zod schema for the diff request const workflowDiffSchema = z.object({ id: z.string(), - operations: z.array(z.object({ + operations: z.preprocess(tryParseJson, z.array(z.object({ type: z.string(), description: z.string().optional(), // Node operations @@ -87,7 +87,7 @@ const workflowDiffSchema = z.object({ } } return op; - })), + }))), validateOnly: z.boolean().optional(), continueOnError: z.boolean().optional(), createBackup: z.boolean().optional(), diff --git a/src/mcp/server.ts b/src/mcp/server.ts index e3a5223..a098153 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -748,6 +748,13 @@ export class N8NDocumentationMCPServer { // tool's inputSchema as the source of truth. processedArgs = this.coerceStringifiedJsonParams(name, processedArgs); + // Strip undefined values from args (#611) — VS Code extension sends + // explicit undefined values which Zod's .optional() rejects. + // Removing them makes Zod treat them as missing (which .optional() allows). + if (processedArgs) { + processedArgs = JSON.parse(JSON.stringify(processedArgs)); + } + try { logger.debug(`Executing tool: ${name}`, { args: processedArgs }); const startTime = Date.now(); diff --git a/tests/unit/http-server-session-management.test.ts b/tests/unit/http-server-session-management.test.ts index af4fb0c..d28828b 100644 --- a/tests/unit/http-server-session-management.test.ts +++ b/tests/unit/http-server-session-management.test.ts @@ -334,14 +334,14 @@ describe('HTTP Server Session Management', () => { server = new SingleSessionHTTPServer(); // Mock expired sessions - // Note: Default session timeout is 5 minutes (configurable via SESSION_TIMEOUT_MINUTES) + // Note: Default session timeout is 30 minutes (configurable via SESSION_TIMEOUT_MINUTES) const mockSessionMetadata = { 'session-1': { - lastAccess: new Date(Date.now() - 10 * 60 * 1000), // 10 minutes ago (expired with 5 min timeout) + lastAccess: new Date(Date.now() - 45 * 60 * 1000), // 45 minutes ago (expired with 30 min timeout) createdAt: new Date(Date.now() - 60 * 60 * 1000) }, 'session-2': { - lastAccess: new Date(Date.now() - 2 * 60 * 1000), // 2 minutes ago (not expired with 5 min timeout) + lastAccess: new Date(Date.now() - 10 * 60 * 1000), // 10 minutes ago (not expired with 30 min timeout) createdAt: new Date(Date.now() - 20 * 60 * 1000) } }; @@ -517,15 +517,15 @@ describe('HTTP Server Session Management', () => { it('should get session metrics correctly', async () => { server = new SingleSessionHTTPServer(); - // Note: Default session timeout is 5 minutes (configurable via SESSION_TIMEOUT_MINUTES) + // Note: Default session timeout is 30 minutes (configurable via SESSION_TIMEOUT_MINUTES) const now = Date.now(); (server as any).sessionMetadata = { 'active-session': { - lastAccess: new Date(now - 2 * 60 * 1000), // 2 minutes ago (not expired with 5 min timeout) + lastAccess: new Date(now - 10 * 60 * 1000), // 10 minutes ago (not expired with 30 min timeout) createdAt: new Date(now - 20 * 60 * 1000) }, 'expired-session': { - lastAccess: new Date(now - 10 * 60 * 1000), // 10 minutes ago (expired with 5 min timeout) + lastAccess: new Date(now - 45 * 60 * 1000), // 45 minutes ago (expired with 30 min timeout) createdAt: new Date(now - 60 * 60 * 1000) } }; diff --git a/tests/unit/mcp/handlers-workflow-diff.test.ts b/tests/unit/mcp/handlers-workflow-diff.test.ts index 11b844c..74faea7 100644 --- a/tests/unit/mcp/handlers-workflow-diff.test.ts +++ b/tests/unit/mcp/handlers-workflow-diff.test.ts @@ -17,9 +17,13 @@ vi.mock('@/services/workflow-diff-engine'); vi.mock('@/services/n8n-api-client'); vi.mock('@/config/n8n-api'); vi.mock('@/utils/logger'); -vi.mock('@/mcp/handlers-n8n-manager', () => ({ - getN8nApiClient: vi.fn(), -})); +vi.mock('@/mcp/handlers-n8n-manager', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getN8nApiClient: vi.fn(), + }; +}); // Import mocked modules import { getN8nApiClient } from '@/mcp/handlers-n8n-manager';