Merge pull request #284 from czlonkowski/fix/resourcelocator-validation

fix: Add resourceLocator validation for AI model nodes
This commit is contained in:
Romuald Członkowski
2025-10-07 18:22:39 +02:00
committed by GitHub
6 changed files with 421 additions and 3 deletions

View File

@@ -5,6 +5,45 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.17.3] - 2025-10-07
### 🔧 Validation
**Fixed critical validation gap for AI model nodes with resourceLocator properties.**
This release adds validation for `resourceLocator` type properties, fixing a critical issue where AI agents could create invalid configurations that passed validation but failed at runtime.
#### Fixed
- **resourceLocator Property Validation**
- **Issue:** No validation existed for `resourceLocator` type properties used in AI model nodes
- **Impact:**
- AI agents could create invalid configurations like `model: "gpt-4o-mini"` (string) instead of `model: {mode: "list", value: "gpt-4o-mini"}` (object)
- Invalid configs passed validation but failed at runtime in n8n
- Affected many langchain nodes: OpenAI Chat Model (v1.2+), Anthropic, Cohere, DeepSeek, Groq, Mistral, OpenRouter, xAI Grok, and embeddings nodes
- **Root Cause:** `validatePropertyTypes()` method in ConfigValidator only validated `string`, `number`, `boolean`, and `options` types - `resourceLocator` was completely missing
- **Fix:** Added comprehensive resourceLocator validation in `config-validator.ts:237-274`
- Validates value is an object (not string, number, null, or array)
- Validates required `mode` property exists and is a string
- Validates required `value` property exists
- Provides helpful error messages with exact fix suggestions
- Example error: `Property 'model' is a resourceLocator and must be an object with 'mode' and 'value' properties, got string`
- Example fix: `Change model to { mode: "list", value: "gpt-4o-mini" } or { mode: "id", value: "gpt-4o-mini" }`
#### Added
- Comprehensive resourceLocator validation with 14 test cases covering:
- String value rejection with helpful fix suggestions
- Null and array value rejection
- Missing `mode` or `value` property detection
- Invalid `mode` type detection (e.g., number instead of string)
- Invalid `mode` value validation (must be 'list', 'id', or 'url')
- Empty object detection (missing both mode and value)
- Extra properties handling (ignored gracefully)
- Valid resourceLocator acceptance for "list", "id", and "url" modes
- JSDoc documentation explaining resourceLocator structure and common mistakes
- All 29 tests passing (100% coverage for new validation logic)
## [2.17.1] - 2025-10-07
### 🔧 Telemetry

Binary file not shown.

View File

