Compare commits

...

1 Commits

Author SHA1 Message Date
Romuald Członkowski
6e4a9d520d fix: raise session timeout default, fix VS Code MCP compatibility (#674)
* fix: raise session timeout default and fix VS Code MCP compatibility (#626, #600, #611)

- #626: Raise SESSION_TIMEOUT_MINUTES default from 5 to 30 minutes.
  Complex editing sessions easily exceed 5 min between LLM calls.

- #600: Add z.preprocess(tryParseJson, ...) to operations parameter
  in n8n_update_partial_workflow. VS Code extension sends arrays as
  JSON strings.

- #611: Strip undefined values from tool args via JSON round-trip
  before Zod validation. VS Code sends explicit undefined which
  Zod's .optional() rejects.

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: deduplicate tryParseJson — export from handlers-n8n-manager

tryParseJson was duplicated in handlers-workflow-diff.ts. Now imported
from handlers-n8n-manager.ts where it was already defined. Updated
test mock to use importOriginal so the real function is available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 18:56:30 +01:00
9 changed files with 42 additions and 20 deletions

View File

@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [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 ## [2.41.2] - 2026-03-27
### Fixed ### Fixed

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "n8n-mcp", "name": "n8n-mcp",
"version": "2.41.2", "version": "2.41.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "n8n-mcp", "name": "n8n-mcp",
"version": "2.41.2", "version": "2.41.3",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "1.28.0", "@modelcontextprotocol/sdk": "1.28.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "n8n-mcp", "name": "n8n-mcp",
"version": "2.41.2", "version": "2.41.3",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@@ -107,11 +107,10 @@ export class SingleSessionHTTPServer {
private session: Session | null = null; // Keep for SSE compatibility private session: Session | null = null; // Keep for SSE compatibility
private consoleManager = new ConsoleManager(); private consoleManager = new ConsoleManager();
private expressServer: any; private expressServer: any;
// Session timeout reduced from 30 minutes to 5 minutes for faster cleanup // Session timeout — configurable via SESSION_TIMEOUT_MINUTES environment variable
// Configurable via SESSION_TIMEOUT_MINUTES environment variable // Default 30 minutes: balances memory cleanup with real editing sessions (#626)
// This prevents memory buildup from stale sessions
private sessionTimeout = parseInt( private sessionTimeout = parseInt(
process.env.SESSION_TIMEOUT_MINUTES || '5', 10 process.env.SESSION_TIMEOUT_MINUTES || '30', 10
) * 60 * 1000; ) * 60 * 1000;
private authToken: string | null = null; private authToken: string | null = null;
private cleanupTimer: NodeJS.Timeout | null = null; private cleanupTimer: NodeJS.Timeout | null = null;

View File

@@ -2731,7 +2731,7 @@ const updateTableSchema = tableIdSchema.extend({
// MCP transports may serialize JSON objects/arrays as strings. // 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. // 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; if (typeof val !== 'string') return val;
try { return JSON.parse(val); } catch { return val; } try { return JSON.parse(val); } catch { return val; }
} }

View File

@@ -7,7 +7,7 @@ import { z } from 'zod';
import { McpToolResponse } from '../types/n8n-api'; import { McpToolResponse } from '../types/n8n-api';
import { WorkflowDiffRequest, WorkflowDiffOperation, WorkflowDiffValidationError } from '../types/workflow-diff'; import { WorkflowDiffRequest, WorkflowDiffOperation, WorkflowDiffValidationError } from '../types/workflow-diff';
import { WorkflowDiffEngine } from '../services/workflow-diff-engine'; 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 { N8nApiError, getUserFriendlyErrorMessage } from '../utils/n8n-errors';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
import { InstanceContext } from '../types/instance-context'; import { InstanceContext } from '../types/instance-context';
@@ -39,7 +39,7 @@ const NODE_TARGETING_OPERATIONS = new Set([
// Zod schema for the diff request // Zod schema for the diff request
const workflowDiffSchema = z.object({ const workflowDiffSchema = z.object({
id: z.string(), id: z.string(),
operations: z.array(z.object({ operations: z.preprocess(tryParseJson, z.array(z.object({
type: z.string(), type: z.string(),
description: z.string().optional(), description: z.string().optional(),
// Node operations // Node operations
@@ -87,7 +87,7 @@ const workflowDiffSchema = z.object({
} }
} }
return op; return op;
})), }))),
validateOnly: z.boolean().optional(), validateOnly: z.boolean().optional(),
continueOnError: z.boolean().optional(), continueOnError: z.boolean().optional(),
createBackup: z.boolean().optional(), createBackup: z.boolean().optional(),

View File

@@ -748,6 +748,13 @@ export class N8NDocumentationMCPServer {
// tool's inputSchema as the source of truth. // tool's inputSchema as the source of truth.
processedArgs = this.coerceStringifiedJsonParams(name, processedArgs); 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 { try {
logger.debug(`Executing tool: ${name}`, { args: processedArgs }); logger.debug(`Executing tool: ${name}`, { args: processedArgs });
const startTime = Date.now(); const startTime = Date.now();

View File

@@ -334,14 +334,14 @@ describe('HTTP Server Session Management', () => {
server = new SingleSessionHTTPServer(); server = new SingleSessionHTTPServer();
// Mock expired sessions // 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 = { const mockSessionMetadata = {
'session-1': { '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) createdAt: new Date(Date.now() - 60 * 60 * 1000)
}, },
'session-2': { '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) createdAt: new Date(Date.now() - 20 * 60 * 1000)
} }
}; };
@@ -517,15 +517,15 @@ describe('HTTP Server Session Management', () => {
it('should get session metrics correctly', async () => { it('should get session metrics correctly', async () => {
server = new SingleSessionHTTPServer(); 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(); const now = Date.now();
(server as any).sessionMetadata = { (server as any).sessionMetadata = {
'active-session': { '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) createdAt: new Date(now - 20 * 60 * 1000)
}, },
'expired-session': { '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) createdAt: new Date(now - 60 * 60 * 1000)
} }
}; };

View File

@@ -17,9 +17,13 @@ vi.mock('@/services/workflow-diff-engine');
vi.mock('@/services/n8n-api-client'); vi.mock('@/services/n8n-api-client');
vi.mock('@/config/n8n-api'); vi.mock('@/config/n8n-api');
vi.mock('@/utils/logger'); vi.mock('@/utils/logger');
vi.mock('@/mcp/handlers-n8n-manager', () => ({ vi.mock('@/mcp/handlers-n8n-manager', async (importOriginal) => {
getN8nApiClient: vi.fn(), const actual = await importOriginal<typeof import('@/mcp/handlers-n8n-manager')>();
})); return {
...actual,
getN8nApiClient: vi.fn(),
};
});
// Import mocked modules // Import mocked modules
import { getN8nApiClient } from '@/mcp/handlers-n8n-manager'; import { getN8nApiClient } from '@/mcp/handlers-n8n-manager';