This is page 40 of 45. Use http://codebase.md/czlonkowski/n8n-mcp?lines=false&page={x} to view the full context. # Directory Structure ``` ├── _config.yml ├── .claude │ └── agents │ ├── code-reviewer.md │ ├── context-manager.md │ ├── debugger.md │ ├── deployment-engineer.md │ ├── mcp-backend-engineer.md │ ├── n8n-mcp-tester.md │ ├── technical-researcher.md │ └── test-automator.md ├── .dockerignore ├── .env.docker ├── .env.example ├── .env.n8n.example ├── .env.test ├── .env.test.example ├── .github │ ├── ABOUT.md │ ├── BENCHMARK_THRESHOLDS.md │ ├── FUNDING.yml │ ├── gh-pages.yml │ ├── secret_scanning.yml │ └── workflows │ ├── benchmark-pr.yml │ ├── benchmark.yml │ ├── docker-build-fast.yml │ ├── docker-build-n8n.yml │ ├── docker-build.yml │ ├── release.yml │ ├── test.yml │ └── update-n8n-deps.yml ├── .gitignore ├── .npmignore ├── ATTRIBUTION.md ├── CHANGELOG.md ├── CLAUDE.md ├── codecov.yml ├── coverage.json ├── data │ ├── .gitkeep │ ├── nodes.db │ ├── nodes.db-shm │ ├── nodes.db-wal │ └── templates.db ├── deploy │ └── quick-deploy-n8n.sh ├── docker │ ├── docker-entrypoint.sh │ ├── n8n-mcp │ ├── parse-config.js │ └── README.md ├── docker-compose.buildkit.yml ├── docker-compose.extract.yml ├── docker-compose.n8n.yml ├── docker-compose.override.yml.example ├── docker-compose.test-n8n.yml ├── docker-compose.yml ├── Dockerfile ├── Dockerfile.railway ├── Dockerfile.test ├── docs │ ├── AUTOMATED_RELEASES.md │ ├── BENCHMARKS.md │ ├── CHANGELOG.md │ ├── CLAUDE_CODE_SETUP.md │ ├── CLAUDE_INTERVIEW.md │ ├── CODECOV_SETUP.md │ ├── CODEX_SETUP.md │ ├── CURSOR_SETUP.md │ ├── DEPENDENCY_UPDATES.md │ ├── DOCKER_README.md │ ├── DOCKER_TROUBLESHOOTING.md │ ├── FINAL_AI_VALIDATION_SPEC.md │ ├── FLEXIBLE_INSTANCE_CONFIGURATION.md │ ├── HTTP_DEPLOYMENT.md │ ├── img │ │ ├── cc_command.png │ │ ├── cc_connected.png │ │ ├── codex_connected.png │ │ ├── cursor_tut.png │ │ ├── Railway_api.png │ │ ├── Railway_server_address.png │ │ ├── vsc_ghcp_chat_agent_mode.png │ │ ├── vsc_ghcp_chat_instruction_files.png │ │ ├── vsc_ghcp_chat_thinking_tool.png │ │ └── windsurf_tut.png │ ├── INSTALLATION.md │ ├── LIBRARY_USAGE.md │ ├── local │ │ ├── DEEP_DIVE_ANALYSIS_2025-10-02.md │ │ ├── DEEP_DIVE_ANALYSIS_README.md │ │ ├── Deep_dive_p1_p2.md │ │ ├── integration-testing-plan.md │ │ ├── integration-tests-phase1-summary.md │ │ ├── N8N_AI_WORKFLOW_BUILDER_ANALYSIS.md │ │ ├── P0_IMPLEMENTATION_PLAN.md │ │ └── TEMPLATE_MINING_ANALYSIS.md │ ├── MCP_ESSENTIALS_README.md │ ├── MCP_QUICK_START_GUIDE.md │ ├── N8N_DEPLOYMENT.md │ ├── RAILWAY_DEPLOYMENT.md │ ├── README_CLAUDE_SETUP.md │ ├── README.md │ ├── tools-documentation-usage.md │ ├── VS_CODE_PROJECT_SETUP.md │ ├── WINDSURF_SETUP.md │ └── workflow-diff-examples.md ├── examples │ └── enhanced-documentation-demo.js ├── fetch_log.txt ├── LICENSE ├── MEMORY_N8N_UPDATE.md ├── MEMORY_TEMPLATE_UPDATE.md ├── monitor_fetch.sh ├── N8N_HTTP_STREAMABLE_SETUP.md ├── n8n-nodes.db ├── P0-R3-TEST-PLAN.md ├── package-lock.json ├── package.json ├── package.runtime.json ├── PRIVACY.md ├── railway.json ├── README.md ├── renovate.json ├── scripts │ ├── analyze-optimization.sh │ ├── audit-schema-coverage.ts │ ├── build-optimized.sh │ ├── compare-benchmarks.js │ ├── demo-optimization.sh │ ├── deploy-http.sh │ ├── deploy-to-vm.sh │ ├── export-webhook-workflows.ts │ ├── extract-changelog.js │ ├── extract-from-docker.js │ ├── extract-nodes-docker.sh │ ├── extract-nodes-simple.sh │ ├── format-benchmark-results.js │ ├── generate-benchmark-stub.js │ ├── generate-detailed-reports.js │ ├── generate-test-summary.js │ ├── http-bridge.js │ ├── mcp-http-client.js │ ├── migrate-nodes-fts.ts │ ├── migrate-tool-docs.ts │ ├── n8n-docs-mcp.service │ ├── nginx-n8n-mcp.conf │ ├── prebuild-fts5.ts │ ├── prepare-release.js │ ├── publish-npm-quick.sh │ ├── publish-npm.sh │ ├── quick-test.ts │ ├── run-benchmarks-ci.js │ ├── sync-runtime-version.js │ ├── test-ai-validation-debug.ts │ ├── test-code-node-enhancements.ts │ ├── test-code-node-fixes.ts │ ├── test-docker-config.sh │ ├── test-docker-fingerprint.ts │ ├── test-docker-optimization.sh │ ├── test-docker.sh │ ├── test-empty-connection-validation.ts │ ├── test-error-message-tracking.ts │ ├── test-error-output-validation.ts │ ├── test-error-validation.js │ ├── test-essentials.ts │ ├── test-expression-code-validation.ts │ ├── test-expression-format-validation.js │ ├── test-fts5-search.ts │ ├── test-fuzzy-fix.ts │ ├── test-fuzzy-simple.ts │ ├── test-helpers-validation.ts │ ├── test-http-search.ts │ ├── test-http.sh │ ├── test-jmespath-validation.ts │ ├── test-multi-tenant-simple.ts │ ├── test-multi-tenant.ts │ ├── test-n8n-integration.sh │ ├── test-node-info.js │ ├── test-node-type-validation.ts │ ├── test-nodes-base-prefix.ts │ ├── test-operation-validation.ts │ ├── test-optimized-docker.sh │ ├── test-release-automation.js │ ├── test-search-improvements.ts │ ├── test-security.ts │ ├── test-single-session.sh │ ├── test-sqljs-triggers.ts │ ├── test-telemetry-debug.ts │ ├── test-telemetry-direct.ts │ ├── test-telemetry-env.ts │ ├── test-telemetry-integration.ts │ ├── test-telemetry-no-select.ts │ ├── test-telemetry-security.ts │ ├── test-telemetry-simple.ts │ ├── test-typeversion-validation.ts │ ├── test-url-configuration.ts │ ├── test-user-id-persistence.ts │ ├── test-webhook-validation.ts │ ├── test-workflow-insert.ts │ ├── test-workflow-sanitizer.ts │ ├── test-workflow-tracking-debug.ts │ ├── update-and-publish-prep.sh │ ├── update-n8n-deps.js │ ├── update-readme-version.js │ ├── vitest-benchmark-json-reporter.js │ └── vitest-benchmark-reporter.ts ├── SECURITY.md ├── src │ ├── config │ │ └── n8n-api.ts │ ├── data │ │ └── canonical-ai-tool-examples.json │ ├── database │ │ ├── database-adapter.ts │ │ ├── migrations │ │ │ └── add-template-node-configs.sql │ │ ├── node-repository.ts │ │ ├── nodes.db │ │ ├── schema-optimized.sql │ │ └── schema.sql │ ├── errors │ │ └── validation-service-error.ts │ ├── http-server-single-session.ts │ ├── http-server.ts │ ├── index.ts │ ├── loaders │ │ └── node-loader.ts │ ├── mappers │ │ └── docs-mapper.ts │ ├── mcp │ │ ├── handlers-n8n-manager.ts │ │ ├── handlers-workflow-diff.ts │ │ ├── index.ts │ │ ├── server.ts │ │ ├── stdio-wrapper.ts │ │ ├── tool-docs │ │ │ ├── configuration │ │ │ │ ├── get-node-as-tool-info.ts │ │ │ │ ├── get-node-documentation.ts │ │ │ │ ├── get-node-essentials.ts │ │ │ │ ├── get-node-info.ts │ │ │ │ ├── get-property-dependencies.ts │ │ │ │ ├── index.ts │ │ │ │ └── search-node-properties.ts │ │ │ ├── discovery │ │ │ │ ├── get-database-statistics.ts │ │ │ │ ├── index.ts │ │ │ │ ├── list-ai-tools.ts │ │ │ │ ├── list-nodes.ts │ │ │ │ └── search-nodes.ts │ │ │ ├── guides │ │ │ │ ├── ai-agents-guide.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── system │ │ │ │ ├── index.ts │ │ │ │ ├── n8n-diagnostic.ts │ │ │ │ ├── n8n-health-check.ts │ │ │ │ ├── n8n-list-available-tools.ts │ │ │ │ └── tools-documentation.ts │ │ │ ├── templates │ │ │ │ ├── get-template.ts │ │ │ │ ├── get-templates-for-task.ts │ │ │ │ ├── index.ts │ │ │ │ ├── list-node-templates.ts │ │ │ │ ├── list-tasks.ts │ │ │ │ ├── search-templates-by-metadata.ts │ │ │ │ └── search-templates.ts │ │ │ ├── types.ts │ │ │ ├── validation │ │ │ │ ├── index.ts │ │ │ │ ├── validate-node-minimal.ts │ │ │ │ ├── validate-node-operation.ts │ │ │ │ ├── validate-workflow-connections.ts │ │ │ │ ├── validate-workflow-expressions.ts │ │ │ │ └── validate-workflow.ts │ │ │ └── workflow_management │ │ │ ├── index.ts │ │ │ ├── n8n-autofix-workflow.ts │ │ │ ├── n8n-create-workflow.ts │ │ │ ├── n8n-delete-execution.ts │ │ │ ├── n8n-delete-workflow.ts │ │ │ ├── n8n-get-execution.ts │ │ │ ├── n8n-get-workflow-details.ts │ │ │ ├── n8n-get-workflow-minimal.ts │ │ │ ├── n8n-get-workflow-structure.ts │ │ │ ├── n8n-get-workflow.ts │ │ │ ├── n8n-list-executions.ts │ │ │ ├── n8n-list-workflows.ts │ │ │ ├── n8n-trigger-webhook-workflow.ts │ │ │ ├── n8n-update-full-workflow.ts │ │ │ ├── n8n-update-partial-workflow.ts │ │ │ └── n8n-validate-workflow.ts │ │ ├── tools-documentation.ts │ │ ├── tools-n8n-friendly.ts │ │ ├── tools-n8n-manager.ts │ │ ├── tools.ts │ │ └── workflow-examples.ts │ ├── mcp-engine.ts │ ├── mcp-tools-engine.ts │ ├── n8n │ │ ├── MCPApi.credentials.ts │ │ └── MCPNode.node.ts │ ├── parsers │ │ ├── node-parser.ts │ │ ├── property-extractor.ts │ │ └── simple-parser.ts │ ├── scripts │ │ ├── debug-http-search.ts │ │ ├── extract-from-docker.ts │ │ ├── fetch-templates-robust.ts │ │ ├── fetch-templates.ts │ │ ├── rebuild-database.ts │ │ ├── rebuild-optimized.ts │ │ ├── rebuild.ts │ │ ├── sanitize-templates.ts │ │ ├── seed-canonical-ai-examples.ts │ │ ├── test-autofix-documentation.ts │ │ ├── test-autofix-workflow.ts │ │ ├── test-execution-filtering.ts │ │ ├── test-node-suggestions.ts │ │ ├── test-protocol-negotiation.ts │ │ ├── test-summary.ts │ │ ├── test-webhook-autofix.ts │ │ ├── validate.ts │ │ └── validation-summary.ts │ ├── services │ │ ├── ai-node-validator.ts │ │ ├── ai-tool-validators.ts │ │ ├── confidence-scorer.ts │ │ ├── config-validator.ts │ │ ├── enhanced-config-validator.ts │ │ ├── example-generator.ts │ │ ├── execution-processor.ts │ │ ├── expression-format-validator.ts │ │ ├── expression-validator.ts │ │ ├── n8n-api-client.ts │ │ ├── n8n-validation.ts │ │ ├── node-documentation-service.ts │ │ ├── node-similarity-service.ts │ │ ├── node-specific-validators.ts │ │ ├── operation-similarity-service.ts │ │ ├── property-dependencies.ts │ │ ├── property-filter.ts │ │ ├── resource-similarity-service.ts │ │ ├── sqlite-storage-service.ts │ │ ├── task-templates.ts │ │ ├── universal-expression-validator.ts │ │ ├── workflow-auto-fixer.ts │ │ ├── workflow-diff-engine.ts │ │ └── workflow-validator.ts │ ├── telemetry │ │ ├── batch-processor.ts │ │ ├── config-manager.ts │ │ ├── early-error-logger.ts │ │ ├── error-sanitization-utils.ts │ │ ├── error-sanitizer.ts │ │ ├── event-tracker.ts │ │ ├── event-validator.ts │ │ ├── index.ts │ │ ├── performance-monitor.ts │ │ ├── rate-limiter.ts │ │ ├── startup-checkpoints.ts │ │ ├── telemetry-error.ts │ │ ├── telemetry-manager.ts │ │ ├── telemetry-types.ts │ │ └── workflow-sanitizer.ts │ ├── templates │ │ ├── batch-processor.ts │ │ ├── metadata-generator.ts │ │ ├── README.md │ │ ├── template-fetcher.ts │ │ ├── template-repository.ts │ │ └── template-service.ts │ ├── types │ │ ├── index.ts │ │ ├── instance-context.ts │ │ ├── n8n-api.ts │ │ ├── node-types.ts │ │ └── workflow-diff.ts │ └── utils │ ├── auth.ts │ ├── bridge.ts │ ├── cache-utils.ts │ ├── console-manager.ts │ ├── documentation-fetcher.ts │ ├── enhanced-documentation-fetcher.ts │ ├── error-handler.ts │ ├── example-generator.ts │ ├── fixed-collection-validator.ts │ ├── logger.ts │ ├── mcp-client.ts │ ├── n8n-errors.ts │ ├── node-source-extractor.ts │ ├── node-type-normalizer.ts │ ├── node-type-utils.ts │ ├── node-utils.ts │ ├── npm-version-checker.ts │ ├── protocol-version.ts │ ├── simple-cache.ts │ ├── ssrf-protection.ts │ ├── template-node-resolver.ts │ ├── template-sanitizer.ts │ ├── url-detector.ts │ ├── validation-schemas.ts │ └── version.ts ├── test-output.txt ├── test-reinit-fix.sh ├── tests │ ├── __snapshots__ │ │ └── .gitkeep │ ├── auth.test.ts │ ├── benchmarks │ │ ├── database-queries.bench.ts │ │ ├── index.ts │ │ ├── mcp-tools.bench.ts │ │ ├── mcp-tools.bench.ts.disabled │ │ ├── mcp-tools.bench.ts.skip │ │ ├── node-loading.bench.ts.disabled │ │ ├── README.md │ │ ├── search-operations.bench.ts.disabled │ │ └── validation-performance.bench.ts.disabled │ ├── bridge.test.ts │ ├── comprehensive-extraction-test.js │ ├── data │ │ └── .gitkeep │ ├── debug-slack-doc.js │ ├── demo-enhanced-documentation.js │ ├── docker-tests-README.md │ ├── error-handler.test.ts │ ├── examples │ │ └── using-database-utils.test.ts │ ├── extracted-nodes-db │ │ ├── database-import.json │ │ ├── extraction-report.json │ │ ├── insert-nodes.sql │ │ ├── n8n-nodes-base__Airtable.json │ │ ├── n8n-nodes-base__Discord.json │ │ ├── n8n-nodes-base__Function.json │ │ ├── n8n-nodes-base__HttpRequest.json │ │ ├── n8n-nodes-base__If.json │ │ ├── n8n-nodes-base__Slack.json │ │ ├── n8n-nodes-base__SplitInBatches.json │ │ └── n8n-nodes-base__Webhook.json │ ├── factories │ │ ├── node-factory.ts │ │ └── property-definition-factory.ts │ ├── fixtures │ │ ├── .gitkeep │ │ ├── database │ │ │ └── test-nodes.json │ │ ├── factories │ │ │ ├── node.factory.ts │ │ │ └── parser-node.factory.ts │ │ └── template-configs.ts │ ├── helpers │ │ └── env-helpers.ts │ ├── http-server-auth.test.ts │ ├── integration │ │ ├── ai-validation │ │ │ ├── ai-agent-validation.test.ts │ │ │ ├── ai-tool-validation.test.ts │ │ │ ├── chat-trigger-validation.test.ts │ │ │ ├── e2e-validation.test.ts │ │ │ ├── helpers.ts │ │ │ ├── llm-chain-validation.test.ts │ │ │ ├── README.md │ │ │ └── TEST_REPORT.md │ │ ├── ci │ │ │ └── database-population.test.ts │ │ ├── database │ │ │ ├── connection-management.test.ts │ │ │ ├── empty-database.test.ts │ │ │ ├── fts5-search.test.ts │ │ │ ├── node-fts5-search.test.ts │ │ │ ├── node-repository.test.ts │ │ │ ├── performance.test.ts │ │ │ ├── template-node-configs.test.ts │ │ │ ├── template-repository.test.ts │ │ │ ├── test-utils.ts │ │ │ └── transactions.test.ts │ │ ├── database-integration.test.ts │ │ ├── docker │ │ │ ├── docker-config.test.ts │ │ │ ├── docker-entrypoint.test.ts │ │ │ └── test-helpers.ts │ │ ├── flexible-instance-config.test.ts │ │ ├── mcp │ │ │ └── template-examples-e2e.test.ts │ │ ├── mcp-protocol │ │ │ ├── basic-connection.test.ts │ │ │ ├── error-handling.test.ts │ │ │ ├── performance.test.ts │ │ │ ├── protocol-compliance.test.ts │ │ │ ├── README.md │ │ │ ├── session-management.test.ts │ │ │ ├── test-helpers.ts │ │ │ ├── tool-invocation.test.ts │ │ │ └── workflow-error-validation.test.ts │ │ ├── msw-setup.test.ts │ │ ├── n8n-api │ │ │ ├── executions │ │ │ │ ├── delete-execution.test.ts │ │ │ │ ├── get-execution.test.ts │ │ │ │ ├── list-executions.test.ts │ │ │ │ └── trigger-webhook.test.ts │ │ │ ├── scripts │ │ │ │ └── cleanup-orphans.ts │ │ │ ├── system │ │ │ │ ├── diagnostic.test.ts │ │ │ │ ├── health-check.test.ts │ │ │ │ └── list-tools.test.ts │ │ │ ├── test-connection.ts │ │ │ ├── types │ │ │ │ └── mcp-responses.ts │ │ │ ├── utils │ │ │ │ ├── cleanup-helpers.ts │ │ │ │ ├── credentials.ts │ │ │ │ ├── factories.ts │ │ │ │ ├── fixtures.ts │ │ │ │ ├── mcp-context.ts │ │ │ │ ├── n8n-client.ts │ │ │ │ ├── node-repository.ts │ │ │ │ ├── response-types.ts │ │ │ │ ├── test-context.ts │ │ │ │ └── webhook-workflows.ts │ │ │ └── workflows │ │ │ ├── autofix-workflow.test.ts │ │ │ ├── create-workflow.test.ts │ │ │ ├── delete-workflow.test.ts │ │ │ ├── get-workflow-details.test.ts │ │ │ ├── get-workflow-minimal.test.ts │ │ │ ├── get-workflow-structure.test.ts │ │ │ ├── get-workflow.test.ts │ │ │ ├── list-workflows.test.ts │ │ │ ├── smart-parameters.test.ts │ │ │ ├── update-partial-workflow.test.ts │ │ │ ├── update-workflow.test.ts │ │ │ └── validate-workflow.test.ts │ │ ├── security │ │ │ ├── command-injection-prevention.test.ts │ │ │ └── rate-limiting.test.ts │ │ ├── setup │ │ │ ├── integration-setup.ts │ │ │ └── msw-test-server.ts │ │ ├── telemetry │ │ │ ├── docker-user-id-stability.test.ts │ │ │ └── mcp-telemetry.test.ts │ │ ├── templates │ │ │ └── metadata-operations.test.ts │ │ └── workflow-creation-node-type-format.test.ts │ ├── logger.test.ts │ ├── MOCKING_STRATEGY.md │ ├── mocks │ │ ├── n8n-api │ │ │ ├── data │ │ │ │ ├── credentials.ts │ │ │ │ ├── executions.ts │ │ │ │ └── workflows.ts │ │ │ ├── handlers.ts │ │ │ └── index.ts │ │ └── README.md │ ├── node-storage-export.json │ ├── setup │ │ ├── global-setup.ts │ │ ├── msw-setup.ts │ │ ├── TEST_ENV_DOCUMENTATION.md │ │ └── test-env.ts │ ├── test-database-extraction.js │ ├── test-direct-extraction.js │ ├── test-enhanced-documentation.js │ ├── test-enhanced-integration.js │ ├── test-mcp-extraction.js │ ├── test-mcp-server-extraction.js │ ├── test-mcp-tools-integration.js │ ├── test-node-documentation-service.js │ ├── test-node-list.js │ ├── test-package-info.js │ ├── test-parsing-operations.js │ ├── test-slack-node-complete.js │ ├── test-small-rebuild.js │ ├── test-sqlite-search.js │ ├── test-storage-system.js │ ├── unit │ │ ├── __mocks__ │ │ │ ├── n8n-nodes-base.test.ts │ │ │ ├── n8n-nodes-base.ts │ │ │ └── README.md │ │ ├── database │ │ │ ├── __mocks__ │ │ │ │ └── better-sqlite3.ts │ │ │ ├── database-adapter-unit.test.ts │ │ │ ├── node-repository-core.test.ts │ │ │ ├── node-repository-operations.test.ts │ │ │ ├── node-repository-outputs.test.ts │ │ │ ├── README.md │ │ │ └── template-repository-core.test.ts │ │ ├── docker │ │ │ ├── config-security.test.ts │ │ │ ├── edge-cases.test.ts │ │ │ ├── parse-config.test.ts │ │ │ └── serve-command.test.ts │ │ ├── errors │ │ │ └── validation-service-error.test.ts │ │ ├── examples │ │ │ └── using-n8n-nodes-base-mock.test.ts │ │ ├── flexible-instance-security-advanced.test.ts │ │ ├── flexible-instance-security.test.ts │ │ ├── http-server │ │ │ └── multi-tenant-support.test.ts │ │ ├── http-server-n8n-mode.test.ts │ │ ├── http-server-n8n-reinit.test.ts │ │ ├── http-server-session-management.test.ts │ │ ├── loaders │ │ │ └── node-loader.test.ts │ │ ├── mappers │ │ │ └── docs-mapper.test.ts │ │ ├── mcp │ │ │ ├── get-node-essentials-examples.test.ts │ │ │ ├── handlers-n8n-manager-simple.test.ts │ │ │ ├── handlers-n8n-manager.test.ts │ │ │ ├── handlers-workflow-diff.test.ts │ │ │ ├── lru-cache-behavior.test.ts │ │ │ ├── multi-tenant-tool-listing.test.ts.disabled │ │ │ ├── parameter-validation.test.ts │ │ │ ├── search-nodes-examples.test.ts │ │ │ ├── tools-documentation.test.ts │ │ │ └── tools.test.ts │ │ ├── monitoring │ │ │ └── cache-metrics.test.ts │ │ ├── MULTI_TENANT_TEST_COVERAGE.md │ │ ├── multi-tenant-integration.test.ts │ │ ├── parsers │ │ │ ├── node-parser-outputs.test.ts │ │ │ ├── node-parser.test.ts │ │ │ ├── property-extractor.test.ts │ │ │ └── simple-parser.test.ts │ │ ├── scripts │ │ │ └── fetch-templates-extraction.test.ts │ │ ├── services │ │ │ ├── ai-node-validator.test.ts │ │ │ ├── ai-tool-validators.test.ts │ │ │ ├── confidence-scorer.test.ts │ │ │ ├── config-validator-basic.test.ts │ │ │ ├── config-validator-edge-cases.test.ts │ │ │ ├── config-validator-node-specific.test.ts │ │ │ ├── config-validator-security.test.ts │ │ │ ├── debug-validator.test.ts │ │ │ ├── enhanced-config-validator-integration.test.ts │ │ │ ├── enhanced-config-validator-operations.test.ts │ │ │ ├── enhanced-config-validator.test.ts │ │ │ ├── example-generator.test.ts │ │ │ ├── execution-processor.test.ts │ │ │ ├── expression-format-validator.test.ts │ │ │ ├── expression-validator-edge-cases.test.ts │ │ │ ├── expression-validator.test.ts │ │ │ ├── fixed-collection-validation.test.ts │ │ │ ├── loop-output-edge-cases.test.ts │ │ │ ├── n8n-api-client.test.ts │ │ │ ├── n8n-validation.test.ts │ │ │ ├── node-similarity-service.test.ts │ │ │ ├── node-specific-validators.test.ts │ │ │ ├── operation-similarity-service-comprehensive.test.ts │ │ │ ├── operation-similarity-service.test.ts │ │ │ ├── property-dependencies.test.ts │ │ │ ├── property-filter-edge-cases.test.ts │ │ │ ├── property-filter.test.ts │ │ │ ├── resource-similarity-service-comprehensive.test.ts │ │ │ ├── resource-similarity-service.test.ts │ │ │ ├── task-templates.test.ts │ │ │ ├── template-service.test.ts │ │ │ ├── universal-expression-validator.test.ts │ │ │ ├── validation-fixes.test.ts │ │ │ ├── workflow-auto-fixer.test.ts │ │ │ ├── workflow-diff-engine.test.ts │ │ │ ├── workflow-fixed-collection-validation.test.ts │ │ │ ├── workflow-validator-comprehensive.test.ts │ │ │ ├── workflow-validator-edge-cases.test.ts │ │ │ ├── workflow-validator-error-outputs.test.ts │ │ │ ├── workflow-validator-expression-format.test.ts │ │ │ ├── workflow-validator-loops-simple.test.ts │ │ │ ├── workflow-validator-loops.test.ts │ │ │ ├── workflow-validator-mocks.test.ts │ │ │ ├── workflow-validator-performance.test.ts │ │ │ ├── workflow-validator-with-mocks.test.ts │ │ │ └── workflow-validator.test.ts │ │ ├── telemetry │ │ │ ├── batch-processor.test.ts │ │ │ ├── config-manager.test.ts │ │ │ ├── event-tracker.test.ts │ │ │ ├── event-validator.test.ts │ │ │ ├── rate-limiter.test.ts │ │ │ ├── telemetry-error.test.ts │ │ │ ├── telemetry-manager.test.ts │ │ │ ├── v2.18.3-fixes-verification.test.ts │ │ │ └── workflow-sanitizer.test.ts │ │ ├── templates │ │ │ ├── batch-processor.test.ts │ │ │ ├── metadata-generator.test.ts │ │ │ ├── template-repository-metadata.test.ts │ │ │ └── template-repository-security.test.ts │ │ ├── test-env-example.test.ts │ │ ├── test-infrastructure.test.ts │ │ ├── types │ │ │ ├── instance-context-coverage.test.ts │ │ │ └── instance-context-multi-tenant.test.ts │ │ ├── utils │ │ │ ├── auth-timing-safe.test.ts │ │ │ ├── cache-utils.test.ts │ │ │ ├── console-manager.test.ts │ │ │ ├── database-utils.test.ts │ │ │ ├── fixed-collection-validator.test.ts │ │ │ ├── n8n-errors.test.ts │ │ │ ├── node-type-normalizer.test.ts │ │ │ ├── node-type-utils.test.ts │ │ │ ├── node-utils.test.ts │ │ │ ├── simple-cache-memory-leak-fix.test.ts │ │ │ ├── ssrf-protection.test.ts │ │ │ └── template-node-resolver.test.ts │ │ └── validation-fixes.test.ts │ └── utils │ ├── assertions.ts │ ├── builders │ │ └── workflow.builder.ts │ ├── data-generators.ts │ ├── database-utils.ts │ ├── README.md │ └── test-helpers.ts ├── thumbnail.png ├── tsconfig.build.json ├── tsconfig.json ├── types │ ├── mcp.d.ts │ └── test-env.d.ts ├── verify-telemetry-fix.js ├── versioned-nodes.md ├── vitest.config.benchmark.ts ├── vitest.config.integration.ts └── vitest.config.ts ``` # Files -------------------------------------------------------------------------------- /src/mcp/server.ts: -------------------------------------------------------------------------------- ```typescript import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, InitializeRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { existsSync, promises as fs } from 'fs'; import path from 'path'; import { n8nDocumentationToolsFinal } from './tools'; import { n8nManagementTools } from './tools-n8n-manager'; import { makeToolsN8nFriendly } from './tools-n8n-friendly'; import { getWorkflowExampleString } from './workflow-examples'; import { logger } from '../utils/logger'; import { NodeRepository } from '../database/node-repository'; import { DatabaseAdapter, createDatabaseAdapter } from '../database/database-adapter'; import { PropertyFilter } from '../services/property-filter'; import { TaskTemplates } from '../services/task-templates'; import { ConfigValidator } from '../services/config-validator'; import { EnhancedConfigValidator, ValidationMode, ValidationProfile } from '../services/enhanced-config-validator'; import { PropertyDependencies } from '../services/property-dependencies'; import { SimpleCache } from '../utils/simple-cache'; import { TemplateService } from '../templates/template-service'; import { WorkflowValidator } from '../services/workflow-validator'; import { isN8nApiConfigured } from '../config/n8n-api'; import * as n8nHandlers from './handlers-n8n-manager'; import { handleUpdatePartialWorkflow } from './handlers-workflow-diff'; import { getToolDocumentation, getToolsOverview } from './tools-documentation'; import { PROJECT_VERSION } from '../utils/version'; import { getNodeTypeAlternatives, getWorkflowNodeType } from '../utils/node-utils'; import { NodeTypeNormalizer } from '../utils/node-type-normalizer'; import { ToolValidation, Validator, ValidationError } from '../utils/validation-schemas'; import { negotiateProtocolVersion, logProtocolNegotiation, STANDARD_PROTOCOL_VERSION } from '../utils/protocol-version'; import { InstanceContext } from '../types/instance-context'; import { telemetry } from '../telemetry'; import { EarlyErrorLogger } from '../telemetry/early-error-logger'; import { STARTUP_CHECKPOINTS } from '../telemetry/startup-checkpoints'; interface NodeRow { node_type: string; package_name: string; display_name: string; description?: string; category?: string; development_style?: string; is_ai_tool: number; is_trigger: number; is_webhook: number; is_versioned: number; version?: string; documentation?: string; properties_schema?: string; operations?: string; credentials_required?: string; } export class N8NDocumentationMCPServer { private server: Server; private db: DatabaseAdapter | null = null; private repository: NodeRepository | null = null; private templateService: TemplateService | null = null; private initialized: Promise<void>; private cache = new SimpleCache(); private clientInfo: any = null; private instanceContext?: InstanceContext; private previousTool: string | null = null; private previousToolTimestamp: number = Date.now(); private earlyLogger: EarlyErrorLogger | null = null; constructor(instanceContext?: InstanceContext, earlyLogger?: EarlyErrorLogger) { this.instanceContext = instanceContext; this.earlyLogger = earlyLogger || null; // Check for test environment first const envDbPath = process.env.NODE_DB_PATH; let dbPath: string | null = null; let possiblePaths: string[] = []; if (envDbPath && (envDbPath === ':memory:' || existsSync(envDbPath))) { dbPath = envDbPath; } else { // Try multiple database paths possiblePaths = [ path.join(process.cwd(), 'data', 'nodes.db'), path.join(__dirname, '../../data', 'nodes.db'), './data/nodes.db' ]; for (const p of possiblePaths) { if (existsSync(p)) { dbPath = p; break; } } } if (!dbPath) { 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.'); } // Initialize database asynchronously this.initialized = this.initializeDatabase(dbPath).then(() => { // After database is ready, check n8n API configuration (v2.18.3) if (this.earlyLogger) { this.earlyLogger.logCheckpoint(STARTUP_CHECKPOINTS.N8N_API_CHECKING); } // Log n8n API configuration status at startup const apiConfigured = isN8nApiConfigured(); const totalTools = apiConfigured ? n8nDocumentationToolsFinal.length + n8nManagementTools.length : n8nDocumentationToolsFinal.length; logger.info(`MCP server initialized with ${totalTools} tools (n8n API: ${apiConfigured ? 'configured' : 'not configured'})`); if (this.earlyLogger) { this.earlyLogger.logCheckpoint(STARTUP_CHECKPOINTS.N8N_API_READY); } }); logger.info('Initializing n8n Documentation MCP server'); this.server = new Server( { name: 'n8n-documentation-mcp', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); this.setupHandlers(); } private async initializeDatabase(dbPath: string): Promise<void> { try { // Checkpoint: Database connecting (v2.18.3) if (this.earlyLogger) { this.earlyLogger.logCheckpoint(STARTUP_CHECKPOINTS.DATABASE_CONNECTING); } logger.debug('Database initialization starting...', { dbPath }); this.db = await createDatabaseAdapter(dbPath); logger.debug('Database adapter created'); // If using in-memory database for tests, initialize schema if (dbPath === ':memory:') { await this.initializeInMemorySchema(); logger.debug('In-memory schema initialized'); } this.repository = new NodeRepository(this.db); logger.debug('Node repository initialized'); this.templateService = new TemplateService(this.db); logger.debug('Template service initialized'); // Initialize similarity services for enhanced validation EnhancedConfigValidator.initializeSimilarityServices(this.repository); logger.debug('Similarity services initialized'); // Checkpoint: Database connected (v2.18.3) if (this.earlyLogger) { this.earlyLogger.logCheckpoint(STARTUP_CHECKPOINTS.DATABASE_CONNECTED); } logger.info(`Database initialized successfully from: ${dbPath}`); } catch (error) { logger.error('Failed to initialize database:', error); throw new Error(`Failed to open database: ${error instanceof Error ? error.message : 'Unknown error'}`); } } private async initializeInMemorySchema(): Promise<void> { if (!this.db) return; // Read and execute schema const schemaPath = path.join(__dirname, '../../src/database/schema.sql'); const schema = await fs.readFile(schemaPath, 'utf-8'); // Parse SQL statements properly (handles BEGIN...END blocks in triggers) const statements = this.parseSQLStatements(schema); for (const statement of statements) { if (statement.trim()) { try { this.db.exec(statement); } catch (error) { logger.error(`Failed to execute SQL statement: ${statement.substring(0, 100)}...`, error); throw error; } } } } /** * Parse SQL statements from schema file, properly handling multi-line statements * including triggers with BEGIN...END blocks */ private parseSQLStatements(sql: string): string[] { const statements: string[] = []; let current = ''; let inBlock = false; const lines = sql.split('\n'); for (const line of lines) { const trimmed = line.trim().toUpperCase(); // Skip comments and empty lines if (trimmed.startsWith('--') || trimmed === '') { continue; } // Track BEGIN...END blocks (triggers, procedures) if (trimmed.includes('BEGIN')) { inBlock = true; } current += line + '\n'; // End of block (trigger/procedure) if (inBlock && trimmed === 'END;') { statements.push(current.trim()); current = ''; inBlock = false; continue; } // Regular statement end (not in block) if (!inBlock && trimmed.endsWith(';')) { statements.push(current.trim()); current = ''; } } // Add any remaining content if (current.trim()) { statements.push(current.trim()); } return statements.filter(s => s.length > 0); } private async ensureInitialized(): Promise<void> { await this.initialized; if (!this.db || !this.repository) { throw new Error('Database not initialized'); } // Validate database health on first access if (!this.dbHealthChecked) { await this.validateDatabaseHealth(); this.dbHealthChecked = true; } } private dbHealthChecked: boolean = false; private async validateDatabaseHealth(): Promise<void> { if (!this.db) return; try { // Check if nodes table has data const nodeCount = this.db.prepare('SELECT COUNT(*) as count FROM nodes').get() as { count: number }; if (nodeCount.count === 0) { 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.'); } // Check if FTS5 table exists const ftsExists = this.db.prepare(` SELECT name FROM sqlite_master WHERE type='table' AND name='nodes_fts' `).get(); if (!ftsExists) { 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() as { count: number }; if (ftsCount.count === 0) { logger.warn('FTS5 index is empty - search will not work properly. Please run: npm run rebuild'); } } logger.info(`Database health check passed: ${nodeCount.count} nodes loaded`); } catch (error) { logger.error('Database health check failed:', error); throw error; } } private setupHandlers(): void { // Handle initialization this.server.setRequestHandler(InitializeRequestSchema, async (request) => { const clientVersion = request.params.protocolVersion; const clientCapabilities = request.params.capabilities; const clientInfo = request.params.clientInfo; logger.info('MCP Initialize request received', { clientVersion, clientCapabilities, clientInfo }); // Track session start telemetry.trackSessionStart(); // Store client info for later use this.clientInfo = clientInfo; // Negotiate protocol version based on client information const negotiationResult = negotiateProtocolVersion( clientVersion, clientInfo, undefined, // no user agent in MCP protocol undefined // no headers in MCP protocol ); logProtocolNegotiation(negotiationResult, logger, 'MCP_INITIALIZE'); // Warn if there's a version mismatch (for debugging) if (clientVersion && clientVersion !== negotiationResult.version) { 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: PROJECT_VERSION, }, }; logger.info('MCP Initialize response', { response }); return response; }); // Handle tool listing this.server.setRequestHandler(ListToolsRequestSchema, async (request) => { // Combine documentation tools with management tools if API is configured let tools = [...n8nDocumentationToolsFinal]; // Check if n8n API tools should be available // 1. Environment variables (backward compatibility) // 2. Instance context (multi-tenant support) // 3. Multi-tenant mode enabled (always show tools, runtime checks will handle auth) const hasEnvConfig = isN8nApiConfigured(); const hasInstanceConfig = !!(this.instanceContext?.n8nApiUrl && this.instanceContext?.n8nApiKey); const isMultiTenantEnabled = process.env.ENABLE_MULTI_TENANT === 'true'; const shouldIncludeManagementTools = hasEnvConfig || hasInstanceConfig || isMultiTenantEnabled; if (shouldIncludeManagementTools) { tools.push(...n8nManagementTools); logger.debug(`Tool listing: ${tools.length} tools available (${n8nDocumentationToolsFinal.length} documentation + ${n8nManagementTools.length} management)`, { hasEnvConfig, hasInstanceConfig, isMultiTenantEnabled }); } else { logger.debug(`Tool listing: ${tools.length} tools available (documentation only)`, { hasEnvConfig, hasInstanceConfig, isMultiTenantEnabled }); } // Check if client is n8n (from initialization) const clientInfo = this.clientInfo; const isN8nClient = clientInfo?.name?.includes('n8n') || clientInfo?.name?.includes('langchain'); if (isN8nClient) { logger.info('Detected n8n client, using n8n-friendly tool descriptions'); tools = makeToolsN8nFriendly(tools); } // Log validation tools' input schemas for debugging const validationTools = tools.filter(t => t.name.startsWith('validate_')); validationTools.forEach(tool => { logger.info('Validation tool schema', { toolName: tool.name, inputSchema: JSON.stringify(tool.inputSchema, null, 2), hasOutputSchema: !!tool.outputSchema, description: tool.description }); }); return { tools }; }); // Handle tool execution this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; // Enhanced logging for debugging tool calls 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) }); // Workaround for n8n's nested output bug // Check if args contains nested 'output' structure from n8n's memory corruption let processedArgs = args; if (args && typeof args === 'object' && 'output' in args) { try { const possibleNestedData = args.output; // If output is a string that looks like JSON, try to parse it if (typeof possibleNestedData === 'string' && possibleNestedData.trim().startsWith('{')) { const parsed = JSON.parse(possibleNestedData); if (parsed && typeof parsed === 'object') { logger.warn('Detected n8n nested output bug, attempting to extract actual arguments', { originalArgs: args, extractedArgs: parsed }); // Validate the extracted arguments match expected tool schema if (this.validateExtractedArgs(name, parsed)) { // Use the extracted data as args processedArgs = parsed; } else { logger.warn('Extracted arguments failed validation, using original args', { toolName: name, extractedArgs: parsed }); } } } } catch (parseError) { logger.debug('Failed to parse nested output, continuing with original args', { error: parseError instanceof Error ? parseError.message : String(parseError) }); } } try { logger.debug(`Executing tool: ${name}`, { args: processedArgs }); const startTime = Date.now(); const result = await this.executeTool(name, processedArgs); const duration = Date.now() - startTime; logger.debug(`Tool ${name} executed successfully`); // Track tool usage and sequence telemetry.trackToolUsage(name, true, duration); // Track tool sequence if there was a previous tool if (this.previousTool) { const timeDelta = Date.now() - this.previousToolTimestamp; telemetry.trackToolSequence(this.previousTool, name, timeDelta); } // Update previous tool tracking this.previousTool = name; this.previousToolTimestamp = Date.now(); // Ensure the result is properly formatted for MCP let responseText: string; let structuredContent: any = null; try { // For validation tools, check if we should use structured content if (name.startsWith('validate_') && typeof result === 'object' && result !== null) { // Clean up the result to ensure it matches the outputSchema 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.warn(`Failed to stringify tool result for ${name}:`, jsonError); responseText = String(result); } // Validate response size (n8n might have limits) if (responseText.length > 1000000) { // 1MB limit 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; // Don't use structured content for truncated responses } // Build MCP response with strict schema compliance const mcpResponse: any = { content: [ { type: 'text' as const, text: responseText, }, ], }; // For tools with outputSchema, structuredContent is REQUIRED by MCP spec if (name.startsWith('validate_') && structuredContent !== null) { mcpResponse.structuredContent = structuredContent; } return mcpResponse; } catch (error) { logger.error(`Error executing tool ${name}`, error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; // Track tool error telemetry.trackToolUsage(name, false); telemetry.trackError( error instanceof Error ? error.constructor.name : 'UnknownError', `tool_execution`, name, errorMessage ); // Track tool sequence even for errors if (this.previousTool) { const timeDelta = Date.now() - this.previousToolTimestamp; telemetry.trackToolSequence(this.previousTool, name, timeDelta); } // Update previous tool tracking (even for failed tools) this.previousTool = name; this.previousToolTimestamp = Date.now(); // Provide more helpful error messages for common n8n issues 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.'; } // For n8n schema errors, add specific guidance 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, }; } }); } /** * Sanitize validation result to match outputSchema */ private sanitizeValidationResult(result: any, toolName: string): any { if (!result || typeof result !== 'object') { return result; } const sanitized = { ...result }; // Ensure required fields exist with proper types and filter to schema-defined fields only if (toolName === 'validate_node_minimal') { // Filter to only schema-defined fields 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') { // Ensure summary exists 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 }; } // Filter to only schema-defined fields 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); // Ensure arrays exist sanitized.errors = Array.isArray(sanitized.errors) ? sanitized.errors : []; sanitized.warnings = Array.isArray(sanitized.warnings) ? sanitized.warnings : []; // Ensure statistics/summary exists 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 }; } } } // Remove undefined values to ensure clean JSON return JSON.parse(JSON.stringify(sanitized)); } /** * Enhanced parameter validation using schemas */ private validateToolParams(toolName: string, args: any, legacyRequiredParams?: string[]): void { try { // If legacy required params are provided, use the new validation but fall back to basic if needed let validationResult; switch (toolName) { case 'validate_node_operation': validationResult = ToolValidation.validateNodeOperation(args); break; case 'validate_node_minimal': validationResult = ToolValidation.validateNodeMinimal(args); break; case 'validate_workflow': case 'validate_workflow_connections': case 'validate_workflow_expressions': validationResult = ToolValidation.validateWorkflow(args); break; case 'search_nodes': validationResult = ToolValidation.validateSearchNodes(args); break; case 'list_node_templates': validationResult = ToolValidation.validateListNodeTemplates(args); break; case 'n8n_create_workflow': validationResult = 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_autofix_workflow': case 'n8n_get_execution': case 'n8n_delete_execution': validationResult = ToolValidation.validateWorkflowId(args); break; default: // For tools not yet migrated to schema validation, use basic validation return this.validateToolParamsBasic(toolName, args, legacyRequiredParams || []); } if (!validationResult.valid) { const errorMessage = Validator.formatErrors(validationResult, toolName); logger.error(`Parameter validation failed for ${toolName}:`, errorMessage); throw new ValidationError(errorMessage); } } catch (error) { // Handle validation errors properly if (error instanceof ValidationError) { throw error; // Re-throw validation errors as-is } // Handle unexpected errors from validation system logger.error(`Validation system error for ${toolName}:`, error); // Provide a user-friendly error message const errorMessage = error instanceof Error ? `Internal validation error: ${error.message}` : `Internal validation error while processing ${toolName}`; throw new Error(errorMessage); } } /** * Legacy parameter validation (fallback) */ private validateToolParamsBasic(toolName: string, args: any, requiredParams: string[]): void { const missing: string[] = []; const invalid: string[] = []; 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.`); } } /** * Validate extracted arguments match expected tool schema */ private validateExtractedArgs(toolName: string, args: any): boolean { if (!args || typeof args !== 'object') { return false; } // Get all available tools const allTools = [...n8nDocumentationToolsFinal, ...n8nManagementTools]; const tool = allTools.find(t => t.name === toolName); if (!tool || !tool.inputSchema) { return true; // If no schema, assume valid } const schema = tool.inputSchema; const required = schema.required || []; const properties = schema.properties || {}; // Check all required fields are present for (const requiredField of required) { if (!(requiredField in args)) { logger.debug(`Extracted args missing required field: ${requiredField}`, { toolName, extractedArgs: args, required }); return false; } } // Check field types match schema for (const [fieldName, fieldValue] of Object.entries(args)) { if (properties[fieldName]) { const expectedType = properties[fieldName].type; const actualType = Array.isArray(fieldValue) ? 'array' : typeof fieldValue; // Basic type validation if (expectedType && expectedType !== actualType) { // Special case: number can be coerced from string if (expectedType === 'number' && actualType === 'string' && !isNaN(Number(fieldValue))) { continue; } logger.debug(`Extracted args field type mismatch: ${fieldName}`, { toolName, expectedType, actualType, fieldValue }); return false; } } } // Check for extraneous fields if additionalProperties is 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.debug(`Extracted args have extra fields`, { toolName, extraFields, allowedFields }); // For n8n compatibility, we'll still consider this valid but log it } } return true; } async executeTool(name: string, args: any): Promise<any> { // Ensure args is an object and validate it args = args || {}; // Log the tool call for debugging n8n issues 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' }); // Validate that args is actually an 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': // No required parameters return this.getToolsDocumentation(args.topic, args.depth); case 'list_nodes': // No required parameters 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']); // Convert limit to number if provided, otherwise use default const limit = args.limit !== undefined ? Number(args.limit) || 20 : 20; return this.searchNodes(args.query, limit, { mode: args.mode, includeExamples: args.includeExamples }); case 'list_ai_tools': // No required parameters return this.listAITools(); case 'get_node_documentation': this.validateToolParams(name, args, ['nodeType']); return this.getNodeDocumentation(args.nodeType); case 'get_database_statistics': // No required parameters return this.getDatabaseStatistics(); case 'get_node_essentials': this.validateToolParams(name, args, ['nodeType']); return this.getNodeEssentials(args.nodeType, args.includeExamples); 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 'list_tasks': // No required parameters return this.listTasks(args.category); case 'validate_node_operation': this.validateToolParams(name, args, ['nodeType', 'config']); // Ensure config is an object if (typeof args.config !== 'object' || args.config === null) { 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']); // Ensure config is an object if (typeof args.config !== 'object' || args.config === null) { 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_templates': // No required params const listLimit = Math.min(Math.max(Number(args.limit) || 10, 1), 100); const listOffset = Math.max(Number(args.offset) || 0, 0); const sortBy = args.sortBy || 'views'; const includeMetadata = Boolean(args.includeMetadata); return this.listTemplates(listLimit, listOffset, sortBy, includeMetadata); case 'list_node_templates': this.validateToolParams(name, args, ['nodeTypes']); const templateLimit = Math.min(Math.max(Number(args.limit) || 10, 1), 100); const templateOffset = Math.max(Number(args.offset) || 0, 0); return this.listNodeTemplates(args.nodeTypes, templateLimit, templateOffset); case 'get_template': this.validateToolParams(name, args, ['templateId']); const templateId = Number(args.templateId); const mode = args.mode || 'full'; return this.getTemplate(templateId, mode); case 'search_templates': this.validateToolParams(name, args, ['query']); const searchLimit = Math.min(Math.max(Number(args.limit) || 20, 1), 100); const searchOffset = Math.max(Number(args.offset) || 0, 0); const searchFields = args.fields as string[] | undefined; return this.searchTemplates(args.query, searchLimit, searchOffset, searchFields); case 'get_templates_for_task': this.validateToolParams(name, args, ['task']); const taskLimit = Math.min(Math.max(Number(args.limit) || 10, 1), 100); const taskOffset = Math.max(Number(args.offset) || 0, 0); return this.getTemplatesForTask(args.task, taskLimit, taskOffset); case 'search_templates_by_metadata': // No required params - all filters are optional const metadataLimit = Math.min(Math.max(Number(args.limit) || 20, 1), 100); const metadataOffset = Math.max(Number(args.offset) || 0, 0); return this.searchTemplatesByMetadata({ category: args.category, complexity: args.complexity, maxSetupMinutes: args.maxSetupMinutes ? Number(args.maxSetupMinutes) : undefined, minSetupMinutes: args.minSetupMinutes ? Number(args.minSetupMinutes) : undefined, requiredService: args.requiredService, targetAudience: args.targetAudience }, metadataLimit, metadataOffset); 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); // n8n Management Tools (if API is configured) case 'n8n_create_workflow': this.validateToolParams(name, args, ['name', 'nodes', 'connections']); return n8nHandlers.handleCreateWorkflow(args, this.instanceContext); case 'n8n_get_workflow': this.validateToolParams(name, args, ['id']); return n8nHandlers.handleGetWorkflow(args, this.instanceContext); case 'n8n_get_workflow_details': this.validateToolParams(name, args, ['id']); return n8nHandlers.handleGetWorkflowDetails(args, this.instanceContext); case 'n8n_get_workflow_structure': this.validateToolParams(name, args, ['id']); return n8nHandlers.handleGetWorkflowStructure(args, this.instanceContext); case 'n8n_get_workflow_minimal': this.validateToolParams(name, args, ['id']); return n8nHandlers.handleGetWorkflowMinimal(args, this.instanceContext); case 'n8n_update_full_workflow': this.validateToolParams(name, args, ['id']); return n8nHandlers.handleUpdateWorkflow(args, this.instanceContext); case 'n8n_update_partial_workflow': this.validateToolParams(name, args, ['id', 'operations']); return handleUpdatePartialWorkflow(args, this.instanceContext); case 'n8n_delete_workflow': this.validateToolParams(name, args, ['id']); return n8nHandlers.handleDeleteWorkflow(args, this.instanceContext); case 'n8n_list_workflows': // No required parameters return n8nHandlers.handleListWorkflows(args, this.instanceContext); case 'n8n_validate_workflow': this.validateToolParams(name, args, ['id']); await this.ensureInitialized(); if (!this.repository) throw new Error('Repository not initialized'); return n8nHandlers.handleValidateWorkflow(args, this.repository, this.instanceContext); case 'n8n_autofix_workflow': this.validateToolParams(name, args, ['id']); await this.ensureInitialized(); if (!this.repository) throw new Error('Repository not initialized'); return n8nHandlers.handleAutofixWorkflow(args, this.repository, this.instanceContext); case 'n8n_trigger_webhook_workflow': this.validateToolParams(name, args, ['webhookUrl']); return n8nHandlers.handleTriggerWebhookWorkflow(args, this.instanceContext); case 'n8n_get_execution': this.validateToolParams(name, args, ['id']); return n8nHandlers.handleGetExecution(args, this.instanceContext); case 'n8n_list_executions': // No required parameters return n8nHandlers.handleListExecutions(args, this.instanceContext); case 'n8n_delete_execution': this.validateToolParams(name, args, ['id']); return n8nHandlers.handleDeleteExecution(args, this.instanceContext); case 'n8n_health_check': // No required parameters return n8nHandlers.handleHealthCheck(this.instanceContext); case 'n8n_list_available_tools': // No required parameters return n8nHandlers.handleListAvailableTools(this.instanceContext); case 'n8n_diagnostic': // No required parameters return n8nHandlers.handleDiagnostic({ params: { arguments: args } }, this.instanceContext); default: throw new Error(`Unknown tool: ${name}`); } } private async listNodes(filters: any = {}): Promise<any> { await this.ensureInitialized(); let query = 'SELECT * FROM nodes WHERE 1=1'; const params: any[] = []; // console.log('DEBUG list_nodes:', { filters, query, params }); // Removed to prevent stdout interference if (filters.package) { // Handle both formats 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) as NodeRow[]; 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, }; } private async getNodeInfo(nodeType: string): Promise<any> { await this.ensureInitialized(); if (!this.repository) throw new Error('Repository not initialized'); // First try with normalized type (repository will also normalize internally) const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType); let node = this.repository.getNode(normalizedType); if (!node && normalizedType !== nodeType) { // Try original if normalization changed it node = this.repository.getNode(nodeType); } if (!node) { // Fallback to other alternatives for edge cases const alternatives = 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`); } // Add AI tool capabilities information with null safety const aiToolCapabilities = { canBeUsedAsTool: true, // Any node can be used as a tool in n8n hasUsableAsToolProperty: node.isAITool ?? false, requiresEnvironmentVariable: !(node.isAITool ?? false) && node.package !== 'n8n-nodes-base', toolConnectionType: 'ai_tool', commonToolUseCases: this.getCommonAIToolUseCases(node.nodeType), environmentRequirement: node.package && node.package !== 'n8n-nodes-base' ? 'N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true' : null }; // Process outputs to provide clear mapping with null safety let outputs = undefined; if (node.outputNames && Array.isArray(node.outputNames) && node.outputNames.length > 0) { outputs = node.outputNames.map((name: string, index: number) => { // Special handling for loop nodes like SplitInBatches const descriptions = this.getOutputDescriptions(node.nodeType, name, index); return { index, name, description: descriptions?.description ?? '', connectionGuidance: descriptions?.connectionGuidance ?? '' }; }); } return { ...node, workflowNodeType: getWorkflowNodeType(node.package ?? 'n8n-nodes-base', node.nodeType), aiToolCapabilities, outputs }; } /** * Primary search method used by ALL MCP search tools. * * This method automatically detects and uses FTS5 full-text search when available * (lines 1189-1203), falling back to LIKE queries only if FTS5 table doesn't exist. * * NOTE: This is separate from NodeRepository.searchNodes() which is legacy LIKE-based. * All MCP tool invocations route through this method to leverage FTS5 performance. */ private async searchNodes( query: string, limit: number = 20, options?: { mode?: 'OR' | 'AND' | 'FUZZY'; includeSource?: boolean; includeExamples?: boolean; } ): Promise<any> { await this.ensureInitialized(); if (!this.db) throw new Error('Database not initialized'); // Normalize the query if it looks like a full node type let normalizedQuery = query; // Check if query contains node type patterns and normalize them 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'; // Check if FTS5 table exists const ftsExists = this.db.prepare(` SELECT name FROM sqlite_master WHERE type='table' AND name='nodes_fts' `).get(); if (ftsExists) { // Use FTS5 search with normalized query logger.debug(`Using FTS5 search with includeExamples=${options?.includeExamples}`); return this.searchNodesFTS(normalizedQuery, limit, searchMode, options); } else { // Fallback to LIKE search with normalized query logger.debug('Using LIKE search (no FTS5)'); return this.searchNodesLIKE(normalizedQuery, limit, options); } } private async searchNodesFTS( query: string, limit: number, mode: 'OR' | 'AND' | 'FUZZY', options?: { includeSource?: boolean; includeExamples?: boolean; } ): Promise<any> { if (!this.db) throw new Error('Database not initialized'); // Clean and prepare the query const cleanedQuery = query.trim(); if (!cleanedQuery) { return { query, results: [], totalCount: 0 }; } // For FUZZY mode, use LIKE search with typo patterns if (mode === 'FUZZY') { return this.searchNodesFuzzy(cleanedQuery, limit); } let ftsQuery: string; // Handle exact phrase searches with quotes if (cleanedQuery.startsWith('"') && cleanedQuery.endsWith('"')) { // Keep exact phrase as is for FTS5 ftsQuery = cleanedQuery; } else { // Split into words and handle based on mode const words = cleanedQuery.split(/\s+/).filter(w => w.length > 0); switch (mode) { case 'AND': // All words must be present ftsQuery = words.join(' AND '); break; case 'OR': default: // Any word can match (default) ftsQuery = words.join(' OR '); break; } } try { // Use FTS5 with ranking 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) as (NodeRow & { rank: number })[]; // Apply additional relevance scoring for better results const scoredNodes = nodes.map(node => { const relevanceScore = this.calculateRelevanceScore(node, cleanedQuery); return { ...node, relevanceScore }; }); // Sort by combined score (FTS rank + relevance score) scoredNodes.sort((a, b) => { // Prioritize exact matches if (a.display_name.toLowerCase() === cleanedQuery.toLowerCase()) return -1; if (b.display_name.toLowerCase() === cleanedQuery.toLowerCase()) return 1; // Then by relevance score if (a.relevanceScore !== b.relevanceScore) { return b.relevanceScore - a.relevanceScore; } // Then by FTS rank return a.rank - b.rank; }); // If FTS didn't find key primary nodes, augment with LIKE search const hasHttpRequest = scoredNodes.some(n => n.node_type === 'nodes-base.httpRequest'); if (cleanedQuery.toLowerCase().includes('http') && !hasHttpRequest) { // FTS missed HTTP Request, fall back to LIKE search logger.debug('FTS missed HTTP Request node, augmenting with LIKE search'); return this.searchNodesLIKE(query, limit); } const result: any = { query, results: scoredNodes.map(node => ({ nodeType: node.node_type, workflowNodeType: 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 }; // Only include mode if it's not the default if (mode !== 'OR') { result.mode = mode; } // Add examples if requested if (options && options.includeExamples) { try { for (const nodeResult of result.results) { const examples = this.db!.prepare(` SELECT parameters_json, template_name, template_views FROM template_node_configs WHERE node_type = ? ORDER BY rank LIMIT 2 `).all(nodeResult.workflowNodeType) as any[]; if (examples.length > 0) { nodeResult.examples = examples.map((ex: any) => ({ configuration: JSON.parse(ex.parameters_json), template: ex.template_name, views: ex.template_views })); } } } catch (error: any) { logger.error(`Failed to add examples:`, error); } } // Track search query telemetry telemetry.trackSearchQuery(query, scoredNodes.length, mode ?? 'OR'); return result; } catch (error: any) { // If FTS5 query fails, fallback to LIKE search logger.warn('FTS5 search failed, falling back to LIKE search:', error.message); // Special handling for syntax errors if (error.message.includes('syntax error') || error.message.includes('fts5')) { logger.warn(`FTS5 syntax error for query "${query}" in mode ${mode}`); // For problematic queries, use LIKE search with mode info const likeResult = await this.searchNodesLIKE(query, limit); // Track search query telemetry for fallback telemetry.trackSearchQuery(query, likeResult.results?.length ?? 0, `${mode}_LIKE_FALLBACK`); return { ...likeResult, mode }; } return this.searchNodesLIKE(query, limit); } } private async searchNodesFuzzy(query: string, limit: number): Promise<any> { if (!this.db) throw new Error('Database not initialized'); // Split into words for fuzzy matching const words = query.toLowerCase().split(/\s+/).filter(w => w.length > 0); if (words.length === 0) { return { query, results: [], totalCount: 0, mode: 'FUZZY' }; } // For fuzzy search, get ALL nodes to ensure we don't miss potential matches // We'll limit results after scoring const candidateNodes = this.db!.prepare(` SELECT * FROM nodes `).all() as NodeRow[]; // Calculate fuzzy scores for candidate nodes const scoredNodes = candidateNodes.map(node => { const score = this.calculateFuzzyScore(node, query); return { node, score }; }); // Filter and sort by score const matchingNodes = scoredNodes .filter(item => item.score >= 200) // Lower threshold for better typo tolerance .sort((a, b) => b.score - a.score) .slice(0, limit) .map(item => item.node); // Debug logging if (matchingNodes.length === 0) { const topScores = scoredNodes .sort((a, b) => b.score - a.score) .slice(0, 5); 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: getWorkflowNodeType(node.package_name, node.node_type), displayName: node.display_name, description: node.description, category: node.category, package: node.package_name })), totalCount: matchingNodes.length }; } private calculateFuzzyScore(node: NodeRow, query: string): number { 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\./, ''); // Exact match gets highest score if (displayNameLower === queryLower || nodeTypeClean === queryLower) { return 1000; } // Calculate edit distances for different parts const nameDistance = this.getEditDistance(queryLower, displayNameLower); const typeDistance = this.getEditDistance(queryLower, nodeTypeClean); // Also check individual words in the display name const nameWords = displayNameLower.split(/\s+/); let minWordDistance = Infinity; for (const word of nameWords) { const distance = this.getEditDistance(queryLower, word); if (distance < minWordDistance) { minWordDistance = distance; } } // Calculate best match score const bestDistance = Math.min(nameDistance, typeDistance, minWordDistance); // Use the length of the matched word for similarity calculation let matchedLen = queryLower.length; if (minWordDistance === bestDistance) { // Find which word matched best 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); // Boost if query is a substring if (displayNameLower.includes(queryLower) || nodeTypeClean.includes(queryLower)) { return 800 + (similarity * 100); } // Check if it's a prefix match if (displayNameLower.startsWith(queryLower) || nodeTypeClean.startsWith(queryLower) || nameWords.some(w => w.startsWith(queryLower))) { return 700 + (similarity * 100); } // Allow up to 1-2 character differences for typos if (bestDistance <= 2) { return 500 + ((2 - bestDistance) * 100) + (similarity * 50); } // Allow up to 3 character differences for longer words if (bestDistance <= 3 && queryLower.length >= 4) { return 400 + ((3 - bestDistance) * 50) + (similarity * 50); } // Base score on similarity return similarity * 300; } private getEditDistance(s1: string, s2: string): number { // Simple Levenshtein distance implementation const m = s1.length; const n = s2.length; const dp: number[][] = 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]; } private async searchNodesLIKE( query: string, limit: number, options?: { includeSource?: boolean; includeExamples?: boolean; } ): Promise<any> { if (!this.db) throw new Error('Database not initialized'); // This is the existing LIKE-based implementation // Handle exact phrase searches with quotes 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) as NodeRow[]; // Apply relevance ranking for exact phrase search const rankedNodes = this.rankSearchResults(nodes, exactPhrase, limit); const result: any = { query, results: rankedNodes.map(node => ({ nodeType: node.node_type, workflowNodeType: getWorkflowNodeType(node.package_name, node.node_type), displayName: node.display_name, description: node.description, category: node.category, package: node.package_name })), totalCount: rankedNodes.length }; // Add examples if requested if (options?.includeExamples) { for (const nodeResult of result.results) { try { const examples = this.db!.prepare(` SELECT parameters_json, template_name, template_views FROM template_node_configs WHERE node_type = ? ORDER BY rank LIMIT 2 `).all(nodeResult.workflowNodeType) as any[]; if (examples.length > 0) { nodeResult.examples = examples.map((ex: any) => ({ configuration: JSON.parse(ex.parameters_json), template: ex.template_name, views: ex.template_views })); } } catch (error: any) { logger.warn(`Failed to fetch examples for ${nodeResult.nodeType}:`, error.message); } } } return result; } // Split into words for normal search const words = query.toLowerCase().split(/\s+/).filter(w => w.length > 0); if (words.length === 0) { return { query, results: [], totalCount: 0 }; } // Build conditions for each word const conditions = words.map(() => '(node_type LIKE ? OR display_name LIKE ? OR description LIKE ?)' ).join(' OR '); const params: any[] = words.flatMap(w => [`%${w}%`, `%${w}%`, `%${w}%`]); // Fetch more results initially to ensure we get the best matches after ranking params.push(limit * 3); const nodes = this.db!.prepare(` SELECT DISTINCT * FROM nodes WHERE ${conditions} LIMIT ? `).all(...params) as NodeRow[]; // Apply relevance ranking const rankedNodes = this.rankSearchResults(nodes, query, limit); const result: any = { query, results: rankedNodes.map(node => ({ nodeType: node.node_type, workflowNodeType: getWorkflowNodeType(node.package_name, node.node_type), displayName: node.display_name, description: node.description, category: node.category, package: node.package_name })), totalCount: rankedNodes.length }; // Add examples if requested if (options?.includeExamples) { for (const nodeResult of result.results) { try { const examples = this.db!.prepare(` SELECT parameters_json, template_name, template_views FROM template_node_configs WHERE node_type = ? ORDER BY rank LIMIT 2 `).all(nodeResult.workflowNodeType) as any[]; if (examples.length > 0) { nodeResult.examples = examples.map((ex: any) => ({ configuration: JSON.parse(ex.parameters_json), template: ex.template_name, views: ex.template_views })); } } catch (error: any) { logger.warn(`Failed to fetch examples for ${nodeResult.nodeType}:`, error.message); } } } return result; } private calculateRelevance(node: NodeRow, query: string): string { const lowerQuery = query.toLowerCase(); if (node.node_type.toLowerCase().includes(lowerQuery)) return 'high'; if (node.display_name.toLowerCase().includes(lowerQuery)) return 'high'; if (node.description?.toLowerCase().includes(lowerQuery)) return 'medium'; return 'low'; } private calculateRelevanceScore(node: NodeRow, query: string): number { const query_lower = query.toLowerCase(); const name_lower = node.display_name.toLowerCase(); const type_lower = node.node_type.toLowerCase(); const type_without_prefix = type_lower.replace(/^nodes-base\./, '').replace(/^nodes-langchain\./, ''); let score = 0; // Exact match in display name (highest priority) if (name_lower === query_lower) { score = 1000; } // Exact match in node type (without prefix) else if (type_without_prefix === query_lower) { score = 950; } // Special boost for common primary nodes else if (query_lower === 'webhook' && node.node_type === 'nodes-base.webhook') { score = 900; } else if ((query_lower === 'http' || query_lower === 'http request' || query_lower === 'http call') && node.node_type === 'nodes-base.httpRequest') { score = 900; } // Additional boost for multi-word queries matching primary nodes else if (query_lower.includes('http') && query_lower.includes('call') && node.node_type === 'nodes-base.httpRequest') { score = 890; } else if (query_lower.includes('http') && node.node_type === 'nodes-base.httpRequest') { score = 850; } // Boost for webhook queries else if (query_lower.includes('webhook') && node.node_type === 'nodes-base.webhook') { score = 850; } // Display name starts with query else if (name_lower.startsWith(query_lower)) { score = 800; } // Word boundary match in display name else if (new RegExp(`\\b${query_lower}\\b`, 'i').test(node.display_name)) { score = 700; } // Contains in display name else if (name_lower.includes(query_lower)) { score = 600; } // Type contains query (without prefix) else if (type_without_prefix.includes(query_lower)) { score = 500; } // Contains in description else if (node.description?.toLowerCase().includes(query_lower)) { score = 400; } return score; } private rankSearchResults(nodes: NodeRow[], query: string, limit: number): NodeRow[] { const query_lower = query.toLowerCase(); // Calculate relevance scores for each node const scoredNodes = nodes.map(node => { const name_lower = node.display_name.toLowerCase(); const type_lower = node.node_type.toLowerCase(); const type_without_prefix = type_lower.replace(/^nodes-base\./, '').replace(/^nodes-langchain\./, ''); let score = 0; // Exact match in display name (highest priority) if (name_lower === query_lower) { score = 1000; } // Exact match in node type (without prefix) else if (type_without_prefix === query_lower) { score = 950; } // Special boost for common primary nodes else if (query_lower === 'webhook' && node.node_type === 'nodes-base.webhook') { score = 900; } else if ((query_lower === 'http' || query_lower === 'http request' || query_lower === 'http call') && node.node_type === 'nodes-base.httpRequest') { score = 900; } // Boost for webhook queries else if (query_lower.includes('webhook') && node.node_type === 'nodes-base.webhook') { score = 850; } // Additional boost for http queries else if (query_lower.includes('http') && node.node_type === 'nodes-base.httpRequest') { score = 850; } // Display name starts with query else if (name_lower.startsWith(query_lower)) { score = 800; } // Word boundary match in display name else if (new RegExp(`\\b${query_lower}\\b`, 'i').test(node.display_name)) { score = 700; } // Contains in display name else if (name_lower.includes(query_lower)) { score = 600; } // Type contains query (without prefix) else if (type_without_prefix.includes(query_lower)) { score = 500; } // Contains in description else if (node.description?.toLowerCase().includes(query_lower)) { score = 400; } // For multi-word queries, check if all words are present const words = query_lower.split(/\s+/).filter(w => w.length > 0); if (words.length > 1) { const allWordsInName = words.every(word => name_lower.includes(word)); const allWordsInDesc = words.every(word => node.description?.toLowerCase().includes(word)); if (allWordsInName) score += 200; else if (allWordsInDesc) score += 100; // Special handling for common multi-word queries if (query_lower === 'http call' && name_lower === 'http request') { score = 920; // Boost HTTP Request for "http call" query } } return { node, score }; }); // Sort by score (descending) and then by display name (ascending) scoredNodes.sort((a, b) => { if (a.score !== b.score) { return b.score - a.score; } return a.node.display_name.localeCompare(b.node.display_name); }); // Return only the requested number of results return scoredNodes.slice(0, limit).map(item => item.node); } private async listAITools(): Promise<any> { await this.ensureInitialized(); if (!this.repository) throw new Error('Repository not initialized'); const tools = this.repository.getAITools(); // Debug: Check if is_ai_tool column is populated const aiCount = this.db!.prepare('SELECT COUNT(*) as ai_count FROM nodes WHERE is_ai_tool = 1').get() as any; // console.log('DEBUG list_ai_tools:', { // toolsLength: tools.length, // aiCountInDB: aiCount.ai_count, // sampleTools: tools.slice(0, 3) // }); // Removed to prevent stdout interference return { tools, totalCount: tools.length, requirements: { environmentVariable: 'N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true', nodeProperty: 'usableAsTool: true', }, usage: { description: 'These nodes have the usableAsTool property set to true, making them optimized for AI agent usage.', note: 'ANY node in n8n can be used as an AI tool by connecting it to the ai_tool port of an AI Agent node.', examples: [ 'Regular nodes like Slack, Google Sheets, or HTTP Request can be used as tools', 'Connect any node to an AI Agent\'s tool port to make it available for AI-driven automation', 'Community nodes require the environment variable to be set' ] } }; } private async getNodeDocumentation(nodeType: string): Promise<any> { await this.ensureInitialized(); if (!this.db) throw new Error('Database not initialized'); // First try with normalized type const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType); let node = this.db!.prepare(` SELECT node_type, display_name, documentation, description FROM nodes WHERE node_type = ? `).get(normalizedType) as NodeRow | undefined; // If not found and normalization changed the type, try original if (!node && normalizedType !== nodeType) { node = this.db!.prepare(` SELECT node_type, display_name, documentation, description FROM nodes WHERE node_type = ? `).get(nodeType) as NodeRow | undefined; } // If still not found, try alternatives if (!node) { const alternatives = getNodeTypeAlternatives(normalizedType); for (const alt of alternatives) { node = this.db!.prepare(` SELECT node_type, display_name, documentation, description FROM nodes WHERE node_type = ? `).get(alt) as NodeRow | undefined; if (node) break; } } if (!node) { throw new Error(`Node ${nodeType} not found`); } // If no documentation, generate fallback with null safety if (!node.documentation) { const essentials = await this.getNodeEssentials(nodeType); return { nodeType: node.node_type, displayName: node.display_name || 'Unknown Node', documentation: ` # ${node.display_name || 'Unknown Node'} ${node.description || 'No description available.'} ## Common Properties ${essentials?.commonProperties?.length > 0 ? essentials.commonProperties.map((p: any) => `### ${p.displayName || 'Property'}\n${p.description || `Type: ${p.type || 'unknown'}`}` ).join('\n\n') : 'No common properties available.'} ## Note Full documentation is being prepared. For now, use get_node_essentials for configuration help. `, hasDocumentation: false }; } return { nodeType: node.node_type, displayName: node.display_name || 'Unknown Node', documentation: node.documentation, hasDocumentation: true, }; } private async getDatabaseStatistics(): Promise<any> { await this.ensureInitialized(); if (!this.db) throw new Error('Database not initialized'); const stats = this.db!.prepare(` SELECT COUNT(*) as total, SUM(is_ai_tool) as ai_tools, SUM(is_trigger) as triggers, SUM(is_versioned) as versioned, SUM(CASE WHEN documentation IS NOT NULL THEN 1 ELSE 0 END) as with_docs, COUNT(DISTINCT package_name) as packages, COUNT(DISTINCT category) as categories FROM nodes `).get() as any; const packages = this.db!.prepare(` SELECT package_name, COUNT(*) as count FROM nodes GROUP BY package_name `).all() as any[]; // Get template statistics const templateStats = this.db!.prepare(` SELECT COUNT(*) as total_templates, AVG(views) as avg_views, MIN(views) as min_views, MAX(views) as max_views FROM templates `).get() as any; return { totalNodes: stats.total, totalTemplates: templateStats.total_templates || 0, statistics: { aiTools: stats.ai_tools, triggers: stats.triggers, versionedNodes: stats.versioned, nodesWithDocumentation: stats.with_docs, documentationCoverage: Math.round((stats.with_docs / stats.total) * 100) + '%', uniquePackages: stats.packages, uniqueCategories: stats.categories, templates: { total: templateStats.total_templates || 0, avgViews: Math.round(templateStats.avg_views || 0), minViews: templateStats.min_views || 0, maxViews: templateStats.max_views || 0 } }, packageBreakdown: packages.map(pkg => ({ package: pkg.package_name, nodeCount: pkg.count, })), }; } private async getNodeEssentials(nodeType: string, includeExamples?: boolean): Promise<any> { await this.ensureInitialized(); if (!this.repository) throw new Error('Repository not initialized'); // Check cache first (cache key includes includeExamples) const cacheKey = `essentials:${nodeType}:${includeExamples ? 'withExamples' : 'basic'}`; const cached = this.cache.get(cacheKey); if (cached) return cached; // Get the full node information // First try with normalized type const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType); let node = this.repository.getNode(normalizedType); if (!node && normalizedType !== nodeType) { // Try original if normalization changed it node = this.repository.getNode(nodeType); } if (!node) { // Fallback to other alternatives for edge cases const alternatives = 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`); } // Get properties (already parsed by repository) const allProperties = node.properties || []; // Get essential properties const essentials = PropertyFilter.getEssentials(allProperties, node.nodeType); // Get operations (already parsed by repository) const operations = node.operations || []; const result = { nodeType: node.nodeType, workflowNodeType: getWorkflowNodeType(node.package ?? 'n8n-nodes-base', node.nodeType), displayName: node.displayName, description: node.description, category: node.category, version: node.version ?? '1', isVersioned: node.isVersioned ?? false, requiredProperties: essentials.required, commonProperties: essentials.common, operations: operations.map((op: any) => ({ name: op.name || op.operation, description: op.description, action: op.action, resource: op.resource })), // Examples removed - use validate_node_operation for working configurations metadata: { totalProperties: allProperties.length, isAITool: node.isAITool ?? false, isTrigger: node.isTrigger ?? false, isWebhook: node.isWebhook ?? false, hasCredentials: node.credentials ? true : false, package: node.package ?? 'n8n-nodes-base', developmentStyle: node.developmentStyle ?? 'programmatic' } }; // Add examples from templates if requested if (includeExamples) { try { // Use the already-computed workflowNodeType from result (line 1888) // This ensures consistency with search_nodes behavior (line 1203) const examples = this.db!.prepare(` SELECT parameters_json, template_name, template_views, complexity, use_cases, has_credentials, has_expressions FROM template_node_configs WHERE node_type = ? ORDER BY rank LIMIT 3 `).all(result.workflowNodeType) as any[]; if (examples.length > 0) { (result as any).examples = examples.map((ex: any) => ({ configuration: JSON.parse(ex.parameters_json), source: { template: ex.template_name, views: ex.template_views, complexity: ex.complexity }, useCases: ex.use_cases ? JSON.parse(ex.use_cases).slice(0, 2) : [], metadata: { hasCredentials: ex.has_credentials === 1, hasExpressions: ex.has_expressions === 1 } })); (result as any).examplesCount = examples.length; } else { (result as any).examples = []; (result as any).examplesCount = 0; } } catch (error: any) { logger.warn(`Failed to fetch examples for ${nodeType}:`, error.message); (result as any).examples = []; (result as any).examplesCount = 0; } } // Cache for 1 hour this.cache.set(cacheKey, result, 3600); return result; } private async searchNodeProperties(nodeType: string, query: string, maxResults: number = 20): Promise<any> { await this.ensureInitialized(); if (!this.repository) throw new Error('Repository not initialized'); // Get the node // First try with normalized type const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType); let node = this.repository.getNode(normalizedType); if (!node && normalizedType !== nodeType) { // Try original if normalization changed it node = this.repository.getNode(nodeType); } if (!node) { // Fallback to other alternatives for edge cases const alternatives = 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`); } // Get properties and search (already parsed by repository) const allProperties = node.properties || []; const matches = PropertyFilter.searchProperties(allProperties, query, maxResults); return { nodeType: node.nodeType, query, matches: matches.map((match: any) => ({ name: match.name, displayName: match.displayName, type: match.type, description: match.description, path: match.path || match.name, required: match.required, default: match.default, options: match.options, showWhen: match.showWhen })), totalMatches: matches.length, searchedIn: allProperties.length + ' properties' }; } private getPropertyValue(config: any, path: string): any { const parts = path.split('.'); let value = config; for (const part of parts) { // Handle array notation like parameters[0] const arrayMatch = part.match(/^(\w+)\[(\d+)\]$/); if (arrayMatch) { value = value?.[arrayMatch[1]]?.[parseInt(arrayMatch[2])]; } else { value = value?.[part]; } } return value; } private async listTasks(category?: string): Promise<any> { if (category) { const categories = TaskTemplates.getTaskCategories(); const tasks = categories[category]; if (!tasks) { throw new Error( `Unknown category: ${category}. Available categories: ${Object.keys(categories).join(', ')}` ); } return { category, tasks: tasks.map(task => { const template = TaskTemplates.getTaskTemplate(task); return { task, description: template?.description || '', nodeType: template?.nodeType || '' }; }) }; } // Return all tasks grouped by category const categories = TaskTemplates.getTaskCategories(); const result: any = { totalTasks: TaskTemplates.getAllTasks().length, categories: {} }; for (const [cat, tasks] of Object.entries(categories)) { result.categories[cat] = tasks.map(task => { const template = TaskTemplates.getTaskTemplate(task); return { task, description: template?.description || '', nodeType: template?.nodeType || '' }; }); } return result; } private async validateNodeConfig( nodeType: string, config: Record<string, any>, mode: ValidationMode = 'operation', profile: ValidationProfile = 'ai-friendly' ): Promise<any> { await this.ensureInitialized(); if (!this.repository) throw new Error('Repository not initialized'); // Get node info to access properties // First try with normalized type const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType); let node = this.repository.getNode(normalizedType); if (!node && normalizedType !== nodeType) { // Try original if normalization changed it node = this.repository.getNode(nodeType); } if (!node) { // Fallback to other alternatives for edge cases const alternatives = 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`); } // Get properties const properties = node.properties || []; // Use enhanced validator with operation mode by default const validationResult = EnhancedConfigValidator.validateWithMode( node.nodeType, config, properties, mode, profile ); // Add node context to result return { nodeType: node.nodeType, workflowNodeType: getWorkflowNodeType(node.package, node.nodeType), displayName: node.displayName, ...validationResult, summary: { hasErrors: !validationResult.valid, errorCount: validationResult.errors.length, warningCount: validationResult.warnings.length, suggestionCount: validationResult.suggestions.length } }; } private async getPropertyDependencies(nodeType: string, config?: Record<string, any>): Promise<any> { await this.ensureInitialized(); if (!this.repository) throw new Error('Repository not initialized'); // Get node info to access properties // First try with normalized type const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType); let node = this.repository.getNode(normalizedType); if (!node && normalizedType !== nodeType) { // Try original if normalization changed it node = this.repository.getNode(nodeType); } if (!node) { // Fallback to other alternatives for edge cases const alternatives = 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`); } // Get properties const properties = node.properties || []; // Analyze dependencies const analysis = PropertyDependencies.analyze(properties); // If config provided, check visibility impact let visibilityImpact = null; if (config) { visibilityImpact = PropertyDependencies.getVisibilityImpact(properties, config); } return { nodeType: node.nodeType, displayName: node.displayName, ...analysis, currentConfig: config ? { providedValues: config, visibilityImpact } : undefined }; } private async getNodeAsToolInfo(nodeType: string): Promise<any> { await this.ensureInitialized(); if (!this.repository) throw new Error('Repository not initialized'); // Get node info // First try with normalized type const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType); let node = this.repository.getNode(normalizedType); if (!node && normalizedType !== nodeType) { // Try original if normalization changed it node = this.repository.getNode(nodeType); } if (!node) { // Fallback to other alternatives for edge cases const alternatives = 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`); } // Determine common AI tool use cases based on node type const commonUseCases = this.getCommonAIToolUseCases(node.nodeType); // Build AI tool capabilities info const aiToolCapabilities = { canBeUsedAsTool: true, // In n8n, ANY node can be used as a tool when connected to AI Agent hasUsableAsToolProperty: node.isAITool, requiresEnvironmentVariable: !node.isAITool && node.package !== 'n8n-nodes-base', connectionType: 'ai_tool', commonUseCases, requirements: { connection: 'Connect to the "ai_tool" port of an AI Agent node', environment: node.package !== 'n8n-nodes-base' ? 'Set N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true for community nodes' : 'No special environment variables needed for built-in nodes' }, examples: this.getAIToolExamples(node.nodeType), tips: [ 'Give the tool a clear, descriptive name in the AI Agent settings', 'Write a detailed tool description to help the AI understand when to use it', 'Test the node independently before connecting it as a tool', node.isAITool ? 'This node is optimized for AI tool usage' : 'This is a regular node that can be used as an AI tool' ] }; return { nodeType: node.nodeType, workflowNodeType: getWorkflowNodeType(node.package, node.nodeType), displayName: node.displayName, description: node.description, package: node.package, isMarkedAsAITool: node.isAITool, aiToolCapabilities }; } private getOutputDescriptions(nodeType: string, outputName: string, index: number): { description: string, connectionGuidance: string } { // Special handling for loop nodes if (nodeType === 'nodes-base.splitInBatches') { if (outputName === 'done' && index === 0) { return { description: 'Final processed data after all iterations complete', connectionGuidance: 'Connect to nodes that should run AFTER the loop completes' }; } else if (outputName === 'loop' && index === 1) { return { description: 'Current batch data for this iteration', connectionGuidance: 'Connect to nodes that process items INSIDE the loop (and connect their output back to this node)' }; } } // Special handling for IF node if (nodeType === 'nodes-base.if') { if (outputName === 'true' && index === 0) { return { description: 'Items that match the condition', connectionGuidance: 'Connect to nodes that handle the TRUE case' }; } else if (outputName === 'false' && index === 1) { return { description: 'Items that do not match the condition', connectionGuidance: 'Connect to nodes that handle the FALSE case' }; } } // Special handling for Switch node if (nodeType === 'nodes-base.switch') { return { description: `Output ${index}: ${outputName || 'Route ' + index}`, connectionGuidance: `Connect to nodes for the "${outputName || 'route ' + index}" case` }; } // Default handling return { description: outputName || `Output ${index}`, connectionGuidance: `Connect to downstream nodes` }; } private getCommonAIToolUseCases(nodeType: string): string[] { const useCaseMap: Record<string, string[]> = { 'nodes-base.slack': [ 'Send notifications about task completion', 'Post updates to channels', 'Send direct messages', 'Create alerts and reminders' ], 'nodes-base.googleSheets': [ 'Read data for analysis', 'Log results and outputs', 'Update spreadsheet records', 'Create reports' ], 'nodes-base.gmail': [ 'Send email notifications', 'Read and process emails', 'Send reports and summaries', 'Handle email-based workflows' ], 'nodes-base.httpRequest': [ 'Call external APIs', 'Fetch data from web services', 'Send webhooks', 'Integrate with any REST API' ], 'nodes-base.postgres': [ 'Query database for information', 'Store analysis results', 'Update records based on AI decisions', 'Generate reports from data' ], 'nodes-base.webhook': [ 'Receive external triggers', 'Create callback endpoints', 'Handle incoming data', 'Integrate with external systems' ] }; // Check for partial matches for (const [key, useCases] of Object.entries(useCaseMap)) { if (nodeType.includes(key)) { return useCases; } } // Generic use cases for unknown nodes return [ 'Perform automated actions', 'Integrate with external services', 'Process and transform data', 'Extend AI agent capabilities' ]; } private getAIToolExamples(nodeType: string): any { const exampleMap: Record<string, any> = { 'nodes-base.slack': { toolName: 'Send Slack Message', toolDescription: 'Sends a message to a specified Slack channel or user. Use this to notify team members about important events or results.', nodeConfig: { resource: 'message', operation: 'post', channel: '={{ $fromAI("channel", "The Slack channel to send to, e.g. #general") }}', text: '={{ $fromAI("message", "The message content to send") }}' } }, 'nodes-base.googleSheets': { toolName: 'Update Google Sheet', toolDescription: 'Reads or updates data in a Google Sheets spreadsheet. Use this to log information, retrieve data, or update records.', nodeConfig: { operation: 'append', sheetId: 'your-sheet-id', range: 'A:Z', dataMode: 'autoMap' } }, 'nodes-base.httpRequest': { toolName: 'Call API', toolDescription: 'Makes HTTP requests to external APIs. Use this to fetch data, trigger webhooks, or integrate with any web service.', nodeConfig: { method: '={{ $fromAI("method", "HTTP method: GET, POST, PUT, DELETE") }}', url: '={{ $fromAI("url", "The complete API endpoint URL") }}', sendBody: true, bodyContentType: 'json', jsonBody: '={{ $fromAI("body", "Request body as JSON object") }}' } } }; // Check for exact match or partial match for (const [key, example] of Object.entries(exampleMap)) { if (nodeType.includes(key)) { return example; } } // Generic example return { toolName: 'Custom Tool', toolDescription: 'Performs specific operations. Describe what this tool does and when to use it.', nodeConfig: { note: 'Configure the node based on its specific requirements' } }; } private async validateNodeMinimal(nodeType: string, config: Record<string, any>): Promise<any> { await this.ensureInitialized(); if (!this.repository) throw new Error('Repository not initialized'); // Get node info // First try with normalized type const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType); let node = this.repository.getNode(normalizedType); if (!node && normalizedType !== nodeType) { // Try original if normalization changed it node = this.repository.getNode(nodeType); } if (!node) { // Fallback to other alternatives for edge cases const alternatives = 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`); } // Get properties const properties = node.properties || []; // Extract operation context (safely handle undefined config properties) const operationContext = { resource: config?.resource, operation: config?.operation, action: config?.action, mode: config?.mode }; // Find missing required fields const missingFields: string[] = []; for (const prop of properties) { // Skip if not required if (!prop.required) continue; // Skip if not visible based on current config if (prop.displayOptions) { let isVisible = true; // Check show conditions if (prop.displayOptions.show) { for (const [key, values] of Object.entries(prop.displayOptions.show)) { const configValue = config?.[key]; const expectedValues = Array.isArray(values) ? values : [values]; if (!expectedValues.includes(configValue)) { isVisible = false; break; } } } // Check hide conditions if (isVisible && prop.displayOptions.hide) { for (const [key, values] of Object.entries(prop.displayOptions.hide)) { const configValue = config?.[key]; const expectedValues = Array.isArray(values) ? values : [values]; if (expectedValues.includes(configValue)) { isVisible = false; break; } } } if (!isVisible) continue; } // Check if field is missing (safely handle null/undefined config) if (!config || !(prop.name in config)) { missingFields.push(prop.displayName || prop.name); } } return { nodeType: node.nodeType, displayName: node.displayName, valid: missingFields.length === 0, missingRequiredFields: missingFields }; } // Method removed - replaced by getToolsDocumentation private async getToolsDocumentation(topic?: string, depth: 'essentials' | 'full' = 'essentials'): Promise<string> { if (!topic || topic === 'overview') { return getToolsOverview(depth); } return getToolDocumentation(topic, depth); } // Add connect method to accept any transport async connect(transport: any): Promise<void> { await this.ensureInitialized(); await this.server.connect(transport); logger.info('MCP Server connected', { transportType: transport.constructor.name }); } // Template-related methods private async listTemplates(limit: number = 10, offset: number = 0, sortBy: 'views' | 'created_at' | 'name' = 'views', includeMetadata: boolean = false): Promise<any> { await this.ensureInitialized(); if (!this.templateService) throw new Error('Template service not initialized'); const result = await this.templateService.listTemplates(limit, offset, sortBy, includeMetadata); return { ...result, tip: result.items.length > 0 ? `Use get_template(templateId) to get full workflow details. Total: ${result.total} templates available.` : "No templates found. Run 'npm run fetch:templates' to update template database" }; } private async listNodeTemplates(nodeTypes: string[], limit: number = 10, offset: number = 0): Promise<any> { await this.ensureInitialized(); if (!this.templateService) throw new Error('Template service not initialized'); const result = await this.templateService.listNodeTemplates(nodeTypes, limit, offset); if (result.items.length === 0 && offset === 0) { return { ...result, message: `No templates found using nodes: ${nodeTypes.join(', ')}`, tip: "Try searching with more common nodes or run 'npm run fetch:templates' to update template database" }; } return { ...result, tip: `Showing ${result.items.length} of ${result.total} templates. Use offset for pagination.` }; } private async getTemplate(templateId: number, mode: 'nodes_only' | 'structure' | 'full' = 'full'): Promise<any> { await this.ensureInitialized(); if (!this.templateService) throw new Error('Template service not initialized'); const template = await this.templateService.getTemplate(templateId, mode); if (!template) { return { error: `Template ${templateId} not found`, tip: "Use list_templates, list_node_templates or search_templates to find available templates" }; } const usage = mode === 'nodes_only' ? "Node list for quick overview" : mode === 'structure' ? "Workflow structure without full details" : "Complete workflow JSON ready to import into n8n"; return { mode, template, usage }; } private async searchTemplates(query: string, limit: number = 20, offset: number = 0, fields?: string[]): Promise<any> { await this.ensureInitialized(); if (!this.templateService) throw new Error('Template service not initialized'); const result = await this.templateService.searchTemplates(query, limit, offset, fields); if (result.items.length === 0 && offset === 0) { return { ...result, message: `No templates found matching: "${query}"`, tip: "Try different keywords or run 'npm run fetch:templates' to update template database" }; } return { ...result, query, tip: `Found ${result.total} templates matching "${query}". Showing ${result.items.length}.` }; } private async getTemplatesForTask(task: string, limit: number = 10, offset: number = 0): Promise<any> { await this.ensureInitialized(); if (!this.templateService) throw new Error('Template service not initialized'); const result = await this.templateService.getTemplatesForTask(task, limit, offset); const availableTasks = this.templateService.listAvailableTasks(); if (result.items.length === 0 && offset === 0) { return { ...result, message: `No templates found for task: ${task}`, availableTasks, tip: "Try a different task or use search_templates for custom searches" }; } return { ...result, task, description: this.getTaskDescription(task), tip: `${result.total} templates available for ${task}. Showing ${result.items.length}.` }; } private async searchTemplatesByMetadata(filters: { category?: string; complexity?: 'simple' | 'medium' | 'complex'; maxSetupMinutes?: number; minSetupMinutes?: number; requiredService?: string; targetAudience?: string; }, limit: number = 20, offset: number = 0): Promise<any> { await this.ensureInitialized(); if (!this.templateService) throw new Error('Template service not initialized'); const result = await this.templateService.searchTemplatesByMetadata(filters, limit, offset); // Build filter summary for feedback const filterSummary: string[] = []; if (filters.category) filterSummary.push(`category: ${filters.category}`); if (filters.complexity) filterSummary.push(`complexity: ${filters.complexity}`); if (filters.maxSetupMinutes) filterSummary.push(`max setup: ${filters.maxSetupMinutes} min`); if (filters.minSetupMinutes) filterSummary.push(`min setup: ${filters.minSetupMinutes} min`); if (filters.requiredService) filterSummary.push(`service: ${filters.requiredService}`); if (filters.targetAudience) filterSummary.push(`audience: ${filters.targetAudience}`); if (result.items.length === 0 && offset === 0) { // Get available categories and audiences for suggestions const availableCategories = await this.templateService.getAvailableCategories(); const availableAudiences = await this.templateService.getAvailableTargetAudiences(); return { ...result, message: `No templates found with filters: ${filterSummary.join(', ')}`, availableCategories: availableCategories.slice(0, 10), availableAudiences: availableAudiences.slice(0, 5), tip: "Try broader filters or different categories. Use list_templates to see all templates." }; } return { ...result, filters, filterSummary: filterSummary.join(', '), tip: `Found ${result.total} templates matching filters. Showing ${result.items.length}. Each includes AI-generated metadata.` }; } private getTaskDescription(task: string): string { const descriptions: Record<string, string> = { 'ai_automation': 'AI-powered workflows using OpenAI, LangChain, and other AI tools', 'data_sync': 'Synchronize data between databases, spreadsheets, and APIs', 'webhook_processing': 'Process incoming webhooks and trigger automated actions', 'email_automation': 'Send, receive, and process emails automatically', 'slack_integration': 'Integrate with Slack for notifications and bot interactions', 'data_transformation': 'Transform, clean, and manipulate data', 'file_processing': 'Handle file uploads, downloads, and transformations', 'scheduling': 'Schedule recurring tasks and time-based automations', 'api_integration': 'Connect to external APIs and web services', 'database_operations': 'Query, insert, update, and manage database records' }; return descriptions[task] || 'Workflow templates for this task'; } private async validateWorkflow(workflow: any, options?: any): Promise<any> { await this.ensureInitialized(); if (!this.repository) throw new Error('Repository not initialized'); // Enhanced logging for workflow validation logger.info('Workflow validation requested', { hasWorkflow: !!workflow, workflowType: typeof workflow, hasNodes: workflow?.nodes !== undefined, nodesType: workflow?.nodes ? typeof workflow.nodes : 'undefined', nodesIsArray: Array.isArray(workflow?.nodes), nodesCount: Array.isArray(workflow?.nodes) ? workflow.nodes.length : 0, hasConnections: workflow?.connections !== undefined, connectionsType: workflow?.connections ? typeof workflow.connections : 'undefined', options: options }); // Help n8n AI agents with common mistakes if (!workflow || typeof workflow !== 'object') { return { valid: false, errors: [{ node: 'workflow', message: 'Workflow must be an object with nodes and connections', details: 'Expected format: ' + getWorkflowExampleString() }], summary: { errorCount: 1 } }; } if (!workflow.nodes || !Array.isArray(workflow.nodes)) { return { valid: false, errors: [{ node: 'workflow', message: 'Workflow must have a nodes array', details: 'Expected: workflow.nodes = [array of node objects]. ' + getWorkflowExampleString() }], summary: { errorCount: 1 } }; } if (!workflow.connections || typeof workflow.connections !== 'object') { return { valid: false, errors: [{ node: 'workflow', message: 'Workflow must have a connections object', details: 'Expected: workflow.connections = {} (can be empty object). ' + getWorkflowExampleString() }], summary: { errorCount: 1 } }; } // Create workflow validator instance const validator = new WorkflowValidator( this.repository, EnhancedConfigValidator ); try { const result = await validator.validateWorkflow(workflow, options); // Format the response for better readability const response: any = { valid: result.valid, summary: { totalNodes: result.statistics.totalNodes, enabledNodes: result.statistics.enabledNodes, triggerNodes: result.statistics.triggerNodes, validConnections: result.statistics.validConnections, invalidConnections: result.statistics.invalidConnections, expressionsValidated: result.statistics.expressionsValidated, errorCount: result.errors.length, warningCount: result.warnings.length }, // Always include errors and warnings arrays for consistent API response errors: result.errors.map(e => ({ node: e.nodeName || 'workflow', message: e.message, details: e.details })), warnings: result.warnings.map(w => ({ node: w.nodeName || 'workflow', message: w.message, details: w.details })) }; if (result.suggestions.length > 0) { response.suggestions = result.suggestions; } // Track validation details in telemetry if (!result.valid && result.errors.length > 0) { // Track each validation error for analysis result.errors.forEach(error => { telemetry.trackValidationDetails( error.nodeName || 'workflow', error.type || 'validation_error', { message: error.message, nodeCount: workflow.nodes?.length ?? 0, hasConnections: Object.keys(workflow.connections || {}).length > 0 } ); }); } // Track successfully validated workflows in telemetry if (result.valid) { telemetry.trackWorkflowCreation(workflow, true); } return response; } catch (error) { logger.error('Error validating workflow:', error); return { valid: false, error: error instanceof Error ? error.message : 'Unknown error validating workflow', tip: 'Ensure the workflow JSON includes nodes array and connections object' }; } } private async validateWorkflowConnections(workflow: any): Promise<any> { await this.ensureInitialized(); if (!this.repository) throw new Error('Repository not initialized'); // Create workflow validator instance const validator = new WorkflowValidator( this.repository, EnhancedConfigValidator ); try { // Validate only connections const result = await validator.validateWorkflow(workflow, { validateNodes: false, validateConnections: true, validateExpressions: false }); const response: any = { valid: result.errors.length === 0, statistics: { totalNodes: result.statistics.totalNodes, triggerNodes: result.statistics.triggerNodes, validConnections: result.statistics.validConnections, invalidConnections: result.statistics.invalidConnections } }; // Filter to only connection-related issues const connectionErrors = result.errors.filter(e => e.message.includes('connection') || e.message.includes('cycle') || e.message.includes('orphaned') ); const connectionWarnings = result.warnings.filter(w => w.message.includes('connection') || w.message.includes('orphaned') || w.message.includes('trigger') ); if (connectionErrors.length > 0) { response.errors = connectionErrors.map(e => ({ node: e.nodeName || 'workflow', message: e.message })); } if (connectionWarnings.length > 0) { response.warnings = connectionWarnings.map(w => ({ node: w.nodeName || 'workflow', message: w.message })); } return response; } catch (error) { logger.error('Error validating workflow connections:', error); return { valid: false, error: error instanceof Error ? error.message : 'Unknown error validating connections' }; } } private async validateWorkflowExpressions(workflow: any): Promise<any> { await this.ensureInitialized(); if (!this.repository) throw new Error('Repository not initialized'); // Create workflow validator instance const validator = new WorkflowValidator( this.repository, EnhancedConfigValidator ); try { // Validate only expressions const result = await validator.validateWorkflow(workflow, { validateNodes: false, validateConnections: false, validateExpressions: true }); const response: any = { valid: result.errors.length === 0, statistics: { totalNodes: result.statistics.totalNodes, expressionsValidated: result.statistics.expressionsValidated } }; // Filter to only expression-related issues const expressionErrors = result.errors.filter(e => e.message.includes('Expression') || e.message.includes('$') || e.message.includes('{{') ); const expressionWarnings = result.warnings.filter(w => w.message.includes('Expression') || w.message.includes('$') || w.message.includes('{{') ); if (expressionErrors.length > 0) { response.errors = expressionErrors.map(e => ({ node: e.nodeName || 'workflow', message: e.message })); } if (expressionWarnings.length > 0) { response.warnings = expressionWarnings.map(w => ({ node: w.nodeName || 'workflow', message: w.message })); } // Add tips for common expression issues if (expressionErrors.length > 0 || expressionWarnings.length > 0) { response.tips = [ 'Use {{ }} to wrap expressions', 'Reference data with $json.propertyName', 'Reference other nodes with $node["Node Name"].json', 'Use $input.item for input data in loops' ]; } return response; } catch (error) { logger.error('Error validating workflow expressions:', error); return { valid: false, error: error instanceof Error ? error.message : 'Unknown error validating expressions' }; } } async run(): Promise<void> { // Ensure database is initialized before starting server await this.ensureInitialized(); const transport = new StdioServerTransport(); await this.server.connect(transport); // Force flush stdout for Docker environments // Docker uses block buffering which can delay MCP responses if (!process.stdout.isTTY || process.env.IS_DOCKER) { // Override write to auto-flush const originalWrite = process.stdout.write.bind(process.stdout); process.stdout.write = function(chunk: any, encoding?: any, callback?: any) { const result = originalWrite(chunk, encoding, callback); // Force immediate flush process.stdout.emit('drain'); return result; }; } logger.info('n8n Documentation MCP Server running on stdio transport'); // Keep the process alive and listening process.stdin.resume(); } async shutdown(): Promise<void> { logger.info('Shutting down MCP server...'); // Clean up cache timers to prevent memory leaks if (this.cache) { try { this.cache.destroy(); logger.info('Cache timers cleaned up'); } catch (error) { logger.error('Error cleaning up cache:', error); } } // Close database connection if it exists if (this.db) { try { await this.db.close(); logger.info('Database connection closed'); } catch (error) { logger.error('Error closing database:', error); } } } } ```