mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-24 03:13:07 +00:00
fix: enable schema-based resourceLocator mode validation
Root cause analysis revealed validator was looking at wrong path for modes data. n8n stores modes at top level of properties, not nested in typeOptions. Changes: - config-validator.ts: Changed from prop.typeOptions?.resourceLocator?.modes to prop.modes (lines 273-310) - property-extractor.ts: Added modes field to normalizeProperties to capture mode definitions from n8n nodes - Updated all test cases to match real n8n schema structure with modes at property top level - Rebuilt database with modes field Results: - 100% coverage: All 70 resourceLocator nodes now have modes defined - Schema-based validation now ACTIVE (was being skipped before) - False positive eliminated: Google Sheets "name" mode now validates - Helpful error messages showing actual allowed modes from schema Testing: - All 33 unit tests pass - Verified with n8n-mcp-tester: valid "name" mode passes, invalid modes fail with clear error listing allowed options [list, url, id, name] Fixes #304 (Google Sheets false positive) Related to #306 (validator improvements) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
BIN
data/nodes.db
BIN
data/nodes.db
Binary file not shown.
@@ -231,6 +231,7 @@ export class PropertyExtractor {
|
|||||||
required: prop.required,
|
required: prop.required,
|
||||||
displayOptions: prop.displayOptions,
|
displayOptions: prop.displayOptions,
|
||||||
typeOptions: prop.typeOptions,
|
typeOptions: prop.typeOptions,
|
||||||
|
modes: prop.modes, // For resourceLocator type properties - modes are at top level
|
||||||
noDataExpression: prop.noDataExpression
|
noDataExpression: prop.noDataExpression
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -270,12 +270,13 @@ export class ConfigValidator {
|
|||||||
message: `resourceLocator '${key}.mode' must be a string, got ${typeof value.mode}`,
|
message: `resourceLocator '${key}.mode' must be a string, got ${typeof value.mode}`,
|
||||||
fix: `Set mode to a valid string value`
|
fix: `Set mode to a valid string value`
|
||||||
});
|
});
|
||||||
} else if (prop.typeOptions?.resourceLocator?.modes) {
|
} else if (prop.modes) {
|
||||||
// Schema-based validation: Check if mode exists in the modes definition
|
// Schema-based validation: Check if mode exists in the modes definition
|
||||||
|
// In n8n, modes are defined at the top level of resourceLocator properties
|
||||||
// Modes can be defined in different ways:
|
// Modes can be defined in different ways:
|
||||||
// 1. Object with mode keys: { list: {...}, id: {...}, url: {...}, name: {...} }
|
// 1. Array of mode objects: [{name: 'list', ...}, {name: 'id', ...}, {name: 'name', ...}]
|
||||||
// 2. Array of mode objects: [{name: 'list', ...}, {name: 'id', ...}]
|
// 2. Object with mode keys: { list: {...}, id: {...}, url: {...}, name: {...} }
|
||||||
const modes = prop.typeOptions.resourceLocator.modes;
|
const modes = prop.modes;
|
||||||
|
|
||||||
// Validate modes structure before processing to prevent crashes
|
// Validate modes structure before processing to prevent crashes
|
||||||
if (!modes || typeof modes !== 'object') {
|
if (!modes || typeof modes !== 'object') {
|
||||||
@@ -286,7 +287,7 @@ export class ConfigValidator {
|
|||||||
let allowedModes: string[] = [];
|
let allowedModes: string[] = [];
|
||||||
|
|
||||||
if (Array.isArray(modes)) {
|
if (Array.isArray(modes)) {
|
||||||
// Array format: extract name property from each mode object
|
// Array format (most common in n8n): extract name property from each mode object
|
||||||
allowedModes = modes
|
allowedModes = modes
|
||||||
.map(m => (typeof m === 'object' && m !== null) ? m.name : m)
|
.map(m => (typeof m === 'object' && m !== null) ? m.name : m)
|
||||||
.filter(m => typeof m === 'string' && m.length > 0);
|
.filter(m => typeof m === 'string' && m.length > 0);
|
||||||
@@ -305,7 +306,7 @@ export class ConfigValidator {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If no typeOptions.resourceLocator.modes defined, skip mode validation
|
// If no modes defined at property level, skip mode validation
|
||||||
// This prevents false positives for nodes with dynamic/runtime-determined modes
|
// This prevents false positives for nodes with dynamic/runtime-determined modes
|
||||||
|
|
||||||
if (value.value === undefined) {
|
if (value.value === undefined) {
|
||||||
|
|||||||
@@ -691,15 +691,12 @@ describe('ConfigValidator - Basic Validation', () => {
|
|||||||
name: 'model',
|
name: 'model',
|
||||||
type: 'resourceLocator',
|
type: 'resourceLocator',
|
||||||
required: true,
|
required: true,
|
||||||
typeOptions: {
|
// In real n8n, modes are at top level, not in typeOptions
|
||||||
resourceLocator: {
|
modes: [
|
||||||
modes: {
|
{ name: 'list', displayName: 'List' },
|
||||||
list: { displayName: 'List' },
|
{ name: 'id', displayName: 'ID' },
|
||||||
id: { displayName: 'ID' },
|
{ name: 'url', displayName: 'URL' }
|
||||||
url: { displayName: 'URL' }
|
]
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -726,15 +723,12 @@ describe('ConfigValidator - Basic Validation', () => {
|
|||||||
name: 'model',
|
name: 'model',
|
||||||
type: 'resourceLocator',
|
type: 'resourceLocator',
|
||||||
required: true,
|
required: true,
|
||||||
typeOptions: {
|
// Array format at top level (real n8n structure)
|
||||||
resourceLocator: {
|
modes: [
|
||||||
modes: [
|
{ name: 'list', displayName: 'List' },
|
||||||
{ name: 'list', displayName: 'List' },
|
{ name: 'id', displayName: 'ID' },
|
||||||
{ name: 'id', displayName: 'ID' },
|
{ name: 'custom', displayName: 'Custom' }
|
||||||
{ name: 'custom', displayName: 'Custom' }
|
]
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -757,11 +751,7 @@ describe('ConfigValidator - Basic Validation', () => {
|
|||||||
name: 'model',
|
name: 'model',
|
||||||
type: 'resourceLocator',
|
type: 'resourceLocator',
|
||||||
required: true,
|
required: true,
|
||||||
typeOptions: {
|
modes: 'invalid-string' // Malformed schema at top level
|
||||||
resourceLocator: {
|
|
||||||
modes: 'invalid-string' // Malformed schema
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -785,11 +775,7 @@ describe('ConfigValidator - Basic Validation', () => {
|
|||||||
name: 'model',
|
name: 'model',
|
||||||
type: 'resourceLocator',
|
type: 'resourceLocator',
|
||||||
required: true,
|
required: true,
|
||||||
typeOptions: {
|
modes: {} // Empty object at top level
|
||||||
resourceLocator: {
|
|
||||||
modes: {} // Empty object
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -800,7 +786,7 @@ describe('ConfigValidator - Basic Validation', () => {
|
|||||||
expect(result.errors.some(e => e.property === 'model.mode')).toBe(false);
|
expect(result.errors.some(e => e.property === 'model.mode')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip mode validation when typeOptions not provided', () => {
|
it('should skip mode validation when modes not provided', () => {
|
||||||
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
|
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
|
||||||
const config = {
|
const config = {
|
||||||
model: {
|
model: {
|
||||||
@@ -813,7 +799,7 @@ describe('ConfigValidator - Basic Validation', () => {
|
|||||||
name: 'model',
|
name: 'model',
|
||||||
type: 'resourceLocator',
|
type: 'resourceLocator',
|
||||||
required: true
|
required: true
|
||||||
// No typeOptions - schema doesn't define modes
|
// No modes property - schema doesn't define modes
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user