mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-28 07:33:08 +00:00
* 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> * 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> * fix: defensive JSON.parse for stringified object/array params (#605) Claude Desktop 1.1.3189 serializes object/array MCP parameters as JSON strings, causing ZodError failures for ~60% of tools. Add schema-driven coercion in the central CallToolRequestSchema handler to detect and parse them back automatically. Conceived by Romuald Czlonkowski - https://www.aiadvisors.pl/en Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
77048347b3
commit
4bad880f44
207
tests/unit/mcp/coerce-stringified-params.test.ts
Normal file
207
tests/unit/mcp/coerce-stringified-params.test.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
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('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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user