n8n-mcp
Version:
Integration between n8n workflow automation and Model Context Protocol (MCP)
1,054 lines • 95.4 kB
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 enhanced_config_validator_1 = require("../services/enhanced-config-validator");
const property_dependencies_1 = require("../services/property-dependencies");
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 validation_schemas_1 = require("../utils/validation-schemas");
const protocol_version_1 = require("../utils/protocol-version");
class N8NDocumentationMCPServer {
constructor() {
this.db = null;
this.repository = null;
this.templateService = null;
this.cache = new simple_cache_1.SimpleCache();
this.clientInfo = 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);
logger_1.logger.info('Initializing n8n Documentation MCP server');
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'})`);
this.server = new index_js_1.Server({
name: 'n8n-documentation-mcp',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
this.setupHandlers();
}
async initializeDatabase(dbPath) {
try {
this.db = await (0, database_adapter_1.createDatabaseAdapter)(dbPath);
if (dbPath === ':memory:') {
await this.initializeInMemorySchema();
}
this.repository = new node_repository_1.NodeRepository(this.db);
this.templateService = new template_service_1.TemplateService(this.db);
logger_1.logger.info(`Initialized database 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 = schema.split(';').filter(stmt => stmt.trim());
for (const statement of statements) {
if (statement.trim()) {
this.db.exec(statement);
}
}
}
async ensureInitialized() {
await this.initialized;
if (!this.db || !this.repository) {
throw new Error('Database not initialized');
}
}
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
});
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) => {
let tools = [...tools_1.n8nDocumentationToolsFinal];
const isConfigured = (0, n8n_api_1.isN8nApiConfigured)();
if (isConfigured) {
tools.push(...tools_n8n_manager_1.n8nManagementTools);
logger_1.logger.debug(`Tool listing: ${tools.length} tools available (${tools_1.n8nDocumentationToolsFinal.length} documentation + ${tools_n8n_manager_1.n8nManagementTools.length} management)`);
}
else {
logger_1.logger.debug(`Tool listing: ${tools.length} tools available (documentation only)`);
}
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)
});
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 result = await this.executeTool(name, processedArgs);
logger_1.logger.debug(`Tool ${name} executed successfully`);
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';
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_operation':
validationResult = validation_schemas_1.ToolValidation.validateNodeOperation(args);
break;
case 'validate_node_minimal':
validationResult = validation_schemas_1.ToolValidation.validateNodeMinimal(args);
break;
case 'validate_workflow':
case 'validate_workflow_connections':
case 'validate_workflow_expressions':
validationResult = validation_schemas_1.ToolValidation.validateWorkflow(args);
break;
case 'search_nodes':
validationResult = validation_schemas_1.ToolValidation.validateSearchNodes(args);
break;
case 'list_node_templates':
validationResult = validation_schemas_1.ToolValidation.validateListNodeTemplates(args);
break;
case 'n8n_create_workflow':
validationResult = validation_schemas_1.ToolValidation.validateCreateWorkflow(args);
break;
case 'n8n_get_workflow':
case 'n8n_get_workflow_details':
case 'n8n_get_workflow_structure':
case 'n8n_get_workflow_minimal':
case 'n8n_update_full_workflow':
case 'n8n_delete_workflow':
case 'n8n_validate_workflow':
case 'n8n_get_execution':
case 'n8n_delete_execution':
validationResult = validation_schemas_1.ToolValidation.validateWorkflowId(args);
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 = [];
for (const param of requiredParams) {
if (!(param in args) || args[param] === undefined || args[param] === null) {
missing.push(param);
}
}
if (missing.length > 0) {
throw new Error(`Missing required parameters for ${toolName}: ${missing.join(', ')}. Please provide the required parameters to use this tool.`);
}
}
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 || {};
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 'list_nodes':
return this.listNodes(args);
case 'get_node_info':
this.validateToolParams(name, args, ['nodeType']);
return this.getNodeInfo(args.nodeType);
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 });
case 'list_ai_tools':
return this.listAITools();
case 'get_node_documentation':
this.validateToolParams(name, args, ['nodeType']);
return this.getNodeDocumentation(args.nodeType);
case 'get_database_statistics':
return this.getDatabaseStatistics();
case 'get_node_essentials':
this.validateToolParams(name, args, ['nodeType']);
return this.getNodeEssentials(args.nodeType);
case 'search_node_properties':
this.validateToolParams(name, args, ['nodeType', 'query']);
const maxResults = args.maxResults !== undefined ? Number(args.maxResults) || 20 : 20;
return this.searchNodeProperties(args.nodeType, args.query, maxResults);
case 'get_node_for_task':
this.validateToolParams(name, args, ['task']);
return this.getNodeForTask(args.task);
case 'list_tasks':
return this.listTasks(args.category);
case 'validate_node_operation':
this.validateToolParams(name, args, ['nodeType', 'config']);
if (typeof args.config !== 'object' || args.config === null) {
logger_1.logger.warn(`validate_node_operation called with invalid config type: ${typeof args.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_essentials 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
}
};
}
return this.validateNodeConfig(args.nodeType, args.config, 'operation', args.profile);
case 'validate_node_minimal':
this.validateToolParams(name, args, ['nodeType', 'config']);
if (typeof args.config !== 'object' || args.config === null) {
logger_1.logger.warn(`validate_node_minimal called with invalid config type: ${typeof args.config}`);
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 this.validateNodeMinimal(args.nodeType, args.config);
case 'get_property_dependencies':
this.validateToolParams(name, args, ['nodeType']);
return this.getPropertyDependencies(args.nodeType, args.config);
case 'get_node_as_tool_info':
this.validateToolParams(name, args, ['nodeType']);
return this.getNodeAsToolInfo(args.nodeType);
case 'list_node_templates':
this.validateToolParams(name, args, ['nodeTypes']);
const templateLimit = args.limit !== undefined ? Number(args.limit) || 10 : 10;
return this.listNodeTemplates(args.nodeTypes, templateLimit);
case 'get_template':
this.validateToolParams(name, args, ['templateId']);
const templateId = Number(args.templateId);
return this.getTemplate(templateId);
case 'search_templates':
this.validateToolParams(name, args, ['query']);
const searchLimit = args.limit !== undefined ? Number(args.limit) || 20 : 20;
return this.searchTemplates(args.query, searchLimit);
case 'get_templates_for_task':
this.validateToolParams(name, args, ['task']);
return this.getTemplatesForTask(args.task);
case 'validate_workflow':
this.validateToolParams(name, args, ['workflow']);
return this.validateWorkflow(args.workflow, args.options);
case 'validate_workflow_connections':
this.validateToolParams(name, args, ['workflow']);
return this.validateWorkflowConnections(args.workflow);
case 'validate_workflow_expressions':
this.validateToolParams(name, args, ['workflow']);
return this.validateWorkflowExpressions(args.workflow);
case 'n8n_create_workflow':
this.validateToolParams(name, args, ['name', 'nodes', 'connections']);
return n8nHandlers.handleCreateWorkflow(args);
case 'n8n_get_workflow':
this.validateToolParams(name, args, ['id']);
return n8nHandlers.handleGetWorkflow(args);
case 'n8n_get_workflow_details':
this.validateToolParams(name, args, ['id']);
return n8nHandlers.handleGetWorkflowDetails(args);
case 'n8n_get_workflow_structure':
this.validateToolParams(name, args, ['id']);
return n8nHandlers.handleGetWorkflowStructure(args);
case 'n8n_get_workflow_minimal':
this.validateToolParams(name, args, ['id']);
return n8nHandlers.handleGetWorkflowMinimal(args);
case 'n8n_update_full_workflow':
this.validateToolParams(name, args, ['id']);
return n8nHandlers.handleUpdateWorkflow(args);
case 'n8n_update_partial_workflow':
this.validateToolParams(name, args, ['id', 'operations']);
return (0, handlers_workflow_diff_1.handleUpdatePartialWorkflow)(args);
case 'n8n_delete_workflow':
this.validateToolParams(name, args, ['id']);
return n8nHandlers.handleDeleteWorkflow(args);
case 'n8n_list_workflows':
return n8nHandlers.handleListWorkflows(args);
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);
case 'n8n_trigger_webhook_workflow':
this.validateToolParams(name, args, ['webhookUrl']);
return n8nHandlers.handleTriggerWebhookWorkflow(args);
case 'n8n_get_execution':
this.validateToolParams(name, args, ['id']);
return n8nHandlers.handleGetExecution(args);
case 'n8n_list_executions':
return n8nHandlers.handleListExecutions(args);
case 'n8n_delete_execution':
this.validateToolParams(name, args, ['id']);
return n8nHandlers.handleDeleteExecution(args);
case 'n8n_health_check':
return n8nHandlers.handleHealthCheck();
case 'n8n_list_available_tools':
return n8nHandlers.handleListAvailableTools();
case 'n8n_diagnostic':
return n8nHandlers.handleDiagnostic({ params: { arguments: args } });
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 = (0, node_utils_1.normalizeNodeType)(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,
requiresEnvironmentVariable: !node.isAITool && node.package !== 'n8n-nodes-base',
toolConnectionType: 'ai_tool',
commonToolUseCases: this.getCommonAIToolUseCases(node.nodeType),
environmentRequirement: node.package !== 'n8n-nodes-base' ?
'N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true' :
null
};
let outputs = undefined;
if (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
};
});
}
return {
...node,
workflowNodeType: (0, node_utils_1.getWorkflowNodeType)(node.package, node.nodeType),
aiToolCapabilities,
outputs
};
}
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) {
return this.searchNodesFTS(normalizedQuery, limit, searchMode);
}
else {
return this.searchNodesLIKE(normalizedQuery, limit);
}
}
async searchNodesFTS(query, limit, mode) {
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 {
const nodes = this.db.prepare(`
SELECT
n.*,
rank
FROM nodes n
JOIN nodes_fts ON n.rowid = nodes_fts.rowid
WHERE nodes_fts MATCH ?
ORDER BY
rank,
CASE
WHEN n.display_name = ? THEN 0
WHEN n.display_name LIKE ? THEN 1
WHEN n.node_type LIKE ? THEN 2
ELSE 3
END,
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 => ({
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)
})),
totalCount: scoredNodes.length
};
if (mode !== 'OR') {
result.mode = mode;
}
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);
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) {
if (!this.db)
throw new Error('Database not initialized');
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 ?
LIMIT ?
`).all(`%${exactPhrase}%`, `%${exactPhrase}%`, `%${exactPhrase}%`, limit * 3);
const rankedNodes = this.rankSearchResults(nodes, exactPhrase, limit);
return {
query,
results: rankedNodes.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: rankedNodes.length
};
}
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}
LIMIT ?
`).all(...params);
const rankedNodes = this.rankSearchResults(nodes, query, limit);
return {
query,
results: rankedNodes.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: rankedNodes.length
};
}
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';
ret