mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-28 13:13:08 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e4a9d520d | ||
|
|
fb2d306dc3 |
8
.github/workflows/docker-build-fast.yml
vendored
8
.github/workflows/docker-build-fast.yml
vendored
@@ -29,9 +29,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
lfs: true
|
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
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
- name: Log in to GitHub Container Registry
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
|
|||||||
6
.github/workflows/docker-build-n8n.yml
vendored
6
.github/workflows/docker-build-n8n.yml
vendored
@@ -55,6 +55,12 @@ jobs:
|
|||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
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
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
|||||||
22
.github/workflows/docker-build.yml
vendored
22
.github/workflows/docker-build.yml
vendored
@@ -71,13 +71,19 @@ jobs:
|
|||||||
"
|
"
|
||||||
echo "✅ Synced package.runtime.json to version $VERSION"
|
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
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
- name: Log in to GitHub Container Registry
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
@@ -85,7 +91,7 @@ jobs:
|
|||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Extract metadata
|
- name: Extract metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
@@ -173,13 +179,19 @@ jobs:
|
|||||||
"
|
"
|
||||||
echo "✅ Synced package.runtime.json to version $VERSION"
|
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
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
- name: Log in to GitHub Container Registry
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
|
|||||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -441,12 +441,18 @@ jobs:
|
|||||||
"
|
"
|
||||||
echo "✅ Synced package.runtime.json to version $VERSION"
|
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
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
- name: Log in to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
20
CHANGELOG.md
20
CHANGELOG.md
@@ -7,6 +7,26 @@ 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
|
||||||
|
|
||||||
|
### 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
|
## [2.41.1] - 2026-03-27
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "n8n-mcp",
|
"name": "n8n-mcp",
|
||||||
"version": "2.41.1",
|
"version": "2.41.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "n8n-mcp",
|
"name": "n8n-mcp",
|
||||||
"version": "2.41.1",
|
"version": "2.41.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "1.28.0",
|
"@modelcontextprotocol/sdk": "1.28.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "n8n-mcp",
|
"name": "n8n-mcp",
|
||||||
"version": "2.41.1",
|
"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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -39,6 +39,25 @@ console.clear = () => {};
|
|||||||
console.count = () => {};
|
console.count = () => {};
|
||||||
console.countReset = () => {};
|
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 and run the server AFTER suppressing output
|
||||||
import { N8NDocumentationMCPServer } from './server';
|
import { N8NDocumentationMCPServer } from './server';
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user