Compare commits

..

2 Commits

Author SHA1 Message Date
czlonkowski
3365878d03 fix: update documentation-generator tests for max_completion_tokens
- Updated test assertions from max_tokens to max_completion_tokens
- Updated testConnection token limit expectation from 10 to 200
- Added temperature to test config to match new configurable behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 08:41:17 +08:00
czlonkowski
07f8be8995 chore: update n8n to 2.8.3 and bump version to 2.35.3
- Updated n8n from 2.6.3 to 2.8.3
- Updated n8n-core from 2.6.1 to 2.8.1
- Updated n8n-workflow from 2.6.0 to 2.8.0
- Updated @n8n/n8n-nodes-langchain from 2.6.2 to 2.8.1
- Fixed node loader to bypass restricted package.json exports in
  @n8n/n8n-nodes-langchain >=2.9.0 (resolves via absolute paths)
- Fixed community doc generator for cloud LLMs: added API key env var
  support, switched to max_completion_tokens, auto-omit temperature
- Rebuilt node database with 1,236 nodes (673 n8n-nodes-base,
  133 @n8n/n8n-nodes-langchain, 430 community)
- Refreshed community nodes (361 verified + 69 npm) with 424 AI summaries
- Updated README badge with new n8n version and node counts
- Updated CHANGELOG with dependency changes

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

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 07:23:42 +08:00
8 changed files with 8 additions and 573 deletions

View File

