Compare commits

...

1 Commits

Author SHA1 Message Date
czlonkowski
b4009cee7c fix: comprehensive param type coercion for Claude Desktop/Claude.ai (#605)
Expand coerceStringifiedJsonParams() to handle ALL type mismatches,
not just stringified objects/arrays. Testing showed 6/9 tools still
failing in Claude Desktop after v2.35.4.

- Coerce string→number, string→boolean, number→string, boolean→string
- Add safeguard for entire args object arriving as JSON string
- Add [Diagnostic] section to error responses with received arg types
- Bump to v2.35.5
- 24 unit tests (9 new)

Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 12:23:43 +07:00
8 changed files with 324 additions and 29 deletions

View File

@@ -7,6 +7,18 @@ 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

View File

@@ -30,6 +30,7 @@ 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;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"}
{"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"}

107
dist/mcp/server.js vendored
View File

@@ -457,6 +457,18 @@ 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;
@@ -485,6 +497,7 @@ class N8NDocumentationMCPServer {
});
}
}
processedArgs = this.coerceStringifiedJsonParams(name, processedArgs);
try {
logger_1.logger.debug(`Executing tool: ${name}`, { args: processedArgs });
const startTime = Date.now();
@@ -556,6 +569,13 @@ 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: [
{
@@ -795,6 +815,93 @@ 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.4",
"version": "2.35.5",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -687,9 +687,23 @@ 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;
@@ -721,9 +735,10 @@ export class N8NDocumentationMCPServer {
}
}
// Workaround for Claude Desktop 1.1.3189 string serialization bug.
// The MCP client serializes object/array parameters as JSON strings.
// Use the tool's inputSchema to detect and parse them back.
// 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 {
@@ -813,7 +828,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')) {
@@ -821,12 +836,20 @@ 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: [
{
@@ -1131,9 +1154,15 @@ export class N8NDocumentationMCPServer {
}
/**
* Coerce stringified JSON parameters back to objects/arrays.
* Workaround for Claude Desktop 1.1.3189 which serializes object/array
* params as JSON strings before sending them to MCP servers.
* 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,
@@ -1147,28 +1176,81 @@ export class N8NDocumentationMCPServer {
const properties = tool.inputSchema.properties;
const coerced = { ...args };
let coercedAny = false;
for (const [key, value] of Object.entries(coerced)) {
if (typeof value !== 'string') continue;
const expectedType = (properties as any)[key]?.type;
if (expectedType !== 'object' && expectedType !== 'array') continue;
if (value === undefined || value === null) continue;
const trimmed = value.trim();
const validPrefix = (expectedType === 'object' && trimmed.startsWith('{'))
|| (expectedType === 'array' && trimmed.startsWith('['));
if (!validPrefix) continue;
const propSchema = (properties as any)[key];
if (!propSchema) continue;
const expectedType = propSchema.type;
if (!expectedType) continue;
try {
const parsed = JSON.parse(trimmed);
const isArray = Array.isArray(parsed);
if ((expectedType === 'object' && typeof parsed === 'object' && !isArray)
|| (expectedType === 'array' && isArray)) {
coerced[key] = parsed;
logger.warn(`Coerced stringified ${expectedType} param "${key}" for tool "${toolName}"`);
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;
}
} catch {
// Not valid JSON — keep original string, downstream validation will report the error
}
// --- 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;

View File

@@ -163,6 +163,85 @@ describe('coerceStringifiedJsonParams', () => {
});
});
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 = [
@@ -203,5 +282,19 @@ describe('coerceStringifiedJsonParams', () => {
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');
});
});
});