Compare commits

..

3 Commits

Author SHA1 Message Date
czlonkowski
22635b708b 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>
2026-03-27 18:45:03 +01:00
czlonkowski
9133f5e483 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>
2026-03-27 17:53:42 +01:00
Romuald Członkowski
fb2d306dc3 fix: intercept stdout writes to prevent JSON-RPC corruption in stdio mode (#673)
* fix: intercept process.stdout.write to prevent JSON-RPC corruption in stdio mode (#628, #627, #567)

Console method suppression alone was insufficient — native modules, n8n packages,
and third-party code can call process.stdout.write() directly, leaking debug output
(refCount, dbPath, clientVersion, protocolVersion, etc.) into the MCP JSON-RPC stream.

Added stdout write interceptor that only allows JSON-RPC messages through (objects
containing "jsonrpc" field). All other writes are redirected to stderr. This fixes
the flood of "Unexpected token is not valid JSON" warnings on every new Claude
Desktop chat.

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

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

* ci: add Docker Hub login to fix buildx bootstrap rate limiting

GitHub-hosted runners hit Docker Hub anonymous pull limits when
setup-buildx-action pulls moby/buildkit. Add docker/login-action
for Docker Hub before setup-buildx-action in all 4 workflows:
docker-build.yml, docker-build-fast.yml, docker-build-n8n.yml, release.yml.

Uses DOCKERHUB_USERNAME and DOCKERHUB_TOKEN repository secrets.

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 17:26:43 +01:00
14 changed files with 107 additions and 28 deletions

View File

@@ -29,9 +29,15 @@ jobs:
with:
lfs: true
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3

View File

@@ -55,6 +55,12 @@ jobs:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

View File

@@ -71,13 +71,19 @@ jobs:
"
echo "✅ Synced package.runtime.json to version $VERSION"
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
@@ -85,7 +91,7 @@ jobs:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
@@ -173,13 +179,19 @@ jobs:
"
echo "✅ Synced package.runtime.json to version $VERSION"
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3

View File

@@ -441,12 +441,18 @@ jobs:
"
echo "✅ Synced package.runtime.json to version $VERSION"
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:

View File

@@ -7,6 +7,26 @@ 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
- **MCP initialization floods Claude Desktop with JSON parse errors** (Issues #628, #627, #567): Intercept `process.stdout.write` in stdio mode to redirect non-JSON-RPC output to stderr. Console method suppression alone was insufficient — native modules (better-sqlite3), n8n packages, and third-party code can call `process.stdout.write()` directly, corrupting the JSON-RPC stream. Only writes containing valid JSON-RPC messages (`{"jsonrpc":...}`) are now allowed through stdout; everything else is redirected to stderr. This fixes the flood of "Unexpected token is not valid JSON" warnings on every new chat in Claude Desktop, including leaked `refCount`, `dbPath`, `clientVersion`, `protocolVersion`, and other debug strings.
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
## [2.41.1] - 2026-03-27
### Fixed

4
package-lock.json generated
View File

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

View File

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

View File

@@ -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;

View File

@@ -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; }
}

View File

@@ -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(),

View File

@@ -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();

View File

@@ -39,6 +39,25 @@ console.clear = () => {};
console.count = () => {};
console.countReset = () => {};
// CRITICAL: Intercept process.stdout.write to prevent non-JSON-RPC output (#628, #627, #567)
// Console suppression alone is insufficient — native modules (better-sqlite3), n8n packages,
// and third-party code can call process.stdout.write() directly, corrupting the JSON-RPC stream.
// Only allow writes that look like JSON-RPC messages; redirect everything else to stderr.
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
const stderrWrite = process.stderr.write.bind(process.stderr);
process.stdout.write = function (chunk: any, encodingOrCallback?: any, callback?: any): boolean {
const str = typeof chunk === 'string' ? chunk : chunk.toString();
// JSON-RPC messages are JSON objects with "jsonrpc" field — let those through
// The MCP SDK sends one JSON object per write call
const trimmed = str.trimStart();
if (trimmed.startsWith('{') && trimmed.includes('"jsonrpc"')) {
return originalStdoutWrite(chunk, encodingOrCallback, callback);
}
// Redirect everything else to stderr so it doesn't corrupt the protocol
return stderrWrite(chunk, encodingOrCallback, callback);
} as typeof process.stdout.write;
// Import and run the server AFTER suppressing output
import { N8NDocumentationMCPServer } from './server';

View File

@@ -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)
}
};

View File

@@ -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<typeof import('@/mcp/handlers-n8n-manager')>();
return {
...actual,
getN8nApiClient: vi.fn(),
};
});
// Import mocked modules
import { getN8nApiClient } from '@/mcp/handlers-n8n-manager';