n8n-mcp
Version:
Integration between n8n workflow automation and Model Context Protocol (MCP)
1,021 lines (1,020 loc) • 166 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 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;