@@ -7,32 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [2.35.5] - 2026-02-22
### Fixed
- **Comprehensive parameter type coercion for Claude Desktop / Claude.ai** (Issue #605): Expanded the v2.35.4 fix to handle ALL type mismatches, not just stringified objects/arrays. Testing revealed 6/9 tools still failing in Claude Desktop after the initial fix.
- Extended `coerceStringifiedJsonParams()` to coerce every schema type: `string→number`, `string→boolean`, `number→string`, `boolean→string` (in addition to existing `string→object` and `string→array`)
- Added top-level safeguard to parse the entire `args` object if it arrives as a JSON string
- Added `[Diagnostic]` section to error responses showing received argument types, enabling users to report exactly what their MCP client sends
- Added 9 new unit tests (24 total) covering number, boolean, and number-to-string coercion
Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en
## [2.35.4] - 2026-02-20
### Fixed
- **Defensive JSON.parse for stringified object/array parameters** (Issue #605): Claude Desktop 1.1.3189 serializes JSON object/array MCP parameters as strings, causing ZodError failures for ~60% of tools that accept nested parameters
- Added schema-driven `coerceStringifiedJsonParams()` in the central `CallToolRequestSchema` handler
- Automatically detects string values where the tool's `inputSchema` expects `object` or `array`, and parses them back
- Safe: prefix check before parsing, type verification after, try/catch preserves original on failure
- No-op for correct clients: native objects pass through unchanged
- Affects 9 tools with object/array params: `validate_node`, `validate_workflow`, `n8n_create_workflow`, `n8n_update_full_workflow`, `n8n_update_partial_workflow`, `n8n_validate_workflow`, `n8n_autofix_workflow`, `n8n_test_workflow`, `n8n_executions`
- Added 15 unit tests covering coercion, no-op, safety, and end-to-end scenarios
Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en
## [2.35.3] - 2026-02-19
### Changed

View File

@@ -30,7 +30,6 @@ export declare class N8NDocumentationMCPServer {
private validateToolParams;
private validateToolParamsBasic;
private validateExtractedArgs;
private coerceStringifiedJsonParams;
private listNodes;
private getNodeInfo;
private searchNodes;

View File

@@ -1 +1 @@
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AA0CA,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAmGnE,qBAAa,yBAAyB;IACpC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,EAAE,CAAgC;IAC1C,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,eAAe,CAAC,CAAkB;IAC1C,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,qBAAqB,CAAsB;IACnD,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,kBAAkB,CAA4B;IACtD,OAAO,CAAC,iBAAiB,CAAkB;IAC3C,OAAO,CAAC,aAAa,CAAoC;IACzD,OAAO,CAAC,UAAU,CAAkB;gBAExB,eAAe,CAAC,EAAE,eAAe,EAAE,WAAW,CAAC,EAAE,gBAAgB;IAuGvE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YA+Cd,kBAAkB;YAiDlB,wBAAwB;IA0BtC,OAAO,CAAC,kBAAkB;YA6CZ,iBAAiB;IAa/B,OAAO,CAAC,eAAe,CAAkB;YAE3B,sBAAsB;IAgDpC,OAAO,CAAC,gBAAgB;IAqCxB,OAAO,CAAC,aAAa;IA0XrB,OAAO,CAAC,wBAAwB;IAoFhC,OAAO,CAAC,kBAAkB;IAqE1B,OAAO,CAAC,uBAAuB;IAwB/B,OAAO,CAAC,qBAAqB;IAiF7B,OAAO,CAAC,2BAA2B;YA0UrB,SAAS;YA2DT,WAAW;YAkFX,WAAW;YA0CX,cAAc;YA8Md,gBAAgB;IAqD9B,OAAO,CAAC,mBAAmB;IAwE3B,OAAO,CAAC,eAAe;YAsBT,eAAe;IA2L7B,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,uBAAuB;IA0D/B,OAAO,CAAC,iBAAiB;YAqFX,WAAW;YAgCX,oBAAoB;IAuFlC,OAAO,CAAC,aAAa;YAQP,qBAAqB;YAwDrB,iBAAiB;YAiKjB,OAAO;YAgDP,cAAc;YAwFd,iBAAiB;IAqC/B,OAAO,CAAC,iBAAiB;IA0BzB,OAAO,CAAC,iBAAiB;IA0BzB,OAAO,CAAC,eAAe;IAwCvB,OAAO,CAAC,kBAAkB;IAiC1B,OAAO,CAAC,aAAa;IAoCrB,OAAO,CAAC,0BAA0B;IAgClC,OAAO,CAAC,4BAA4B;YAKtB,oBAAoB;IAsDlC,OAAO,CAAC,gBAAgB;YAiBV,SAAS;YA6CT,kBAAkB;YAqElB,uBAAuB;YAsDvB,iBAAiB;IAqE/B,OAAO,CAAC,qBAAqB;IA8C7B,OAAO,CAAC,uBAAuB;IA4D/B,OAAO,CAAC,wBAAwB;IAkChC,OAAO,CAAC,iBAAiB;YAoDX,mBAAmB;YAoEnB,qBAAqB;IAS7B,OAAO,CAAC,SAAS,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;YAS9B,aAAa;YAcb,iBAAiB;YAoBjB,WAAW;YAwBX,eAAe;YAqBf,mBAAmB;YAwBnB,yBAAyB;IA4CvC,OAAO,CAAC,kBAAkB;YAiBZ,gBAAgB;YA6HhB,2BAA2B;YAiE3B,2BAA2B;IAyEnC,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IA0BpB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAgEhC"}
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AA0CA,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAmGnE,qBAAa,yBAAyB;IACpC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,EAAE,CAAgC;IAC1C,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,eAAe,CAAC,CAAkB;IAC1C,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,qBAAqB,CAAsB;IACnD,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,kBAAkB,CAA4B;IACtD,OAAO,CAAC,iBAAiB,CAAkB;IAC3C,OAAO,CAAC,aAAa,CAAoC;IACzD,OAAO,CAAC,UAAU,CAAkB;gBAExB,eAAe,CAAC,EAAE,eAAe,EAAE,WAAW,CAAC,EAAE,gBAAgB;IAuGvE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YA+Cd,kBAAkB;YAiDlB,wBAAwB;IA0BtC,OAAO,CAAC,kBAAkB;YA6CZ,iBAAiB;IAa/B,OAAO,CAAC,eAAe,CAAkB;YAE3B,sBAAsB;IAgDpC,OAAO,CAAC,gBAAgB;IAqCxB,OAAO,CAAC,aAAa;IA8VrB,OAAO,CAAC,wBAAwB;IAoFhC,OAAO,CAAC,kBAAkB;IAqE1B,OAAO,CAAC,uBAAuB;IAwB/B,OAAO,CAAC,qBAAqB;YAoTf,SAAS;YA2DT,WAAW;YAkFX,WAAW;YA0CX,cAAc;YA8Md,gBAAgB;IAqD9B,OAAO,CAAC,mBAAmB;IAwE3B,OAAO,CAAC,eAAe;YAsBT,eAAe;IA2L7B,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,uBAAuB;IA0D/B,OAAO,CAAC,iBAAiB;YAqFX,WAAW;YAgCX,oBAAoB;IAuFlC,OAAO,CAAC,aAAa;YAQP,qBAAqB;YAwDrB,iBAAiB;YAiKjB,OAAO;YAgDP,cAAc;YAwFd,iBAAiB;IAqC/B,OAAO,CAAC,iBAAiB;IA0BzB,OAAO,CAAC,iBAAiB;IA0BzB,OAAO,CAAC,eAAe;IAwCvB,OAAO,CAAC,kBAAkB;IAiC1B,OAAO,CAAC,aAAa;IAoCrB,OAAO,CAAC,0BAA0B;IAgClC,OAAO,CAAC,4BAA4B;YAKtB,oBAAoB;IAsDlC,OAAO,CAAC,gBAAgB;YAiBV,SAAS;YA6CT,kBAAkB;YAqElB,uBAAuB;YAsDvB,iBAAiB;IAqE/B,OAAO,CAAC,qBAAqB;IA8C7B,OAAO,CAAC,uBAAuB;IA4D/B,OAAO,CAAC,wBAAwB;IAkChC,OAAO,CAAC,iBAAiB;YAoDX,mBAAmB;YAoEnB,qBAAqB;IAS7B,OAAO,CAAC,SAAS,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;YAS9B,aAAa;YAcb,iBAAiB;YAoBjB,WAAW;YAwBX,eAAe;YAqBf,mBAAmB;YAwBnB,yBAAyB;IA4CvC,OAAO,CAAC,kBAAkB;YAiBZ,gBAAgB;YA6HhB,2BAA2B;YAiE3B,2BAA2B;IAyEnC,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IA0BpB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAgEhC"}

107
dist/mcp/server.js vendored
View File

@@ -457,18 +457,6 @@ class N8NDocumentationMCPServer {
};
}
let processedArgs = args;
if (typeof args === 'string') {
try {
const parsed = JSON.parse(args);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
processedArgs = parsed;
logger_1.logger.warn(`Coerced stringified args object for tool "${name}"`);
}
}
catch {
logger_1.logger.warn(`Tool "${name}" received string args that are not valid JSON`);
}
}
if (args && typeof args === 'object' && 'output' in args) {
try {
const possibleNestedData = args.output;
@@ -497,7 +485,6 @@ class N8NDocumentationMCPServer {
});
}
}
processedArgs = this.coerceStringifiedJsonParams(name, processedArgs);
try {
logger_1.logger.debug(`Executing tool: ${name}`, { args: processedArgs });
const startTime = Date.now();
@@ -569,13 +556,6 @@ class N8NDocumentationMCPServer {
if (name.startsWith('validate_') && (errorMessage.includes('config') || errorMessage.includes('nodeType'))) {
helpfulMessage += '\n\nFor validation tools:\n- nodeType should be a string (e.g., "nodes-base.webhook")\n- config should be an object (e.g., {})';
}
try {
const argDiag = processedArgs && typeof processedArgs === 'object'
? Object.entries(processedArgs).map(([k, v]) => `${k}: ${typeof v}`).join(', ')
: `args type: ${typeof processedArgs}`;
helpfulMessage += `\n\n[Diagnostic] Received arg types: {${argDiag}}`;
}
catch { }
return {
content: [
{
@@ -815,93 +795,6 @@ class N8NDocumentationMCPServer {
}
return true;
}
coerceStringifiedJsonParams(toolName, args) {
if (!args || typeof args !== 'object')
return args;
const allTools = [...tools_1.n8nDocumentationToolsFinal, ...tools_n8n_manager_1.n8nManagementTools];
const tool = allTools.find(t => t.name === toolName);
if (!tool?.inputSchema?.properties)
return args;
const properties = tool.inputSchema.properties;
const coerced = { ...args };
let coercedAny = false;
for (const [key, value] of Object.entries(coerced)) {
if (value === undefined || value === null)
continue;
const propSchema = properties[key];
if (!propSchema)
continue;
const expectedType = propSchema.type;
if (!expectedType)
continue;
const actualType = typeof value;
if (expectedType === 'string' && actualType === 'string')
continue;
if ((expectedType === 'number' || expectedType === 'integer') && actualType === 'number')
continue;
if (expectedType === 'boolean' && actualType === 'boolean')
continue;
if (expectedType === 'object' && actualType === 'object' && !Array.isArray(value))
continue;
if (expectedType === 'array' && Array.isArray(value))
continue;
if (actualType === 'string') {
const trimmed = value.trim();
if (expectedType === 'object' && trimmed.startsWith('{')) {
try {
const parsed = JSON.parse(trimmed);
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
coerced[key] = parsed;
coercedAny = true;
}
}
catch { }
continue;
}
if (expectedType === 'array' && trimmed.startsWith('[')) {
try {
const parsed = JSON.parse(trimmed);
if (Array.isArray(parsed)) {
coerced[key] = parsed;
coercedAny = true;
}
}
catch { }
continue;
}
if (expectedType === 'number' || expectedType === 'integer') {
const num = Number(trimmed);
if (!isNaN(num) && trimmed !== '') {
coerced[key] = expectedType === 'integer' ? Math.trunc(num) : num;
coercedAny = true;
}
continue;
}
if (expectedType === 'boolean') {
if (trimmed === 'true') {
coerced[key] = true;
coercedAny = true;
}
else if (trimmed === 'false') {
coerced[key] = false;
coercedAny = true;
}
continue;
}
}
if (expectedType === 'string' && (actualType === 'number' || actualType === 'boolean')) {
coerced[key] = String(value);
coercedAny = true;
continue;
}
}
if (coercedAny) {
logger_1.logger.warn(`Coerced mistyped params for tool "${toolName}"`, {
original: Object.fromEntries(Object.entries(args).map(([k, v]) => [k, `${typeof v}: ${typeof v === 'string' ? v.substring(0, 80) : v}`])),
});
}
return coerced;
}
async executeTool(name, args) {
args = args || {};
const disabledTools = this.getDisabledTools();

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -687,23 +687,9 @@ export class N8NDocumentationMCPServer {
};
}
// Safeguard: if the entire args object arrives as a JSON string, parse it.
// Some MCP clients may serialize the arguments object itself.
let processedArgs: Record<string, any> | undefined = args;
if (typeof args === 'string') {
try {
const parsed = JSON.parse(args as unknown as string);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
processedArgs = parsed;
logger.warn(`Coerced stringified args object for tool "${name}"`);
}
} catch {
logger.warn(`Tool "${name}" received string args that are not valid JSON`);
}
}
// Workaround for n8n's nested output bug
// Check if args contains nested 'output' structure from n8n's memory corruption
let processedArgs = args;
if (args && typeof args === 'object' && 'output' in args) {
try {
const possibleNestedData = args.output;
@@ -734,13 +720,7 @@ export class N8NDocumentationMCPServer {
});
}
}
// Workaround for Claude Desktop / Claude.ai MCP client bugs that
// serialize parameters with wrong types. Coerces ALL mismatched types
// (string↔object, string↔number, string↔boolean, etc.) using the
// tool's inputSchema as the source of truth.
processedArgs = this.coerceStringifiedJsonParams(name, processedArgs);
try {
logger.debug(`Executing tool: ${name}`, { args: processedArgs });
const startTime = Date.now();
@@ -828,7 +808,7 @@ export class N8NDocumentationMCPServer {
// Provide more helpful error messages for common n8n issues
let helpfulMessage = `Error executing tool ${name}: ${errorMessage}`;
if (errorMessage.includes('required') || errorMessage.includes('missing')) {
helpfulMessage += '\n\nNote: This error often occurs when the AI agent sends incomplete or incorrectly formatted parameters. Please ensure all required fields are provided with the correct types.';
} else if (errorMessage.includes('type') || errorMessage.includes('expected')) {
@@ -836,20 +816,12 @@ export class N8NDocumentationMCPServer {
} else if (errorMessage.includes('Unknown category') || errorMessage.includes('not found')) {
helpfulMessage += '\n\nNote: The requested resource or category was not found. Please check the available options.';
}
// For n8n schema errors, add specific guidance
if (name.startsWith('validate_') && (errorMessage.includes('config') || errorMessage.includes('nodeType'))) {
helpfulMessage += '\n\nFor validation tools:\n- nodeType should be a string (e.g., "nodes-base.webhook")\n- config should be an object (e.g., {})';
}
// Include diagnostic info about received args to help debug client issues
try {
const argDiag = processedArgs && typeof processedArgs === 'object'
? Object.entries(processedArgs).map(([k, v]) => `${k}: ${typeof v}`).join(', ')
: `args type: ${typeof processedArgs}`;
helpfulMessage += `\n\n[Diagnostic] Received arg types: {${argDiag}}`;
} catch { /* ignore diagnostic errors */ }
return {
content: [
{
@@ -1153,109 +1125,6 @@ export class N8NDocumentationMCPServer {
return true;
}
/**
* Coerce mistyped parameters back to their expected types.
* Workaround for Claude Desktop / Claude.ai MCP client bugs that serialize
* parameters incorrectly (objects as strings, numbers as strings, etc.).
*
* Handles ALL type mismatches based on the tool's inputSchema:
* string→object, string→array : JSON.parse
* string→number, string→integer : Number()
* string→boolean : "true"/"false" parsing
* number→string, boolean→string : .toString()
*/
private coerceStringifiedJsonParams(
toolName: string,
args: Record<string, any> | undefined
): Record<string, any> | undefined {
if (!args || typeof args !== 'object') return args;
const allTools = [...n8nDocumentationToolsFinal, ...n8nManagementTools];
const tool = allTools.find(t => t.name === toolName);
if (!tool?.inputSchema?.properties) return args;
const properties = tool.inputSchema.properties;
const coerced = { ...args };
let coercedAny = false;
for (const [key, value] of Object.entries(coerced)) {
if (value === undefined || value === null) continue;
const propSchema = (properties as any)[key];
if (!propSchema) continue;
const expectedType = propSchema.type;
if (!expectedType) continue;
const actualType = typeof value;
// Already correct type — skip
if (expectedType === 'string' && actualType === 'string') continue;
if ((expectedType === 'number' || expectedType === 'integer') && actualType === 'number') continue;
if (expectedType === 'boolean' && actualType === 'boolean') continue;
if (expectedType === 'object' && actualType === 'object' && !Array.isArray(value)) continue;
if (expectedType === 'array' && Array.isArray(value)) continue;
// --- Coercion: string value → expected type ---
if (actualType === 'string') {
const trimmed = (value as string).trim();
if (expectedType === 'object' && trimmed.startsWith('{')) {
try {
const parsed = JSON.parse(trimmed);
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
coerced[key] = parsed;
coercedAny = true;
}
} catch { /* keep original */ }
continue;
}
if (expectedType === 'array' && trimmed.startsWith('[')) {
try {
const parsed = JSON.parse(trimmed);
if (Array.isArray(parsed)) {
coerced[key] = parsed;
coercedAny = true;
}
} catch { /* keep original */ }
continue;
}
if (expectedType === 'number' || expectedType === 'integer') {
const num = Number(trimmed);
if (!isNaN(num) && trimmed !== '') {
coerced[key] = expectedType === 'integer' ? Math.trunc(num) : num;
coercedAny = true;
}
continue;
}
if (expectedType === 'boolean') {
if (trimmed === 'true') { coerced[key] = true; coercedAny = true; }
else if (trimmed === 'false') { coerced[key] = false; coercedAny = true; }
continue;
}
}
// --- Coercion: number/boolean value → expected string ---
if (expectedType === 'string' && (actualType === 'number' || actualType === 'boolean')) {
coerced[key] = String(value);
coercedAny = true;
continue;
}
}
if (coercedAny) {
logger.warn(`Coerced mistyped params for tool "${toolName}"`, {
original: Object.fromEntries(
Object.entries(args).map(([k, v]) => [k, `${typeof v}: ${typeof v === 'string' ? v.substring(0, 80) : v}`])
),
});
}
return coerced;
}
async executeTool(name: string, args: any): Promise<any> {
// Ensure args is an object and validate it
args = args || {};

View File

@@ -1,300 +0,0 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { N8NDocumentationMCPServer } from '../../../src/mcp/server';
// Mock the database and dependencies
vi.mock('../../../src/database/database-adapter');
vi.mock('../../../src/database/node-repository');
vi.mock('../../../src/templates/template-service');
vi.mock('../../../src/utils/logger');
class TestableN8NMCPServer extends N8NDocumentationMCPServer {
public testCoerceStringifiedJsonParams(
toolName: string,
args: Record<string, any>
): Record<string, any> {
return (this as any).coerceStringifiedJsonParams(toolName, args);
}
}
describe('coerceStringifiedJsonParams', () => {
let server: TestableN8NMCPServer;
beforeEach(() => {
process.env.NODE_DB_PATH = ':memory:';
server = new TestableN8NMCPServer();
});
afterEach(() => {
delete process.env.NODE_DB_PATH;
});
describe('Object coercion', () => {
it('should coerce stringified object for validate_node config', () => {
const args = {
nodeType: 'nodes-base.slack',
config: '{"resource":"channel","operation":"create"}'
};
const result = server.testCoerceStringifiedJsonParams('validate_node', args);
expect(result.config).toEqual({ resource: 'channel', operation: 'create' });
expect(result.nodeType).toBe('nodes-base.slack');
});
it('should coerce stringified object for n8n_create_workflow connections', () => {
const connections = { 'Webhook': { main: [[{ node: 'Slack', type: 'main', index: 0 }]] } };
const args = {
name: 'Test Workflow',
nodes: [{ id: '1', name: 'Webhook', type: 'n8n-nodes-base.webhook' }],
connections: JSON.stringify(connections)
};
const result = server.testCoerceStringifiedJsonParams('n8n_create_workflow', args);
expect(result.connections).toEqual(connections);
});
it('should coerce stringified object for validate_workflow workflow param', () => {
const workflow = { nodes: [], connections: {} };
const args = {
workflow: JSON.stringify(workflow)
};
const result = server.testCoerceStringifiedJsonParams('validate_workflow', args);
expect(result.workflow).toEqual(workflow);
});
});
describe('Array coercion', () => {
it('should coerce stringified array for n8n_update_partial_workflow operations', () => {
const operations = [
{ type: 'addNode', node: { id: '1', name: 'Test', type: 'n8n-nodes-base.noOp' } }
];
const args = {
id: '123',
operations: JSON.stringify(operations)
};
const result = server.testCoerceStringifiedJsonParams('n8n_update_partial_workflow', args);
expect(result.operations).toEqual(operations);
expect(result.id).toBe('123');
});
it('should coerce stringified array for n8n_autofix_workflow fixTypes', () => {
const fixTypes = ['expression-format', 'typeversion-correction'];
const args = {
id: '456',
fixTypes: JSON.stringify(fixTypes)
};
const result = server.testCoerceStringifiedJsonParams('n8n_autofix_workflow', args);
expect(result.fixTypes).toEqual(fixTypes);
});
});
describe('No-op cases', () => {
it('should not modify object params that are already objects', () => {
const config = { resource: 'channel', operation: 'create' };
const args = {
nodeType: 'nodes-base.slack',
config
};
const result = server.testCoerceStringifiedJsonParams('validate_node', args);
expect(result.config).toEqual(config);
expect(result.config).toBe(config); // same reference
});
it('should not modify string params even if they contain JSON', () => {
const args = {
query: '{"some":"json"}',
limit: 10
};
const result = server.testCoerceStringifiedJsonParams('search_nodes', args);
expect(result.query).toBe('{"some":"json"}');
});
it('should not modify args for tools with no object/array params', () => {
const args = {
query: 'webhook',
limit: 20,
mode: 'OR'
};
const result = server.testCoerceStringifiedJsonParams('search_nodes', args);
expect(result).toEqual(args);
});
});
describe('Safety cases', () => {
it('should keep original string for invalid JSON', () => {
const args = {
nodeType: 'nodes-base.slack',
config: '{invalid json here}'
};
const result = server.testCoerceStringifiedJsonParams('validate_node', args);
expect(result.config).toBe('{invalid json here}');
});
it('should not attempt parse when object param starts with [', () => {
const args = {
nodeType: 'nodes-base.slack',
config: '[1, 2, 3]'
};
const result = server.testCoerceStringifiedJsonParams('validate_node', args);
expect(result.config).toBe('[1, 2, 3]');
});
it('should not attempt parse when array param starts with {', () => {
const args = {
id: '123',
operations: '{"not":"an array"}'
};
const result = server.testCoerceStringifiedJsonParams('n8n_update_partial_workflow', args);
expect(result.operations).toBe('{"not":"an array"}');
});
it('should handle null args gracefully', () => {
const result = server.testCoerceStringifiedJsonParams('validate_node', null as any);
expect(result).toBeNull();
});
it('should handle undefined args gracefully', () => {
const result = server.testCoerceStringifiedJsonParams('validate_node', undefined as any);
expect(result).toBeUndefined();
});
it('should return args unchanged for unknown tool', () => {
const args = { config: '{"key":"value"}' };
const result = server.testCoerceStringifiedJsonParams('nonexistent_tool', args);
expect(result).toEqual(args);
expect(result.config).toBe('{"key":"value"}');
});
});
describe('Number coercion', () => {
it('should coerce string to number for search_nodes limit', () => {
const args = {
query: 'webhook',
limit: '10'
};
const result = server.testCoerceStringifiedJsonParams('search_nodes', args);
expect(result.limit).toBe(10);
expect(result.query).toBe('webhook');
});
it('should coerce string to number for n8n_executions limit', () => {
const args = {
action: 'list',
limit: '50'
};
const result = server.testCoerceStringifiedJsonParams('n8n_executions', args);
expect(result.limit).toBe(50);
});
it('should not coerce non-numeric string to number', () => {
const args = {
query: 'webhook',
limit: 'abc'
};
const result = server.testCoerceStringifiedJsonParams('search_nodes', args);
expect(result.limit).toBe('abc');
});
});
describe('Boolean coercion', () => {
it('should coerce "true" string to boolean', () => {
const args = {
query: 'webhook',
includeExamples: 'true'
};
const result = server.testCoerceStringifiedJsonParams('search_nodes', args);
expect(result.includeExamples).toBe(true);
});
it('should coerce "false" string to boolean', () => {
const args = {
query: 'webhook',
includeExamples: 'false'
};
const result = server.testCoerceStringifiedJsonParams('search_nodes', args);
expect(result.includeExamples).toBe(false);
});
it('should not coerce non-boolean string to boolean', () => {
const args = {
query: 'webhook',
includeExamples: 'yes'
};
const result = server.testCoerceStringifiedJsonParams('search_nodes', args);
expect(result.includeExamples).toBe('yes');
});
});
describe('Number-to-string coercion', () => {
it('should coerce number to string for n8n_get_workflow id', () => {
const args = {
id: 123,
mode: 'minimal'
};
const result = server.testCoerceStringifiedJsonParams('n8n_get_workflow', args);
expect(result.id).toBe('123');
expect(result.mode).toBe('minimal');
});
it('should coerce boolean to string when string expected', () => {
const args = {
id: true
};
const result = server.testCoerceStringifiedJsonParams('n8n_get_workflow', args);
expect(result.id).toBe('true');
});
});
describe('End-to-end Claude Desktop scenario', () => {
it('should coerce all stringified params for n8n_create_workflow', () => {
const nodes = [
{
id: 'webhook_1',
name: 'Webhook',
type: 'n8n-nodes-base.webhook',
typeVersion: 1,
position: [250, 300],
parameters: { httpMethod: 'POST', path: 'slack-notify' }
},
{
id: 'slack_1',
name: 'Slack',
type: 'n8n-nodes-base.slack',
typeVersion: 1,
position: [450, 300],
parameters: { resource: 'message', operation: 'post', channel: '#general' }
}
];
const connections = {
'Webhook': { main: [[{ node: 'Slack', type: 'main', index: 0 }]] }
};
const settings = { executionOrder: 'v1', timezone: 'America/New_York' };
// Simulate Claude Desktop sending all object/array params as strings
const args = {
name: 'Webhook to Slack',
nodes: JSON.stringify(nodes),
connections: JSON.stringify(connections),
settings: JSON.stringify(settings)
};
const result = server.testCoerceStringifiedJsonParams('n8n_create_workflow', args);
expect(result.name).toBe('Webhook to Slack');
expect(result.nodes).toEqual(nodes);
expect(result.connections).toEqual(connections);
expect(result.settings).toEqual(settings);
});
it('should handle mixed type mismatches from Claude Desktop', () => {
// Simulate Claude Desktop sending object params as strings
const args = {
nodeType: 'nodes-base.httpRequest',
config: '{"method":"GET","url":"https://example.com"}',
mode: 'full',
profile: 'ai-friendly'
};
const result = server.testCoerceStringifiedJsonParams('validate_node', args);
expect(result.config).toEqual({ method: 'GET', url: 'https://example.com' });
expect(result.nodeType).toBe('nodes-base.httpRequest');
expect(result.mode).toBe('full');
});
});
});