@@ -1,6 +1,6 @@
{
"name": "n8n-mcp",
"version": "2.17.2",
"version": "2.17.3",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js",
"bin": {

View File

@@ -1,6 +1,6 @@
{
"name": "n8n-mcp-runtime",
"version": "2.17.1",
"version": "2.17.3",
"description": "n8n MCP Server Runtime Dependencies Only",
"private": true,
"dependencies": {

View File

@@ -234,8 +234,56 @@ export class ConfigValidator {
message: `Property '${key}' must be a boolean, got ${typeof value}`,
fix: `Change ${key} to true or false`
});
} else if (prop.type === 'resourceLocator') {
// resourceLocator validation: Used by AI model nodes (OpenAI, Anthropic, etc.)
// Must be an object with required properties:
// - mode: string ('list' | 'id' | 'url')
// - value: any (the actual model/resource identifier)
// Common mistake: passing string directly instead of object structure
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
const fixValue = typeof value === 'string' ? value : JSON.stringify(value);
errors.push({
type: 'invalid_type',
property: key,
message: `Property '${key}' is a resourceLocator and must be an object with 'mode' and 'value' properties, got ${typeof value}`,
fix: `Change ${key} to { mode: "list", value: ${JSON.stringify(fixValue)} } or { mode: "id", value: ${JSON.stringify(fixValue)} }`
});
} else {
// Check required properties
if (!value.mode) {
errors.push({
type: 'missing_required',
property: `${key}.mode`,
message: `resourceLocator '${key}' is missing required property 'mode'`,
fix: `Add mode property: { mode: "list", value: ${JSON.stringify(value.value || '')} }`
});
} else if (typeof value.mode !== 'string') {
errors.push({
type: 'invalid_type',
property: `${key}.mode`,
message: `resourceLocator '${key}.mode' must be a string, got ${typeof value.mode}`,
fix: `Set mode to "list" or "id"`
});
} else if (!['list', 'id', 'url'].includes(value.mode)) {
errors.push({
type: 'invalid_value',
property: `${key}.mode`,
message: `resourceLocator '${key}.mode' must be 'list', 'id', or 'url', got '${value.mode}'`,
fix: `Change mode to "list", "id", or "url"`
});
}
if (value.value === undefined) {
errors.push({
type: 'missing_required',
property: `${key}.value`,
message: `resourceLocator '${key}' is missing required property 'value'`,
fix: `Add value property to specify the ${prop.displayName || key}`
});
}
}
}
// Options validation
if (prop.type === 'options' && prop.options) {
const validValues = prop.options.map((opt: any) =>

View File

@@ -439,4 +439,335 @@ describe('ConfigValidator - Basic Validation', () => {
expect(result.suggestions.length).toBeGreaterThanOrEqual(0);
});
});
describe('resourceLocator validation', () => {
it('should reject string value when resourceLocator object is required', () => {
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
const config = {
model: 'gpt-4o-mini' // Wrong - should be object with mode and value
};
const properties = [
{
name: 'model',
displayName: 'Model',
type: 'resourceLocator',
required: true,
default: { mode: 'list', value: 'gpt-4o-mini' }
}
];
const result = ConfigValidator.validate(nodeType, config, properties);
expect(result.valid).toBe(false);
expect(result.errors).toHaveLength(1);
expect(result.errors[0]).toMatchObject({
type: 'invalid_type',
property: 'model',
message: expect.stringContaining('must be an object with \'mode\' and \'value\' properties')
});
expect(result.errors[0].fix).toContain('mode');
expect(result.errors[0].fix).toContain('value');
});
it('should accept valid resourceLocator with mode and value', () => {
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
const config = {
model: {
mode: 'list',
value: 'gpt-4o-mini'
}
};
const properties = [
{
name: 'model',
displayName: 'Model',
type: 'resourceLocator',
required: true,
default: { mode: 'list', value: 'gpt-4o-mini' }
}
];
const result = ConfigValidator.validate(nodeType, config, properties);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should reject null value for resourceLocator', () => {
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
const config = {
model: null
};
const properties = [
{
name: 'model',
type: 'resourceLocator',
required: true
}
];
const result = ConfigValidator.validate(nodeType, config, properties);
expect(result.valid).toBe(false);
expect(result.errors.some(e =>
e.property === 'model' &&
e.type === 'invalid_type'
)).toBe(true);
});
it('should reject array value for resourceLocator', () => {
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
const config = {
model: ['gpt-4o-mini']
};
const properties = [
{
name: 'model',
type: 'resourceLocator',
required: true
}
];
const result = ConfigValidator.validate(nodeType, config, properties);
expect(result.valid).toBe(false);
expect(result.errors.some(e =>
e.property === 'model' &&
e.type === 'invalid_type' &&
e.message.includes('must be an object')
)).toBe(true);
});
it('should detect missing mode property in resourceLocator', () => {
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
const config = {
model: {
value: 'gpt-4o-mini'
// Missing mode property
}
};
const properties = [
{
name: 'model',
type: 'resourceLocator',
required: true
}
];
const result = ConfigValidator.validate(nodeType, config, properties);
expect(result.valid).toBe(false);
expect(result.errors.some(e =>
e.property === 'model.mode' &&
e.type === 'missing_required' &&
e.message.includes('missing required property \'mode\'')
)).toBe(true);
});
it('should detect missing value property in resourceLocator', () => {
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
const config = {
model: {
mode: 'list'
// Missing value property
}
};
const properties = [
{
name: 'model',
displayName: 'Model',
type: 'resourceLocator',
required: true
}
];
const result = ConfigValidator.validate(nodeType, config, properties);
expect(result.valid).toBe(false);
expect(result.errors.some(e =>
e.property === 'model.value' &&
e.type === 'missing_required' &&
e.message.includes('missing required property \'value\'')
)).toBe(true);
});
it('should detect invalid mode type in resourceLocator', () => {
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
const config = {
model: {
mode: 123, // Should be string
value: 'gpt-4o-mini'
}
};
const properties = [
{
name: 'model',
type: 'resourceLocator',
required: true
}
];
const result = ConfigValidator.validate(nodeType, config, properties);
expect(result.valid).toBe(false);
expect(result.errors.some(e =>
e.property === 'model.mode' &&
e.type === 'invalid_type' &&
e.message.includes('must be a string')
)).toBe(true);
});
it('should accept resourceLocator with mode "id"', () => {
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
const config = {
model: {
mode: 'id',
value: 'gpt-4o-2024-11-20'
}
};
const properties = [
{
name: 'model',
type: 'resourceLocator',
required: true
}
];
const result = ConfigValidator.validate(nodeType, config, properties);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should reject number value when resourceLocator is required', () => {
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
const config = {
model: 12345 // Wrong type
};
const properties = [
{
name: 'model',
type: 'resourceLocator',
required: true
}
];
const result = ConfigValidator.validate(nodeType, config, properties);
expect(result.valid).toBe(false);
expect(result.errors[0].type).toBe('invalid_type');
expect(result.errors[0].message).toContain('must be an object');
});
it('should provide helpful fix suggestion for string to resourceLocator conversion', () => {
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
const config = {
model: 'gpt-4o-mini'
};
const properties = [
{
name: 'model',
type: 'resourceLocator',
required: true
}
];
const result = ConfigValidator.validate(nodeType, config, properties);
expect(result.errors[0].fix).toContain('{ mode: "list", value: "gpt-4o-mini" }');
expect(result.errors[0].fix).toContain('{ mode: "id", value: "gpt-4o-mini" }');
});
it('should reject invalid mode values', () => {
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
const config = {
model: {
mode: 'invalid-mode',
value: 'gpt-4o-mini'
}
};
const properties = [
{
name: 'model',
type: 'resourceLocator',
required: true
}
];
const result = ConfigValidator.validate(nodeType, config, properties);
expect(result.valid).toBe(false);
expect(result.errors.some(e =>
e.property === 'model.mode' &&
e.type === 'invalid_value' &&
e.message.includes("must be 'list', 'id', or 'url'")
)).toBe(true);
});
it('should accept resourceLocator with mode "url"', () => {
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
const config = {
model: {
mode: 'url',
value: 'https://api.example.com/models/custom'
}
};
const properties = [
{
name: 'model',
type: 'resourceLocator',
required: true
}
];
const result = ConfigValidator.validate(nodeType, config, properties);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should detect empty resourceLocator object', () => {
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
const config = {
model: {} // Empty object, missing both mode and value
};
const properties = [
{
name: 'model',
type: 'resourceLocator',
required: true
}
];
const result = ConfigValidator.validate(nodeType, config, properties);
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThanOrEqual(2); // Both mode and value missing
expect(result.errors.some(e => e.property === 'model.mode')).toBe(true);
expect(result.errors.some(e => e.property === 'model.value')).toBe(true);
});
it('should handle resourceLocator with extra properties gracefully', () => {
const nodeType = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
const config = {
model: {
mode: 'list',
value: 'gpt-4o-mini',
extraProperty: 'ignored' // Extra properties should be ignored
}
};
const properties = [
{
name: 'model',
type: 'resourceLocator',
required: true
}
];
const result = ConfigValidator.validate(nodeType, config, properties);
expect(result.valid).toBe(true); // Should pass with extra properties
expect(result.errors).toHaveLength(0);
});
});
});