mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-29 22:12:05 +00:00
- Updated n8n from 2.2.3 to 2.3.3 - Updated n8n-core from 2.2.2 to 2.3.2 - Updated n8n-workflow from 2.2.2 to 2.3.2 - Updated @n8n/n8n-nodes-langchain from 2.2.2 to 2.3.2 - Rebuilt node database with 537 nodes (434 from n8n-nodes-base, 103 from @n8n/n8n-nodes-langchain) - Updated README badge with new n8n version - 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.5 <noreply@anthropic.com>
2914 lines
129 KiB
JavaScript
2914 lines
129 KiB
JavaScript
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.N8NDocumentationMCPServer = void 0;
|
|
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
const fs_1 = require("fs");
|
|
const path_1 = __importDefault(require("path"));
|
|
const tools_1 = require("./tools");
|
|
const tools_n8n_manager_1 = require("./tools-n8n-manager");
|
|
const tools_n8n_friendly_1 = require("./tools-n8n-friendly");
|
|
const workflow_examples_1 = require("./workflow-examples");
|
|
const logger_1 = require("../utils/logger");
|
|
const node_repository_1 = require("../database/node-repository");
|
|
const database_adapter_1 = require("../database/database-adapter");
|
|
const property_filter_1 = require("../services/property-filter");
|
|
const task_templates_1 = require("../services/task-templates");
|
|
const config_validator_1 = require("../services/config-validator");
|
|
const enhanced_config_validator_1 = require("../services/enhanced-config-validator");
|
|
const property_dependencies_1 = require("../services/property-dependencies");
|
|
const type_structure_service_1 = require("../services/type-structure-service");
|
|
const simple_cache_1 = require("../utils/simple-cache");
|
|
const template_service_1 = require("../templates/template-service");
|
|
const workflow_validator_1 = require("../services/workflow-validator");
|
|
const n8n_api_1 = require("../config/n8n-api");
|
|
const n8nHandlers = __importStar(require("./handlers-n8n-manager"));
|
|
const handlers_workflow_diff_1 = require("./handlers-workflow-diff");
|
|
const tools_documentation_1 = require("./tools-documentation");
|
|
const version_1 = require("../utils/version");
|
|
const node_utils_1 = require("../utils/node-utils");
|
|
const node_type_normalizer_1 = require("../utils/node-type-normalizer");
|
|
const validation_schemas_1 = require("../utils/validation-schemas");
|
|
const protocol_version_1 = require("../utils/protocol-version");
|
|
const telemetry_1 = require("../telemetry");
|
|
const startup_checkpoints_1 = require("../telemetry/startup-checkpoints");
|
|
class N8NDocumentationMCPServer {
|
|
constructor(instanceContext, earlyLogger) {
|
|
this.db = null;
|
|
this.repository = null;
|
|
this.templateService = null;
|
|
this.cache = new simple_cache_1.SimpleCache();
|
|
this.clientInfo = null;
|
|
this.previousTool = null;
|
|
this.previousToolTimestamp = Date.now();
|
|
this.earlyLogger = null;
|
|
this.disabledToolsCache = null;
|
|
this.dbHealthChecked = false;
|
|
this.instanceContext = instanceContext;
|
|
this.earlyLogger = earlyLogger || null;
|
|
const envDbPath = process.env.NODE_DB_PATH;
|
|
let dbPath = null;
|
|
let possiblePaths = [];
|
|
if (envDbPath && (envDbPath === ':memory:' || (0, fs_1.existsSync)(envDbPath))) {
|
|
dbPath = envDbPath;
|
|
}
|
|
else {
|
|
possiblePaths = [
|
|
path_1.default.join(process.cwd(), 'data', 'nodes.db'),
|
|
path_1.default.join(__dirname, '../../data', 'nodes.db'),
|
|
'./data/nodes.db'
|
|
];
|
|
for (const p of possiblePaths) {
|
|
if ((0, fs_1.existsSync)(p)) {
|
|
dbPath = p;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!dbPath) {
|
|
logger_1.logger.error('Database not found in any of the expected locations:', possiblePaths);
|
|
throw new Error('Database nodes.db not found. Please run npm run rebuild first.');
|
|
}
|
|
this.initialized = this.initializeDatabase(dbPath).then(() => {
|
|
if (this.earlyLogger) {
|
|
this.earlyLogger.logCheckpoint(startup_checkpoints_1.STARTUP_CHECKPOINTS.N8N_API_CHECKING);
|
|
}
|
|
const apiConfigured = (0, n8n_api_1.isN8nApiConfigured)();
|
|
const totalTools = apiConfigured ?
|
|
tools_1.n8nDocumentationToolsFinal.length + tools_n8n_manager_1.n8nManagementTools.length :
|
|
tools_1.n8nDocumentationToolsFinal.length;
|
|
logger_1.logger.info(`MCP server initialized with ${totalTools} tools (n8n API: ${apiConfigured ? 'configured' : 'not configured'})`);
|
|
if (this.earlyLogger) {
|
|
this.earlyLogger.logCheckpoint(startup_checkpoints_1.STARTUP_CHECKPOINTS.N8N_API_READY);
|
|
}
|
|
});
|
|
logger_1.logger.info('Initializing n8n Documentation MCP server');
|
|
this.server = new index_js_1.Server({
|
|
name: 'n8n-documentation-mcp',
|
|
version: version_1.PROJECT_VERSION,
|
|
icons: [
|
|
{
|
|
src: "https://www.n8n-mcp.com/logo.png",
|
|
mimeType: "image/png",
|
|
sizes: ["192x192"]
|
|
},
|
|
{
|
|
src: "https://www.n8n-mcp.com/logo-128.png",
|
|
mimeType: "image/png",
|
|
sizes: ["128x128"]
|
|
},
|
|
{
|
|
src: "https://www.n8n-mcp.com/logo-48.png",
|
|
mimeType: "image/png",
|
|
sizes: ["48x48"]
|
|
}
|
|
],
|
|
websiteUrl: "https://n8n-mcp.com"
|
|
}, {
|
|
capabilities: {
|
|
tools: {},
|
|
},
|
|
});
|
|
this.setupHandlers();
|
|
}
|
|
async close() {
|
|
try {
|
|
await this.server.close();
|
|
this.cache.destroy();
|
|
if (this.db) {
|
|
try {
|
|
this.db.close();
|
|
}
|
|
catch (dbError) {
|
|
logger_1.logger.warn('Error closing database', {
|
|
error: dbError instanceof Error ? dbError.message : String(dbError)
|
|
});
|
|
}
|
|
}
|
|
this.db = null;
|
|
this.repository = null;
|
|
this.templateService = null;
|
|
this.earlyLogger = null;
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.warn('Error closing MCP server', { error: error instanceof Error ? error.message : String(error) });
|
|
}
|
|
}
|
|
async initializeDatabase(dbPath) {
|
|
try {
|
|
if (this.earlyLogger) {
|
|
this.earlyLogger.logCheckpoint(startup_checkpoints_1.STARTUP_CHECKPOINTS.DATABASE_CONNECTING);
|
|
}
|
|
logger_1.logger.debug('Database initialization starting...', { dbPath });
|
|
this.db = await (0, database_adapter_1.createDatabaseAdapter)(dbPath);
|
|
logger_1.logger.debug('Database adapter created');
|
|
if (dbPath === ':memory:') {
|
|
await this.initializeInMemorySchema();
|
|
logger_1.logger.debug('In-memory schema initialized');
|
|
}
|
|
this.repository = new node_repository_1.NodeRepository(this.db);
|
|
logger_1.logger.debug('Node repository initialized');
|
|
this.templateService = new template_service_1.TemplateService(this.db);
|
|
logger_1.logger.debug('Template service initialized');
|
|
enhanced_config_validator_1.EnhancedConfigValidator.initializeSimilarityServices(this.repository);
|
|
logger_1.logger.debug('Similarity services initialized');
|
|
if (this.earlyLogger) {
|
|
this.earlyLogger.logCheckpoint(startup_checkpoints_1.STARTUP_CHECKPOINTS.DATABASE_CONNECTED);
|
|
}
|
|
logger_1.logger.info(`Database initialized successfully from: ${dbPath}`);
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.error('Failed to initialize database:', error);
|
|
throw new Error(`Failed to open database: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
}
|
|
}
|
|
async initializeInMemorySchema() {
|
|
if (!this.db)
|
|
return;
|
|
const schemaPath = path_1.default.join(__dirname, '../../src/database/schema.sql');
|
|
const schema = await fs_1.promises.readFile(schemaPath, 'utf-8');
|
|
const statements = this.parseSQLStatements(schema);
|
|
for (const statement of statements) {
|
|
if (statement.trim()) {
|
|
try {
|
|
this.db.exec(statement);
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.error(`Failed to execute SQL statement: ${statement.substring(0, 100)}...`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
parseSQLStatements(sql) {
|
|
const statements = [];
|
|
let current = '';
|
|
let inBlock = false;
|
|
const lines = sql.split('\n');
|
|
for (const line of lines) {
|
|
const trimmed = line.trim().toUpperCase();
|
|
if (trimmed.startsWith('--') || trimmed === '') {
|
|
continue;
|
|
}
|
|
if (trimmed.includes('BEGIN')) {
|
|
inBlock = true;
|
|
}
|
|
current += line + '\n';
|
|
if (inBlock && trimmed === 'END;') {
|
|
statements.push(current.trim());
|
|
current = '';
|
|
inBlock = false;
|
|
continue;
|
|
}
|
|
if (!inBlock && trimmed.endsWith(';')) {
|
|
statements.push(current.trim());
|
|
current = '';
|
|
}
|
|
}
|
|
if (current.trim()) {
|
|
statements.push(current.trim());
|
|
}
|
|
return statements.filter(s => s.length > 0);
|
|
}
|
|
async ensureInitialized() {
|
|
await this.initialized;
|
|
if (!this.db || !this.repository) {
|
|
throw new Error('Database not initialized');
|
|
}
|
|
if (!this.dbHealthChecked) {
|
|
await this.validateDatabaseHealth();
|
|
this.dbHealthChecked = true;
|
|
}
|
|
}
|
|
async validateDatabaseHealth() {
|
|
if (!this.db)
|
|
return;
|
|
try {
|
|
const nodeCount = this.db.prepare('SELECT COUNT(*) as count FROM nodes').get();
|
|
if (nodeCount.count === 0) {
|
|
logger_1.logger.error('CRITICAL: Database is empty - no nodes found! Please run: npm run rebuild');
|
|
throw new Error('Database is empty. Run "npm run rebuild" to populate node data.');
|
|
}
|
|
try {
|
|
const ftsExists = this.db.prepare(`
|
|
SELECT name FROM sqlite_master
|
|
WHERE type='table' AND name='nodes_fts'
|
|
`).get();
|
|
if (!ftsExists) {
|
|
logger_1.logger.warn('FTS5 table missing - search performance will be degraded. Please run: npm run rebuild');
|
|
}
|
|
else {
|
|
const ftsCount = this.db.prepare('SELECT COUNT(*) as count FROM nodes_fts').get();
|
|
if (ftsCount.count === 0) {
|
|
logger_1.logger.warn('FTS5 index is empty - search will not work properly. Please run: npm run rebuild');
|
|
}
|
|
}
|
|
}
|
|
catch (ftsError) {
|
|
logger_1.logger.warn('FTS5 not available - using fallback search. For better performance, ensure better-sqlite3 is properly installed.');
|
|
}
|
|
logger_1.logger.info(`Database health check passed: ${nodeCount.count} nodes loaded`);
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.error('Database health check failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
getDisabledTools() {
|
|
if (this.disabledToolsCache !== null) {
|
|
return this.disabledToolsCache;
|
|
}
|
|
let disabledToolsEnv = process.env.DISABLED_TOOLS || '';
|
|
if (!disabledToolsEnv) {
|
|
this.disabledToolsCache = new Set();
|
|
return this.disabledToolsCache;
|
|
}
|
|
if (disabledToolsEnv.length > 10000) {
|
|
logger_1.logger.warn(`DISABLED_TOOLS environment variable too long (${disabledToolsEnv.length} chars), truncating to 10000`);
|
|
disabledToolsEnv = disabledToolsEnv.substring(0, 10000);
|
|
}
|
|
let tools = disabledToolsEnv
|
|
.split(',')
|
|
.map(t => t.trim())
|
|
.filter(Boolean);
|
|
if (tools.length > 200) {
|
|
logger_1.logger.warn(`DISABLED_TOOLS contains ${tools.length} tools, limiting to first 200`);
|
|
tools = tools.slice(0, 200);
|
|
}
|
|
if (tools.length > 0) {
|
|
logger_1.logger.info(`Disabled tools configured: ${tools.join(', ')}`);
|
|
}
|
|
this.disabledToolsCache = new Set(tools);
|
|
return this.disabledToolsCache;
|
|
}
|
|
setupHandlers() {
|
|
this.server.setRequestHandler(types_js_1.InitializeRequestSchema, async (request) => {
|
|
const clientVersion = request.params.protocolVersion;
|
|
const clientCapabilities = request.params.capabilities;
|
|
const clientInfo = request.params.clientInfo;
|
|
logger_1.logger.info('MCP Initialize request received', {
|
|
clientVersion,
|
|
clientCapabilities,
|
|
clientInfo
|
|
});
|
|
telemetry_1.telemetry.trackSessionStart();
|
|
this.clientInfo = clientInfo;
|
|
const negotiationResult = (0, protocol_version_1.negotiateProtocolVersion)(clientVersion, clientInfo, undefined, undefined);
|
|
(0, protocol_version_1.logProtocolNegotiation)(negotiationResult, logger_1.logger, 'MCP_INITIALIZE');
|
|
if (clientVersion && clientVersion !== negotiationResult.version) {
|
|
logger_1.logger.warn(`Protocol version negotiated: client requested ${clientVersion}, server will use ${negotiationResult.version}`, {
|
|
reasoning: negotiationResult.reasoning
|
|
});
|
|
}
|
|
const response = {
|
|
protocolVersion: negotiationResult.version,
|
|
capabilities: {
|
|
tools: {},
|
|
},
|
|
serverInfo: {
|
|
name: 'n8n-documentation-mcp',
|
|
version: version_1.PROJECT_VERSION,
|
|
},
|
|
};
|
|
logger_1.logger.info('MCP Initialize response', { response });
|
|
return response;
|
|
});
|
|
this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async (request) => {
|
|
const disabledTools = this.getDisabledTools();
|
|
const enabledDocTools = tools_1.n8nDocumentationToolsFinal.filter(tool => !disabledTools.has(tool.name));
|
|
let tools = [...enabledDocTools];
|
|
const hasEnvConfig = (0, n8n_api_1.isN8nApiConfigured)();
|
|
const hasInstanceConfig = !!(this.instanceContext?.n8nApiUrl && this.instanceContext?.n8nApiKey);
|
|
const isMultiTenantEnabled = process.env.ENABLE_MULTI_TENANT === 'true';
|
|
const shouldIncludeManagementTools = hasEnvConfig || hasInstanceConfig || isMultiTenantEnabled;
|
|
if (shouldIncludeManagementTools) {
|
|
const enabledMgmtTools = tools_n8n_manager_1.n8nManagementTools.filter(tool => !disabledTools.has(tool.name));
|
|
tools.push(...enabledMgmtTools);
|
|
logger_1.logger.debug(`Tool listing: ${tools.length} tools available (${enabledDocTools.length} documentation + ${enabledMgmtTools.length} management)`, {
|
|
hasEnvConfig,
|
|
hasInstanceConfig,
|
|
isMultiTenantEnabled,
|
|
disabledToolsCount: disabledTools.size
|
|
});
|
|
}
|
|
else {
|
|
logger_1.logger.debug(`Tool listing: ${tools.length} tools available (documentation only)`, {
|
|
hasEnvConfig,
|
|
hasInstanceConfig,
|
|
isMultiTenantEnabled,
|
|
disabledToolsCount: disabledTools.size
|
|
});
|
|
}
|
|
if (disabledTools.size > 0) {
|
|
const totalAvailableTools = tools_1.n8nDocumentationToolsFinal.length + (shouldIncludeManagementTools ? tools_n8n_manager_1.n8nManagementTools.length : 0);
|
|
logger_1.logger.debug(`Filtered ${disabledTools.size} disabled tools, ${tools.length}/${totalAvailableTools} tools available`);
|
|
}
|
|
const clientInfo = this.clientInfo;
|
|
const isN8nClient = clientInfo?.name?.includes('n8n') ||
|
|
clientInfo?.name?.includes('langchain');
|
|
if (isN8nClient) {
|
|
logger_1.logger.info('Detected n8n client, using n8n-friendly tool descriptions');
|
|
tools = (0, tools_n8n_friendly_1.makeToolsN8nFriendly)(tools);
|
|
}
|
|
const validationTools = tools.filter(t => t.name.startsWith('validate_'));
|
|
validationTools.forEach(tool => {
|
|
logger_1.logger.info('Validation tool schema', {
|
|
toolName: tool.name,
|
|
inputSchema: JSON.stringify(tool.inputSchema, null, 2),
|
|
hasOutputSchema: !!tool.outputSchema,
|
|
description: tool.description
|
|
});
|
|
});
|
|
return { tools };
|
|
});
|
|
this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
const { name, arguments: args } = request.params;
|
|
logger_1.logger.info('Tool call received - DETAILED DEBUG', {
|
|
toolName: name,
|
|
arguments: JSON.stringify(args, null, 2),
|
|
argumentsType: typeof args,
|
|
argumentsKeys: args ? Object.keys(args) : [],
|
|
hasNodeType: args && 'nodeType' in args,
|
|
hasConfig: args && 'config' in args,
|
|
configType: args && args.config ? typeof args.config : 'N/A',
|
|
rawRequest: JSON.stringify(request.params)
|
|
});
|
|
const disabledTools = this.getDisabledTools();
|
|
if (disabledTools.has(name)) {
|
|
logger_1.logger.warn(`Attempted to call disabled tool: ${name}`);
|
|
return {
|
|
content: [{
|
|
type: 'text',
|
|
text: JSON.stringify({
|
|
error: 'TOOL_DISABLED',
|
|
message: `Tool '${name}' is not available in this deployment. It has been disabled via DISABLED_TOOLS environment variable.`,
|
|
tool: name
|
|
}, null, 2)
|
|
}]
|
|
};
|
|
}
|
|
let processedArgs = args;
|
|
if (args && typeof args === 'object' && 'output' in args) {
|
|
try {
|
|
const possibleNestedData = args.output;
|
|
if (typeof possibleNestedData === 'string' && possibleNestedData.trim().startsWith('{')) {
|
|
const parsed = JSON.parse(possibleNestedData);
|
|
if (parsed && typeof parsed === 'object') {
|
|
logger_1.logger.warn('Detected n8n nested output bug, attempting to extract actual arguments', {
|
|
originalArgs: args,
|
|
extractedArgs: parsed
|
|
});
|
|
if (this.validateExtractedArgs(name, parsed)) {
|
|
processedArgs = parsed;
|
|
}
|
|
else {
|
|
logger_1.logger.warn('Extracted arguments failed validation, using original args', {
|
|
toolName: name,
|
|
extractedArgs: parsed
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (parseError) {
|
|
logger_1.logger.debug('Failed to parse nested output, continuing with original args', {
|
|
error: parseError instanceof Error ? parseError.message : String(parseError)
|
|
});
|
|
}
|
|
}
|
|
try {
|
|
logger_1.logger.debug(`Executing tool: ${name}`, { args: processedArgs });
|
|
const startTime = Date.now();
|
|
const result = await this.executeTool(name, processedArgs);
|
|
const duration = Date.now() - startTime;
|
|
logger_1.logger.debug(`Tool ${name} executed successfully`);
|
|
telemetry_1.telemetry.trackToolUsage(name, true, duration);
|
|
if (this.previousTool) {
|
|
const timeDelta = Date.now() - this.previousToolTimestamp;
|
|
telemetry_1.telemetry.trackToolSequence(this.previousTool, name, timeDelta);
|
|
}
|
|
this.previousTool = name;
|
|
this.previousToolTimestamp = Date.now();
|
|
let responseText;
|
|
let structuredContent = null;
|
|
try {
|
|
if (name.startsWith('validate_') && typeof result === 'object' && result !== null) {
|
|
const cleanResult = this.sanitizeValidationResult(result, name);
|
|
structuredContent = cleanResult;
|
|
responseText = JSON.stringify(cleanResult, null, 2);
|
|
}
|
|
else {
|
|
responseText = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
}
|
|
}
|
|
catch (jsonError) {
|
|
logger_1.logger.warn(`Failed to stringify tool result for ${name}:`, jsonError);
|
|
responseText = String(result);
|
|
}
|
|
if (responseText.length > 1000000) {
|
|
logger_1.logger.warn(`Tool ${name} response is very large (${responseText.length} chars), truncating`);
|
|
responseText = responseText.substring(0, 999000) + '\n\n[Response truncated due to size limits]';
|
|
structuredContent = null;
|
|
}
|
|
const mcpResponse = {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: responseText,
|
|
},
|
|
],
|
|
};
|
|
if (name.startsWith('validate_') && structuredContent !== null) {
|
|
mcpResponse.structuredContent = structuredContent;
|
|
}
|
|
return mcpResponse;
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.error(`Error executing tool ${name}`, error);
|
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
telemetry_1.telemetry.trackToolUsage(name, false);
|
|
telemetry_1.telemetry.trackError(error instanceof Error ? error.constructor.name : 'UnknownError', `tool_execution`, name, errorMessage);
|
|
if (this.previousTool) {
|
|
const timeDelta = Date.now() - this.previousToolTimestamp;
|
|
telemetry_1.telemetry.trackToolSequence(this.previousTool, name, timeDelta);
|
|
}
|
|
this.previousTool = name;
|
|
this.previousToolTimestamp = Date.now();
|
|
let helpfulMessage = `Error executing tool ${name}: ${errorMessage}`;
|
|
if (errorMessage.includes('required') || errorMessage.includes('missing')) {
|
|
helpfulMessage += '\n\nNote: This error often occurs when the AI agent sends incomplete or incorrectly formatted parameters. Please ensure all required fields are provided with the correct types.';
|
|
}
|
|
else if (errorMessage.includes('type') || errorMessage.includes('expected')) {
|
|
helpfulMessage += '\n\nNote: This error indicates a type mismatch. The AI agent may be sending data in the wrong format (e.g., string instead of object).';
|
|
}
|
|
else if (errorMessage.includes('Unknown category') || errorMessage.includes('not found')) {
|
|
helpfulMessage += '\n\nNote: The requested resource or category was not found. Please check the available options.';
|
|
}
|
|
if (name.startsWith('validate_') && (errorMessage.includes('config') || errorMessage.includes('nodeType'))) {
|
|
helpfulMessage += '\n\nFor validation tools:\n- nodeType should be a string (e.g., "nodes-base.webhook")\n- config should be an object (e.g., {})';
|
|
}
|
|
return {
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: helpfulMessage,
|
|
},
|
|
],
|
|
isError: true,
|
|
};
|
|
}
|
|
});
|
|
}
|
|
sanitizeValidationResult(result, toolName) {
|
|
if (!result || typeof result !== 'object') {
|
|
return result;
|
|
}
|
|
const sanitized = { ...result };
|
|
if (toolName === 'validate_node_minimal') {
|
|
const filtered = {
|
|
nodeType: String(sanitized.nodeType || ''),
|
|
displayName: String(sanitized.displayName || ''),
|
|
valid: Boolean(sanitized.valid),
|
|
missingRequiredFields: Array.isArray(sanitized.missingRequiredFields)
|
|
? sanitized.missingRequiredFields.map(String)
|
|
: []
|
|
};
|
|
return filtered;
|
|
}
|
|
else if (toolName === 'validate_node_operation') {
|
|
let summary = sanitized.summary;
|
|
if (!summary || typeof summary !== 'object') {
|
|
summary = {
|
|
hasErrors: Array.isArray(sanitized.errors) ? sanitized.errors.length > 0 : false,
|
|
errorCount: Array.isArray(sanitized.errors) ? sanitized.errors.length : 0,
|
|
warningCount: Array.isArray(sanitized.warnings) ? sanitized.warnings.length : 0,
|
|
suggestionCount: Array.isArray(sanitized.suggestions) ? sanitized.suggestions.length : 0
|
|
};
|
|
}
|
|
const filtered = {
|
|
nodeType: String(sanitized.nodeType || ''),
|
|
workflowNodeType: String(sanitized.workflowNodeType || sanitized.nodeType || ''),
|
|
displayName: String(sanitized.displayName || ''),
|
|
valid: Boolean(sanitized.valid),
|
|
errors: Array.isArray(sanitized.errors) ? sanitized.errors : [],
|
|
warnings: Array.isArray(sanitized.warnings) ? sanitized.warnings : [],
|
|
suggestions: Array.isArray(sanitized.suggestions) ? sanitized.suggestions : [],
|
|
summary: summary
|
|
};
|
|
return filtered;
|
|
}
|
|
else if (toolName.startsWith('validate_workflow')) {
|
|
sanitized.valid = Boolean(sanitized.valid);
|
|
sanitized.errors = Array.isArray(sanitized.errors) ? sanitized.errors : [];
|
|
sanitized.warnings = Array.isArray(sanitized.warnings) ? sanitized.warnings : [];
|
|
if (toolName === 'validate_workflow') {
|
|
if (!sanitized.summary || typeof sanitized.summary !== 'object') {
|
|
sanitized.summary = {
|
|
totalNodes: 0,
|
|
enabledNodes: 0,
|
|
triggerNodes: 0,
|
|
validConnections: 0,
|
|
invalidConnections: 0,
|
|
expressionsValidated: 0,
|
|
errorCount: sanitized.errors.length,
|
|
warningCount: sanitized.warnings.length
|
|
};
|
|
}
|
|
}
|
|
else {
|
|
if (!sanitized.statistics || typeof sanitized.statistics !== 'object') {
|
|
sanitized.statistics = {
|
|
totalNodes: 0,
|
|
triggerNodes: 0,
|
|
validConnections: 0,
|
|
invalidConnections: 0,
|
|
expressionsValidated: 0
|
|
};
|
|
}
|
|
}
|
|
}
|
|
return JSON.parse(JSON.stringify(sanitized));
|
|
}
|
|
validateToolParams(toolName, args, legacyRequiredParams) {
|
|
try {
|
|
let validationResult;
|
|
switch (toolName) {
|
|
case 'validate_node':
|
|
validationResult = validation_schemas_1.ToolValidation.validateNodeOperation(args);
|
|
break;
|
|
case 'validate_workflow':
|
|
validationResult = validation_schemas_1.ToolValidation.validateWorkflow(args);
|
|
break;
|
|
case 'search_nodes':
|
|
validationResult = validation_schemas_1.ToolValidation.validateSearchNodes(args);
|
|
break;
|
|
case 'n8n_create_workflow':
|
|
validationResult = validation_schemas_1.ToolValidation.validateCreateWorkflow(args);
|
|
break;
|
|
case 'n8n_get_workflow':
|
|
case 'n8n_update_full_workflow':
|
|
case 'n8n_delete_workflow':
|
|
case 'n8n_validate_workflow':
|
|
case 'n8n_autofix_workflow':
|
|
validationResult = validation_schemas_1.ToolValidation.validateWorkflowId(args);
|
|
break;
|
|
case 'n8n_executions':
|
|
validationResult = args.action
|
|
? { valid: true, errors: [] }
|
|
: { valid: false, errors: [{ field: 'action', message: 'action is required' }] };
|
|
break;
|
|
case 'n8n_deploy_template':
|
|
validationResult = args.templateId !== undefined
|
|
? { valid: true, errors: [] }
|
|
: { valid: false, errors: [{ field: 'templateId', message: 'templateId is required' }] };
|
|
break;
|
|
default:
|
|
return this.validateToolParamsBasic(toolName, args, legacyRequiredParams || []);
|
|
}
|
|
if (!validationResult.valid) {
|
|
const errorMessage = validation_schemas_1.Validator.formatErrors(validationResult, toolName);
|
|
logger_1.logger.error(`Parameter validation failed for ${toolName}:`, errorMessage);
|
|
throw new validation_schemas_1.ValidationError(errorMessage);
|
|
}
|
|
}
|
|
catch (error) {
|
|
if (error instanceof validation_schemas_1.ValidationError) {
|
|
throw error;
|
|
}
|
|
logger_1.logger.error(`Validation system error for ${toolName}:`, error);
|
|
const errorMessage = error instanceof Error
|
|
? `Internal validation error: ${error.message}`
|
|
: `Internal validation error while processing ${toolName}`;
|
|
throw new Error(errorMessage);
|
|
}
|
|
}
|
|
validateToolParamsBasic(toolName, args, requiredParams) {
|
|
const missing = [];
|
|
const invalid = [];
|
|
for (const param of requiredParams) {
|
|
if (!(param in args) || args[param] === undefined || args[param] === null) {
|
|
missing.push(param);
|
|
}
|
|
else if (typeof args[param] === 'string' && args[param].trim() === '') {
|
|
invalid.push(`${param} (empty string)`);
|
|
}
|
|
}
|
|
if (missing.length > 0) {
|
|
throw new Error(`Missing required parameters for ${toolName}: ${missing.join(', ')}. Please provide the required parameters to use this tool.`);
|
|
}
|
|
if (invalid.length > 0) {
|
|
throw new Error(`Invalid parameters for ${toolName}: ${invalid.join(', ')}. String parameters cannot be empty.`);
|
|
}
|
|
}
|
|
validateExtractedArgs(toolName, args) {
|
|
if (!args || typeof args !== 'object') {
|
|
return false;
|
|
}
|
|
const allTools = [...tools_1.n8nDocumentationToolsFinal, ...tools_n8n_manager_1.n8nManagementTools];
|
|
const tool = allTools.find(t => t.name === toolName);
|
|
if (!tool || !tool.inputSchema) {
|
|
return true;
|
|
}
|
|
const schema = tool.inputSchema;
|
|
const required = schema.required || [];
|
|
const properties = schema.properties || {};
|
|
for (const requiredField of required) {
|
|
if (!(requiredField in args)) {
|
|
logger_1.logger.debug(`Extracted args missing required field: ${requiredField}`, {
|
|
toolName,
|
|
extractedArgs: args,
|
|
required
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
for (const [fieldName, fieldValue] of Object.entries(args)) {
|
|
if (properties[fieldName]) {
|
|
const expectedType = properties[fieldName].type;
|
|
const actualType = Array.isArray(fieldValue) ? 'array' : typeof fieldValue;
|
|
if (expectedType && expectedType !== actualType) {
|
|
if (expectedType === 'number' && actualType === 'string' && !isNaN(Number(fieldValue))) {
|
|
continue;
|
|
}
|
|
logger_1.logger.debug(`Extracted args field type mismatch: ${fieldName}`, {
|
|
toolName,
|
|
expectedType,
|
|
actualType,
|
|
fieldValue
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
if (schema.additionalProperties === false) {
|
|
const allowedFields = Object.keys(properties);
|
|
const extraFields = Object.keys(args).filter(field => !allowedFields.includes(field));
|
|
if (extraFields.length > 0) {
|
|
logger_1.logger.debug(`Extracted args have extra fields`, {
|
|
toolName,
|
|
extraFields,
|
|
allowedFields
|
|
});
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
async executeTool(name, args) {
|
|
args = args || {};
|
|
const disabledTools = this.getDisabledTools();
|
|
if (disabledTools.has(name)) {
|
|
throw new Error(`Tool '${name}' is disabled via DISABLED_TOOLS environment variable`);
|
|
}
|
|
logger_1.logger.info(`Tool execution: ${name}`, {
|
|
args: typeof args === 'object' ? JSON.stringify(args) : args,
|
|
argsType: typeof args,
|
|
argsKeys: typeof args === 'object' ? Object.keys(args) : 'not-object'
|
|
});
|
|
if (typeof args !== 'object' || args === null) {
|
|
throw new Error(`Invalid arguments for tool ${name}: expected object, got ${typeof args}`);
|
|
}
|
|
switch (name) {
|
|
case 'tools_documentation':
|
|
return this.getToolsDocumentation(args.topic, args.depth);
|
|
case 'search_nodes':
|
|
this.validateToolParams(name, args, ['query']);
|
|
const limit = args.limit !== undefined ? Number(args.limit) || 20 : 20;
|
|
return this.searchNodes(args.query, limit, {
|
|
mode: args.mode,
|
|
includeExamples: args.includeExamples,
|
|
source: args.source
|
|
});
|
|
case 'get_node':
|
|
this.validateToolParams(name, args, ['nodeType']);
|
|
if (args.mode === 'docs') {
|
|
return this.getNodeDocumentation(args.nodeType);
|
|
}
|
|
if (args.mode === 'search_properties') {
|
|
if (!args.propertyQuery) {
|
|
throw new Error('propertyQuery is required for mode=search_properties');
|
|
}
|
|
const maxResults = args.maxPropertyResults !== undefined ? Number(args.maxPropertyResults) || 20 : 20;
|
|
return this.searchNodeProperties(args.nodeType, args.propertyQuery, maxResults);
|
|
}
|
|
return this.getNode(args.nodeType, args.detail, args.mode, args.includeTypeInfo, args.includeExamples, args.fromVersion, args.toVersion);
|
|
case 'validate_node':
|
|
this.validateToolParams(name, args, ['nodeType', 'config']);
|
|
if (typeof args.config !== 'object' || args.config === null) {
|
|
logger_1.logger.warn(`validate_node called with invalid config type: ${typeof args.config}`);
|
|
const validationMode = args.mode || 'full';
|
|
if (validationMode === 'minimal') {
|
|
return {
|
|
nodeType: args.nodeType || 'unknown',
|
|
displayName: 'Unknown Node',
|
|
valid: false,
|
|
missingRequiredFields: [
|
|
'Invalid config format - expected object',
|
|
'🔧 RECOVERY: Use format { "resource": "...", "operation": "..." } or {} for empty config'
|
|
]
|
|
};
|
|
}
|
|
return {
|
|
nodeType: args.nodeType || 'unknown',
|
|
workflowNodeType: args.nodeType || 'unknown',
|
|
displayName: 'Unknown Node',
|
|
valid: false,
|
|
errors: [{
|
|
type: 'config',
|
|
property: 'config',
|
|
message: 'Invalid config format - expected object',
|
|
fix: 'Provide config as an object with node properties'
|
|
}],
|
|
warnings: [],
|
|
suggestions: [
|
|
'🔧 RECOVERY: Invalid config detected. Fix with:',
|
|
' • Ensure config is an object: { "resource": "...", "operation": "..." }',
|
|
' • Use get_node to see required fields for this node type',
|
|
' • Check if the node type is correct before configuring it'
|
|
],
|
|
summary: {
|
|
hasErrors: true,
|
|
errorCount: 1,
|
|
warningCount: 0,
|
|
suggestionCount: 3
|
|
}
|
|
};
|
|
}
|
|
const validationMode = args.mode || 'full';
|
|
if (validationMode === 'minimal') {
|
|
return this.validateNodeMinimal(args.nodeType, args.config);
|
|
}
|
|
return this.validateNodeConfig(args.nodeType, args.config, 'operation', args.profile);
|
|
case 'get_template':
|
|
this.validateToolParams(name, args, ['templateId']);
|
|
const templateId = Number(args.templateId);
|
|
const templateMode = args.mode || 'full';
|
|
return this.getTemplate(templateId, templateMode);
|
|
case 'search_templates': {
|
|
const searchMode = args.searchMode || 'keyword';
|
|
const searchLimit = Math.min(Math.max(Number(args.limit) || 20, 1), 100);
|
|
const searchOffset = Math.max(Number(args.offset) || 0, 0);
|
|
switch (searchMode) {
|
|
case 'by_nodes':
|
|
if (!args.nodeTypes || !Array.isArray(args.nodeTypes) || args.nodeTypes.length === 0) {
|
|
throw new Error('nodeTypes array is required for searchMode=by_nodes');
|
|
}
|
|
return this.listNodeTemplates(args.nodeTypes, searchLimit, searchOffset);
|
|
case 'by_task':
|
|
if (!args.task) {
|
|
throw new Error('task is required for searchMode=by_task');
|
|
}
|
|
return this.getTemplatesForTask(args.task, searchLimit, searchOffset);
|
|
case 'by_metadata':
|
|
return this.searchTemplatesByMetadata({
|
|
category: args.category,
|
|
complexity: args.complexity,
|
|
maxSetupMinutes: args.maxSetupMinutes ? Number(args.maxSetupMinutes) : undefined,
|
|
minSetupMinutes: args.minSetupMinutes ? Number(args.minSetupMinutes) : undefined,
|
|
requiredService: args.requiredService,
|
|
targetAudience: args.targetAudience
|
|
}, searchLimit, searchOffset);
|
|
case 'keyword':
|
|
default:
|
|
if (!args.query) {
|
|
throw new Error('query is required for searchMode=keyword');
|
|
}
|
|
const searchFields = args.fields;
|
|
return this.searchTemplates(args.query, searchLimit, searchOffset, searchFields);
|
|
}
|
|
}
|
|
case 'validate_workflow':
|
|
this.validateToolParams(name, args, ['workflow']);
|
|
return this.validateWorkflow(args.workflow, args.options);
|
|
case 'n8n_create_workflow':
|
|
this.validateToolParams(name, args, ['name', 'nodes', 'connections']);
|
|
return n8nHandlers.handleCreateWorkflow(args, this.instanceContext);
|
|
case 'n8n_get_workflow': {
|
|
this.validateToolParams(name, args, ['id']);
|
|
const workflowMode = args.mode || 'full';
|
|
switch (workflowMode) {
|
|
case 'details':
|
|
return n8nHandlers.handleGetWorkflowDetails(args, this.instanceContext);
|
|
case 'structure':
|
|
return n8nHandlers.handleGetWorkflowStructure(args, this.instanceContext);
|
|
case 'minimal':
|
|
return n8nHandlers.handleGetWorkflowMinimal(args, this.instanceContext);
|
|
case 'full':
|
|
default:
|
|
return n8nHandlers.handleGetWorkflow(args, this.instanceContext);
|
|
}
|
|
}
|
|
case 'n8n_update_full_workflow':
|
|
this.validateToolParams(name, args, ['id']);
|
|
return n8nHandlers.handleUpdateWorkflow(args, this.repository, this.instanceContext);
|
|
case 'n8n_update_partial_workflow':
|
|
this.validateToolParams(name, args, ['id', 'operations']);
|
|
return (0, handlers_workflow_diff_1.handleUpdatePartialWorkflow)(args, this.repository, this.instanceContext);
|
|
case 'n8n_delete_workflow':
|
|
this.validateToolParams(name, args, ['id']);
|
|
return n8nHandlers.handleDeleteWorkflow(args, this.instanceContext);
|
|
case 'n8n_list_workflows':
|
|
return n8nHandlers.handleListWorkflows(args, this.instanceContext);
|
|
case 'n8n_validate_workflow':
|
|
this.validateToolParams(name, args, ['id']);
|
|
await this.ensureInitialized();
|
|
if (!this.repository)
|
|
throw new Error('Repository not initialized');
|
|
return n8nHandlers.handleValidateWorkflow(args, this.repository, this.instanceContext);
|
|
case 'n8n_autofix_workflow':
|
|
this.validateToolParams(name, args, ['id']);
|
|
await this.ensureInitialized();
|
|
if (!this.repository)
|
|
throw new Error('Repository not initialized');
|
|
return n8nHandlers.handleAutofixWorkflow(args, this.repository, this.instanceContext);
|
|
case 'n8n_test_workflow':
|
|
this.validateToolParams(name, args, ['workflowId']);
|
|
return n8nHandlers.handleTestWorkflow(args, this.instanceContext);
|
|
case 'n8n_executions': {
|
|
this.validateToolParams(name, args, ['action']);
|
|
const execAction = args.action;
|
|
switch (execAction) {
|
|
case 'get':
|
|
if (!args.id) {
|
|
throw new Error('id is required for action=get');
|
|
}
|
|
return n8nHandlers.handleGetExecution(args, this.instanceContext);
|
|
case 'list':
|
|
return n8nHandlers.handleListExecutions(args, this.instanceContext);
|
|
case 'delete':
|
|
if (!args.id) {
|
|
throw new Error('id is required for action=delete');
|
|
}
|
|
return n8nHandlers.handleDeleteExecution(args, this.instanceContext);
|
|
default:
|
|
throw new Error(`Unknown action: ${execAction}. Valid actions: get, list, delete`);
|
|
}
|
|
}
|
|
case 'n8n_health_check':
|
|
if (args.mode === 'diagnostic') {
|
|
return n8nHandlers.handleDiagnostic({ params: { arguments: args } }, this.instanceContext);
|
|
}
|
|
return n8nHandlers.handleHealthCheck(this.instanceContext);
|
|
case 'n8n_workflow_versions':
|
|
this.validateToolParams(name, args, ['mode']);
|
|
return n8nHandlers.handleWorkflowVersions(args, this.repository, this.instanceContext);
|
|
case 'n8n_deploy_template':
|
|
this.validateToolParams(name, args, ['templateId']);
|
|
await this.ensureInitialized();
|
|
if (!this.templateService)
|
|
throw new Error('Template service not initialized');
|
|
if (!this.repository)
|
|
throw new Error('Repository not initialized');
|
|
return n8nHandlers.handleDeployTemplate(args, this.templateService, this.repository, this.instanceContext);
|
|
default:
|
|
throw new Error(`Unknown tool: ${name}`);
|
|
}
|
|
}
|
|
async listNodes(filters = {}) {
|
|
await this.ensureInitialized();
|
|
let query = 'SELECT * FROM nodes WHERE 1=1';
|
|
const params = [];
|
|
if (filters.package) {
|
|
const packageVariants = [
|
|
filters.package,
|
|
`@n8n/${filters.package}`,
|
|
filters.package.replace('@n8n/', '')
|
|
];
|
|
query += ' AND package_name IN (' + packageVariants.map(() => '?').join(',') + ')';
|
|
params.push(...packageVariants);
|
|
}
|
|
if (filters.category) {
|
|
query += ' AND category = ?';
|
|
params.push(filters.category);
|
|
}
|
|
if (filters.developmentStyle) {
|
|
query += ' AND development_style = ?';
|
|
params.push(filters.developmentStyle);
|
|
}
|
|
if (filters.isAITool !== undefined) {
|
|
query += ' AND is_ai_tool = ?';
|
|
params.push(filters.isAITool ? 1 : 0);
|
|
}
|
|
query += ' ORDER BY display_name';
|
|
if (filters.limit) {
|
|
query += ' LIMIT ?';
|
|
params.push(filters.limit);
|
|
}
|
|
const nodes = this.db.prepare(query).all(...params);
|
|
return {
|
|
nodes: nodes.map(node => ({
|
|
nodeType: node.node_type,
|
|
displayName: node.display_name,
|
|
description: node.description,
|
|
category: node.category,
|
|
package: node.package_name,
|
|
developmentStyle: node.development_style,
|
|
isAITool: Number(node.is_ai_tool) === 1,
|
|
isTrigger: Number(node.is_trigger) === 1,
|
|
isVersioned: Number(node.is_versioned) === 1,
|
|
})),
|
|
totalCount: nodes.length,
|
|
};
|
|
}
|
|
async getNodeInfo(nodeType) {
|
|
await this.ensureInitialized();
|
|
if (!this.repository)
|
|
throw new Error('Repository not initialized');
|
|
const normalizedType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(nodeType);
|
|
let node = this.repository.getNode(normalizedType);
|
|
if (!node && normalizedType !== nodeType) {
|
|
node = this.repository.getNode(nodeType);
|
|
}
|
|
if (!node) {
|
|
const alternatives = (0, node_utils_1.getNodeTypeAlternatives)(normalizedType);
|
|
for (const alt of alternatives) {
|
|
const found = this.repository.getNode(alt);
|
|
if (found) {
|
|
node = found;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!node) {
|
|
throw new Error(`Node ${nodeType} not found`);
|
|
}
|
|
const aiToolCapabilities = {
|
|
canBeUsedAsTool: true,
|
|
hasUsableAsToolProperty: node.isAITool ?? false,
|
|
requiresEnvironmentVariable: !(node.isAITool ?? false) && node.package !== 'n8n-nodes-base',
|
|
toolConnectionType: 'ai_tool',
|
|
commonToolUseCases: this.getCommonAIToolUseCases(node.nodeType),
|
|
environmentRequirement: node.package && node.package !== 'n8n-nodes-base' ?
|
|
'N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true' :
|
|
null
|
|
};
|
|
let outputs = undefined;
|
|
if (node.outputNames && Array.isArray(node.outputNames) && node.outputNames.length > 0) {
|
|
outputs = node.outputNames.map((name, index) => {
|
|
const descriptions = this.getOutputDescriptions(node.nodeType, name, index);
|
|
return {
|
|
index,
|
|
name,
|
|
description: descriptions?.description ?? '',
|
|
connectionGuidance: descriptions?.connectionGuidance ?? ''
|
|
};
|
|
});
|
|
}
|
|
const result = {
|
|
...node,
|
|
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package ?? 'n8n-nodes-base', node.nodeType),
|
|
aiToolCapabilities,
|
|
outputs
|
|
};
|
|
const toolVariantInfo = this.buildToolVariantGuidance(node);
|
|
if (toolVariantInfo) {
|
|
result.toolVariantInfo = toolVariantInfo;
|
|
}
|
|
return result;
|
|
}
|
|
async searchNodes(query, limit = 20, options) {
|
|
await this.ensureInitialized();
|
|
if (!this.db)
|
|
throw new Error('Database not initialized');
|
|
let normalizedQuery = query;
|
|
if (query.includes('n8n-nodes-base.') || query.includes('@n8n/n8n-nodes-langchain.')) {
|
|
normalizedQuery = query
|
|
.replace(/n8n-nodes-base\./g, 'nodes-base.')
|
|
.replace(/@n8n\/n8n-nodes-langchain\./g, 'nodes-langchain.');
|
|
}
|
|
const searchMode = options?.mode || 'OR';
|
|
const ftsExists = this.db.prepare(`
|
|
SELECT name FROM sqlite_master
|
|
WHERE type='table' AND name='nodes_fts'
|
|
`).get();
|
|
if (ftsExists) {
|
|
logger_1.logger.debug(`Using FTS5 search with includeExamples=${options?.includeExamples}`);
|
|
return this.searchNodesFTS(normalizedQuery, limit, searchMode, options);
|
|
}
|
|
else {
|
|
logger_1.logger.debug('Using LIKE search (no FTS5)');
|
|
return this.searchNodesLIKE(normalizedQuery, limit, options);
|
|
}
|
|
}
|
|
async searchNodesFTS(query, limit, mode, options) {
|
|
if (!this.db)
|
|
throw new Error('Database not initialized');
|
|
const cleanedQuery = query.trim();
|
|
if (!cleanedQuery) {
|
|
return { query, results: [], totalCount: 0 };
|
|
}
|
|
if (mode === 'FUZZY') {
|
|
return this.searchNodesFuzzy(cleanedQuery, limit);
|
|
}
|
|
let ftsQuery;
|
|
if (cleanedQuery.startsWith('"') && cleanedQuery.endsWith('"')) {
|
|
ftsQuery = cleanedQuery;
|
|
}
|
|
else {
|
|
const words = cleanedQuery.split(/\s+/).filter(w => w.length > 0);
|
|
switch (mode) {
|
|
case 'AND':
|
|
ftsQuery = words.join(' AND ');
|
|
break;
|
|
case 'OR':
|
|
default:
|
|
ftsQuery = words.join(' OR ');
|
|
break;
|
|
}
|
|
}
|
|
try {
|
|
let sourceFilter = '';
|
|
const sourceValue = options?.source || 'all';
|
|
switch (sourceValue) {
|
|
case 'core':
|
|
sourceFilter = 'AND n.is_community = 0';
|
|
break;
|
|
case 'community':
|
|
sourceFilter = 'AND n.is_community = 1';
|
|
break;
|
|
case 'verified':
|
|
sourceFilter = 'AND n.is_community = 1 AND n.is_verified = 1';
|
|
break;
|
|
}
|
|
const nodes = this.db.prepare(`
|
|
SELECT
|
|
n.*,
|
|
rank
|
|
FROM nodes n
|
|
JOIN nodes_fts ON n.rowid = nodes_fts.rowid
|
|
WHERE nodes_fts MATCH ?
|
|
${sourceFilter}
|
|
ORDER BY
|
|
CASE
|
|
WHEN LOWER(n.display_name) = LOWER(?) THEN 0
|
|
WHEN LOWER(n.display_name) LIKE LOWER(?) THEN 1
|
|
WHEN LOWER(n.node_type) LIKE LOWER(?) THEN 2
|
|
ELSE 3
|
|
END,
|
|
rank,
|
|
n.display_name
|
|
LIMIT ?
|
|
`).all(ftsQuery, cleanedQuery, `%${cleanedQuery}%`, `%${cleanedQuery}%`, limit);
|
|
const scoredNodes = nodes.map(node => {
|
|
const relevanceScore = this.calculateRelevanceScore(node, cleanedQuery);
|
|
return { ...node, relevanceScore };
|
|
});
|
|
scoredNodes.sort((a, b) => {
|
|
if (a.display_name.toLowerCase() === cleanedQuery.toLowerCase())
|
|
return -1;
|
|
if (b.display_name.toLowerCase() === cleanedQuery.toLowerCase())
|
|
return 1;
|
|
if (a.relevanceScore !== b.relevanceScore) {
|
|
return b.relevanceScore - a.relevanceScore;
|
|
}
|
|
return a.rank - b.rank;
|
|
});
|
|
const hasHttpRequest = scoredNodes.some(n => n.node_type === 'nodes-base.httpRequest');
|
|
if (cleanedQuery.toLowerCase().includes('http') && !hasHttpRequest) {
|
|
logger_1.logger.debug('FTS missed HTTP Request node, augmenting with LIKE search');
|
|
return this.searchNodesLIKE(query, limit);
|
|
}
|
|
const result = {
|
|
query,
|
|
results: scoredNodes.map(node => {
|
|
const nodeResult = {
|
|
nodeType: node.node_type,
|
|
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package_name, node.node_type),
|
|
displayName: node.display_name,
|
|
description: node.description,
|
|
category: node.category,
|
|
package: node.package_name,
|
|
relevance: this.calculateRelevance(node, cleanedQuery)
|
|
};
|
|
if (node.is_community === 1) {
|
|
nodeResult.isCommunity = true;
|
|
nodeResult.isVerified = node.is_verified === 1;
|
|
if (node.author_name) {
|
|
nodeResult.authorName = node.author_name;
|
|
}
|
|
if (node.npm_downloads) {
|
|
nodeResult.npmDownloads = node.npm_downloads;
|
|
}
|
|
}
|
|
return nodeResult;
|
|
}),
|
|
totalCount: scoredNodes.length
|
|
};
|
|
if (mode !== 'OR') {
|
|
result.mode = mode;
|
|
}
|
|
if (options && options.includeExamples) {
|
|
try {
|
|
for (const nodeResult of result.results) {
|
|
const examples = this.db.prepare(`
|
|
SELECT
|
|
parameters_json,
|
|
template_name,
|
|
template_views
|
|
FROM template_node_configs
|
|
WHERE node_type = ?
|
|
ORDER BY rank
|
|
LIMIT 2
|
|
`).all(nodeResult.workflowNodeType);
|
|
if (examples.length > 0) {
|
|
nodeResult.examples = examples.map((ex) => ({
|
|
configuration: JSON.parse(ex.parameters_json),
|
|
template: ex.template_name,
|
|
views: ex.template_views
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.error(`Failed to add examples:`, error);
|
|
}
|
|
}
|
|
telemetry_1.telemetry.trackSearchQuery(query, scoredNodes.length, mode ?? 'OR');
|
|
return result;
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.warn('FTS5 search failed, falling back to LIKE search:', error.message);
|
|
if (error.message.includes('syntax error') || error.message.includes('fts5')) {
|
|
logger_1.logger.warn(`FTS5 syntax error for query "${query}" in mode ${mode}`);
|
|
const likeResult = await this.searchNodesLIKE(query, limit);
|
|
telemetry_1.telemetry.trackSearchQuery(query, likeResult.results?.length ?? 0, `${mode}_LIKE_FALLBACK`);
|
|
return {
|
|
...likeResult,
|
|
mode
|
|
};
|
|
}
|
|
return this.searchNodesLIKE(query, limit);
|
|
}
|
|
}
|
|
async searchNodesFuzzy(query, limit) {
|
|
if (!this.db)
|
|
throw new Error('Database not initialized');
|
|
const words = query.toLowerCase().split(/\s+/).filter(w => w.length > 0);
|
|
if (words.length === 0) {
|
|
return { query, results: [], totalCount: 0, mode: 'FUZZY' };
|
|
}
|
|
const candidateNodes = this.db.prepare(`
|
|
SELECT * FROM nodes
|
|
`).all();
|
|
const scoredNodes = candidateNodes.map(node => {
|
|
const score = this.calculateFuzzyScore(node, query);
|
|
return { node, score };
|
|
});
|
|
const matchingNodes = scoredNodes
|
|
.filter(item => item.score >= 200)
|
|
.sort((a, b) => b.score - a.score)
|
|
.slice(0, limit)
|
|
.map(item => item.node);
|
|
if (matchingNodes.length === 0) {
|
|
const topScores = scoredNodes
|
|
.sort((a, b) => b.score - a.score)
|
|
.slice(0, 5);
|
|
logger_1.logger.debug(`FUZZY search for "${query}" - no matches above 400. Top scores:`, topScores.map(s => ({ name: s.node.display_name, score: s.score })));
|
|
}
|
|
return {
|
|
query,
|
|
mode: 'FUZZY',
|
|
results: matchingNodes.map(node => ({
|
|
nodeType: node.node_type,
|
|
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package_name, node.node_type),
|
|
displayName: node.display_name,
|
|
description: node.description,
|
|
category: node.category,
|
|
package: node.package_name
|
|
})),
|
|
totalCount: matchingNodes.length
|
|
};
|
|
}
|
|
calculateFuzzyScore(node, query) {
|
|
const queryLower = query.toLowerCase();
|
|
const displayNameLower = node.display_name.toLowerCase();
|
|
const nodeTypeLower = node.node_type.toLowerCase();
|
|
const nodeTypeClean = nodeTypeLower.replace(/^nodes-base\./, '').replace(/^nodes-langchain\./, '');
|
|
if (displayNameLower === queryLower || nodeTypeClean === queryLower) {
|
|
return 1000;
|
|
}
|
|
const nameDistance = this.getEditDistance(queryLower, displayNameLower);
|
|
const typeDistance = this.getEditDistance(queryLower, nodeTypeClean);
|
|
const nameWords = displayNameLower.split(/\s+/);
|
|
let minWordDistance = Infinity;
|
|
for (const word of nameWords) {
|
|
const distance = this.getEditDistance(queryLower, word);
|
|
if (distance < minWordDistance) {
|
|
minWordDistance = distance;
|
|
}
|
|
}
|
|
const bestDistance = Math.min(nameDistance, typeDistance, minWordDistance);
|
|
let matchedLen = queryLower.length;
|
|
if (minWordDistance === bestDistance) {
|
|
for (const word of nameWords) {
|
|
if (this.getEditDistance(queryLower, word) === minWordDistance) {
|
|
matchedLen = Math.max(queryLower.length, word.length);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if (typeDistance === bestDistance) {
|
|
matchedLen = Math.max(queryLower.length, nodeTypeClean.length);
|
|
}
|
|
else {
|
|
matchedLen = Math.max(queryLower.length, displayNameLower.length);
|
|
}
|
|
const similarity = 1 - (bestDistance / matchedLen);
|
|
if (displayNameLower.includes(queryLower) || nodeTypeClean.includes(queryLower)) {
|
|
return 800 + (similarity * 100);
|
|
}
|
|
if (displayNameLower.startsWith(queryLower) ||
|
|
nodeTypeClean.startsWith(queryLower) ||
|
|
nameWords.some(w => w.startsWith(queryLower))) {
|
|
return 700 + (similarity * 100);
|
|
}
|
|
if (bestDistance <= 2) {
|
|
return 500 + ((2 - bestDistance) * 100) + (similarity * 50);
|
|
}
|
|
if (bestDistance <= 3 && queryLower.length >= 4) {
|
|
return 400 + ((3 - bestDistance) * 50) + (similarity * 50);
|
|
}
|
|
return similarity * 300;
|
|
}
|
|
getEditDistance(s1, s2) {
|
|
const m = s1.length;
|
|
const n = s2.length;
|
|
const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
|
|
for (let i = 0; i <= m; i++)
|
|
dp[i][0] = i;
|
|
for (let j = 0; j <= n; j++)
|
|
dp[0][j] = j;
|
|
for (let i = 1; i <= m; i++) {
|
|
for (let j = 1; j <= n; j++) {
|
|
if (s1[i - 1] === s2[j - 1]) {
|
|
dp[i][j] = dp[i - 1][j - 1];
|
|
}
|
|
else {
|
|
dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
}
|
|
}
|
|
}
|
|
return dp[m][n];
|
|
}
|
|
async searchNodesLIKE(query, limit, options) {
|
|
if (!this.db)
|
|
throw new Error('Database not initialized');
|
|
let sourceFilter = '';
|
|
const sourceValue = options?.source || 'all';
|
|
switch (sourceValue) {
|
|
case 'core':
|
|
sourceFilter = 'AND is_community = 0';
|
|
break;
|
|
case 'community':
|
|
sourceFilter = 'AND is_community = 1';
|
|
break;
|
|
case 'verified':
|
|
sourceFilter = 'AND is_community = 1 AND is_verified = 1';
|
|
break;
|
|
}
|
|
if (query.startsWith('"') && query.endsWith('"')) {
|
|
const exactPhrase = query.slice(1, -1);
|
|
const nodes = this.db.prepare(`
|
|
SELECT * FROM nodes
|
|
WHERE (node_type LIKE ? OR display_name LIKE ? OR description LIKE ?)
|
|
${sourceFilter}
|
|
LIMIT ?
|
|
`).all(`%${exactPhrase}%`, `%${exactPhrase}%`, `%${exactPhrase}%`, limit * 3);
|
|
const rankedNodes = this.rankSearchResults(nodes, exactPhrase, limit);
|
|
const result = {
|
|
query,
|
|
results: rankedNodes.map(node => {
|
|
const nodeResult = {
|
|
nodeType: node.node_type,
|
|
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package_name, node.node_type),
|
|
displayName: node.display_name,
|
|
description: node.description,
|
|
category: node.category,
|
|
package: node.package_name
|
|
};
|
|
if (node.is_community === 1) {
|
|
nodeResult.isCommunity = true;
|
|
nodeResult.isVerified = node.is_verified === 1;
|
|
if (node.author_name) {
|
|
nodeResult.authorName = node.author_name;
|
|
}
|
|
if (node.npm_downloads) {
|
|
nodeResult.npmDownloads = node.npm_downloads;
|
|
}
|
|
}
|
|
return nodeResult;
|
|
}),
|
|
totalCount: rankedNodes.length
|
|
};
|
|
if (options?.includeExamples) {
|
|
for (const nodeResult of result.results) {
|
|
try {
|
|
const examples = this.db.prepare(`
|
|
SELECT
|
|
parameters_json,
|
|
template_name,
|
|
template_views
|
|
FROM template_node_configs
|
|
WHERE node_type = ?
|
|
ORDER BY rank
|
|
LIMIT 2
|
|
`).all(nodeResult.workflowNodeType);
|
|
if (examples.length > 0) {
|
|
nodeResult.examples = examples.map((ex) => ({
|
|
configuration: JSON.parse(ex.parameters_json),
|
|
template: ex.template_name,
|
|
views: ex.template_views
|
|
}));
|
|
}
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.warn(`Failed to fetch examples for ${nodeResult.nodeType}:`, error.message);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
const words = query.toLowerCase().split(/\s+/).filter(w => w.length > 0);
|
|
if (words.length === 0) {
|
|
return { query, results: [], totalCount: 0 };
|
|
}
|
|
const conditions = words.map(() => '(node_type LIKE ? OR display_name LIKE ? OR description LIKE ?)').join(' OR ');
|
|
const params = words.flatMap(w => [`%${w}%`, `%${w}%`, `%${w}%`]);
|
|
params.push(limit * 3);
|
|
const nodes = this.db.prepare(`
|
|
SELECT DISTINCT * FROM nodes
|
|
WHERE (${conditions})
|
|
${sourceFilter}
|
|
LIMIT ?
|
|
`).all(...params);
|
|
const rankedNodes = this.rankSearchResults(nodes, query, limit);
|
|
const result = {
|
|
query,
|
|
results: rankedNodes.map(node => {
|
|
const nodeResult = {
|
|
nodeType: node.node_type,
|
|
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package_name, node.node_type),
|
|
displayName: node.display_name,
|
|
description: node.description,
|
|
category: node.category,
|
|
package: node.package_name
|
|
};
|
|
if (node.is_community === 1) {
|
|
nodeResult.isCommunity = true;
|
|
nodeResult.isVerified = node.is_verified === 1;
|
|
if (node.author_name) {
|
|
nodeResult.authorName = node.author_name;
|
|
}
|
|
if (node.npm_downloads) {
|
|
nodeResult.npmDownloads = node.npm_downloads;
|
|
}
|
|
}
|
|
return nodeResult;
|
|
}),
|
|
totalCount: rankedNodes.length
|
|
};
|
|
if (options?.includeExamples) {
|
|
for (const nodeResult of result.results) {
|
|
try {
|
|
const examples = this.db.prepare(`
|
|
SELECT
|
|
parameters_json,
|
|
template_name,
|
|
template_views
|
|
FROM template_node_configs
|
|
WHERE node_type = ?
|
|
ORDER BY rank
|
|
LIMIT 2
|
|
`).all(nodeResult.workflowNodeType);
|
|
if (examples.length > 0) {
|
|
nodeResult.examples = examples.map((ex) => ({
|
|
configuration: JSON.parse(ex.parameters_json),
|
|
template: ex.template_name,
|
|
views: ex.template_views
|
|
}));
|
|
}
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.warn(`Failed to fetch examples for ${nodeResult.nodeType}:`, error.message);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
calculateRelevance(node, query) {
|
|
const lowerQuery = query.toLowerCase();
|
|
if (node.node_type.toLowerCase().includes(lowerQuery))
|
|
return 'high';
|
|
if (node.display_name.toLowerCase().includes(lowerQuery))
|
|
return 'high';
|
|
if (node.description?.toLowerCase().includes(lowerQuery))
|
|
return 'medium';
|
|
return 'low';
|
|
}
|
|
calculateRelevanceScore(node, query) {
|
|
const query_lower = query.toLowerCase();
|
|
const name_lower = node.display_name.toLowerCase();
|
|
const type_lower = node.node_type.toLowerCase();
|
|
const type_without_prefix = type_lower.replace(/^nodes-base\./, '').replace(/^nodes-langchain\./, '');
|
|
let score = 0;
|
|
if (name_lower === query_lower) {
|
|
score = 1000;
|
|
}
|
|
else if (type_without_prefix === query_lower) {
|
|
score = 950;
|
|
}
|
|
else if (query_lower === 'webhook' && node.node_type === 'nodes-base.webhook') {
|
|
score = 900;
|
|
}
|
|
else if ((query_lower === 'http' || query_lower === 'http request' || query_lower === 'http call') && node.node_type === 'nodes-base.httpRequest') {
|
|
score = 900;
|
|
}
|
|
else if (query_lower.includes('http') && query_lower.includes('call') && node.node_type === 'nodes-base.httpRequest') {
|
|
score = 890;
|
|
}
|
|
else if (query_lower.includes('http') && node.node_type === 'nodes-base.httpRequest') {
|
|
score = 850;
|
|
}
|
|
else if (query_lower.includes('webhook') && node.node_type === 'nodes-base.webhook') {
|
|
score = 850;
|
|
}
|
|
else if (name_lower.startsWith(query_lower)) {
|
|
score = 800;
|
|
}
|
|
else if (new RegExp(`\\b${query_lower}\\b`, 'i').test(node.display_name)) {
|
|
score = 700;
|
|
}
|
|
else if (name_lower.includes(query_lower)) {
|
|
score = 600;
|
|
}
|
|
else if (type_without_prefix.includes(query_lower)) {
|
|
score = 500;
|
|
}
|
|
else if (node.description?.toLowerCase().includes(query_lower)) {
|
|
score = 400;
|
|
}
|
|
return score;
|
|
}
|
|
rankSearchResults(nodes, query, limit) {
|
|
const query_lower = query.toLowerCase();
|
|
const scoredNodes = nodes.map(node => {
|
|
const name_lower = node.display_name.toLowerCase();
|
|
const type_lower = node.node_type.toLowerCase();
|
|
const type_without_prefix = type_lower.replace(/^nodes-base\./, '').replace(/^nodes-langchain\./, '');
|
|
let score = 0;
|
|
if (name_lower === query_lower) {
|
|
score = 1000;
|
|
}
|
|
else if (type_without_prefix === query_lower) {
|
|
score = 950;
|
|
}
|
|
else if (query_lower === 'webhook' && node.node_type === 'nodes-base.webhook') {
|
|
score = 900;
|
|
}
|
|
else if ((query_lower === 'http' || query_lower === 'http request' || query_lower === 'http call') && node.node_type === 'nodes-base.httpRequest') {
|
|
score = 900;
|
|
}
|
|
else if (query_lower.includes('webhook') && node.node_type === 'nodes-base.webhook') {
|
|
score = 850;
|
|
}
|
|
else if (query_lower.includes('http') && node.node_type === 'nodes-base.httpRequest') {
|
|
score = 850;
|
|
}
|
|
else if (name_lower.startsWith(query_lower)) {
|
|
score = 800;
|
|
}
|
|
else if (new RegExp(`\\b${query_lower}\\b`, 'i').test(node.display_name)) {
|
|
score = 700;
|
|
}
|
|
else if (name_lower.includes(query_lower)) {
|
|
score = 600;
|
|
}
|
|
else if (type_without_prefix.includes(query_lower)) {
|
|
score = 500;
|
|
}
|
|
else if (node.description?.toLowerCase().includes(query_lower)) {
|
|
score = 400;
|
|
}
|
|
const words = query_lower.split(/\s+/).filter(w => w.length > 0);
|
|
if (words.length > 1) {
|
|
const allWordsInName = words.every(word => name_lower.includes(word));
|
|
const allWordsInDesc = words.every(word => node.description?.toLowerCase().includes(word));
|
|
if (allWordsInName)
|
|
score += 200;
|
|
else if (allWordsInDesc)
|
|
score += 100;
|
|
if (query_lower === 'http call' && name_lower === 'http request') {
|
|
score = 920;
|
|
}
|
|
}
|
|
return { node, score };
|
|
});
|
|
scoredNodes.sort((a, b) => {
|
|
if (a.score !== b.score) {
|
|
return b.score - a.score;
|
|
}
|
|
return a.node.display_name.localeCompare(b.node.display_name);
|
|
});
|
|
return scoredNodes.slice(0, limit).map(item => item.node);
|
|
}
|
|
async listAITools() {
|
|
await this.ensureInitialized();
|
|
if (!this.repository)
|
|
throw new Error('Repository not initialized');
|
|
const tools = this.repository.getAITools();
|
|
const aiCount = this.db.prepare('SELECT COUNT(*) as ai_count FROM nodes WHERE is_ai_tool = 1').get();
|
|
return {
|
|
tools,
|
|
totalCount: tools.length,
|
|
requirements: {
|
|
environmentVariable: 'N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true',
|
|
nodeProperty: 'usableAsTool: true',
|
|
},
|
|
usage: {
|
|
description: 'These nodes have the usableAsTool property set to true, making them optimized for AI agent usage.',
|
|
note: 'ANY node in n8n can be used as an AI tool by connecting it to the ai_tool port of an AI Agent node.',
|
|
examples: [
|
|
'Regular nodes like Slack, Google Sheets, or HTTP Request can be used as tools',
|
|
'Connect any node to an AI Agent\'s tool port to make it available for AI-driven automation',
|
|
'Community nodes require the environment variable to be set'
|
|
]
|
|
}
|
|
};
|
|
}
|
|
async getNodeDocumentation(nodeType) {
|
|
await this.ensureInitialized();
|
|
if (!this.db)
|
|
throw new Error('Database not initialized');
|
|
const normalizedType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(nodeType);
|
|
let node = this.db.prepare(`
|
|
SELECT node_type, display_name, documentation, description,
|
|
ai_documentation_summary, ai_summary_generated_at
|
|
FROM nodes
|
|
WHERE node_type = ?
|
|
`).get(normalizedType);
|
|
if (!node && normalizedType !== nodeType) {
|
|
node = this.db.prepare(`
|
|
SELECT node_type, display_name, documentation, description,
|
|
ai_documentation_summary, ai_summary_generated_at
|
|
FROM nodes
|
|
WHERE node_type = ?
|
|
`).get(nodeType);
|
|
}
|
|
if (!node) {
|
|
const alternatives = (0, node_utils_1.getNodeTypeAlternatives)(normalizedType);
|
|
for (const alt of alternatives) {
|
|
node = this.db.prepare(`
|
|
SELECT node_type, display_name, documentation, description,
|
|
ai_documentation_summary, ai_summary_generated_at
|
|
FROM nodes
|
|
WHERE node_type = ?
|
|
`).get(alt);
|
|
if (node)
|
|
break;
|
|
}
|
|
}
|
|
if (!node) {
|
|
throw new Error(`Node ${nodeType} not found`);
|
|
}
|
|
const aiDocSummary = node.ai_documentation_summary
|
|
? this.safeJsonParse(node.ai_documentation_summary, null)
|
|
: null;
|
|
if (!node.documentation) {
|
|
const essentials = await this.getNodeEssentials(nodeType);
|
|
return {
|
|
nodeType: node.node_type,
|
|
displayName: node.display_name || 'Unknown Node',
|
|
documentation: `
|
|
# ${node.display_name || 'Unknown Node'}
|
|
|
|
${node.description || 'No description available.'}
|
|
|
|
## Common Properties
|
|
|
|
${essentials?.commonProperties?.length > 0 ?
|
|
essentials.commonProperties.map((p) => `### ${p.displayName || 'Property'}\n${p.description || `Type: ${p.type || 'unknown'}`}`).join('\n\n') :
|
|
'No common properties available.'}
|
|
|
|
## Note
|
|
Full documentation is being prepared. For now, use get_node_essentials for configuration help.
|
|
`,
|
|
hasDocumentation: false,
|
|
aiDocumentationSummary: aiDocSummary,
|
|
aiSummaryGeneratedAt: node.ai_summary_generated_at || null,
|
|
};
|
|
}
|
|
return {
|
|
nodeType: node.node_type,
|
|
displayName: node.display_name || 'Unknown Node',
|
|
documentation: node.documentation,
|
|
hasDocumentation: true,
|
|
aiDocumentationSummary: aiDocSummary,
|
|
aiSummaryGeneratedAt: node.ai_summary_generated_at || null,
|
|
};
|
|
}
|
|
safeJsonParse(json, defaultValue = null) {
|
|
try {
|
|
return JSON.parse(json);
|
|
}
|
|
catch {
|
|
return defaultValue;
|
|
}
|
|
}
|
|
async getDatabaseStatistics() {
|
|
await this.ensureInitialized();
|
|
if (!this.db)
|
|
throw new Error('Database not initialized');
|
|
const stats = this.db.prepare(`
|
|
SELECT
|
|
COUNT(*) as total,
|
|
SUM(is_ai_tool) as ai_tools,
|
|
SUM(is_trigger) as triggers,
|
|
SUM(is_versioned) as versioned,
|
|
SUM(CASE WHEN documentation IS NOT NULL THEN 1 ELSE 0 END) as with_docs,
|
|
COUNT(DISTINCT package_name) as packages,
|
|
COUNT(DISTINCT category) as categories
|
|
FROM nodes
|
|
`).get();
|
|
const packages = this.db.prepare(`
|
|
SELECT package_name, COUNT(*) as count
|
|
FROM nodes
|
|
GROUP BY package_name
|
|
`).all();
|
|
const templateStats = this.db.prepare(`
|
|
SELECT
|
|
COUNT(*) as total_templates,
|
|
AVG(views) as avg_views,
|
|
MIN(views) as min_views,
|
|
MAX(views) as max_views
|
|
FROM templates
|
|
`).get();
|
|
return {
|
|
totalNodes: stats.total,
|
|
totalTemplates: templateStats.total_templates || 0,
|
|
statistics: {
|
|
aiTools: stats.ai_tools,
|
|
triggers: stats.triggers,
|
|
versionedNodes: stats.versioned,
|
|
nodesWithDocumentation: stats.with_docs,
|
|
documentationCoverage: Math.round((stats.with_docs / stats.total) * 100) + '%',
|
|
uniquePackages: stats.packages,
|
|
uniqueCategories: stats.categories,
|
|
templates: {
|
|
total: templateStats.total_templates || 0,
|
|
avgViews: Math.round(templateStats.avg_views || 0),
|
|
minViews: templateStats.min_views || 0,
|
|
maxViews: templateStats.max_views || 0
|
|
}
|
|
},
|
|
packageBreakdown: packages.map(pkg => ({
|
|
package: pkg.package_name,
|
|
nodeCount: pkg.count,
|
|
})),
|
|
};
|
|
}
|
|
async getNodeEssentials(nodeType, includeExamples) {
|
|
await this.ensureInitialized();
|
|
if (!this.repository)
|
|
throw new Error('Repository not initialized');
|
|
const cacheKey = `essentials:${nodeType}:${includeExamples ? 'withExamples' : 'basic'}`;
|
|
const cached = this.cache.get(cacheKey);
|
|
if (cached)
|
|
return cached;
|
|
const normalizedType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(nodeType);
|
|
let node = this.repository.getNode(normalizedType);
|
|
if (!node && normalizedType !== nodeType) {
|
|
node = this.repository.getNode(nodeType);
|
|
}
|
|
if (!node) {
|
|
const alternatives = (0, node_utils_1.getNodeTypeAlternatives)(normalizedType);
|
|
for (const alt of alternatives) {
|
|
const found = this.repository.getNode(alt);
|
|
if (found) {
|
|
node = found;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!node) {
|
|
throw new Error(`Node ${nodeType} not found`);
|
|
}
|
|
const allProperties = node.properties || [];
|
|
const essentials = property_filter_1.PropertyFilter.getEssentials(allProperties, node.nodeType);
|
|
const operations = node.operations || [];
|
|
const latestVersion = node.version ?? '1';
|
|
const result = {
|
|
nodeType: node.nodeType,
|
|
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package ?? 'n8n-nodes-base', node.nodeType),
|
|
displayName: node.displayName,
|
|
description: node.description,
|
|
category: node.category,
|
|
version: latestVersion,
|
|
isVersioned: node.isVersioned ?? false,
|
|
versionNotice: `⚠️ Use typeVersion: ${latestVersion} when creating this node`,
|
|
requiredProperties: essentials.required,
|
|
commonProperties: essentials.common,
|
|
operations: operations.map((op) => ({
|
|
name: op.name || op.operation,
|
|
description: op.description,
|
|
action: op.action,
|
|
resource: op.resource
|
|
})),
|
|
metadata: {
|
|
totalProperties: allProperties.length,
|
|
isAITool: node.isAITool ?? false,
|
|
isTrigger: node.isTrigger ?? false,
|
|
isWebhook: node.isWebhook ?? false,
|
|
hasCredentials: node.credentials ? true : false,
|
|
package: node.package ?? 'n8n-nodes-base',
|
|
developmentStyle: node.developmentStyle ?? 'programmatic'
|
|
}
|
|
};
|
|
const toolVariantInfo = this.buildToolVariantGuidance(node);
|
|
if (toolVariantInfo) {
|
|
result.toolVariantInfo = toolVariantInfo;
|
|
}
|
|
if (includeExamples) {
|
|
try {
|
|
const examples = this.db.prepare(`
|
|
SELECT
|
|
parameters_json,
|
|
template_name,
|
|
template_views,
|
|
complexity,
|
|
use_cases,
|
|
has_credentials,
|
|
has_expressions
|
|
FROM template_node_configs
|
|
WHERE node_type = ?
|
|
ORDER BY rank
|
|
LIMIT 3
|
|
`).all(result.workflowNodeType);
|
|
if (examples.length > 0) {
|
|
result.examples = examples.map((ex) => ({
|
|
configuration: JSON.parse(ex.parameters_json),
|
|
source: {
|
|
template: ex.template_name,
|
|
views: ex.template_views,
|
|
complexity: ex.complexity
|
|
},
|
|
useCases: ex.use_cases ? JSON.parse(ex.use_cases).slice(0, 2) : [],
|
|
metadata: {
|
|
hasCredentials: ex.has_credentials === 1,
|
|
hasExpressions: ex.has_expressions === 1
|
|
}
|
|
}));
|
|
result.examplesCount = examples.length;
|
|
}
|
|
else {
|
|
result.examples = [];
|
|
result.examplesCount = 0;
|
|
}
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.warn(`Failed to fetch examples for ${nodeType}:`, error.message);
|
|
result.examples = [];
|
|
result.examplesCount = 0;
|
|
}
|
|
}
|
|
this.cache.set(cacheKey, result, 3600);
|
|
return result;
|
|
}
|
|
async getNode(nodeType, detail = 'standard', mode = 'info', includeTypeInfo, includeExamples, fromVersion, toVersion) {
|
|
await this.ensureInitialized();
|
|
if (!this.repository)
|
|
throw new Error('Repository not initialized');
|
|
const validDetailLevels = ['minimal', 'standard', 'full'];
|
|
const validModes = ['info', 'versions', 'compare', 'breaking', 'migrations'];
|
|
if (!validDetailLevels.includes(detail)) {
|
|
throw new Error(`get_node: Invalid detail level "${detail}". Valid options: ${validDetailLevels.join(', ')}`);
|
|
}
|
|
if (!validModes.includes(mode)) {
|
|
throw new Error(`get_node: Invalid mode "${mode}". Valid options: ${validModes.join(', ')}`);
|
|
}
|
|
const normalizedType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(nodeType);
|
|
if (mode !== 'info') {
|
|
return this.handleVersionMode(normalizedType, mode, fromVersion, toVersion);
|
|
}
|
|
return this.handleInfoMode(normalizedType, detail, includeTypeInfo, includeExamples);
|
|
}
|
|
async handleInfoMode(nodeType, detail, includeTypeInfo, includeExamples) {
|
|
switch (detail) {
|
|
case 'minimal': {
|
|
let node = this.repository.getNode(nodeType);
|
|
if (!node) {
|
|
const alternatives = (0, node_utils_1.getNodeTypeAlternatives)(nodeType);
|
|
for (const alt of alternatives) {
|
|
const found = this.repository.getNode(alt);
|
|
if (found) {
|
|
node = found;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!node) {
|
|
throw new Error(`Node ${nodeType} not found`);
|
|
}
|
|
const result = {
|
|
nodeType: node.nodeType,
|
|
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package ?? 'n8n-nodes-base', node.nodeType),
|
|
displayName: node.displayName,
|
|
description: node.description,
|
|
category: node.category,
|
|
package: node.package,
|
|
isAITool: node.isAITool,
|
|
isTrigger: node.isTrigger,
|
|
isWebhook: node.isWebhook
|
|
};
|
|
const toolVariantInfo = this.buildToolVariantGuidance(node);
|
|
if (toolVariantInfo) {
|
|
result.toolVariantInfo = toolVariantInfo;
|
|
}
|
|
return result;
|
|
}
|
|
case 'standard': {
|
|
const essentials = await this.getNodeEssentials(nodeType, includeExamples);
|
|
const versionSummary = this.getVersionSummary(nodeType);
|
|
if (includeTypeInfo) {
|
|
essentials.requiredProperties = this.enrichPropertiesWithTypeInfo(essentials.requiredProperties);
|
|
essentials.commonProperties = this.enrichPropertiesWithTypeInfo(essentials.commonProperties);
|
|
}
|
|
return {
|
|
...essentials,
|
|
versionInfo: versionSummary
|
|
};
|
|
}
|
|
case 'full': {
|
|
const fullInfo = await this.getNodeInfo(nodeType);
|
|
const versionSummary = this.getVersionSummary(nodeType);
|
|
if (includeTypeInfo && fullInfo.properties) {
|
|
fullInfo.properties = this.enrichPropertiesWithTypeInfo(fullInfo.properties);
|
|
}
|
|
return {
|
|
...fullInfo,
|
|
versionInfo: versionSummary
|
|
};
|
|
}
|
|
default:
|
|
throw new Error(`Unknown detail level: ${detail}`);
|
|
}
|
|
}
|
|
async handleVersionMode(nodeType, mode, fromVersion, toVersion) {
|
|
switch (mode) {
|
|
case 'versions':
|
|
return this.getVersionHistory(nodeType);
|
|
case 'compare':
|
|
if (!fromVersion) {
|
|
throw new Error(`get_node: fromVersion is required for compare mode (nodeType: ${nodeType})`);
|
|
}
|
|
return this.compareVersions(nodeType, fromVersion, toVersion);
|
|
case 'breaking':
|
|
if (!fromVersion) {
|
|
throw new Error(`get_node: fromVersion is required for breaking mode (nodeType: ${nodeType})`);
|
|
}
|
|
return this.getBreakingChanges(nodeType, fromVersion, toVersion);
|
|
case 'migrations':
|
|
if (!fromVersion || !toVersion) {
|
|
throw new Error(`get_node: Both fromVersion and toVersion are required for migrations mode (nodeType: ${nodeType})`);
|
|
}
|
|
return this.getMigrations(nodeType, fromVersion, toVersion);
|
|
default:
|
|
throw new Error(`get_node: Unknown mode: ${mode} (nodeType: ${nodeType})`);
|
|
}
|
|
}
|
|
getVersionSummary(nodeType) {
|
|
const cacheKey = `version-summary:${nodeType}`;
|
|
const cached = this.cache.get(cacheKey);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
const versions = this.repository.getNodeVersions(nodeType);
|
|
const latest = this.repository.getLatestNodeVersion(nodeType);
|
|
const summary = {
|
|
currentVersion: latest?.version || 'unknown',
|
|
totalVersions: versions.length,
|
|
hasVersionHistory: versions.length > 0
|
|
};
|
|
this.cache.set(cacheKey, summary, 86400000);
|
|
return summary;
|
|
}
|
|
getVersionHistory(nodeType) {
|
|
const versions = this.repository.getNodeVersions(nodeType);
|
|
return {
|
|
nodeType,
|
|
totalVersions: versions.length,
|
|
versions: versions.map(v => ({
|
|
version: v.version,
|
|
isCurrent: v.isCurrentMax,
|
|
minimumN8nVersion: v.minimumN8nVersion,
|
|
releasedAt: v.releasedAt,
|
|
hasBreakingChanges: (v.breakingChanges || []).length > 0,
|
|
breakingChangesCount: (v.breakingChanges || []).length,
|
|
deprecatedProperties: v.deprecatedProperties || [],
|
|
addedProperties: v.addedProperties || []
|
|
})),
|
|
available: versions.length > 0,
|
|
message: versions.length === 0 ?
|
|
'No version history available. Version tracking may not be enabled for this node.' :
|
|
undefined
|
|
};
|
|
}
|
|
compareVersions(nodeType, fromVersion, toVersion) {
|
|
const latest = this.repository.getLatestNodeVersion(nodeType);
|
|
const targetVersion = toVersion || latest?.version;
|
|
if (!targetVersion) {
|
|
throw new Error('No target version available');
|
|
}
|
|
const changes = this.repository.getPropertyChanges(nodeType, fromVersion, targetVersion);
|
|
return {
|
|
nodeType,
|
|
fromVersion,
|
|
toVersion: targetVersion,
|
|
totalChanges: changes.length,
|
|
breakingChanges: changes.filter(c => c.isBreaking).length,
|
|
changes: changes.map(c => ({
|
|
property: c.propertyName,
|
|
changeType: c.changeType,
|
|
isBreaking: c.isBreaking,
|
|
severity: c.severity,
|
|
oldValue: c.oldValue,
|
|
newValue: c.newValue,
|
|
migrationHint: c.migrationHint,
|
|
autoMigratable: c.autoMigratable
|
|
}))
|
|
};
|
|
}
|
|
getBreakingChanges(nodeType, fromVersion, toVersion) {
|
|
const breakingChanges = this.repository.getBreakingChanges(nodeType, fromVersion, toVersion);
|
|
return {
|
|
nodeType,
|
|
fromVersion,
|
|
toVersion: toVersion || 'latest',
|
|
totalBreakingChanges: breakingChanges.length,
|
|
changes: breakingChanges.map(c => ({
|
|
fromVersion: c.fromVersion,
|
|
toVersion: c.toVersion,
|
|
property: c.propertyName,
|
|
changeType: c.changeType,
|
|
severity: c.severity,
|
|
migrationHint: c.migrationHint,
|
|
oldValue: c.oldValue,
|
|
newValue: c.newValue
|
|
})),
|
|
upgradeSafe: breakingChanges.length === 0
|
|
};
|
|
}
|
|
getMigrations(nodeType, fromVersion, toVersion) {
|
|
const migrations = this.repository.getAutoMigratableChanges(nodeType, fromVersion, toVersion);
|
|
const allChanges = this.repository.getPropertyChanges(nodeType, fromVersion, toVersion);
|
|
return {
|
|
nodeType,
|
|
fromVersion,
|
|
toVersion,
|
|
autoMigratableChanges: migrations.length,
|
|
totalChanges: allChanges.length,
|
|
migrations: migrations.map(m => ({
|
|
property: m.propertyName,
|
|
changeType: m.changeType,
|
|
migrationStrategy: m.migrationStrategy,
|
|
severity: m.severity
|
|
})),
|
|
requiresManualMigration: migrations.length < allChanges.length
|
|
};
|
|
}
|
|
enrichPropertyWithTypeInfo(property) {
|
|
if (!property || !property.type)
|
|
return property;
|
|
const structure = type_structure_service_1.TypeStructureService.getStructure(property.type);
|
|
if (!structure)
|
|
return property;
|
|
return {
|
|
...property,
|
|
typeInfo: {
|
|
category: structure.type,
|
|
jsType: structure.jsType,
|
|
description: structure.description,
|
|
isComplex: type_structure_service_1.TypeStructureService.isComplexType(property.type),
|
|
isPrimitive: type_structure_service_1.TypeStructureService.isPrimitiveType(property.type),
|
|
allowsExpressions: structure.validation?.allowExpressions ?? true,
|
|
allowsEmpty: structure.validation?.allowEmpty ?? false,
|
|
...(structure.structure && {
|
|
structureHints: {
|
|
hasProperties: !!structure.structure.properties,
|
|
hasItems: !!structure.structure.items,
|
|
isFlexible: structure.structure.flexible ?? false,
|
|
requiredFields: structure.structure.required ?? []
|
|
}
|
|
}),
|
|
...(structure.notes && { notes: structure.notes })
|
|
}
|
|
};
|
|
}
|
|
enrichPropertiesWithTypeInfo(properties) {
|
|
if (!properties || !Array.isArray(properties))
|
|
return properties;
|
|
return properties.map((prop) => this.enrichPropertyWithTypeInfo(prop));
|
|
}
|
|
async searchNodeProperties(nodeType, query, maxResults = 20) {
|
|
await this.ensureInitialized();
|
|
if (!this.repository)
|
|
throw new Error('Repository not initialized');
|
|
const normalizedType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(nodeType);
|
|
let node = this.repository.getNode(normalizedType);
|
|
if (!node && normalizedType !== nodeType) {
|
|
node = this.repository.getNode(nodeType);
|
|
}
|
|
if (!node) {
|
|
const alternatives = (0, node_utils_1.getNodeTypeAlternatives)(normalizedType);
|
|
for (const alt of alternatives) {
|
|
const found = this.repository.getNode(alt);
|
|
if (found) {
|
|
node = found;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!node) {
|
|
throw new Error(`Node ${nodeType} not found`);
|
|
}
|
|
const allProperties = node.properties || [];
|
|
const matches = property_filter_1.PropertyFilter.searchProperties(allProperties, query, maxResults);
|
|
return {
|
|
nodeType: node.nodeType,
|
|
query,
|
|
matches: matches.map((match) => ({
|
|
name: match.name,
|
|
displayName: match.displayName,
|
|
type: match.type,
|
|
description: match.description,
|
|
path: match.path || match.name,
|
|
required: match.required,
|
|
default: match.default,
|
|
options: match.options,
|
|
showWhen: match.showWhen
|
|
})),
|
|
totalMatches: matches.length,
|
|
searchedIn: allProperties.length + ' properties'
|
|
};
|
|
}
|
|
getPropertyValue(config, path) {
|
|
const parts = path.split('.');
|
|
let value = config;
|
|
for (const part of parts) {
|
|
const arrayMatch = part.match(/^(\w+)\[(\d+)\]$/);
|
|
if (arrayMatch) {
|
|
value = value?.[arrayMatch[1]]?.[parseInt(arrayMatch[2])];
|
|
}
|
|
else {
|
|
value = value?.[part];
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
async listTasks(category) {
|
|
if (category) {
|
|
const categories = task_templates_1.TaskTemplates.getTaskCategories();
|
|
const tasks = categories[category];
|
|
if (!tasks) {
|
|
throw new Error(`Unknown category: ${category}. Available categories: ${Object.keys(categories).join(', ')}`);
|
|
}
|
|
return {
|
|
category,
|
|
tasks: tasks.map(task => {
|
|
const template = task_templates_1.TaskTemplates.getTaskTemplate(task);
|
|
return {
|
|
task,
|
|
description: template?.description || '',
|
|
nodeType: template?.nodeType || ''
|
|
};
|
|
})
|
|
};
|
|
}
|
|
const categories = task_templates_1.TaskTemplates.getTaskCategories();
|
|
const result = {
|
|
totalTasks: task_templates_1.TaskTemplates.getAllTasks().length,
|
|
categories: {}
|
|
};
|
|
for (const [cat, tasks] of Object.entries(categories)) {
|
|
result.categories[cat] = tasks.map(task => {
|
|
const template = task_templates_1.TaskTemplates.getTaskTemplate(task);
|
|
return {
|
|
task,
|
|
description: template?.description || '',
|
|
nodeType: template?.nodeType || ''
|
|
};
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
async validateNodeConfig(nodeType, config, mode = 'operation', profile = 'ai-friendly') {
|
|
await this.ensureInitialized();
|
|
if (!this.repository)
|
|
throw new Error('Repository not initialized');
|
|
const normalizedType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(nodeType);
|
|
let node = this.repository.getNode(normalizedType);
|
|
if (!node && normalizedType !== nodeType) {
|
|
node = this.repository.getNode(nodeType);
|
|
}
|
|
if (!node) {
|
|
const alternatives = (0, node_utils_1.getNodeTypeAlternatives)(normalizedType);
|
|
for (const alt of alternatives) {
|
|
const found = this.repository.getNode(alt);
|
|
if (found) {
|
|
node = found;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!node) {
|
|
throw new Error(`Node ${nodeType} not found`);
|
|
}
|
|
const properties = node.properties || [];
|
|
const configWithVersion = {
|
|
'@version': node.version || 1,
|
|
...config
|
|
};
|
|
const validationResult = enhanced_config_validator_1.EnhancedConfigValidator.validateWithMode(node.nodeType, configWithVersion, properties, mode, profile);
|
|
return {
|
|
nodeType: node.nodeType,
|
|
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package, node.nodeType),
|
|
displayName: node.displayName,
|
|
...validationResult,
|
|
summary: {
|
|
hasErrors: !validationResult.valid,
|
|
errorCount: validationResult.errors.length,
|
|
warningCount: validationResult.warnings.length,
|
|
suggestionCount: validationResult.suggestions.length
|
|
}
|
|
};
|
|
}
|
|
async getPropertyDependencies(nodeType, config) {
|
|
await this.ensureInitialized();
|
|
if (!this.repository)
|
|
throw new Error('Repository not initialized');
|
|
const normalizedType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(nodeType);
|
|
let node = this.repository.getNode(normalizedType);
|
|
if (!node && normalizedType !== nodeType) {
|
|
node = this.repository.getNode(nodeType);
|
|
}
|
|
if (!node) {
|
|
const alternatives = (0, node_utils_1.getNodeTypeAlternatives)(normalizedType);
|
|
for (const alt of alternatives) {
|
|
const found = this.repository.getNode(alt);
|
|
if (found) {
|
|
node = found;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!node) {
|
|
throw new Error(`Node ${nodeType} not found`);
|
|
}
|
|
const properties = node.properties || [];
|
|
const analysis = property_dependencies_1.PropertyDependencies.analyze(properties);
|
|
let visibilityImpact = null;
|
|
if (config) {
|
|
visibilityImpact = property_dependencies_1.PropertyDependencies.getVisibilityImpact(properties, config);
|
|
}
|
|
return {
|
|
nodeType: node.nodeType,
|
|
displayName: node.displayName,
|
|
...analysis,
|
|
currentConfig: config ? {
|
|
providedValues: config,
|
|
visibilityImpact
|
|
} : undefined
|
|
};
|
|
}
|
|
async getNodeAsToolInfo(nodeType) {
|
|
await this.ensureInitialized();
|
|
if (!this.repository)
|
|
throw new Error('Repository not initialized');
|
|
const normalizedType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(nodeType);
|
|
let node = this.repository.getNode(normalizedType);
|
|
if (!node && normalizedType !== nodeType) {
|
|
node = this.repository.getNode(nodeType);
|
|
}
|
|
if (!node) {
|
|
const alternatives = (0, node_utils_1.getNodeTypeAlternatives)(normalizedType);
|
|
for (const alt of alternatives) {
|
|
const found = this.repository.getNode(alt);
|
|
if (found) {
|
|
node = found;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!node) {
|
|
throw new Error(`Node ${nodeType} not found`);
|
|
}
|
|
const commonUseCases = this.getCommonAIToolUseCases(node.nodeType);
|
|
const aiToolCapabilities = {
|
|
canBeUsedAsTool: true,
|
|
hasUsableAsToolProperty: node.isAITool,
|
|
requiresEnvironmentVariable: !node.isAITool && node.package !== 'n8n-nodes-base',
|
|
connectionType: 'ai_tool',
|
|
commonUseCases,
|
|
requirements: {
|
|
connection: 'Connect to the "ai_tool" port of an AI Agent node',
|
|
environment: node.package !== 'n8n-nodes-base' ?
|
|
'Set N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true for community nodes' :
|
|
'No special environment variables needed for built-in nodes'
|
|
},
|
|
examples: this.getAIToolExamples(node.nodeType),
|
|
tips: [
|
|
'Give the tool a clear, descriptive name in the AI Agent settings',
|
|
'Write a detailed tool description to help the AI understand when to use it',
|
|
'Test the node independently before connecting it as a tool',
|
|
node.isAITool ?
|
|
'This node is optimized for AI tool usage' :
|
|
'This is a regular node that can be used as an AI tool'
|
|
]
|
|
};
|
|
return {
|
|
nodeType: node.nodeType,
|
|
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package, node.nodeType),
|
|
displayName: node.displayName,
|
|
description: node.description,
|
|
package: node.package,
|
|
isMarkedAsAITool: node.isAITool,
|
|
aiToolCapabilities
|
|
};
|
|
}
|
|
getOutputDescriptions(nodeType, outputName, index) {
|
|
if (nodeType === 'nodes-base.splitInBatches') {
|
|
if (outputName === 'done' && index === 0) {
|
|
return {
|
|
description: 'Final processed data after all iterations complete',
|
|
connectionGuidance: 'Connect to nodes that should run AFTER the loop completes'
|
|
};
|
|
}
|
|
else if (outputName === 'loop' && index === 1) {
|
|
return {
|
|
description: 'Current batch data for this iteration',
|
|
connectionGuidance: 'Connect to nodes that process items INSIDE the loop (and connect their output back to this node)'
|
|
};
|
|
}
|
|
}
|
|
if (nodeType === 'nodes-base.if') {
|
|
if (outputName === 'true' && index === 0) {
|
|
return {
|
|
description: 'Items that match the condition',
|
|
connectionGuidance: 'Connect to nodes that handle the TRUE case'
|
|
};
|
|
}
|
|
else if (outputName === 'false' && index === 1) {
|
|
return {
|
|
description: 'Items that do not match the condition',
|
|
connectionGuidance: 'Connect to nodes that handle the FALSE case'
|
|
};
|
|
}
|
|
}
|
|
if (nodeType === 'nodes-base.switch') {
|
|
return {
|
|
description: `Output ${index}: ${outputName || 'Route ' + index}`,
|
|
connectionGuidance: `Connect to nodes for the "${outputName || 'route ' + index}" case`
|
|
};
|
|
}
|
|
return {
|
|
description: outputName || `Output ${index}`,
|
|
connectionGuidance: `Connect to downstream nodes`
|
|
};
|
|
}
|
|
getCommonAIToolUseCases(nodeType) {
|
|
const useCaseMap = {
|
|
'nodes-base.slack': [
|
|
'Send notifications about task completion',
|
|
'Post updates to channels',
|
|
'Send direct messages',
|
|
'Create alerts and reminders'
|
|
],
|
|
'nodes-base.googleSheets': [
|
|
'Read data for analysis',
|
|
'Log results and outputs',
|
|
'Update spreadsheet records',
|
|
'Create reports'
|
|
],
|
|
'nodes-base.gmail': [
|
|
'Send email notifications',
|
|
'Read and process emails',
|
|
'Send reports and summaries',
|
|
'Handle email-based workflows'
|
|
],
|
|
'nodes-base.httpRequest': [
|
|
'Call external APIs',
|
|
'Fetch data from web services',
|
|
'Send webhooks',
|
|
'Integrate with any REST API'
|
|
],
|
|
'nodes-base.postgres': [
|
|
'Query database for information',
|
|
'Store analysis results',
|
|
'Update records based on AI decisions',
|
|
'Generate reports from data'
|
|
],
|
|
'nodes-base.webhook': [
|
|
'Receive external triggers',
|
|
'Create callback endpoints',
|
|
'Handle incoming data',
|
|
'Integrate with external systems'
|
|
]
|
|
};
|
|
for (const [key, useCases] of Object.entries(useCaseMap)) {
|
|
if (nodeType.includes(key)) {
|
|
return useCases;
|
|
}
|
|
}
|
|
return [
|
|
'Perform automated actions',
|
|
'Integrate with external services',
|
|
'Process and transform data',
|
|
'Extend AI agent capabilities'
|
|
];
|
|
}
|
|
buildToolVariantGuidance(node) {
|
|
const isToolVariant = !!node.isToolVariant;
|
|
const hasToolVariant = !!node.hasToolVariant;
|
|
const toolVariantOf = node.toolVariantOf;
|
|
if (!isToolVariant && !hasToolVariant) {
|
|
return undefined;
|
|
}
|
|
if (isToolVariant) {
|
|
return {
|
|
isToolVariant: true,
|
|
toolVariantOf,
|
|
hasToolVariant: false,
|
|
guidance: `This is the Tool variant for AI Agent integration. Use this node type when connecting to AI Agents. The base node is: ${toolVariantOf}`
|
|
};
|
|
}
|
|
if (hasToolVariant && node.nodeType) {
|
|
const toolVariantNodeType = `${node.nodeType}Tool`;
|
|
return {
|
|
isToolVariant: false,
|
|
hasToolVariant: true,
|
|
toolVariantNodeType,
|
|
guidance: `To use this node with AI Agents, use the Tool variant: ${toolVariantNodeType}. The Tool variant has an additional 'toolDescription' property and outputs 'ai_tool' instead of 'main'.`
|
|
};
|
|
}
|
|
return undefined;
|
|
}
|
|
getAIToolExamples(nodeType) {
|
|
const exampleMap = {
|
|
'nodes-base.slack': {
|
|
toolName: 'Send Slack Message',
|
|
toolDescription: 'Sends a message to a specified Slack channel or user. Use this to notify team members about important events or results.',
|
|
nodeConfig: {
|
|
resource: 'message',
|
|
operation: 'post',
|
|
channel: '={{ $fromAI("channel", "The Slack channel to send to, e.g. #general") }}',
|
|
text: '={{ $fromAI("message", "The message content to send") }}'
|
|
}
|
|
},
|
|
'nodes-base.googleSheets': {
|
|
toolName: 'Update Google Sheet',
|
|
toolDescription: 'Reads or updates data in a Google Sheets spreadsheet. Use this to log information, retrieve data, or update records.',
|
|
nodeConfig: {
|
|
operation: 'append',
|
|
sheetId: 'your-sheet-id',
|
|
range: 'A:Z',
|
|
dataMode: 'autoMap'
|
|
}
|
|
},
|
|
'nodes-base.httpRequest': {
|
|
toolName: 'Call API',
|
|
toolDescription: 'Makes HTTP requests to external APIs. Use this to fetch data, trigger webhooks, or integrate with any web service.',
|
|
nodeConfig: {
|
|
method: '={{ $fromAI("method", "HTTP method: GET, POST, PUT, DELETE") }}',
|
|
url: '={{ $fromAI("url", "The complete API endpoint URL") }}',
|
|
sendBody: true,
|
|
bodyContentType: 'json',
|
|
jsonBody: '={{ $fromAI("body", "Request body as JSON object") }}'
|
|
}
|
|
}
|
|
};
|
|
for (const [key, example] of Object.entries(exampleMap)) {
|
|
if (nodeType.includes(key)) {
|
|
return example;
|
|
}
|
|
}
|
|
return {
|
|
toolName: 'Custom Tool',
|
|
toolDescription: 'Performs specific operations. Describe what this tool does and when to use it.',
|
|
nodeConfig: {
|
|
note: 'Configure the node based on its specific requirements'
|
|
}
|
|
};
|
|
}
|
|
async validateNodeMinimal(nodeType, config) {
|
|
await this.ensureInitialized();
|
|
if (!this.repository)
|
|
throw new Error('Repository not initialized');
|
|
const normalizedType = node_type_normalizer_1.NodeTypeNormalizer.normalizeToFullForm(nodeType);
|
|
let node = this.repository.getNode(normalizedType);
|
|
if (!node && normalizedType !== nodeType) {
|
|
node = this.repository.getNode(nodeType);
|
|
}
|
|
if (!node) {
|
|
const alternatives = (0, node_utils_1.getNodeTypeAlternatives)(normalizedType);
|
|
for (const alt of alternatives) {
|
|
const found = this.repository.getNode(alt);
|
|
if (found) {
|
|
node = found;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!node) {
|
|
throw new Error(`Node ${nodeType} not found`);
|
|
}
|
|
const properties = node.properties || [];
|
|
const configWithVersion = {
|
|
'@version': node.version || 1,
|
|
...(config || {})
|
|
};
|
|
const missingFields = [];
|
|
for (const prop of properties) {
|
|
if (!prop.required)
|
|
continue;
|
|
if (prop.displayOptions && !config_validator_1.ConfigValidator.isPropertyVisible(prop, configWithVersion)) {
|
|
continue;
|
|
}
|
|
if (!config || !(prop.name in config)) {
|
|
missingFields.push(prop.displayName || prop.name);
|
|
}
|
|
}
|
|
return {
|
|
nodeType: node.nodeType,
|
|
displayName: node.displayName,
|
|
valid: missingFields.length === 0,
|
|
missingRequiredFields: missingFields
|
|
};
|
|
}
|
|
async getToolsDocumentation(topic, depth = 'essentials') {
|
|
if (!topic || topic === 'overview') {
|
|
return (0, tools_documentation_1.getToolsOverview)(depth);
|
|
}
|
|
return (0, tools_documentation_1.getToolDocumentation)(topic, depth);
|
|
}
|
|
async connect(transport) {
|
|
await this.ensureInitialized();
|
|
await this.server.connect(transport);
|
|
logger_1.logger.info('MCP Server connected', {
|
|
transportType: transport.constructor.name
|
|
});
|
|
}
|
|
async listTemplates(limit = 10, offset = 0, sortBy = 'views', includeMetadata = false) {
|
|
await this.ensureInitialized();
|
|
if (!this.templateService)
|
|
throw new Error('Template service not initialized');
|
|
const result = await this.templateService.listTemplates(limit, offset, sortBy, includeMetadata);
|
|
return {
|
|
...result,
|
|
tip: result.items.length > 0 ?
|
|
`Use get_template(templateId) to get full workflow details. Total: ${result.total} templates available.` :
|
|
"No templates found. Run 'npm run fetch:templates' to update template database"
|
|
};
|
|
}
|
|
async listNodeTemplates(nodeTypes, limit = 10, offset = 0) {
|
|
await this.ensureInitialized();
|
|
if (!this.templateService)
|
|
throw new Error('Template service not initialized');
|
|
const result = await this.templateService.listNodeTemplates(nodeTypes, limit, offset);
|
|
if (result.items.length === 0 && offset === 0) {
|
|
return {
|
|
...result,
|
|
message: `No templates found using nodes: ${nodeTypes.join(', ')}`,
|
|
tip: "Try searching with more common nodes or run 'npm run fetch:templates' to update template database"
|
|
};
|
|
}
|
|
return {
|
|
...result,
|
|
tip: `Showing ${result.items.length} of ${result.total} templates. Use offset for pagination.`
|
|
};
|
|
}
|
|
async getTemplate(templateId, mode = 'full') {
|
|
await this.ensureInitialized();
|
|
if (!this.templateService)
|
|
throw new Error('Template service not initialized');
|
|
const template = await this.templateService.getTemplate(templateId, mode);
|
|
if (!template) {
|
|
return {
|
|
error: `Template ${templateId} not found`,
|
|
tip: "Use list_templates, list_node_templates or search_templates to find available templates"
|
|
};
|
|
}
|
|
const usage = mode === 'nodes_only' ? "Node list for quick overview" :
|
|
mode === 'structure' ? "Workflow structure without full details" :
|
|
"Complete workflow JSON ready to import into n8n";
|
|
return {
|
|
mode,
|
|
template,
|
|
usage
|
|
};
|
|
}
|
|
async searchTemplates(query, limit = 20, offset = 0, fields) {
|
|
await this.ensureInitialized();
|
|
if (!this.templateService)
|
|
throw new Error('Template service not initialized');
|
|
const result = await this.templateService.searchTemplates(query, limit, offset, fields);
|
|
if (result.items.length === 0 && offset === 0) {
|
|
return {
|
|
...result,
|
|
message: `No templates found matching: "${query}"`,
|
|
tip: "Try different keywords or run 'npm run fetch:templates' to update template database"
|
|
};
|
|
}
|
|
return {
|
|
...result,
|
|
query,
|
|
tip: `Found ${result.total} templates matching "${query}". Showing ${result.items.length}.`
|
|
};
|
|
}
|
|
async getTemplatesForTask(task, limit = 10, offset = 0) {
|
|
await this.ensureInitialized();
|
|
if (!this.templateService)
|
|
throw new Error('Template service not initialized');
|
|
const result = await this.templateService.getTemplatesForTask(task, limit, offset);
|
|
const availableTasks = this.templateService.listAvailableTasks();
|
|
if (result.items.length === 0 && offset === 0) {
|
|
return {
|
|
...result,
|
|
message: `No templates found for task: ${task}`,
|
|
availableTasks,
|
|
tip: "Try a different task or use search_templates for custom searches"
|
|
};
|
|
}
|
|
return {
|
|
...result,
|
|
task,
|
|
description: this.getTaskDescription(task),
|
|
tip: `${result.total} templates available for ${task}. Showing ${result.items.length}.`
|
|
};
|
|
}
|
|
async searchTemplatesByMetadata(filters, limit = 20, offset = 0) {
|
|
await this.ensureInitialized();
|
|
if (!this.templateService)
|
|
throw new Error('Template service not initialized');
|
|
const result = await this.templateService.searchTemplatesByMetadata(filters, limit, offset);
|
|
const filterSummary = [];
|
|
if (filters.category)
|
|
filterSummary.push(`category: ${filters.category}`);
|
|
if (filters.complexity)
|
|
filterSummary.push(`complexity: ${filters.complexity}`);
|
|
if (filters.maxSetupMinutes)
|
|
filterSummary.push(`max setup: ${filters.maxSetupMinutes} min`);
|
|
if (filters.minSetupMinutes)
|
|
filterSummary.push(`min setup: ${filters.minSetupMinutes} min`);
|
|
if (filters.requiredService)
|
|
filterSummary.push(`service: ${filters.requiredService}`);
|
|
if (filters.targetAudience)
|
|
filterSummary.push(`audience: ${filters.targetAudience}`);
|
|
if (result.items.length === 0 && offset === 0) {
|
|
const availableCategories = await this.templateService.getAvailableCategories();
|
|
const availableAudiences = await this.templateService.getAvailableTargetAudiences();
|
|
return {
|
|
...result,
|
|
message: `No templates found with filters: ${filterSummary.join(', ')}`,
|
|
availableCategories: availableCategories.slice(0, 10),
|
|
availableAudiences: availableAudiences.slice(0, 5),
|
|
tip: "Try broader filters or different categories. Use list_templates to see all templates."
|
|
};
|
|
}
|
|
return {
|
|
...result,
|
|
filters,
|
|
filterSummary: filterSummary.join(', '),
|
|
tip: `Found ${result.total} templates matching filters. Showing ${result.items.length}. Each includes AI-generated metadata.`
|
|
};
|
|
}
|
|
getTaskDescription(task) {
|
|
const descriptions = {
|
|
'ai_automation': 'AI-powered workflows using OpenAI, LangChain, and other AI tools',
|
|
'data_sync': 'Synchronize data between databases, spreadsheets, and APIs',
|
|
'webhook_processing': 'Process incoming webhooks and trigger automated actions',
|
|
'email_automation': 'Send, receive, and process emails automatically',
|
|
'slack_integration': 'Integrate with Slack for notifications and bot interactions',
|
|
'data_transformation': 'Transform, clean, and manipulate data',
|
|
'file_processing': 'Handle file uploads, downloads, and transformations',
|
|
'scheduling': 'Schedule recurring tasks and time-based automations',
|
|
'api_integration': 'Connect to external APIs and web services',
|
|
'database_operations': 'Query, insert, update, and manage database records'
|
|
};
|
|
return descriptions[task] || 'Workflow templates for this task';
|
|
}
|
|
async validateWorkflow(workflow, options) {
|
|
await this.ensureInitialized();
|
|
if (!this.repository)
|
|
throw new Error('Repository not initialized');
|
|
logger_1.logger.info('Workflow validation requested', {
|
|
hasWorkflow: !!workflow,
|
|
workflowType: typeof workflow,
|
|
hasNodes: workflow?.nodes !== undefined,
|
|
nodesType: workflow?.nodes ? typeof workflow.nodes : 'undefined',
|
|
nodesIsArray: Array.isArray(workflow?.nodes),
|
|
nodesCount: Array.isArray(workflow?.nodes) ? workflow.nodes.length : 0,
|
|
hasConnections: workflow?.connections !== undefined,
|
|
connectionsType: workflow?.connections ? typeof workflow.connections : 'undefined',
|
|
options: options
|
|
});
|
|
if (!workflow || typeof workflow !== 'object') {
|
|
return {
|
|
valid: false,
|
|
errors: [{
|
|
node: 'workflow',
|
|
message: 'Workflow must be an object with nodes and connections',
|
|
details: 'Expected format: ' + (0, workflow_examples_1.getWorkflowExampleString)()
|
|
}],
|
|
summary: { errorCount: 1 }
|
|
};
|
|
}
|
|
if (!workflow.nodes || !Array.isArray(workflow.nodes)) {
|
|
return {
|
|
valid: false,
|
|
errors: [{
|
|
node: 'workflow',
|
|
message: 'Workflow must have a nodes array',
|
|
details: 'Expected: workflow.nodes = [array of node objects]. ' + (0, workflow_examples_1.getWorkflowExampleString)()
|
|
}],
|
|
summary: { errorCount: 1 }
|
|
};
|
|
}
|
|
if (!workflow.connections || typeof workflow.connections !== 'object') {
|
|
return {
|
|
valid: false,
|
|
errors: [{
|
|
node: 'workflow',
|
|
message: 'Workflow must have a connections object',
|
|
details: 'Expected: workflow.connections = {} (can be empty object). ' + (0, workflow_examples_1.getWorkflowExampleString)()
|
|
}],
|
|
summary: { errorCount: 1 }
|
|
};
|
|
}
|
|
const validator = new workflow_validator_1.WorkflowValidator(this.repository, enhanced_config_validator_1.EnhancedConfigValidator);
|
|
try {
|
|
const result = await validator.validateWorkflow(workflow, options);
|
|
const response = {
|
|
valid: result.valid,
|
|
summary: {
|
|
totalNodes: result.statistics.totalNodes,
|
|
enabledNodes: result.statistics.enabledNodes,
|
|
triggerNodes: result.statistics.triggerNodes,
|
|
validConnections: result.statistics.validConnections,
|
|
invalidConnections: result.statistics.invalidConnections,
|
|
expressionsValidated: result.statistics.expressionsValidated,
|
|
errorCount: result.errors.length,
|
|
warningCount: result.warnings.length
|
|
},
|
|
errors: result.errors.map(e => ({
|
|
node: e.nodeName || 'workflow',
|
|
message: e.message,
|
|
details: e.details
|
|
})),
|
|
warnings: result.warnings.map(w => ({
|
|
node: w.nodeName || 'workflow',
|
|
message: w.message,
|
|
details: w.details
|
|
}))
|
|
};
|
|
if (result.suggestions.length > 0) {
|
|
response.suggestions = result.suggestions;
|
|
}
|
|
if (!result.valid && result.errors.length > 0) {
|
|
result.errors.forEach(error => {
|
|
telemetry_1.telemetry.trackValidationDetails(error.nodeName || 'workflow', error.type || 'validation_error', {
|
|
message: error.message,
|
|
nodeCount: workflow.nodes?.length ?? 0,
|
|
hasConnections: Object.keys(workflow.connections || {}).length > 0
|
|
});
|
|
});
|
|
}
|
|
if (result.valid) {
|
|
telemetry_1.telemetry.trackWorkflowCreation(workflow, true);
|
|
}
|
|
return response;
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.error('Error validating workflow:', error);
|
|
return {
|
|
valid: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error validating workflow',
|
|
tip: 'Ensure the workflow JSON includes nodes array and connections object'
|
|
};
|
|
}
|
|
}
|
|
async validateWorkflowConnections(workflow) {
|
|
await this.ensureInitialized();
|
|
if (!this.repository)
|
|
throw new Error('Repository not initialized');
|
|
const validator = new workflow_validator_1.WorkflowValidator(this.repository, enhanced_config_validator_1.EnhancedConfigValidator);
|
|
try {
|
|
const result = await validator.validateWorkflow(workflow, {
|
|
validateNodes: false,
|
|
validateConnections: true,
|
|
validateExpressions: false
|
|
});
|
|
const response = {
|
|
valid: result.errors.length === 0,
|
|
statistics: {
|
|
totalNodes: result.statistics.totalNodes,
|
|
triggerNodes: result.statistics.triggerNodes,
|
|
validConnections: result.statistics.validConnections,
|
|
invalidConnections: result.statistics.invalidConnections
|
|
}
|
|
};
|
|
const connectionErrors = result.errors.filter(e => e.message.includes('connection') ||
|
|
e.message.includes('cycle') ||
|
|
e.message.includes('orphaned'));
|
|
const connectionWarnings = result.warnings.filter(w => w.message.includes('connection') ||
|
|
w.message.includes('orphaned') ||
|
|
w.message.includes('trigger'));
|
|
if (connectionErrors.length > 0) {
|
|
response.errors = connectionErrors.map(e => ({
|
|
node: e.nodeName || 'workflow',
|
|
message: e.message
|
|
}));
|
|
}
|
|
if (connectionWarnings.length > 0) {
|
|
response.warnings = connectionWarnings.map(w => ({
|
|
node: w.nodeName || 'workflow',
|
|
message: w.message
|
|
}));
|
|
}
|
|
return response;
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.error('Error validating workflow connections:', error);
|
|
return {
|
|
valid: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error validating connections'
|
|
};
|
|
}
|
|
}
|
|
async validateWorkflowExpressions(workflow) {
|
|
await this.ensureInitialized();
|
|
if (!this.repository)
|
|
throw new Error('Repository not initialized');
|
|
const validator = new workflow_validator_1.WorkflowValidator(this.repository, enhanced_config_validator_1.EnhancedConfigValidator);
|
|
try {
|
|
const result = await validator.validateWorkflow(workflow, {
|
|
validateNodes: false,
|
|
validateConnections: false,
|
|
validateExpressions: true
|
|
});
|
|
const response = {
|
|
valid: result.errors.length === 0,
|
|
statistics: {
|
|
totalNodes: result.statistics.totalNodes,
|
|
expressionsValidated: result.statistics.expressionsValidated
|
|
}
|
|
};
|
|
const expressionErrors = result.errors.filter(e => e.message.includes('Expression') ||
|
|
e.message.includes('$') ||
|
|
e.message.includes('{{'));
|
|
const expressionWarnings = result.warnings.filter(w => w.message.includes('Expression') ||
|
|
w.message.includes('$') ||
|
|
w.message.includes('{{'));
|
|
if (expressionErrors.length > 0) {
|
|
response.errors = expressionErrors.map(e => ({
|
|
node: e.nodeName || 'workflow',
|
|
message: e.message
|
|
}));
|
|
}
|
|
if (expressionWarnings.length > 0) {
|
|
response.warnings = expressionWarnings.map(w => ({
|
|
node: w.nodeName || 'workflow',
|
|
message: w.message
|
|
}));
|
|
}
|
|
if (expressionErrors.length > 0 || expressionWarnings.length > 0) {
|
|
response.tips = [
|
|
'Use {{ }} to wrap expressions',
|
|
'Reference data with $json.propertyName',
|
|
'Reference other nodes with $node["Node Name"].json',
|
|
'Use $input.item for input data in loops'
|
|
];
|
|
}
|
|
return response;
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.error('Error validating workflow expressions:', error);
|
|
return {
|
|
valid: false,
|
|
error: error instanceof Error ? error.message : 'Unknown error validating expressions'
|
|
};
|
|
}
|
|
}
|
|
async run() {
|
|
await this.ensureInitialized();
|
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
await this.server.connect(transport);
|
|
if (!process.stdout.isTTY || process.env.IS_DOCKER) {
|
|
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
process.stdout.write = function (chunk, encoding, callback) {
|
|
const result = originalWrite(chunk, encoding, callback);
|
|
process.stdout.emit('drain');
|
|
return result;
|
|
};
|
|
}
|
|
logger_1.logger.info('n8n Documentation MCP Server running on stdio transport');
|
|
process.stdin.resume();
|
|
}
|
|
async shutdown() {
|
|
logger_1.logger.info('Shutting down MCP server...');
|
|
if (this.cache) {
|
|
try {
|
|
this.cache.destroy();
|
|
logger_1.logger.info('Cache timers cleaned up');
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.error('Error cleaning up cache:', error);
|
|
}
|
|
}
|
|
if (this.db) {
|
|
try {
|
|
await this.db.close();
|
|
logger_1.logger.info('Database connection closed');
|
|
}
|
|
catch (error) {
|
|
logger_1.logger.error('Error closing database:', error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
exports.N8NDocumentationMCPServer = N8NDocumentationMCPServer;
|
|
//# sourceMappingURL=server.js.map
|