UNPKG

n8n-mcp

Version:

Integration between n8n workflow automation and Model Context Protocol (MCP)

1,021 lines (1,020 loc) 166 kB
"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 ui_1 = require("./ui"); const skills_1 = require("./skills"); 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 redaction_1 = require("../utils/redaction"); const node_repository_1 = require("../database/node-repository"); const database_adapter_1 = require("../database/database-adapter"); const shared_database_1 = require("../database/shared-database"); 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 typeversion_1 = require("../utils/typeversion"); 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"); function escapeRegExp(input) { return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } class N8NDocumentationMCPServer { constructor(instanceContext, earlyLogger, options) { 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.disabledToolOperationsCache = null; this.filteredToolDefinitionsCache = null; this.useSharedDatabase = false; this.sharedDbState = null; this.isShutdown = false; this.additionalToolsByName = new Map(); this.dbHealthChecked = false; this.workflowPatternsCache = null; this.instanceContext = instanceContext; this.earlyLogger = earlyLogger || null; this.registerAdditionalTools(options?.additionalTools || []); 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); } }); this.initialized.catch(() => { }); 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: {}, resources: {}, }, }); ui_1.UIAppRegistry.load(); skills_1.SkillResourceRegistry.load(); this.setupHandlers(); } registerAdditionalTools(additionalTools) { const builtInToolNames = new Set([ ...tools_1.n8nDocumentationToolsFinal.map(tool => tool.name), ...tools_n8n_manager_1.n8nManagementTools.map(tool => tool.name), ]); for (const additionalTool of additionalTools) { const toolName = additionalTool.tool.name; if (builtInToolNames.has(toolName)) { throw new Error(`Additional tool "${toolName}" collides with a built-in tool`); } if (this.additionalToolsByName.has(toolName)) { throw new Error(`Duplicate additional tool "${toolName}" provided`); } this.additionalToolsByName.set(toolName, { tool: structuredClone(additionalTool.tool), handler: additionalTool.handler, }); } } getEnabledAdditionalTools(disabledTools) { return Array.from(this.additionalToolsByName.values()) .map(toolDef => toolDef.tool) .filter(tool => !disabledTools.has(tool.name)); } findToolSchema(name) { return tools_1.n8nDocumentationToolsFinal.find(t => t.name === name) ?? tools_n8n_manager_1.n8nManagementTools.find(t => t.name === name) ?? this.additionalToolsByName.get(name)?.tool; } async close() { try { await this.initialized; } catch (error) { logger_1.logger.debug('Initialization had failed, proceeding with cleanup', { error: error instanceof Error ? error.message : String(error) }); } try { await this.server.close(); this.cache.destroy(); if (this.useSharedDatabase && this.sharedDbState) { (0, shared_database_1.releaseSharedDatabase)(this.sharedDbState); logger_1.logger.debug('Released shared database reference'); } else 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; this.sharedDbState = 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 }); if (dbPath === ':memory:') { this.db = await (0, database_adapter_1.createDatabaseAdapter)(dbPath); logger_1.logger.debug('Database adapter created (in-memory mode)'); await this.initializeInMemorySchema(); logger_1.logger.debug('In-memory schema initialized'); this.repository = new node_repository_1.NodeRepository(this.db); this.templateService = new template_service_1.TemplateService(this.db); enhanced_config_validator_1.EnhancedConfigValidator.initializeSimilarityServices(this.repository); this.useSharedDatabase = false; } else { const sharedState = await (0, shared_database_1.getSharedDatabase)(dbPath); this.db = sharedState.db; this.repository = sharedState.repository; this.templateService = sharedState.templateService; this.sharedDbState = sharedState; this.useSharedDatabase = true; logger_1.logger.debug('Using shared database connection'); } logger_1.logger.debug('Node repository initialized'); logger_1.logger.debug('Template service initialized'); 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; } getDisabledToolOperations() { if (this.disabledToolOperationsCache !== null) { return this.disabledToolOperationsCache; } const result = new Map(); let envVal = process.env.DISABLED_TOOL_OPERATIONS || ''; if (!envVal) { this.disabledToolOperationsCache = result; this.filteredToolDefinitionsCache = new Map(); return result; } if (envVal.length > 10000) { logger_1.logger.warn(`DISABLED_TOOL_OPERATIONS environment variable too long (${envVal.length} chars), truncating to 10000`); envVal = envVal.substring(0, 10000); } let entries = envVal.split(';').map(e => e.trim()).filter(Boolean); if (entries.length > 50) { logger_1.logger.warn(`DISABLED_TOOL_OPERATIONS contains ${entries.length} entries, limiting to first 50`); entries = entries.slice(0, 50); } for (const entry of entries) { const colonIdx = entry.indexOf(':'); if (colonIdx === -1) continue; const toolName = entry.substring(0, colonIdx).trim(); const opsStr = entry.substring(colonIdx + 1).trim(); if (!toolName || !opsStr) continue; const ops = opsStr.split(',').map(o => o.trim().toLowerCase()).filter(Boolean); if (ops.length === 0) continue; const existing = result.get(toolName) ?? new Set(); ops.forEach(op => existing.add(op)); result.set(toolName, existing); } for (const [toolName, ops] of result) { const paramName = tools_n8n_manager_1.TOOL_OPERATION_PARAM[toolName]; if (!paramName) { logger_1.logger.warn(`DISABLED_TOOL_OPERATIONS: unknown tool '${toolName}' — no per-operation filtering applied. Eligible tools: ${Object.keys(tools_n8n_manager_1.TOOL_OPERATION_PARAM).join(', ')}`); continue; } const tool = tools_n8n_manager_1.n8nManagementTools.find(t => t.name === toolName); const enumValues = tool?.inputSchema?.properties?.[paramName]?.enum ?? []; for (const op of ops) { if (enumValues.length > 0 && !enumValues.includes(op)) { logger_1.logger.warn(`DISABLED_TOOL_OPERATIONS: '${op}' is not a valid ${paramName} for '${toolName}' (valid: ${enumValues.join(', ')}); it will have no effect.`); } } } if (result.size > 0) { const summary = [...result.entries()] .map(([t, ops]) => `${t}: [${[...ops].join(', ')}]`) .join('; '); logger_1.logger.info(`Disabled tool operations configured: ${summary}`); } this.disabledToolOperationsCache = result; this.filteredToolDefinitionsCache = this.buildFilteredToolDefinitions(result); return result; } buildFilteredToolDefinitions(disabledOps) { const cache = new Map(); for (const [toolName, ops] of disabledOps) { const paramName = tools_n8n_manager_1.TOOL_OPERATION_PARAM[toolName]; if (!paramName) continue; const original = tools_n8n_manager_1.n8nManagementTools.find(t => t.name === toolName); if (!original) continue; const cloned = JSON.parse(JSON.stringify(original)); const param = cloned.inputSchema?.properties?.[paramName]; if (param?.enum) { param.enum = param.enum.filter(v => !ops.has(v.toLowerCase())); if (param.enum.length === 0) { logger_1.logger.warn(`DISABLED_TOOL_OPERATIONS: all operations for '${toolName}' are disabled ` + `but the tool still appears in ListTools. ` + `Consider adding '${toolName}' to DISABLED_TOOLS instead.`); } if (param.description) { const disabledList = [...ops].join(', '); param.description = `${param.description} (disabled by server policy: ${disabledList})`; } } const disabledList = [...ops].join(', '); cloned.description = `${cloned.description}\n\n> Operations disabled by server policy: ${disabledList}`; const destructive = tools_n8n_manager_1.DESTRUCTIVE_TOOL_OPERATIONS[toolName]; if (destructive && cloned.annotations) { const remaining = param?.enum ?? []; const stillDestructive = remaining.some(v => destructive.has(String(v).toLowerCase())); if (!stillDestructive) { cloned.annotations = { ...cloned.annotations, readOnlyHint: true, destructiveHint: false }; } } cache.set(toolName, cloned); } return cache; } 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: {}, resources: {}, }, 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 }); } tools.push(...this.getEnabledAdditionalTools(disabledTools)); if (disabledTools.size > 0) { const totalAvailableTools = tools_1.n8nDocumentationToolsFinal.length + (shouldIncludeManagementTools ? tools_n8n_manager_1.n8nManagementTools.length : 0) + this.additionalToolsByName.size; 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 }); }); const disabledToolOps = this.getDisabledToolOperations(); if (disabledToolOps.size > 0 && this.filteredToolDefinitionsCache) { tools = tools.map(tool => this.filteredToolDefinitionsCache.get(tool.name) ?? tool); } ui_1.UIAppRegistry.injectToolMeta(tools); return { tools }; }); this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; logger_1.logger.info('Tool call received', { toolName: name, ...(0, redaction_1.summarizeToolCallArgs)(args), hasNodeType: !!(args && typeof args === 'object' && 'nodeType' in args), hasConfig: !!(args && typeof args === 'object' && 'config' in args), }); 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) }], isError: true }; } let processedArgs = args; if (typeof args === 'string') { try { const parsed = JSON.parse(args); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { processedArgs = parsed; logger_1.logger.warn(`Coerced stringified args object for tool "${name}"`); } } catch { logger_1.logger.warn(`Tool "${name}" received string args that are not valid JSON`); } } 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', { toolName: name, originalArgsKeys: Object.keys(args), extractedArgsKeys: Object.keys(parsed), }); if (this.validateExtractedArgs(name, parsed)) { processedArgs = parsed; } else { logger_1.logger.warn('Extracted arguments failed validation, using original args', { toolName: name, extractedArgsKeys: Object.keys(parsed), }); } } } } catch (parseError) { logger_1.logger.debug('Failed to parse nested output, continuing with original args', { error: parseError instanceof Error ? parseError.message : String(parseError) }); } } processedArgs = this.coerceStringifiedJsonParams(name, processedArgs); if (processedArgs) { processedArgs = JSON.parse(JSON.stringify(processedArgs)); } const disabledToolOps = this.getDisabledToolOperations(); const disabledOpsForTool = disabledToolOps.get(name); if (disabledOpsForTool && disabledOpsForTool.size > 0) { const paramName = tools_n8n_manager_1.TOOL_OPERATION_PARAM[name]; if (paramName) { const requestedOp = processedArgs?.[paramName]; if (requestedOp && disabledOpsForTool.has(String(requestedOp).toLowerCase())) { logger_1.logger.warn(`Attempted to call disabled operation: ${name}.${requestedOp}`); return { content: [{ type: 'text', text: JSON.stringify({ error: 'OPERATION_DISABLED', message: `Operation '${requestedOp}' on tool '${name}' is disabled by server policy.`, tool: name, operation: requestedOp, disabledOperations: [...disabledOpsForTool] }, null, 2) }], isError: true }; } } } const isAdditionalTool = this.additionalToolsByName.has(name); try { logger_1.logger.debug(`Executing tool: ${name}`, (0, redaction_1.summarizeToolCallArgs)(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(); if (isAdditionalTool) { return result; } 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(); if (isAdditionalTool) { return { content: [ { type: 'text', text: `Error executing tool ${name}: ${errorMessage}`, }, ], isError: true, }; } 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., {})'; } try { const argDiag = processedArgs && typeof processedArgs === 'object' ? Object.entries(processedArgs).map(([k, v]) => `${k}: ${typeof v}`).join(', ') : `args type: ${typeof processedArgs}`; helpfulMessage += `\n\n[Diagnostic] Received arg types: {${argDiag}}`; } catch { } return { content: [ { type: 'text', text: helpfulMessage, }, ], isError: true, }; } }); this.server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => { const apps = ui_1.UIAppRegistry.getAllApps(); const skills = skills_1.SkillResourceRegistry.getAll(); return { resources: [ ...apps .filter(app => app.html !== null) .map(app => ({ uri: app.config.uri, name: app.config.displayName, description: app.config.description, mimeType: app.config.mimeType, })), ...skills.map(skill => ({ uri: skill.uri, name: skill.name, description: skill.description, mimeType: skill.mimeType, })), ], }; }); this.server.setRequestHandler(types_js_1.ListResourceTemplatesRequestSchema, async () => ({ resourceTemplates: skills_1.SkillResourceRegistry.getTemplates(), })); this.server.setRequestHandler(types_js_1.ReadResourceRequestSchema, async (request) => { const uri = request.params.uri; const uiMatch = uri.match(/^ui:\/\/n8n-mcp\/(.+)$/); if (uiMatch) { const app = ui_1.UIAppRegistry.getAppById(uiMatch[1]); if (!app || !app.html) { throw new Error(`UI app not found or not built: ${uiMatch[1]}`); } return { contents: [ { uri: app.config.uri, mimeType: app.config.mimeType, text: app.html }, ], }; } if (uri.startsWith('skill://n8n-mcp/')) { const skill = skills_1.SkillResourceRegistry.getByUri(uri); if (!skill) { throw new Error(`Skill resource not found: ${uri}`); } return { contents: [ { uri: skill.uri, mimeType: skill.mimeType, text: skill.content }, ], }; } throw new Error(`Unknown resource URI: ${uri}`); }); } 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_evaluations': { const evalErrors = []; if (!args.action) evalErrors.push({ field: 'action', message: 'action is required' }); if (!args.workflowId) evalErrors.push({ field: 'workflowId', message: 'workflowId is required' }); validationResult = evalErrors.length === 0 ? { valid: true, errors: [] } : { valid: false, errors: evalErrors }; break; } case 'n8n_manage_datatable': validationResult = args.action ? { valid: true, errors: [] } : { valid: false, errors: [{ field: 'action', message: 'action is required' }] }; break; case 'n8n_manage_credentials': validationResult = args.action ? { valid: true, errors: [] } : { valid: false, errors: [{ field: 'action', message: 'action is required' }] }; break; case 'n8n_audit_instance': validationResult = { valid: true, errors: [] }; 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 tool = this.findToolSchema(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, extractedArgsKeys: Object.keys(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;