fix: comprehensive param type coercion for Claude Desktop/Claude.ai (#605) (#609)

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>
This commit is contained in:
Romuald Członkowski
2026-02-22 07:07:30 +01:00
committed by GitHub
parent 4bad880f44
commit 87f26eef18
8 changed files with 324 additions and 29 deletions

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