fix: AI connection type propagation and get_node improvements (v2.28.1)

Bug fixes:
- Issue #458: addConnection now preserves AI connection types (ai_tool, ai_memory, ai_languageModel) instead of defaulting to 'main'
- Fixed false positive "AI Agent has no tools connected" validation warning

Enhancements:
- Added expectedFormat field to resourceLocator properties in get_node output
- Added versionNotice field to make typeVersion more prominent

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-12-01 16:50:45 +01:00
parent 33690c5650
commit f2e2d704fb
7 changed files with 308 additions and 9 deletions

View File

@@ -7,6 +7,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [2.28.1] - 2025-12-01
### 🐛 Bug Fixes
**Issue #458: AI Connection Type Propagation**
Fixed `addConnection` operation in workflow diff engine defaulting `targetInput` to "main" instead of preserving the source output type. This caused AI tool connections to be created with incorrect type.
- **Root Cause**: `targetInput` defaulted to `'main'` regardless of `sourceOutput` type
- **Fix**: Changed default to `sourceOutput` to preserve connection type (ai_tool, ai_memory, ai_languageModel)
- **Files**: `src/services/workflow-diff-engine.ts:760`
**AI Agent Validation False Positive**
Fixed false positive "AI Agent has no tools connected" warning when tools were properly connected.
- **Root Cause**: Validation checked connections FROM agent instead of TO agent
- **Fix**: Search all connections where target node is the agent
- **Files**: `src/services/workflow-validator.ts:1148-1163`
### ✨ Enhancements
**get_node: expectedFormat for resourceLocator Properties**
Added `expectedFormat` field to resourceLocator properties in `get_node` output. This helps AI models understand the correct format for these complex property types.
```json
{
"name": "model",
"type": "resourceLocator",
"expectedFormat": {
"structure": { "mode": "string", "value": "string" },
"modes": ["list", "id"],
"example": { "mode": "id", "value": "gpt-4o-mini" }
}
}
```
**get_node: versionNotice Field**
Added `versionNotice` field to make typeVersion more prominent in get_node output, reducing the chance of AI models using outdated versions.
```json
{
"version": "1.3",
"versionNotice": "⚠️ Use typeVersion: 1.3 when creating this node"
}
```
**Conceived by Romuald Członkowski - [AiAdvisors](https://www.aiadvisors.pl/en)**
## [2.28.0] - 2025-12-01 ## [2.28.0] - 2025-12-01
### ✨ Features ### ✨ Features

View File

@@ -1,6 +1,6 @@
{ {
"name": "n8n-mcp", "name": "n8n-mcp",
"version": "2.28.0", "version": "2.28.1",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)", "description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@@ -2203,14 +2203,19 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
// Get operations (already parsed by repository) // Get operations (already parsed by repository)
const operations = node.operations || []; const operations = node.operations || [];
// Get the latest version - this is important for AI to use correct typeVersion
const latestVersion = node.version ?? '1';
const result = { const result = {
nodeType: node.nodeType, nodeType: node.nodeType,
workflowNodeType: getWorkflowNodeType(node.package ?? 'n8n-nodes-base', node.nodeType), workflowNodeType: getWorkflowNodeType(node.package ?? 'n8n-nodes-base', node.nodeType),
displayName: node.displayName, displayName: node.displayName,
description: node.description, description: node.description,
category: node.category, category: node.category,
version: node.version ?? '1', version: latestVersion,
isVersioned: node.isVersioned ?? false, isVersioned: node.isVersioned ?? false,
// Prominent warning to use the correct typeVersion
versionNotice: `⚠️ Use typeVersion: ${latestVersion} when creating this node`,
requiredProperties: essentials.required, requiredProperties: essentials.required,
commonProperties: essentials.common, commonProperties: essentials.common,
operations: operations.map((op: any) => ({ operations: operations.map((op: any) => ({

View File

@@ -16,6 +16,11 @@ export interface SimplifiedProperty {
placeholder?: string; placeholder?: string;
showWhen?: Record<string, any>; showWhen?: Record<string, any>;
usageHint?: string; usageHint?: string;
expectedFormat?: {
structure: Record<string, string>;
modes?: string[];
example: Record<string, any>;
};
} }
export interface EssentialConfig { export interface EssentialConfig {
@@ -322,7 +327,18 @@ export class PropertyFilter {
}; };
}); });
} }
// Add expectedFormat for resourceLocator types - critical for correct configuration
if (prop.type === 'resourceLocator') {
const modes = prop.modes?.map((m: any) => m.name || m) || ['list', 'id'];
const defaultValue = prop.default?.value || 'your-resource-id';
simplified.expectedFormat = {
structure: { mode: 'string', value: 'string' },
modes,
example: { mode: 'id', value: defaultValue }
};
}
// Include simple display conditions (max 2 conditions) // Include simple display conditions (max 2 conditions)
if (prop.displayOptions?.show) { if (prop.displayOptions?.show) {
const conditions = Object.keys(prop.displayOptions.show); const conditions = Object.keys(prop.displayOptions.show);

View File

@@ -757,7 +757,8 @@ export class WorkflowDiffEngine {
const { sourceOutput, sourceIndex } = this.resolveSmartParameters(workflow, operation); const { sourceOutput, sourceIndex } = this.resolveSmartParameters(workflow, operation);
// Use nullish coalescing to properly handle explicit 0 values // Use nullish coalescing to properly handle explicit 0 values
const targetInput = operation.targetInput ?? 'main'; // Default targetInput to sourceOutput to preserve connection type for AI connections (ai_tool, ai_memory, etc.)
const targetInput = operation.targetInput ?? sourceOutput;
const targetIndex = operation.targetIndex ?? 0; const targetIndex = operation.targetIndex ?? 0;
// Initialize source node connections object // Initialize source node connections object

View File

@@ -1137,16 +1137,23 @@ export class WorkflowValidator {
} }
// Check for AI Agent workflows // Check for AI Agent workflows
const aiAgentNodes = workflow.nodes.filter(n => const aiAgentNodes = workflow.nodes.filter(n =>
n.type.toLowerCase().includes('agent') || n.type.toLowerCase().includes('agent') ||
n.type.includes('langchain.agent') n.type.includes('langchain.agent')
); );
if (aiAgentNodes.length > 0) { if (aiAgentNodes.length > 0) {
// Check if AI agents have tools connected // Check if AI agents have tools connected
// Tools connect TO the agent, so we need to find connections where the target is the agent
for (const agentNode of aiAgentNodes) { for (const agentNode of aiAgentNodes) {
const connections = workflow.connections[agentNode.name]; // Search all connections to find ones targeting this agent via ai_tool
if (!connections?.ai_tool || connections.ai_tool.flat().filter(c => c).length === 0) { const hasToolConnected = Object.values(workflow.connections).some(sourceOutputs => {
const aiToolConnections = sourceOutputs.ai_tool;
if (!aiToolConnections) return false;
return aiToolConnections.flat().some(conn => conn && conn.node === agentNode.name);
});
if (!hasToolConnected) {
result.warnings.push({ result.warnings.push({
type: 'warning', type: 'warning',
nodeId: agentNode.id, nodeId: agentNode.id,

View File

@@ -4665,4 +4665,223 @@ describe('WorkflowDiffEngine', () => {
expect(result.errors![0].message).toContain('executeWorkflowTrigger cannot activate workflows'); expect(result.errors![0].message).toContain('executeWorkflowTrigger cannot activate workflows');
}); });
}); });
// Issue #458: AI connection type propagation
describe('AI Connection Type Propagation (Issue #458)', () => {
it('should propagate ai_tool connection type when targetInput is not specified', async () => {
const workflowWithAI = {
...baseWorkflow,
nodes: [
{
id: 'agent1',
name: 'AI Agent',
type: '@n8n/n8n-nodes-langchain.agent',
typeVersion: 2.1,
position: [500, 300] as [number, number],
parameters: {}
},
{
id: 'tool1',
name: 'Calculator',
type: '@n8n/n8n-nodes-langchain.toolCalculator',
typeVersion: 1,
position: [300, 400] as [number, number],
parameters: {}
}
],
connections: {}
};
const operation: AddConnectionOperation = {
type: 'addConnection',
source: 'Calculator',
target: 'AI Agent',
sourceOutput: 'ai_tool'
// targetInput not specified - should default to sourceOutput ('ai_tool')
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithAI as Workflow, request);
expect(result.success).toBe(true);
expect(result.workflow.connections['Calculator']).toBeDefined();
expect(result.workflow.connections['Calculator']['ai_tool']).toBeDefined();
// The inner type should be 'ai_tool', NOT 'main'
expect(result.workflow.connections['Calculator']['ai_tool'][0][0].type).toBe('ai_tool');
expect(result.workflow.connections['Calculator']['ai_tool'][0][0].node).toBe('AI Agent');
});
it('should propagate ai_languageModel connection type', async () => {
const workflowWithAI = {
...baseWorkflow,
nodes: [
{
id: 'agent1',
name: 'AI Agent',
type: '@n8n/n8n-nodes-langchain.agent',
typeVersion: 2.1,
position: [500, 300] as [number, number],
parameters: {}
},
{
id: 'llm1',
name: 'OpenAI Chat Model',
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
typeVersion: 1.2,
position: [300, 200] as [number, number],
parameters: {}
}
],
connections: {}
};
const operation: AddConnectionOperation = {
type: 'addConnection',
source: 'OpenAI Chat Model',
target: 'AI Agent',
sourceOutput: 'ai_languageModel'
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithAI as Workflow, request);
expect(result.success).toBe(true);
expect(result.workflow.connections['OpenAI Chat Model']['ai_languageModel'][0][0].type).toBe('ai_languageModel');
});
it('should propagate ai_memory connection type', async () => {
const workflowWithAI = {
...baseWorkflow,
nodes: [
{
id: 'agent1',
name: 'AI Agent',
type: '@n8n/n8n-nodes-langchain.agent',
typeVersion: 2.1,
position: [500, 300] as [number, number],
parameters: {}
},
{
id: 'memory1',
name: 'Window Buffer Memory',
type: '@n8n/n8n-nodes-langchain.memoryBufferWindow',
typeVersion: 1.3,
position: [300, 500] as [number, number],
parameters: {}
}
],
connections: {}
};
const operation: AddConnectionOperation = {
type: 'addConnection',
source: 'Window Buffer Memory',
target: 'AI Agent',
sourceOutput: 'ai_memory'
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithAI as Workflow, request);
expect(result.success).toBe(true);
expect(result.workflow.connections['Window Buffer Memory']['ai_memory'][0][0].type).toBe('ai_memory');
});
it('should allow explicit targetInput override for mixed connection types', async () => {
const workflowWithNodes = {
...baseWorkflow,
nodes: [
{
id: 'node1',
name: 'Source Node',
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [300, 300] as [number, number],
parameters: {}
},
{
id: 'node2',
name: 'Target Node',
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [500, 300] as [number, number],
parameters: {}
}
],
connections: {}
};
const operation: AddConnectionOperation = {
type: 'addConnection',
source: 'Source Node',
target: 'Target Node',
sourceOutput: 'main',
targetInput: 'main' // Explicit override
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithNodes as Workflow, request);
expect(result.success).toBe(true);
expect(result.workflow.connections['Source Node']['main'][0][0].type).toBe('main');
});
it('should default to main for regular connections when sourceOutput is not specified', async () => {
const workflowWithNodes = {
...baseWorkflow,
nodes: [
{
id: 'node1',
name: 'Source Node',
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [300, 300] as [number, number],
parameters: {}
},
{
id: 'node2',
name: 'Target Node',
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [500, 300] as [number, number],
parameters: {}
}
],
connections: {}
};
const operation: AddConnectionOperation = {
type: 'addConnection',
source: 'Source Node',
target: 'Target Node'
// Neither sourceOutput nor targetInput specified - should default to 'main'
};
const request: WorkflowDiffRequest = {
id: 'test-workflow',
operations: [operation]
};
const result = await diffEngine.applyDiff(workflowWithNodes as Workflow, request);
expect(result.success).toBe(true);
expect(result.workflow.connections['Source Node']['main'][0][0].type).toBe('main');
});
});
}); });