Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.65.1] - 2026-07-16

### Fixed

- **Evaluation Trigger restored to the node database (#937).** `nodes-base.evaluationTrigger` was silently dropped during every database rebuild: the loader's node-name regex did not match n8n's enterprise `.node.ee.js` file suffix, and the export-resolution fallback picked the module's first export — for EvaluationTrigger a numeric constant, not the node class — so parsing failed and the node never reached the database. Agents could not discover the node via `search_nodes`/`get_node`, and `validate_workflow` falsely rejected valid evaluation workflows with "Unknown node type: n8n-nodes-base.evaluationTrigger". The regex now accepts the `.ee` segment, the fallback only ever resolves function-typed exports, and both `nodes-base.evaluation` and `nodes-base.evaluationTrigger` joined the canonical core-node completeness gate so a future silent drop fails the rebuild loudly. Rebuilt database: 827 core nodes (677 from `n8n-nodes-base` + 150 from `@n8n/n8n-nodes-langchain`).

## [2.65.0] - 2026-07-15

### Added
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
[![Docker](https://img.shields.io/badge/docker-ghcr.io%2Fczlonkowski%2Fn8n--mcp-green.svg)](https://github.com/czlonkowski/n8n-mcp/pkgs/container/n8n-mcp)
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/n8n-mcp?referralCode=n8n-mcp)

A Model Context Protocol (MCP) server that provides AI assistants with comprehensive access to n8n node documentation, properties, and operations. Deploy in minutes to give Claude and other AI assistants deep knowledge about n8n's 2,174 workflow automation nodes (826 core + 1,348 community).
A Model Context Protocol (MCP) server that provides AI assistants with comprehensive access to n8n node documentation, properties, and operations. Deploy in minutes to give Claude and other AI assistants deep knowledge about n8n's 2,175 workflow automation nodes (827 core + 1,348 community).

## Overview

n8n-MCP serves as a bridge between n8n's workflow automation platform and AI models, enabling them to understand and work with n8n nodes effectively. It provides structured access to:

- **2,174 n8n nodes** - 826 core nodes + 1,348 community nodes (1,195 verified)
- **2,175 n8n nodes** - 827 core nodes + 1,348 community nodes (1,195 verified)
- **Node properties** - 99% coverage with detailed schemas
- **Node operations** - 63.6% coverage of available actions
- **Documentation** - 87% coverage from official n8n docs (including AI nodes)
Expand Down
Binary file modified data/nodes.db
Binary file not shown.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "n8n-mcp",
"version": "2.65.0",
"version": "2.65.1",
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
30 changes: 23 additions & 7 deletions src/loaders/node-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,21 @@ export class N8nNodeLoader {
}
}

/**
* Resolve the node class from a module's exports: default export, then the
* export matching the node name, then the first function-typed export. The
* last step must skip non-function exports — EvaluationTrigger.node.ee.js
* exports the constant DEFAULT_STARTING_ROW before the class, and picking
* it silently dropped the node from the database (#937).
*/
private resolveNodeClass(nodeModule: any, nodeName: string): any {
return (
nodeModule.default ||
nodeModule[nodeName] ||
Object.values(nodeModule).find(exported => typeof exported === 'function')
);
}
Comment on lines +136 to +142

private async loadPackageNodes(packageName: string, packagePath: string, packageJson: any): Promise<LoadedNode[]> {
const n8nConfig = packageJson.n8n || {};
const nodes: LoadedNode[] = [];
Expand All @@ -142,12 +157,14 @@ export class N8nNodeLoader {
const fullPath = path.join(packageDir, nodePath);
const nodeModule = this.loadNodeModule(fullPath);

// Extract node name from path (e.g., "dist/nodes/Slack/Slack.node.js" -> "Slack")
const nodeNameMatch = nodePath.match(/\/([^\/]+)\.node\.(js|ts)$/);
const nodeName = nodeNameMatch ? nodeNameMatch[1] : path.basename(nodePath, '.node.js');
// Extract node name from path, including enterprise ".node.ee.js"
// files (e.g. "dist/nodes/Evaluation/EvaluationTrigger/EvaluationTrigger.node.ee.js" -> "EvaluationTrigger")
const nodeNameMatch = nodePath.match(/\/([^\/]+)\.node(?:\.ee)?\.(js|ts)$/);
const nodeName = nodeNameMatch
? nodeNameMatch[1]
: path.basename(nodePath).replace(/\.node(?:\.ee)?\.(js|ts)$/, '');

// Handle default export and various export patterns
const NodeClass = nodeModule.default || nodeModule[nodeName] || Object.values(nodeModule)[0];
const NodeClass = this.resolveNodeClass(nodeModule, nodeName);
if (NodeClass) {
nodes.push({ packageName, nodeName, NodeClass });
console.log(` ✓ Loaded ${nodeName} from ${packageName}`);
Expand All @@ -165,8 +182,7 @@ export class N8nNodeLoader {
const fullPath = path.join(packageDir, nodePath as string);
const nodeModule = this.loadNodeModule(fullPath);

// Handle default export and various export patterns
const NodeClass = nodeModule.default || nodeModule[nodeName] || Object.values(nodeModule)[0];
const NodeClass = this.resolveNodeClass(nodeModule, nodeName);
if (NodeClass) {
nodes.push({ packageName, nodeName, NodeClass });
console.log(` ✓ Loaded ${nodeName} from ${packageName}`);
Expand Down
2 changes: 2 additions & 0 deletions src/scripts/core-node-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
export const CANONICAL_CORE_NODES: readonly string[] = [
'nodes-base.code',
'nodes-base.convertToFile',
'nodes-base.evaluation',
'nodes-base.evaluationTrigger',
'nodes-base.executeWorkflow',
'nodes-base.extractFromFile',
'nodes-base.httpRequest',
Expand Down
130 changes: 130 additions & 0 deletions tests/unit/loaders/node-loader-ee-nodes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { describe, it, expect, vi, beforeAll, afterAll, beforeEach, afterEach, MockInstance } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { N8nNodeLoader } from '@/loaders/node-loader';

/**
* Regression tests for issue #937: nodes-base.evaluationTrigger was silently
* dropped from the database. Two compounding causes:
*
* 1. The node-name regex only matched `.node.js`/`.node.ts`, so enterprise
* `.node.ee.js` paths produced a garbage node name and the named-export
* lookup missed.
* 2. The export fallback took the FIRST module export, which for
* EvaluationTrigger is the constant DEFAULT_STARTING_ROW (a number), not
* the node class — so a number was passed downstream and parsing failed.
*
* These tests exercise the REAL loader against fixture packages on disk.
*/
describe('N8nNodeLoader enterprise (.node.ee.js) modules', () => {
let fixtureDir: string;
let consoleLogSpy: MockInstance;
let consoleErrorSpy: MockInstance;
let consoleWarnSpy: MockInstance;

const writeFixture = (relPath: string, content: string) => {
const fullPath = path.join(fixtureDir, relPath);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, content);
};

beforeAll(() => {
fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), 'n8n-mcp-loader-ee-fixture-'));

writeFixture('package.json', JSON.stringify({ name: 'fixture-pkg', version: '1.0.0' }));

// Mirrors n8n-nodes-base EvaluationTrigger.node.ee.js: a non-class export
// listed BEFORE the node class.
writeFixture(
'dist/nodes/Evaluation/EvaluationTrigger/EvaluationTrigger.node.ee.js',
`const DEFAULT_STARTING_ROW = 2;
class EvaluationTrigger {
constructor() {
this.description = { name: 'evaluationTrigger', displayName: 'Evaluation Trigger', properties: [] };
}
}
module.exports = { DEFAULT_STARTING_ROW, EvaluationTrigger };`
);

// Mirrors n8n-nodes-base Evaluation.node.ee.js: single class export.
writeFixture(
'dist/nodes/Evaluation/Evaluation/Evaluation.node.ee.js',
`class Evaluation {
constructor() {
this.description = { name: 'evaluation', displayName: 'Evaluation', properties: [] };
}
}
module.exports = { Evaluation };`
);

// A module whose export name matches neither the file name nor a default
// export, with a non-class export first: the fallback must still resolve
// the class, never a constant.
writeFixture(
'dist/nodes/Renamed/Renamed.node.js',
`const SOME_CONSTANT = 42;
class InternalName {
constructor() {
this.description = { name: 'renamed', displayName: 'Renamed', properties: [] };
}
}
module.exports = { SOME_CONSTANT, InternalName };`
);
});

afterAll(() => {
fs.rmSync(fixtureDir, { recursive: true, force: true });
});

beforeEach(() => {
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
});

afterEach(() => {
consoleLogSpy.mockRestore();
consoleErrorSpy.mockRestore();
consoleWarnSpy.mockRestore();
});

const loadFixturePackage = async (nodePaths: string[]) => {
const loader = new N8nNodeLoader();
const packageJson = { n8n: { nodes: nodePaths } };
return (loader as any).loadPackageNodes('fixture-pkg', fixtureDir, packageJson);
};

it('extracts the node name from a .node.ee.js path and resolves the class by name', async () => {
const results = await loadFixturePackage([
'dist/nodes/Evaluation/EvaluationTrigger/EvaluationTrigger.node.ee.js'
]);

expect(results).toHaveLength(1);
expect(results[0].nodeName).toBe('EvaluationTrigger');
expect(typeof results[0].NodeClass).toBe('function');
const instance = new results[0].NodeClass();
expect(instance.description.name).toBe('evaluationTrigger');
});

it('loads a single-export .node.ee.js module', async () => {
const results = await loadFixturePackage([
'dist/nodes/Evaluation/Evaluation/Evaluation.node.ee.js'
]);

expect(results).toHaveLength(1);
expect(results[0].nodeName).toBe('Evaluation');
const instance = new results[0].NodeClass();
expect(instance.description.name).toBe('evaluation');
});

it('never resolves a non-class export as the node class', async () => {
const results = await loadFixturePackage(['dist/nodes/Renamed/Renamed.node.js']);

expect(results).toHaveLength(1);
expect(results[0].nodeName).toBe('Renamed');
expect(typeof results[0].NodeClass).toBe('function');
const instance = new results[0].NodeClass();
expect(instance.description.name).toBe('renamed');
});
});
12 changes: 6 additions & 6 deletions tests/unit/loaders/node-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,10 @@ describe('N8nNodeLoader', () => {
const fullPath = mockRequireResolve(`${packagePath}/${nodePath}`);
const nodeModule = mockRequire(fullPath);

const nodeNameMatch = nodePath.match(/\/([^\/]+)\.node\.(js|ts)$/);
const nodeName = nodeNameMatch ? nodeNameMatch[1] : nodePath.replace(/.*\//, '').replace(/\.node\.(js|ts)$/, '');
const NodeClass = nodeModule.default || nodeModule[nodeName] || Object.values(nodeModule)[0];
const nodeNameMatch = nodePath.match(/\/([^\/]+)\.node(?:\.ee)?\.(js|ts)$/);
const nodeName = nodeNameMatch ? nodeNameMatch[1] : nodePath.replace(/.*\//, '').replace(/\.node(?:\.ee)?\.(js|ts)$/, '');

const NodeClass = nodeModule.default || nodeModule[nodeName] || Object.values(nodeModule).find((v: any) => typeof v === 'function');
if (NodeClass) {
nodes.push({ packageName, nodeName, NodeClass });
console.log(` ✓ Loaded ${nodeName} from ${packageName}`);
Expand All @@ -107,8 +107,8 @@ describe('N8nNodeLoader', () => {
try {
const fullPath = mockRequireResolve(`${packagePath}/${nodePath as string}`);
const nodeModule = mockRequire(fullPath);
const NodeClass = nodeModule.default || nodeModule[nodeName] || Object.values(nodeModule)[0];

const NodeClass = nodeModule.default || nodeModule[nodeName] || Object.values(nodeModule).find((v: any) => typeof v === 'function');
if (NodeClass) {
nodes.push({ packageName, nodeName, NodeClass });
console.log(` ✓ Loaded ${nodeName} from ${packageName}`);
Expand Down
Loading