This is page 5 of 59. Use http://codebase.md/czlonkowski/n8n-mcp?lines=true&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/tool-docs/configuration/get-node-essentials.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolDocumentation } from '../types'; 2 | 3 | export const getNodeEssentialsDoc: ToolDocumentation = { 4 | name: 'get_node_essentials', 5 | category: 'configuration', 6 | essentials: { 7 | description: 'Returns only the most commonly-used properties for a node (10-20 fields). Response is 95% smaller than get_node_info (5KB vs 100KB+). Essential properties include required fields, common options, and authentication settings. Use validate_node_operation for working configurations.', 8 | keyParameters: ['nodeType'], 9 | example: 'get_node_essentials({nodeType: "nodes-base.slack"})', 10 | performance: '<10ms, ~5KB response', 11 | tips: [ 12 | 'Always use this before get_node_info', 13 | 'Use validate_node_operation for examples', 14 | 'Perfect for understanding node structure' 15 | ] 16 | }, 17 | full: { 18 | description: 'Returns a curated subset of node properties focusing on the most commonly-used fields. Essential properties are hand-picked for each node type and include: required fields, primary operations, authentication options, and the most frequent configuration patterns. NOTE: Examples have been removed to avoid confusion - use validate_node_operation to get working configurations with proper validation.', 19 | parameters: { 20 | nodeType: { type: 'string', description: 'Full node type with prefix, e.g., "nodes-base.slack", "nodes-base.httpRequest"', required: true } 21 | }, 22 | returns: `Object containing: 23 | { 24 | "nodeType": "nodes-base.slack", 25 | "displayName": "Slack", 26 | "description": "Consume Slack API", 27 | "category": "output", 28 | "version": "2.3", 29 | "requiredProperties": [], // Most nodes have no strictly required fields 30 | "commonProperties": [ 31 | { 32 | "name": "resource", 33 | "displayName": "Resource", 34 | "type": "options", 35 | "options": ["channel", "message", "user"], 36 | "default": "message" 37 | }, 38 | { 39 | "name": "operation", 40 | "displayName": "Operation", 41 | "type": "options", 42 | "options": ["post", "update", "delete"], 43 | "default": "post" 44 | }, 45 | // ... 10-20 most common properties 46 | ], 47 | "operations": [ 48 | {"name": "Post", "description": "Post a message"}, 49 | {"name": "Update", "description": "Update a message"} 50 | ], 51 | "metadata": { 52 | "totalProperties": 121, 53 | "isAITool": false, 54 | "hasCredentials": true 55 | } 56 | }`, 57 | examples: [ 58 | 'get_node_essentials({nodeType: "nodes-base.httpRequest"}) - HTTP configuration basics', 59 | 'get_node_essentials({nodeType: "nodes-base.slack"}) - Slack messaging essentials', 60 | 'get_node_essentials({nodeType: "nodes-base.googleSheets"}) - Sheets operations', 61 | '// Workflow: search → essentials → validate', 62 | 'const nodes = search_nodes({query: "database"});', 63 | 'const mysql = get_node_essentials({nodeType: "nodes-base.mySql"});', 64 | 'validate_node_operation("nodes-base.mySql", {operation: "select"}, "minimal");' 65 | ], 66 | useCases: [ 67 | 'Quickly understand node structure without information overload', 68 | 'Identify which properties are most important', 69 | 'Learn node basics before diving into advanced features', 70 | 'Build workflows faster with curated property sets' 71 | ], 72 | performance: '<10ms response time, ~5KB payload (vs 100KB+ for full schema)', 73 | bestPractices: [ 74 | 'Always start with essentials, only use get_node_info if needed', 75 | 'Use validate_node_operation to get working configurations', 76 | 'Check authentication requirements first', 77 | 'Use search_node_properties if specific property not in essentials' 78 | ], 79 | pitfalls: [ 80 | 'Advanced properties not included - use get_node_info for complete schema', 81 | 'Node-specific validators may require additional fields', 82 | 'Some nodes have 50+ properties, essentials shows only top 10-20' 83 | ], 84 | relatedTools: ['get_node_info for complete schema', 'search_node_properties for finding specific fields', 'validate_node_minimal to check configuration'] 85 | } 86 | }; ``` -------------------------------------------------------------------------------- /scripts/test-http.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | # Test script for n8n-MCP HTTP Server 3 | 4 | set -e 5 | 6 | # Configuration 7 | URL="${1:-http://localhost:3000}" 8 | TOKEN="${AUTH_TOKEN:-test-token}" 9 | VERBOSE="${VERBOSE:-0}" 10 | 11 | # Colors for output 12 | RED='\033[0;31m' 13 | GREEN='\033[0;32m' 14 | YELLOW='\033[1;33m' 15 | NC='\033[0m' # No Color 16 | 17 | echo "🧪 Testing n8n-MCP HTTP Server" 18 | echo "================================" 19 | echo "Server URL: $URL" 20 | echo "" 21 | 22 | # Check if jq is installed 23 | if ! command -v jq &> /dev/null; then 24 | echo -e "${YELLOW}Warning: jq not installed. Output will not be formatted.${NC}" 25 | echo "Install with: brew install jq (macOS) or apt-get install jq (Linux)" 26 | echo "" 27 | JQ="cat" 28 | else 29 | JQ="jq ." 30 | fi 31 | 32 | # Function to make requests 33 | make_request() { 34 | local method="$1" 35 | local endpoint="$2" 36 | local data="$3" 37 | local headers="$4" 38 | local expected_status="$5" 39 | 40 | if [ "$VERBOSE" = "1" ]; then 41 | echo -e "${YELLOW}Request:${NC} $method $URL$endpoint" 42 | [ -n "$data" ] && echo -e "${YELLOW}Data:${NC} $data" 43 | fi 44 | 45 | # Build curl command 46 | local cmd="curl -s -w '\n%{http_code}' -X $method '$URL$endpoint'" 47 | [ -n "$headers" ] && cmd="$cmd $headers" 48 | [ -n "$data" ] && cmd="$cmd -d '$data'" 49 | 50 | # Execute and capture response 51 | local response=$(eval "$cmd") 52 | local body=$(echo "$response" | sed '$d') 53 | local status=$(echo "$response" | tail -n 1) 54 | 55 | # Check status 56 | if [ "$status" = "$expected_status" ]; then 57 | echo -e "${GREEN}✓${NC} $method $endpoint - Status: $status" 58 | else 59 | echo -e "${RED}✗${NC} $method $endpoint - Expected: $expected_status, Got: $status" 60 | fi 61 | 62 | # Show response body 63 | if [ -n "$body" ]; then 64 | echo "$body" | $JQ 65 | fi 66 | echo "" 67 | } 68 | 69 | # Test 1: Health check 70 | echo "1. Testing health endpoint..." 71 | make_request "GET" "/health" "" "" "200" 72 | 73 | # Test 2: OPTIONS request (CORS preflight) 74 | echo "2. Testing CORS preflight..." 75 | make_request "OPTIONS" "/mcp" "" "-H 'Origin: http://localhost' -H 'Access-Control-Request-Method: POST'" "204" 76 | 77 | # Test 3: Authentication failure 78 | echo "3. Testing authentication (should fail)..." 79 | make_request "POST" "/mcp" \ 80 | '{"jsonrpc":"2.0","method":"tools/list","id":1}' \ 81 | "-H 'Content-Type: application/json' -H 'Authorization: Bearer wrong-token'" \ 82 | "401" 83 | 84 | # Test 4: Missing authentication 85 | echo "4. Testing missing authentication..." 86 | make_request "POST" "/mcp" \ 87 | '{"jsonrpc":"2.0","method":"tools/list","id":1}' \ 88 | "-H 'Content-Type: application/json'" \ 89 | "401" 90 | 91 | # Test 5: Valid MCP request to list tools 92 | echo "5. Testing valid MCP request (list tools)..." 93 | make_request "POST" "/mcp" \ 94 | '{"jsonrpc":"2.0","method":"tools/list","id":1}' \ 95 | "-H 'Content-Type: application/json' -H 'Authorization: Bearer $TOKEN' -H 'Accept: application/json, text/event-stream'" \ 96 | "200" 97 | 98 | # Test 6: 404 for unknown endpoint 99 | echo "6. Testing 404 response..." 100 | make_request "GET" "/unknown" "" "" "404" 101 | 102 | # Test 7: Invalid JSON 103 | echo "7. Testing invalid JSON..." 104 | make_request "POST" "/mcp" \ 105 | '{invalid json}' \ 106 | "-H 'Content-Type: application/json' -H 'Authorization: Bearer $TOKEN'" \ 107 | "400" 108 | 109 | # Test 8: Request size limit 110 | echo "8. Testing request size limit..." 111 | # Use a different approach for large data 112 | echo "Skipping large payload test (would exceed bash limits)" 113 | 114 | # Test 9: MCP initialization 115 | if [ "$VERBOSE" = "1" ]; then 116 | echo "9. Testing MCP initialization..." 117 | make_request "POST" "/mcp" \ 118 | '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{"roots":{}}},"id":1}' \ 119 | "-H 'Content-Type: application/json' -H 'Authorization: Bearer $TOKEN' -H 'Accept: text/event-stream'" \ 120 | "200" 121 | fi 122 | 123 | echo "================================" 124 | echo "🎉 Tests completed!" 125 | echo "" 126 | echo "To run with verbose output: VERBOSE=1 $0" 127 | echo "To test a different server: $0 https://your-server.com" 128 | echo "To use a different token: AUTH_TOKEN=your-token $0" ``` -------------------------------------------------------------------------------- /tests/unit/services/debug-validator.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, vi, beforeEach } from 'vitest'; 2 | import { WorkflowValidator } from '@/services/workflow-validator'; 3 | 4 | // Mock dependencies - don't use vi.mock for complex mocks 5 | vi.mock('@/services/expression-validator', () => ({ 6 | ExpressionValidator: { 7 | validateNodeExpressions: () => ({ 8 | valid: true, 9 | errors: [], 10 | warnings: [], 11 | variables: [], 12 | expressions: [] 13 | }) 14 | } 15 | })); 16 | vi.mock('@/utils/logger', () => ({ 17 | Logger: vi.fn().mockImplementation(() => ({ 18 | error: vi.fn(), 19 | warn: vi.fn(), 20 | info: vi.fn(), 21 | debug: vi.fn() 22 | })) 23 | })); 24 | 25 | describe('Debug Validator Tests', () => { 26 | let validator: WorkflowValidator; 27 | let mockNodeRepository: any; 28 | let mockEnhancedConfigValidator: any; 29 | 30 | beforeEach(() => { 31 | // Create mock repository 32 | mockNodeRepository = { 33 | getNode: (nodeType: string) => { 34 | // Handle both n8n-nodes-base.set and nodes-base.set (normalized) 35 | if (nodeType === 'n8n-nodes-base.set' || nodeType === 'nodes-base.set') { 36 | return { 37 | name: 'Set', 38 | type: 'nodes-base.set', 39 | typeVersion: 1, 40 | properties: [], 41 | package: 'n8n-nodes-base', 42 | version: 1, 43 | displayName: 'Set' 44 | }; 45 | } 46 | return null; 47 | } 48 | }; 49 | 50 | // Create mock EnhancedConfigValidator 51 | mockEnhancedConfigValidator = { 52 | validateWithMode: () => ({ 53 | valid: true, 54 | errors: [], 55 | warnings: [], 56 | suggestions: [], 57 | mode: 'operation', 58 | visibleProperties: [], 59 | hiddenProperties: [] 60 | }) 61 | }; 62 | 63 | // Create validator instance 64 | validator = new WorkflowValidator(mockNodeRepository, mockEnhancedConfigValidator as any); 65 | }); 66 | 67 | it('should handle nodes at extreme positions - debug', async () => { 68 | const workflow = { 69 | nodes: [ 70 | { id: '1', name: 'FarLeft', type: 'n8n-nodes-base.set', position: [-999999, -999999] as [number, number], parameters: {} }, 71 | { id: '2', name: 'FarRight', type: 'n8n-nodes-base.set', position: [999999, 999999] as [number, number], parameters: {} }, 72 | { id: '3', name: 'Zero', type: 'n8n-nodes-base.set', position: [0, 0] as [number, number], parameters: {} } 73 | ], 74 | connections: { 75 | 'FarLeft': { 76 | main: [[{ node: 'FarRight', type: 'main', index: 0 }]] 77 | }, 78 | 'FarRight': { 79 | main: [[{ node: 'Zero', type: 'main', index: 0 }]] 80 | } 81 | } 82 | }; 83 | 84 | const result = await validator.validateWorkflow(workflow); 85 | 86 | 87 | // Test should pass with extreme positions 88 | expect(result.valid).toBe(true); 89 | expect(result.errors).toHaveLength(0); 90 | }); 91 | 92 | it('should handle special characters in node names - debug', async () => { 93 | const workflow = { 94 | nodes: [ 95 | { id: '1', name: 'Node@#$%', type: 'n8n-nodes-base.set', position: [0, 0] as [number, number], parameters: {} }, 96 | { id: '2', name: 'Node 中文', type: 'n8n-nodes-base.set', position: [100, 0] as [number, number], parameters: {} }, 97 | { id: '3', name: 'Node😊', type: 'n8n-nodes-base.set', position: [200, 0] as [number, number], parameters: {} } 98 | ], 99 | connections: { 100 | 'Node@#$%': { 101 | main: [[{ node: 'Node 中文', type: 'main', index: 0 }]] 102 | }, 103 | 'Node 中文': { 104 | main: [[{ node: 'Node😊', type: 'main', index: 0 }]] 105 | } 106 | } 107 | }; 108 | 109 | const result = await validator.validateWorkflow(workflow); 110 | 111 | 112 | // Test should pass with special characters in node names 113 | expect(result.valid).toBe(true); 114 | expect(result.errors).toHaveLength(0); 115 | }); 116 | 117 | it('should handle non-array nodes - debug', async () => { 118 | const workflow = { 119 | nodes: 'not-an-array', 120 | connections: {} 121 | }; 122 | const result = await validator.validateWorkflow(workflow as any); 123 | 124 | 125 | expect(result.valid).toBe(false); 126 | expect(result.errors[0].message).toContain('nodes must be an array'); 127 | }); 128 | }); ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/configuration/search-node-properties.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolDocumentation } from '../types'; 2 | 3 | export const searchNodePropertiesDoc: ToolDocumentation = { 4 | name: 'search_node_properties', 5 | category: 'configuration', 6 | essentials: { 7 | description: 'Find specific properties in a node without downloading all 200+ properties.', 8 | keyParameters: ['nodeType', 'query'], 9 | example: 'search_node_properties({nodeType: "nodes-base.httpRequest", query: "auth"})', 10 | performance: 'Fast - searches indexed properties', 11 | tips: [ 12 | 'Search for "auth", "header", "body", "json", "credential"', 13 | 'Returns property paths and descriptions', 14 | 'Much faster than get_node_info for finding specific fields' 15 | ] 16 | }, 17 | full: { 18 | description: `Searches for specific properties within a node's configuration schema. Essential for finding authentication fields, headers, body parameters, or any specific property without downloading the entire node schema (which can be 100KB+). Returns matching properties with their paths, types, and descriptions.`, 19 | parameters: { 20 | nodeType: { 21 | type: 'string', 22 | required: true, 23 | description: 'Full type with prefix', 24 | examples: [ 25 | 'nodes-base.httpRequest', 26 | 'nodes-base.slack', 27 | 'nodes-base.postgres', 28 | 'nodes-base.googleSheets' 29 | ] 30 | }, 31 | query: { 32 | type: 'string', 33 | required: true, 34 | description: 'Property to find: "auth", "header", "body", "json"', 35 | examples: [ 36 | 'auth', 37 | 'header', 38 | 'body', 39 | 'json', 40 | 'credential', 41 | 'timeout', 42 | 'retry', 43 | 'pagination' 44 | ] 45 | }, 46 | maxResults: { 47 | type: 'number', 48 | required: false, 49 | description: 'Max results (default 20)', 50 | default: 20 51 | } 52 | }, 53 | returns: `Object containing: 54 | - nodeType: The searched node type 55 | - query: Your search term 56 | - matches: Array of matching properties with: 57 | - name: Property identifier 58 | - displayName: Human-readable name 59 | - type: Property type (string, number, options, etc.) 60 | - description: Property description 61 | - path: Full path to property (for nested properties) 62 | - required: Whether property is required 63 | - default: Default value if any 64 | - options: Available options for selection properties 65 | - showWhen: Visibility conditions 66 | - totalMatches: Number of matches found 67 | - searchedIn: Total properties searched`, 68 | examples: [ 69 | 'search_node_properties({nodeType: "nodes-base.httpRequest", query: "auth"}) - Find authentication fields', 70 | 'search_node_properties({nodeType: "nodes-base.slack", query: "channel"}) - Find channel-related properties', 71 | 'search_node_properties({nodeType: "nodes-base.postgres", query: "query"}) - Find query fields', 72 | 'search_node_properties({nodeType: "nodes-base.webhook", query: "response"}) - Find response options' 73 | ], 74 | useCases: [ 75 | 'Finding authentication/credential fields quickly', 76 | 'Locating specific parameters without full node info', 77 | 'Discovering header or body configuration options', 78 | 'Finding nested properties in complex nodes', 79 | 'Checking if a node supports specific features (retry, pagination, etc.)' 80 | ], 81 | performance: 'Very fast - searches pre-indexed property metadata', 82 | bestPractices: [ 83 | 'Use before get_node_info to find specific properties', 84 | 'Search for common terms: auth, header, body, credential', 85 | 'Check showWhen conditions to understand visibility', 86 | 'Use with get_property_dependencies for complete understanding', 87 | 'Limit results if you only need to check existence' 88 | ], 89 | pitfalls: [ 90 | 'Some properties may be hidden due to visibility conditions', 91 | 'Property names may differ from display names', 92 | 'Nested properties show full path (e.g., "options.retry.limit")', 93 | 'Search is case-sensitive for property names' 94 | ], 95 | relatedTools: ['get_node_essentials', 'get_property_dependencies', 'get_node_info'] 96 | } 97 | }; ``` -------------------------------------------------------------------------------- /scripts/vitest-benchmark-json-reporter.js: -------------------------------------------------------------------------------- ```javascript 1 | const { writeFileSync } = require('fs'); 2 | const { resolve } = require('path'); 3 | 4 | class BenchmarkJsonReporter { 5 | constructor() { 6 | this.results = []; 7 | console.log('[BenchmarkJsonReporter] Initialized'); 8 | } 9 | 10 | onInit(ctx) { 11 | console.log('[BenchmarkJsonReporter] onInit called'); 12 | } 13 | 14 | onCollected(files) { 15 | console.log('[BenchmarkJsonReporter] onCollected called with', files ? files.length : 0, 'files'); 16 | } 17 | 18 | onTaskUpdate(tasks) { 19 | console.log('[BenchmarkJsonReporter] onTaskUpdate called'); 20 | } 21 | 22 | onBenchmarkResult(file, benchmark) { 23 | console.log('[BenchmarkJsonReporter] onBenchmarkResult called for', benchmark.name); 24 | } 25 | 26 | onFinished(files, errors) { 27 | console.log('[BenchmarkJsonReporter] onFinished called with', files ? files.length : 0, 'files'); 28 | 29 | const results = { 30 | timestamp: new Date().toISOString(), 31 | files: [] 32 | }; 33 | 34 | try { 35 | for (const file of files || []) { 36 | if (!file) continue; 37 | 38 | const fileResult = { 39 | filepath: file.filepath || file.name || 'unknown', 40 | groups: [] 41 | }; 42 | 43 | // Handle both file.tasks and file.benchmarks 44 | const tasks = file.tasks || file.benchmarks || []; 45 | 46 | // Process tasks/benchmarks 47 | for (const task of tasks) { 48 | if (task.type === 'suite' && task.tasks) { 49 | // This is a suite containing benchmarks 50 | const group = { 51 | name: task.name, 52 | benchmarks: [] 53 | }; 54 | 55 | for (const benchmark of task.tasks) { 56 | if (benchmark.result?.benchmark) { 57 | group.benchmarks.push({ 58 | name: benchmark.name, 59 | result: { 60 | mean: benchmark.result.benchmark.mean, 61 | min: benchmark.result.benchmark.min, 62 | max: benchmark.result.benchmark.max, 63 | hz: benchmark.result.benchmark.hz, 64 | p75: benchmark.result.benchmark.p75, 65 | p99: benchmark.result.benchmark.p99, 66 | p995: benchmark.result.benchmark.p995, 67 | p999: benchmark.result.benchmark.p999, 68 | rme: benchmark.result.benchmark.rme, 69 | samples: benchmark.result.benchmark.samples 70 | } 71 | }); 72 | } 73 | } 74 | 75 | if (group.benchmarks.length > 0) { 76 | fileResult.groups.push(group); 77 | } 78 | } else if (task.result?.benchmark) { 79 | // This is a direct benchmark (not in a suite) 80 | if (!fileResult.groups.length) { 81 | fileResult.groups.push({ 82 | name: 'Default', 83 | benchmarks: [] 84 | }); 85 | } 86 | 87 | fileResult.groups[0].benchmarks.push({ 88 | name: task.name, 89 | result: { 90 | mean: task.result.benchmark.mean, 91 | min: task.result.benchmark.min, 92 | max: task.result.benchmark.max, 93 | hz: task.result.benchmark.hz, 94 | p75: task.result.benchmark.p75, 95 | p99: task.result.benchmark.p99, 96 | p995: task.result.benchmark.p995, 97 | p999: task.result.benchmark.p999, 98 | rme: task.result.benchmark.rme, 99 | samples: task.result.benchmark.samples 100 | } 101 | }); 102 | } 103 | } 104 | 105 | if (fileResult.groups.length > 0) { 106 | results.files.push(fileResult); 107 | } 108 | } 109 | 110 | // Write results 111 | const outputPath = resolve(process.cwd(), 'benchmark-results.json'); 112 | writeFileSync(outputPath, JSON.stringify(results, null, 2)); 113 | console.log(`[BenchmarkJsonReporter] Benchmark results written to ${outputPath}`); 114 | console.log(`[BenchmarkJsonReporter] Total files processed: ${results.files.length}`); 115 | } catch (error) { 116 | console.error('[BenchmarkJsonReporter] Error writing results:', error); 117 | } 118 | } 119 | } 120 | 121 | module.exports = BenchmarkJsonReporter; ``` -------------------------------------------------------------------------------- /scripts/migrate-tool-docs.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env tsx 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | 5 | // This is a helper script to migrate tool documentation to the new structure 6 | // It creates a template file for each tool that needs to be migrated 7 | 8 | const toolsByCategory = { 9 | discovery: [ 10 | 'search_nodes', 11 | 'list_nodes', 12 | 'list_ai_tools', 13 | 'get_database_statistics' 14 | ], 15 | configuration: [ 16 | 'get_node_info', 17 | 'get_node_essentials', 18 | 'get_node_documentation', 19 | 'search_node_properties', 20 | 'get_node_as_tool_info', 21 | 'get_property_dependencies' 22 | ], 23 | validation: [ 24 | 'validate_node_minimal', 25 | 'validate_node_operation', 26 | 'validate_workflow', 27 | 'validate_workflow_connections', 28 | 'validate_workflow_expressions' 29 | ], 30 | templates: [ 31 | 'get_node_for_task', 32 | 'list_tasks', 33 | 'list_node_templates', 34 | 'get_template', 35 | 'search_templates', 36 | 'get_templates_for_task' 37 | ], 38 | workflow_management: [ 39 | 'n8n_create_workflow', 40 | 'n8n_get_workflow', 41 | 'n8n_get_workflow_details', 42 | 'n8n_get_workflow_structure', 43 | 'n8n_get_workflow_minimal', 44 | 'n8n_update_full_workflow', 45 | 'n8n_update_partial_workflow', 46 | 'n8n_delete_workflow', 47 | 'n8n_list_workflows', 48 | 'n8n_validate_workflow', 49 | 'n8n_trigger_webhook_workflow', 50 | 'n8n_get_execution', 51 | 'n8n_list_executions', 52 | 'n8n_delete_execution' 53 | ], 54 | system: [ 55 | 'tools_documentation', 56 | 'n8n_diagnostic', 57 | 'n8n_health_check', 58 | 'n8n_list_available_tools' 59 | ], 60 | special: [ 61 | 'code_node_guide' 62 | ] 63 | }; 64 | 65 | const template = (toolName: string, category: string) => `import { ToolDocumentation } from '../types'; 66 | 67 | export const ${toCamelCase(toolName)}Doc: ToolDocumentation = { 68 | name: '${toolName}', 69 | category: '${category}', 70 | essentials: { 71 | description: 'TODO: Add description from old file', 72 | keyParameters: ['TODO'], 73 | example: '${toolName}({TODO})', 74 | performance: 'TODO', 75 | tips: [ 76 | 'TODO: Add tips' 77 | ] 78 | }, 79 | full: { 80 | description: 'TODO: Add full description', 81 | parameters: { 82 | // TODO: Add parameters 83 | }, 84 | returns: 'TODO: Add return description', 85 | examples: [ 86 | '${toolName}({TODO}) - TODO' 87 | ], 88 | useCases: [ 89 | 'TODO: Add use cases' 90 | ], 91 | performance: 'TODO: Add performance description', 92 | bestPractices: [ 93 | 'TODO: Add best practices' 94 | ], 95 | pitfalls: [ 96 | 'TODO: Add pitfalls' 97 | ], 98 | relatedTools: ['TODO'] 99 | } 100 | };`; 101 | 102 | function toCamelCase(str: string): string { 103 | return str.split('_').map((part, index) => 104 | index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1) 105 | ).join(''); 106 | } 107 | 108 | function toKebabCase(str: string): string { 109 | return str.replace(/_/g, '-'); 110 | } 111 | 112 | // Create template files for tools that don't exist yet 113 | Object.entries(toolsByCategory).forEach(([category, tools]) => { 114 | tools.forEach(toolName => { 115 | const fileName = toKebabCase(toolName) + '.ts'; 116 | const filePath = path.join('src/mcp/tool-docs', category, fileName); 117 | 118 | // Skip if file already exists 119 | if (fs.existsSync(filePath)) { 120 | console.log(`✓ ${filePath} already exists`); 121 | return; 122 | } 123 | 124 | // Create the file with template 125 | fs.writeFileSync(filePath, template(toolName, category)); 126 | console.log(`✨ Created ${filePath}`); 127 | }); 128 | 129 | // Create index file for the category 130 | const indexPath = path.join('src/mcp/tool-docs', category, 'index.ts'); 131 | if (!fs.existsSync(indexPath)) { 132 | const indexContent = tools.map(toolName => 133 | `export { ${toCamelCase(toolName)}Doc } from './${toKebabCase(toolName)}';` 134 | ).join('\n'); 135 | 136 | fs.writeFileSync(indexPath, indexContent); 137 | console.log(`✨ Created ${indexPath}`); 138 | } 139 | }); 140 | 141 | console.log('\n📝 Migration templates created!'); 142 | console.log('Next steps:'); 143 | console.log('1. Copy documentation from the old tools-documentation.ts file'); 144 | console.log('2. Update each template file with the actual documentation'); 145 | console.log('3. Update src/mcp/tool-docs/index.ts to import all tools'); 146 | console.log('4. Replace the old tools-documentation.ts with the new one'); ``` -------------------------------------------------------------------------------- /tests/docker-tests-README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Docker Config File Support Tests 2 | 3 | This directory contains comprehensive tests for the Docker config file support feature added to n8n-mcp. 4 | 5 | ## Test Structure 6 | 7 | ### Unit Tests (`tests/unit/docker/`) 8 | 9 | 1. **parse-config.test.ts** - Tests for the JSON config parser 10 | - Basic JSON parsing functionality 11 | - Environment variable precedence 12 | - Shell escaping and quoting 13 | - Nested object flattening 14 | - Error handling for invalid JSON 15 | 16 | 2. **serve-command.test.ts** - Tests for "n8n-mcp serve" command 17 | - Command transformation logic 18 | - Argument preservation 19 | - Integration with config loading 20 | - Backwards compatibility 21 | 22 | 3. **config-security.test.ts** - Security-focused tests 23 | - Command injection prevention 24 | - Shell metacharacter handling 25 | - Path traversal protection 26 | - Polyglot payload defense 27 | - Real-world attack scenarios 28 | 29 | 4. **edge-cases.test.ts** - Edge case and stress tests 30 | - JavaScript number edge cases 31 | - Unicode handling 32 | - Deep nesting performance 33 | - Large config files 34 | - Invalid data types 35 | 36 | ### Integration Tests (`tests/integration/docker/`) 37 | 38 | 1. **docker-config.test.ts** - Full Docker container tests with config files 39 | - Config file loading and parsing 40 | - Environment variable precedence 41 | - Security in container context 42 | - Complex configuration scenarios 43 | 44 | 2. **docker-entrypoint.test.ts** - Docker entrypoint script tests 45 | - MCP mode handling 46 | - Database initialization 47 | - Permission management 48 | - Signal handling 49 | - Authentication validation 50 | 51 | ## Running the Tests 52 | 53 | ### Prerequisites 54 | - Node.js and npm installed 55 | - Docker installed (for integration tests) 56 | - Build the project first: `npm run build` 57 | 58 | ### Commands 59 | 60 | ```bash 61 | # Run all Docker config tests 62 | npm run test:docker 63 | 64 | # Run only unit tests (no Docker required) 65 | npm run test:docker:unit 66 | 67 | # Run only integration tests (requires Docker) 68 | npm run test:docker:integration 69 | 70 | # Run security-focused tests 71 | npm run test:docker:security 72 | 73 | # Run with coverage 74 | ./scripts/test-docker-config.sh coverage 75 | ``` 76 | 77 | ### Individual test files 78 | ```bash 79 | # Run a specific test file 80 | npm test -- tests/unit/docker/parse-config.test.ts 81 | 82 | # Run with watch mode 83 | npm run test:watch -- tests/unit/docker/ 84 | 85 | # Run with coverage 86 | npm run test:coverage -- tests/unit/docker/config-security.test.ts 87 | ``` 88 | 89 | ## Test Coverage 90 | 91 | The tests cover: 92 | 93 | 1. **Functionality** 94 | - JSON parsing and environment variable conversion 95 | - Nested object flattening with underscore separation 96 | - Environment variable precedence (env vars override config) 97 | - "n8n-mcp serve" command auto-enables HTTP mode 98 | 99 | 2. **Security** 100 | - Command injection prevention through proper shell escaping 101 | - Protection against malicious config values 102 | - Safe handling of special characters and Unicode 103 | - Prevention of path traversal attacks 104 | 105 | 3. **Edge Cases** 106 | - Invalid JSON handling 107 | - Missing config files 108 | - Permission errors 109 | - Very large config files 110 | - Deep nesting performance 111 | 112 | 4. **Integration** 113 | - Full Docker container behavior 114 | - Database initialization with file locking 115 | - Permission handling (root vs nodejs user) 116 | - Signal propagation and process management 117 | 118 | ## CI/CD Considerations 119 | 120 | Integration tests are skipped by default unless: 121 | - Running in CI (CI=true environment variable) 122 | - Explicitly enabled (RUN_DOCKER_TESTS=true) 123 | 124 | This prevents test failures on developer machines without Docker. 125 | 126 | ## Security Notes 127 | 128 | The config parser implements defense in depth: 129 | 1. All values are wrapped in single quotes for shell safety 130 | 2. Single quotes within values are escaped as '"'"' 131 | 3. No variable expansion occurs within single quotes 132 | 4. Arrays and null values are ignored (not exported) 133 | 5. The parser exits silently on any error to prevent container startup issues 134 | 135 | ## Troubleshooting 136 | 137 | If tests fail: 138 | 1. Ensure Docker is running (for integration tests) 139 | 2. Check that the project is built (`npm run build`) 140 | 3. Verify no containers are left running: `docker ps -a | grep n8n-mcp-test` 141 | 4. Clean up test containers: `docker rm $(docker ps -aq -f name=n8n-mcp-test)` ``` -------------------------------------------------------------------------------- /tests/integration/n8n-api/workflows/get-workflow.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Integration Tests: handleGetWorkflow 3 | * 4 | * Tests workflow retrieval against a real n8n instance. 5 | * Covers successful retrieval and error handling. 6 | */ 7 | 8 | import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest'; 9 | import { createTestContext, TestContext, createTestWorkflowName } from '../utils/test-context'; 10 | import { getTestN8nClient } from '../utils/n8n-client'; 11 | import { N8nApiClient } from '../../../../src/services/n8n-api-client'; 12 | import { Workflow } from '../../../../src/types/n8n-api'; 13 | import { SIMPLE_WEBHOOK_WORKFLOW } from '../utils/fixtures'; 14 | import { cleanupOrphanedWorkflows } from '../utils/cleanup-helpers'; 15 | import { createMcpContext } from '../utils/mcp-context'; 16 | import { InstanceContext } from '../../../../src/types/instance-context'; 17 | import { handleGetWorkflow } from '../../../../src/mcp/handlers-n8n-manager'; 18 | 19 | describe('Integration: handleGetWorkflow', () => { 20 | let context: TestContext; 21 | let client: N8nApiClient; 22 | let mcpContext: InstanceContext; 23 | 24 | beforeEach(() => { 25 | context = createTestContext(); 26 | client = getTestN8nClient(); 27 | mcpContext = createMcpContext(); 28 | }); 29 | 30 | afterEach(async () => { 31 | await context.cleanup(); 32 | }); 33 | 34 | afterAll(async () => { 35 | if (!process.env.CI) { 36 | await cleanupOrphanedWorkflows(); 37 | } 38 | }); 39 | 40 | // ====================================================================== 41 | // Successful Retrieval 42 | // ====================================================================== 43 | 44 | describe('Successful Retrieval', () => { 45 | it('should retrieve complete workflow data', async () => { 46 | // Create a workflow first 47 | const workflow = { 48 | ...SIMPLE_WEBHOOK_WORKFLOW, 49 | name: createTestWorkflowName('Get Workflow - Complete Data'), 50 | tags: ['mcp-integration-test'] 51 | }; 52 | 53 | const created = await client.createWorkflow(workflow); 54 | expect(created).toBeDefined(); 55 | expect(created.id).toBeTruthy(); 56 | 57 | if (!created.id) throw new Error('Workflow ID is missing'); 58 | context.trackWorkflow(created.id); 59 | 60 | // Retrieve the workflow using MCP handler 61 | const response = await handleGetWorkflow({ id: created.id }, mcpContext); 62 | 63 | // Verify MCP response structure 64 | expect(response.success).toBe(true); 65 | expect(response.data).toBeDefined(); 66 | 67 | const retrieved = response.data as Workflow; 68 | 69 | // Verify all expected fields are present 70 | expect(retrieved).toBeDefined(); 71 | expect(retrieved.id).toBe(created.id); 72 | expect(retrieved.name).toBe(workflow.name); 73 | expect(retrieved.nodes).toBeDefined(); 74 | expect(retrieved.nodes).toHaveLength(workflow.nodes!.length); 75 | expect(retrieved.connections).toBeDefined(); 76 | expect(retrieved.active).toBeDefined(); 77 | expect(retrieved.createdAt).toBeDefined(); 78 | expect(retrieved.updatedAt).toBeDefined(); 79 | 80 | // Verify node data integrity 81 | const retrievedNode = retrieved.nodes[0]; 82 | const originalNode = workflow.nodes![0]; 83 | expect(retrievedNode.name).toBe(originalNode.name); 84 | expect(retrievedNode.type).toBe(originalNode.type); 85 | expect(retrievedNode.parameters).toBeDefined(); 86 | }); 87 | }); 88 | 89 | // ====================================================================== 90 | // Error Handling 91 | // ====================================================================== 92 | 93 | describe('Error Handling', () => { 94 | it('should return error for non-existent workflow (invalid ID)', async () => { 95 | const invalidId = '99999999'; 96 | 97 | const response = await handleGetWorkflow({ id: invalidId }, mcpContext); 98 | 99 | // MCP handlers return success: false on error 100 | expect(response.success).toBe(false); 101 | expect(response.error).toBeDefined(); 102 | }); 103 | 104 | it('should return error for malformed workflow ID', async () => { 105 | const malformedId = 'not-a-valid-id-format'; 106 | 107 | const response = await handleGetWorkflow({ id: malformedId }, mcpContext); 108 | 109 | // MCP handlers return success: false on error 110 | expect(response.success).toBe(false); 111 | expect(response.error).toBeDefined(); 112 | }); 113 | }); 114 | }); 115 | ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/configuration/get-node-info.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolDocumentation } from '../types'; 2 | 3 | export const getNodeInfoDoc: ToolDocumentation = { 4 | name: 'get_node_info', 5 | category: 'configuration', 6 | essentials: { 7 | description: 'Returns complete node schema with ALL properties (100KB+ response). Only use when you need advanced properties not in get_node_essentials. Contains 200+ properties for complex nodes like HTTP Request. Requires full prefix like "nodes-base.httpRequest".', 8 | keyParameters: ['nodeType'], 9 | example: 'get_node_info({nodeType: "nodes-base.slack"})', 10 | performance: '100-500ms, 50-500KB response', 11 | tips: [ 12 | 'Try get_node_essentials first (95% smaller)', 13 | 'Use only for advanced configurations', 14 | 'Response may have 200+ properties' 15 | ] 16 | }, 17 | full: { 18 | description: 'Returns the complete JSON schema for a node including all properties, operations, authentication methods, version information, and metadata. Response sizes range from 50KB to 500KB. Use this only when get_node_essentials doesn\'t provide the specific property you need.', 19 | parameters: { 20 | nodeType: { type: 'string', required: true, description: 'Full node type with prefix. Examples: "nodes-base.slack", "nodes-base.httpRequest", "nodes-langchain.openAi"' } 21 | }, 22 | returns: `Complete node object containing: 23 | { 24 | "displayName": "Slack", 25 | "name": "slack", 26 | "type": "nodes-base.slack", 27 | "typeVersion": 2.2, 28 | "description": "Consume Slack API", 29 | "defaults": {"name": "Slack"}, 30 | "inputs": ["main"], 31 | "outputs": ["main"], 32 | "credentials": [ 33 | { 34 | "name": "slackApi", 35 | "required": true, 36 | "displayOptions": {...} 37 | } 38 | ], 39 | "properties": [ 40 | // 200+ property definitions including: 41 | { 42 | "displayName": "Resource", 43 | "name": "resource", 44 | "type": "options", 45 | "options": ["channel", "message", "user", "file", ...], 46 | "default": "message" 47 | }, 48 | { 49 | "displayName": "Operation", 50 | "name": "operation", 51 | "type": "options", 52 | "displayOptions": { 53 | "show": {"resource": ["message"]} 54 | }, 55 | "options": ["post", "update", "delete", "get", ...], 56 | "default": "post" 57 | }, 58 | // ... 200+ more properties with complex conditions 59 | ], 60 | "version": 2.2, 61 | "subtitle": "={{$parameter[\"operation\"] + \": \" + $parameter[\"resource\"]}}", 62 | "codex": {...}, 63 | "supportedWebhooks": [...] 64 | }`, 65 | examples: [ 66 | 'get_node_info({nodeType: "nodes-base.httpRequest"}) - 300+ properties for HTTP requests', 67 | 'get_node_info({nodeType: "nodes-base.googleSheets"}) - Complex operations and auth', 68 | '// When to use get_node_info:', 69 | '// 1. First try essentials', 70 | 'const essentials = get_node_essentials({nodeType: "nodes-base.slack"});', 71 | '// 2. If property missing, search for it', 72 | 'const props = search_node_properties({nodeType: "nodes-base.slack", query: "thread"});', 73 | '// 3. Only if needed, get full schema', 74 | 'const full = get_node_info({nodeType: "nodes-base.slack"});' 75 | ], 76 | useCases: [ 77 | 'Analyzing all available operations for a node', 78 | 'Understanding complex property dependencies', 79 | 'Discovering all authentication methods', 80 | 'Building UI that shows all node options', 81 | 'Debugging property visibility conditions' 82 | ], 83 | performance: '100-500ms depending on node complexity. HTTP Request node: ~300KB, Simple nodes: ~50KB', 84 | bestPractices: [ 85 | 'Always try get_node_essentials first - it\'s 95% smaller', 86 | 'Use search_node_properties to find specific advanced properties', 87 | 'Cache results locally - schemas rarely change', 88 | 'Parse incrementally - don\'t load entire response into memory at once' 89 | ], 90 | pitfalls: [ 91 | 'Response can exceed 500KB for complex nodes', 92 | 'Contains many rarely-used properties that add noise', 93 | 'Property conditions can be deeply nested and complex', 94 | 'Must use full node type with prefix (nodes-base.X not just X)' 95 | ], 96 | relatedTools: ['get_node_essentials for common properties', 'search_node_properties to find specific fields', 'get_property_dependencies to understand conditions'] 97 | } 98 | }; ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/validation/validate-workflow.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolDocumentation } from '../types'; 2 | 3 | export const validateWorkflowDoc: ToolDocumentation = { 4 | name: 'validate_workflow', 5 | category: 'validation', 6 | essentials: { 7 | description: 'Full workflow validation: structure, connections, expressions, AI tools. Returns errors/warnings/fixes. Essential before deploy.', 8 | keyParameters: ['workflow', 'options'], 9 | example: 'validate_workflow({workflow: {nodes: [...], connections: {...}}})', 10 | performance: 'Moderate (100-500ms)', 11 | tips: [ 12 | 'Always validate before n8n_create_workflow to catch errors early', 13 | 'Use options.profile="minimal" for quick checks during development', 14 | 'AI tool connections are automatically validated for proper node references' 15 | ] 16 | }, 17 | full: { 18 | description: 'Performs comprehensive validation of n8n workflows including structure, node configurations, connections, and expressions. This is a three-layer validation system that catches errors before deployment, validates complex multi-node workflows, checks all n8n expressions for syntax errors, and ensures proper node connections and data flow.', 19 | parameters: { 20 | workflow: { 21 | type: 'object', 22 | required: true, 23 | description: 'The complete workflow JSON to validate. Must include nodes array and connections object.' 24 | }, 25 | options: { 26 | type: 'object', 27 | required: false, 28 | description: 'Validation options object' 29 | }, 30 | 'options.validateNodes': { 31 | type: 'boolean', 32 | required: false, 33 | description: 'Validate individual node configurations. Default: true' 34 | }, 35 | 'options.validateConnections': { 36 | type: 'boolean', 37 | required: false, 38 | description: 'Validate node connections and flow. Default: true' 39 | }, 40 | 'options.validateExpressions': { 41 | type: 'boolean', 42 | required: false, 43 | description: 'Validate n8n expressions syntax and references. Default: true' 44 | }, 45 | 'options.profile': { 46 | type: 'string', 47 | required: false, 48 | description: 'Validation profile for node validation: minimal, runtime (default), ai-friendly, strict' 49 | } 50 | }, 51 | returns: 'Object with valid (boolean), errors (array), warnings (array), statistics (object), and suggestions (array)', 52 | examples: [ 53 | 'validate_workflow({workflow: myWorkflow}) - Full validation with default settings', 54 | 'validate_workflow({workflow: myWorkflow, options: {profile: "minimal"}}) - Quick validation for editing', 55 | 'validate_workflow({workflow: myWorkflow, options: {validateExpressions: false}}) - Skip expression validation' 56 | ], 57 | useCases: [ 58 | 'Pre-deployment validation to catch all workflow issues', 59 | 'Quick validation during workflow development', 60 | 'Validate workflows with AI Agent nodes and tool connections', 61 | 'Check expression syntax before workflow execution', 62 | 'Ensure workflow structure integrity after modifications' 63 | ], 64 | performance: 'Moderate (100-500ms). Depends on workflow size and validation options. Expression validation adds ~50-100ms.', 65 | bestPractices: [ 66 | 'Always validate workflows before creating or updating in n8n', 67 | 'Use minimal profile during development, strict profile before production', 68 | 'Pay attention to warnings - they often indicate potential runtime issues', 69 | 'Validate after any workflow modifications, especially connection changes', 70 | 'Check statistics to understand workflow complexity' 71 | ], 72 | pitfalls: [ 73 | 'Large workflows (100+ nodes) may take longer to validate', 74 | 'Expression validation requires proper node references to exist', 75 | 'Some warnings may be acceptable depending on use case', 76 | 'Validation cannot catch all runtime errors (e.g., API failures)', 77 | 'Profile setting only affects node validation, not connection/expression checks' 78 | ], 79 | relatedTools: ['validate_workflow_connections', 'validate_workflow_expressions', 'validate_node_operation', 'n8n_create_workflow', 'n8n_update_partial_workflow', 'n8n_autofix_workflow'] 80 | } 81 | }; ``` -------------------------------------------------------------------------------- /scripts/test-expression-code-validation.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env npx tsx 2 | 3 | /** 4 | * Test script for Expression vs Code Node validation 5 | * Tests that we properly detect and warn about expression syntax in Code nodes 6 | */ 7 | 8 | import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator.js'; 9 | 10 | console.log('🧪 Testing Expression vs Code Node Validation\n'); 11 | 12 | // Test cases with expression syntax that shouldn't work in Code nodes 13 | const testCases = [ 14 | { 15 | name: 'Expression syntax in Code node', 16 | config: { 17 | language: 'javaScript', 18 | jsCode: `// Using expression syntax 19 | const value = {{$json.field}}; 20 | return [{json: {value}}];` 21 | }, 22 | expectedError: 'Expression syntax {{...}} is not valid in Code nodes' 23 | }, 24 | { 25 | name: 'Wrong $node syntax', 26 | config: { 27 | language: 'javaScript', 28 | jsCode: `// Using expression $node syntax 29 | const data = $node['Previous Node'].json; 30 | return [{json: data}];` 31 | }, 32 | expectedWarning: 'Use $(\'Node Name\') instead of $node[\'Node Name\'] in Code nodes' 33 | }, 34 | { 35 | name: 'Expression-only functions', 36 | config: { 37 | language: 'javaScript', 38 | jsCode: `// Using expression functions 39 | const now = $now(); 40 | const unique = items.unique(); 41 | return [{json: {now, unique}}];` 42 | }, 43 | expectedWarning: '$now() is an expression-only function' 44 | }, 45 | { 46 | name: 'Wrong JMESPath parameter order', 47 | config: { 48 | language: 'javaScript', 49 | jsCode: `// Wrong parameter order 50 | const result = $jmespath("users[*].name", data); 51 | return [{json: {result}}];` 52 | }, 53 | expectedWarning: 'Code node $jmespath has reversed parameter order' 54 | }, 55 | { 56 | name: 'Correct Code node syntax', 57 | config: { 58 | language: 'javaScript', 59 | jsCode: `// Correct syntax 60 | const prevData = $('Previous Node').first(); 61 | const now = DateTime.now(); 62 | const result = $jmespath(data, "users[*].name"); 63 | return [{json: {prevData, now, result}}];` 64 | }, 65 | shouldBeValid: true 66 | } 67 | ]; 68 | 69 | // Basic node properties for Code node 70 | const codeNodeProperties = [ 71 | { name: 'language', type: 'options', options: ['javaScript', 'python'] }, 72 | { name: 'jsCode', type: 'string' }, 73 | { name: 'pythonCode', type: 'string' }, 74 | { name: 'mode', type: 'options', options: ['runOnceForAllItems', 'runOnceForEachItem'] } 75 | ]; 76 | 77 | console.log('Running validation tests...\n'); 78 | 79 | testCases.forEach((test, index) => { 80 | console.log(`Test ${index + 1}: ${test.name}`); 81 | console.log('─'.repeat(50)); 82 | 83 | const result = EnhancedConfigValidator.validateWithMode( 84 | 'nodes-base.code', 85 | test.config, 86 | codeNodeProperties, 87 | 'operation', 88 | 'ai-friendly' 89 | ); 90 | 91 | console.log(`Valid: ${result.valid}`); 92 | console.log(`Errors: ${result.errors.length}`); 93 | console.log(`Warnings: ${result.warnings.length}`); 94 | 95 | if (test.expectedError) { 96 | const hasExpectedError = result.errors.some(e => 97 | e.message.includes(test.expectedError) 98 | ); 99 | console.log(`✅ Expected error found: ${hasExpectedError}`); 100 | if (!hasExpectedError) { 101 | console.log('❌ Missing expected error:', test.expectedError); 102 | console.log('Actual errors:', result.errors.map(e => e.message)); 103 | } 104 | } 105 | 106 | if (test.expectedWarning) { 107 | const hasExpectedWarning = result.warnings.some(w => 108 | w.message.includes(test.expectedWarning) 109 | ); 110 | console.log(`✅ Expected warning found: ${hasExpectedWarning}`); 111 | if (!hasExpectedWarning) { 112 | console.log('❌ Missing expected warning:', test.expectedWarning); 113 | console.log('Actual warnings:', result.warnings.map(w => w.message)); 114 | } 115 | } 116 | 117 | if (test.shouldBeValid) { 118 | console.log(`✅ Should be valid: ${result.valid && result.errors.length === 0}`); 119 | if (!result.valid || result.errors.length > 0) { 120 | console.log('❌ Unexpected errors:', result.errors); 121 | } 122 | } 123 | 124 | // Show actual messages 125 | if (result.errors.length > 0) { 126 | console.log('\nErrors:'); 127 | result.errors.forEach(e => console.log(` - ${e.message}`)); 128 | } 129 | 130 | if (result.warnings.length > 0) { 131 | console.log('\nWarnings:'); 132 | result.warnings.forEach(w => console.log(` - ${w.message}`)); 133 | } 134 | 135 | console.log('\n'); 136 | }); 137 | 138 | console.log('✅ Expression vs Code Node validation tests completed!'); ``` -------------------------------------------------------------------------------- /tests/test-sqlite-search.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Test SQLite database search functionality 5 | */ 6 | 7 | const { SQLiteStorageService } = require('../dist/services/sqlite-storage-service'); 8 | const { NodeSourceExtractor } = require('../dist/utils/node-source-extractor'); 9 | 10 | async function testDatabaseSearch() { 11 | console.log('=== SQLite Database Search Test ===\n'); 12 | 13 | const storage = new SQLiteStorageService(); 14 | const extractor = new NodeSourceExtractor(); 15 | 16 | // First, ensure we have some data 17 | console.log('1️⃣ Checking database status...'); 18 | let stats = await storage.getStatistics(); 19 | 20 | if (stats.totalNodes === 0) { 21 | console.log(' Database is empty. Adding some test nodes...\n'); 22 | 23 | const testNodes = [ 24 | 'n8n-nodes-base.Function', 25 | 'n8n-nodes-base.Webhook', 26 | 'n8n-nodes-base.HttpRequest', 27 | 'n8n-nodes-base.If', 28 | 'n8n-nodes-base.Slack', 29 | 'n8n-nodes-base.Discord' 30 | ]; 31 | 32 | for (const nodeType of testNodes) { 33 | try { 34 | const nodeInfo = await extractor.extractNodeSource(nodeType); 35 | await storage.storeNode(nodeInfo); 36 | console.log(` ✅ Stored ${nodeType}`); 37 | } catch (error) { 38 | console.log(` ❌ Failed to store ${nodeType}: ${error.message}`); 39 | } 40 | } 41 | 42 | stats = await storage.getStatistics(); 43 | } 44 | 45 | console.log(`\n Total nodes in database: ${stats.totalNodes}`); 46 | console.log(` Total packages: ${stats.totalPackages}`); 47 | console.log(` Database size: ${(stats.totalCodeSize / 1024).toFixed(2)} KB\n`); 48 | 49 | // Test different search scenarios 50 | console.log('2️⃣ Testing search functionality...\n'); 51 | 52 | const searchTests = [ 53 | { 54 | name: 'Search by partial name (func)', 55 | query: { query: 'func' } 56 | }, 57 | { 58 | name: 'Search by partial name (web)', 59 | query: { query: 'web' } 60 | }, 61 | { 62 | name: 'Search for HTTP', 63 | query: { query: 'http' } 64 | }, 65 | { 66 | name: 'Search for multiple terms', 67 | query: { query: 'slack discord' } 68 | }, 69 | { 70 | name: 'Filter by package', 71 | query: { packageName: 'n8n-nodes-base' } 72 | }, 73 | { 74 | name: 'Search with package filter', 75 | query: { query: 'func', packageName: 'n8n-nodes-base' } 76 | }, 77 | { 78 | name: 'Search by node type', 79 | query: { nodeType: 'Webhook' } 80 | }, 81 | { 82 | name: 'Limit results', 83 | query: { query: 'node', limit: 3 } 84 | } 85 | ]; 86 | 87 | for (const test of searchTests) { 88 | console.log(` 📍 ${test.name}:`); 89 | console.log(` Query: ${JSON.stringify(test.query)}`); 90 | 91 | try { 92 | const results = await storage.searchNodes(test.query); 93 | console.log(` Results: ${results.length} nodes found`); 94 | 95 | if (results.length > 0) { 96 | console.log(' Matches:'); 97 | results.slice(0, 3).forEach(node => { 98 | console.log(` - ${node.nodeType} (${node.displayName || node.name})`); 99 | }); 100 | if (results.length > 3) { 101 | console.log(` ... and ${results.length - 3} more`); 102 | } 103 | } 104 | } catch (error) { 105 | console.log(` ❌ Error: ${error.message}`); 106 | } 107 | 108 | console.log(''); 109 | } 110 | 111 | // Test specific node retrieval 112 | console.log('3️⃣ Testing specific node retrieval...\n'); 113 | 114 | const specificNode = await storage.getNode('n8n-nodes-base.Function'); 115 | if (specificNode) { 116 | console.log(` ✅ Found node: ${specificNode.nodeType}`); 117 | console.log(` Display name: ${specificNode.displayName}`); 118 | console.log(` Code size: ${specificNode.codeLength} bytes`); 119 | console.log(` Has credentials: ${specificNode.hasCredentials}`); 120 | } else { 121 | console.log(' ❌ Node not found'); 122 | } 123 | 124 | // Test package listing 125 | console.log('\n4️⃣ Testing package listing...\n'); 126 | 127 | const packages = await storage.getPackages(); 128 | console.log(` Found ${packages.length} packages:`); 129 | packages.forEach(pkg => { 130 | console.log(` - ${pkg.name}: ${pkg.nodeCount} nodes`); 131 | }); 132 | 133 | // Close database 134 | storage.close(); 135 | 136 | console.log('\n✅ Search functionality test completed!'); 137 | } 138 | 139 | // Run the test 140 | testDatabaseSearch().catch(error => { 141 | console.error('Test failed:', error); 142 | process.exit(1); 143 | }); ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/validation/validate-node-operation.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolDocumentation } from '../types'; 2 | 3 | export const validateNodeOperationDoc: ToolDocumentation = { 4 | name: 'validate_node_operation', 5 | category: 'validation', 6 | essentials: { 7 | description: 'Validates node configuration with operation awareness. Checks required fields, data types, and operation-specific rules. Returns specific errors with automated fix suggestions. Different profiles for different validation needs.', 8 | keyParameters: ['nodeType', 'config', 'profile'], 9 | example: 'validate_node_operation({nodeType: "nodes-base.slack", config: {resource: "message", operation: "post", text: "Hi"}})', 10 | performance: '<100ms', 11 | tips: [ 12 | 'Profile choices: minimal (editing), runtime (execution), ai-friendly (balanced), strict (deployment)', 13 | 'Returns fixes you can apply directly', 14 | 'Operation-aware - knows Slack post needs text' 15 | ] 16 | }, 17 | full: { 18 | description: 'Comprehensive node configuration validation that understands operation context. For example, it knows Slack message posting requires text field, while channel listing doesn\'t. Provides different validation profiles for different stages of workflow development.', 19 | parameters: { 20 | nodeType: { type: 'string', required: true, description: 'Full node type with prefix: "nodes-base.slack", "nodes-base.httpRequest"' }, 21 | config: { type: 'object', required: true, description: 'Node configuration. Must include operation fields (resource/operation/action) if the node has multiple operations' }, 22 | profile: { type: 'string', required: false, description: 'Validation profile - controls what\'s checked. Default: "ai-friendly"' } 23 | }, 24 | returns: `Object containing: 25 | { 26 | "isValid": false, 27 | "errors": [ 28 | { 29 | "field": "channel", 30 | "message": "Required field 'channel' is missing", 31 | "severity": "error", 32 | "fix": "#general" 33 | } 34 | ], 35 | "warnings": [ 36 | { 37 | "field": "retryOnFail", 38 | "message": "Consider enabling retry for reliability", 39 | "severity": "warning", 40 | "fix": true 41 | } 42 | ], 43 | "suggestions": [ 44 | { 45 | "field": "timeout", 46 | "message": "Set timeout to prevent hanging", 47 | "fix": 30000 48 | } 49 | ], 50 | "fixes": { 51 | "channel": "#general", 52 | "retryOnFail": true, 53 | "timeout": 30000 54 | } 55 | }`, 56 | examples: [ 57 | '// Missing required field', 58 | 'validate_node_operation({nodeType: "nodes-base.slack", config: {resource: "message", operation: "post"}})', 59 | '// Returns: {isValid: false, errors: [{field: "text", message: "Required field missing"}], fixes: {text: "Message text"}}', 60 | '', 61 | '// Validate with strict profile for production', 62 | 'validate_node_operation({nodeType: "nodes-base.httpRequest", config: {method: "POST", url: "https://api.example.com"}, profile: "strict"})', 63 | '', 64 | '// Apply fixes automatically', 65 | 'const result = validate_node_operation({nodeType: "nodes-base.slack", config: myConfig});', 66 | 'if (!result.isValid) {', 67 | ' myConfig = {...myConfig, ...result.fixes};', 68 | '}' 69 | ], 70 | useCases: [ 71 | 'Validate configuration before workflow execution', 72 | 'Debug why a node isn\'t working as expected', 73 | 'Generate configuration fixes automatically', 74 | 'Different validation for editing vs production' 75 | ], 76 | performance: '<100ms for most nodes, <200ms for complex nodes with many conditions', 77 | bestPractices: [ 78 | 'Use "minimal" profile during user editing for fast feedback', 79 | 'Use "runtime" profile (default) before execution', 80 | 'Use "ai-friendly" when AI configures nodes', 81 | 'Use "strict" profile before production deployment', 82 | 'Always include operation fields (resource/operation) in config', 83 | 'Apply suggested fixes to resolve issues quickly' 84 | ], 85 | pitfalls: [ 86 | 'Must include operation fields for multi-operation nodes', 87 | 'Fixes are suggestions - review before applying', 88 | 'Profile affects what\'s validated - minimal skips many checks' 89 | ], 90 | relatedTools: ['validate_node_minimal for quick checks', 'get_node_essentials for valid examples', 'validate_workflow for complete workflow validation'] 91 | } 92 | }; ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/system/n8n-health-check.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolDocumentation } from '../types'; 2 | 3 | export const n8nHealthCheckDoc: ToolDocumentation = { 4 | name: 'n8n_health_check', 5 | category: 'system', 6 | essentials: { 7 | description: 'Check n8n instance health, API connectivity, version status, and performance metrics', 8 | keyParameters: [], 9 | example: 'n8n_health_check({})', 10 | performance: 'Fast - single API call (~150-200ms median)', 11 | tips: [ 12 | 'Use before starting workflow operations to ensure n8n is responsive', 13 | 'Automatically checks if n8n-mcp version is outdated', 14 | 'Returns version info, performance metrics, and next-step recommendations', 15 | 'New: Shows cache hit rate and response time for performance monitoring' 16 | ] 17 | }, 18 | full: { 19 | description: `Performs a comprehensive health check of the configured n8n instance through its API. 20 | 21 | This tool verifies: 22 | - API endpoint accessibility and response time 23 | - n8n instance version and build information 24 | - Authentication status and permissions 25 | - Available features and enterprise capabilities 26 | - Database connectivity (as reported by n8n) 27 | - Queue system status (if configured) 28 | 29 | Health checks are crucial for: 30 | - Monitoring n8n instance availability 31 | - Detecting performance degradation 32 | - Verifying API compatibility before operations 33 | - Ensuring authentication is working correctly`, 34 | parameters: {}, 35 | returns: `Health status object containing: 36 | - status: Overall health status ('healthy', 'degraded', 'error') 37 | - n8nVersion: n8n instance version information 38 | - instanceId: Unique identifier for the n8n instance 39 | - features: Object listing available features and their status 40 | - mcpVersion: Current n8n-mcp version 41 | - supportedN8nVersion: Recommended n8n version for compatibility 42 | - versionCheck: Version status information 43 | - current: Current n8n-mcp version 44 | - latest: Latest available version from npm 45 | - upToDate: Boolean indicating if version is current 46 | - message: Formatted version status message 47 | - updateCommand: Command to update (if outdated) 48 | - performance: Performance metrics 49 | - responseTimeMs: API response time in milliseconds 50 | - cacheHitRate: Cache efficiency percentage 51 | - cachedInstances: Number of cached API instances 52 | - nextSteps: Recommended actions after health check 53 | - updateWarning: Warning if version is outdated (if applicable)`, 54 | examples: [ 55 | 'n8n_health_check({}) - Complete health check with version and performance data', 56 | '// Use in monitoring scripts\nconst health = await n8n_health_check({});\nif (health.status !== "ok") alert("n8n is down!");\nif (!health.versionCheck.upToDate) console.log("Update available:", health.versionCheck.updateCommand);', 57 | '// Check before critical operations\nconst health = await n8n_health_check({});\nif (health.performance.responseTimeMs > 1000) console.warn("n8n is slow");\nif (health.versionCheck.isOutdated) console.log(health.updateWarning);' 58 | ], 59 | useCases: [ 60 | 'Pre-flight checks before workflow deployments', 61 | 'Continuous monitoring of n8n instance health', 62 | 'Troubleshooting connectivity or performance issues', 63 | 'Verifying n8n version compatibility with workflows', 64 | 'Detecting feature availability (enterprise features, queue mode, etc.)' 65 | ], 66 | performance: `Fast response expected: 67 | - Single HTTP request to /health endpoint 68 | - Typically responds in <100ms for healthy instances 69 | - Timeout after 10 seconds indicates severe issues 70 | - Minimal server load - safe for frequent polling`, 71 | bestPractices: [ 72 | 'Run health checks before batch operations or deployments', 73 | 'Set up automated monitoring with regular health checks', 74 | 'Log response times to detect performance trends', 75 | 'Check version compatibility when deploying workflows', 76 | 'Use health status to implement circuit breaker patterns' 77 | ], 78 | pitfalls: [ 79 | 'Requires N8N_API_URL and N8N_API_KEY to be configured', 80 | 'Network issues may cause false negatives', 81 | 'Does not check individual workflow health', 82 | 'Health endpoint might be cached - not real-time for all metrics' 83 | ], 84 | relatedTools: ['n8n_diagnostic', 'n8n_list_available_tools', 'n8n_list_workflows'] 85 | } 86 | }; ``` -------------------------------------------------------------------------------- /scripts/quick-test.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env ts-node 2 | /** 3 | * Quick test script to validate the essentials implementation 4 | */ 5 | 6 | import { spawn } from 'child_process'; 7 | import { join } from 'path'; 8 | 9 | const colors = { 10 | reset: '\x1b[0m', 11 | bright: '\x1b[1m', 12 | green: '\x1b[32m', 13 | red: '\x1b[31m', 14 | yellow: '\x1b[33m', 15 | blue: '\x1b[34m', 16 | cyan: '\x1b[36m' 17 | }; 18 | 19 | function log(message: string, color: string = colors.reset) { 20 | console.log(`${color}${message}${colors.reset}`); 21 | } 22 | 23 | async function runMCPCommand(toolName: string, args: any): Promise<any> { 24 | return new Promise((resolve, reject) => { 25 | const request = { 26 | jsonrpc: '2.0', 27 | method: 'tools/call', 28 | params: { 29 | name: toolName, 30 | arguments: args 31 | }, 32 | id: 1 33 | }; 34 | 35 | const mcp = spawn('npm', ['start'], { 36 | cwd: join(__dirname, '..'), 37 | stdio: ['pipe', 'pipe', 'pipe'] 38 | }); 39 | 40 | let output = ''; 41 | let error = ''; 42 | 43 | mcp.stdout.on('data', (data) => { 44 | output += data.toString(); 45 | }); 46 | 47 | mcp.stderr.on('data', (data) => { 48 | error += data.toString(); 49 | }); 50 | 51 | mcp.on('close', (code) => { 52 | if (code !== 0) { 53 | reject(new Error(`Process exited with code ${code}: ${error}`)); 54 | return; 55 | } 56 | 57 | try { 58 | // Parse JSON-RPC response 59 | const lines = output.split('\n'); 60 | for (const line of lines) { 61 | if (line.trim() && line.includes('"jsonrpc"')) { 62 | const response = JSON.parse(line); 63 | if (response.result) { 64 | resolve(JSON.parse(response.result.content[0].text)); 65 | return; 66 | } else if (response.error) { 67 | reject(new Error(response.error.message)); 68 | return; 69 | } 70 | } 71 | } 72 | reject(new Error('No valid response found')); 73 | } catch (err) { 74 | reject(err); 75 | } 76 | }); 77 | 78 | // Send request 79 | mcp.stdin.write(JSON.stringify(request) + '\n'); 80 | mcp.stdin.end(); 81 | }); 82 | } 83 | 84 | async function quickTest() { 85 | log('\n🚀 Quick Test - n8n MCP Essentials', colors.bright + colors.cyan); 86 | 87 | try { 88 | // Test 1: Get essentials for HTTP Request 89 | log('\n1️⃣ Testing get_node_essentials for HTTP Request...', colors.yellow); 90 | const essentials = await runMCPCommand('get_node_essentials', { 91 | nodeType: 'nodes-base.httpRequest' 92 | }); 93 | 94 | log('✅ Success! Got essentials:', colors.green); 95 | log(` Required properties: ${essentials.requiredProperties?.map((p: any) => p.name).join(', ') || 'None'}`); 96 | log(` Common properties: ${essentials.commonProperties?.map((p: any) => p.name).join(', ') || 'None'}`); 97 | log(` Examples: ${Object.keys(essentials.examples || {}).join(', ')}`); 98 | log(` Response size: ${JSON.stringify(essentials).length} bytes`, colors.green); 99 | 100 | // Test 2: Search properties 101 | log('\n2️⃣ Testing search_node_properties...', colors.yellow); 102 | const searchResults = await runMCPCommand('search_node_properties', { 103 | nodeType: 'nodes-base.httpRequest', 104 | query: 'auth' 105 | }); 106 | 107 | log('✅ Success! Found properties:', colors.green); 108 | log(` Matches: ${searchResults.totalMatches}`); 109 | searchResults.matches?.slice(0, 3).forEach((match: any) => { 110 | log(` - ${match.name}: ${match.description}`); 111 | }); 112 | 113 | // Test 3: Compare sizes 114 | log('\n3️⃣ Comparing response sizes...', colors.yellow); 115 | const fullInfo = await runMCPCommand('get_node_info', { 116 | nodeType: 'nodes-base.httpRequest' 117 | }); 118 | 119 | const fullSize = JSON.stringify(fullInfo).length; 120 | const essentialSize = JSON.stringify(essentials).length; 121 | const reduction = ((fullSize - essentialSize) / fullSize * 100).toFixed(1); 122 | 123 | log(`✅ Size comparison:`, colors.green); 124 | log(` Full response: ${(fullSize / 1024).toFixed(1)} KB`); 125 | log(` Essential response: ${(essentialSize / 1024).toFixed(1)} KB`); 126 | log(` Size reduction: ${reduction}% 🎉`, colors.bright + colors.green); 127 | 128 | log('\n✨ All tests passed!', colors.bright + colors.green); 129 | 130 | } catch (error) { 131 | log(`\n❌ Test failed: ${error}`, colors.red); 132 | process.exit(1); 133 | } 134 | } 135 | 136 | // Run if called directly 137 | if (require.main === module) { 138 | quickTest().catch(console.error); 139 | } ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/workflow_management/n8n-list-executions.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolDocumentation } from '../types'; 2 | 3 | export const n8nListExecutionsDoc: ToolDocumentation = { 4 | name: 'n8n_list_executions', 5 | category: 'workflow_management', 6 | essentials: { 7 | description: 'List workflow executions with optional filters. Supports pagination for large result sets.', 8 | keyParameters: ['workflowId', 'status', 'limit'], 9 | example: 'n8n_list_executions({workflowId: "abc123", status: "error"})', 10 | performance: 'Fast metadata retrieval, use pagination for large datasets', 11 | tips: [ 12 | 'Filter by status (success/error/waiting) to find specific execution types', 13 | 'Use workflowId to see all executions for a specific workflow', 14 | 'Pagination via cursor allows retrieving large execution histories' 15 | ] 16 | }, 17 | full: { 18 | description: `Lists workflow executions with powerful filtering options. This tool is essential for monitoring workflow performance, finding failed executions, and tracking workflow activity. Supports pagination for retrieving large execution histories and filtering by workflow, status, and project.`, 19 | parameters: { 20 | limit: { 21 | type: 'number', 22 | required: false, 23 | description: 'Number of executions to return (1-100, default: 100). Use with cursor for pagination' 24 | }, 25 | cursor: { 26 | type: 'string', 27 | required: false, 28 | description: 'Pagination cursor from previous response. Used to retrieve next page of results' 29 | }, 30 | workflowId: { 31 | type: 'string', 32 | required: false, 33 | description: 'Filter executions by specific workflow ID. Shows all executions for that workflow' 34 | }, 35 | projectId: { 36 | type: 'string', 37 | required: false, 38 | description: 'Filter by project ID (enterprise feature). Groups executions by project' 39 | }, 40 | status: { 41 | type: 'string', 42 | required: false, 43 | enum: ['success', 'error', 'waiting'], 44 | description: 'Filter by execution status. Success = completed, Error = failed, Waiting = running' 45 | }, 46 | includeData: { 47 | type: 'boolean', 48 | required: false, 49 | description: 'Include execution data in results (default: false). Significantly increases response size' 50 | } 51 | }, 52 | returns: `Array of execution objects with metadata, pagination cursor for next page, and optionally execution data. Each execution includes ID, status, start/end times, and workflow reference.`, 53 | examples: [ 54 | 'n8n_list_executions({limit: 10}) - Get 10 most recent executions', 55 | 'n8n_list_executions({workflowId: "abc123"}) - All executions for specific workflow', 56 | 'n8n_list_executions({status: "error", limit: 50}) - Find failed executions', 57 | 'n8n_list_executions({status: "waiting"}) - Monitor currently running workflows', 58 | 'n8n_list_executions({cursor: "next-page-token"}) - Get next page of results' 59 | ], 60 | useCases: [ 61 | 'Monitor workflow execution history and patterns', 62 | 'Find and debug failed workflow executions', 63 | 'Track currently running workflows (waiting status)', 64 | 'Analyze workflow performance and execution frequency', 65 | 'Generate execution reports for specific workflows' 66 | ], 67 | performance: `Listing executions is fast for metadata only. Including data (includeData: true) significantly impacts performance. Use pagination (limit + cursor) for large result sets. Default limit of 100 balances performance with usability.`, 68 | bestPractices: [ 69 | 'Use status filters to focus on specific execution types', 70 | 'Implement pagination for large execution histories', 71 | 'Avoid includeData unless you need execution details', 72 | 'Filter by workflowId when monitoring specific workflows', 73 | 'Check for cursor in response to detect more pages' 74 | ], 75 | pitfalls: [ 76 | 'Large limits with includeData can cause timeouts', 77 | 'Execution retention depends on n8n configuration', 78 | 'Cursor tokens expire - use them promptly', 79 | 'Status "waiting" includes both running and queued executions', 80 | 'Deleted workflows still show in execution history' 81 | ], 82 | relatedTools: ['n8n_get_execution', 'n8n_trigger_webhook_workflow', 'n8n_delete_execution', 'n8n_list_workflows'] 83 | } 84 | }; ``` -------------------------------------------------------------------------------- /tests/benchmarks/database-queries.bench.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { bench, describe } from 'vitest'; 2 | import { NodeRepository } from '../../src/database/node-repository'; 3 | import { SQLiteStorageService } from '../../src/services/sqlite-storage-service'; 4 | import { NodeFactory } from '../factories/node-factory'; 5 | import { PropertyDefinitionFactory } from '../factories/property-definition-factory'; 6 | 7 | /** 8 | * Database Query Performance Benchmarks 9 | * 10 | * NOTE: These benchmarks use MOCK DATA (500 artificial test nodes) 11 | * created with factories, not the real production database. 12 | * 13 | * This is useful for tracking database layer performance in isolation, 14 | * but may not reflect real-world performance characteristics. 15 | * 16 | * For end-to-end MCP tool performance with real data, see mcp-tools.bench.ts 17 | */ 18 | describe('Database Query Performance', () => { 19 | let repository: NodeRepository; 20 | let storage: SQLiteStorageService; 21 | const testNodeCount = 500; 22 | 23 | beforeAll(async () => { 24 | storage = new SQLiteStorageService(':memory:'); 25 | repository = new NodeRepository(storage); 26 | 27 | // Seed database with test data 28 | for (let i = 0; i < testNodeCount; i++) { 29 | const node = NodeFactory.build({ 30 | displayName: `TestNode${i}`, 31 | nodeType: `nodes-base.testNode${i}`, 32 | category: i % 2 === 0 ? 'transform' : 'trigger', 33 | packageName: 'n8n-nodes-base', 34 | documentation: `Test documentation for node ${i}`, 35 | properties: PropertyDefinitionFactory.buildList(5) 36 | }); 37 | await repository.upsertNode(node); 38 | } 39 | }); 40 | 41 | afterAll(() => { 42 | storage.close(); 43 | }); 44 | 45 | bench('getNodeByType - existing node', async () => { 46 | await repository.getNodeByType('nodes-base.testNode100'); 47 | }, { 48 | iterations: 1000, 49 | warmupIterations: 100, 50 | warmupTime: 500, 51 | time: 3000 52 | }); 53 | 54 | bench('getNodeByType - non-existing node', async () => { 55 | await repository.getNodeByType('nodes-base.nonExistentNode'); 56 | }, { 57 | iterations: 1000, 58 | warmupIterations: 100, 59 | warmupTime: 500, 60 | time: 3000 61 | }); 62 | 63 | bench('getNodesByCategory - transform', async () => { 64 | await repository.getNodesByCategory('transform'); 65 | }, { 66 | iterations: 100, 67 | warmupIterations: 10, 68 | warmupTime: 500, 69 | time: 3000 70 | }); 71 | 72 | bench('searchNodes - OR mode', async () => { 73 | await repository.searchNodes('test node data', 'OR', 20); 74 | }, { 75 | iterations: 100, 76 | warmupIterations: 10, 77 | warmupTime: 500, 78 | time: 3000 79 | }); 80 | 81 | bench('searchNodes - AND mode', async () => { 82 | await repository.searchNodes('test node', 'AND', 20); 83 | }, { 84 | iterations: 100, 85 | warmupIterations: 10, 86 | warmupTime: 500, 87 | time: 3000 88 | }); 89 | 90 | bench('searchNodes - FUZZY mode', async () => { 91 | await repository.searchNodes('tst nde', 'FUZZY', 20); 92 | }, { 93 | iterations: 100, 94 | warmupIterations: 10, 95 | warmupTime: 500, 96 | time: 3000 97 | }); 98 | 99 | bench('getAllNodes - no limit', async () => { 100 | await repository.getAllNodes(); 101 | }, { 102 | iterations: 50, 103 | warmupIterations: 5, 104 | warmupTime: 500, 105 | time: 3000 106 | }); 107 | 108 | bench('getAllNodes - with limit', async () => { 109 | await repository.getAllNodes(50); 110 | }, { 111 | iterations: 100, 112 | warmupIterations: 10, 113 | warmupTime: 500, 114 | time: 3000 115 | }); 116 | 117 | bench('getNodeCount', async () => { 118 | await repository.getNodeCount(); 119 | }, { 120 | iterations: 1000, 121 | warmupIterations: 100, 122 | warmupTime: 100, 123 | time: 2000 124 | }); 125 | 126 | bench('getAIToolNodes', async () => { 127 | await repository.getAIToolNodes(); 128 | }, { 129 | iterations: 100, 130 | warmupIterations: 10, 131 | warmupTime: 500, 132 | time: 3000 133 | }); 134 | 135 | bench('upsertNode - new node', async () => { 136 | const node = NodeFactory.build({ 137 | displayName: `BenchNode${Date.now()}`, 138 | nodeType: `nodes-base.benchNode${Date.now()}` 139 | }); 140 | await repository.upsertNode(node); 141 | }, { 142 | iterations: 100, 143 | warmupIterations: 10, 144 | warmupTime: 500, 145 | time: 3000 146 | }); 147 | 148 | bench('upsertNode - existing node update', async () => { 149 | const existingNode = await repository.getNodeByType('nodes-base.testNode0'); 150 | if (existingNode) { 151 | existingNode.description = `Updated description ${Date.now()}`; 152 | await repository.upsertNode(existingNode); 153 | } 154 | }, { 155 | iterations: 100, 156 | warmupIterations: 10, 157 | warmupTime: 500, 158 | time: 3000 159 | }); 160 | }); ``` -------------------------------------------------------------------------------- /tests/unit/test-infrastructure.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, vi, beforeEach } from 'vitest'; 2 | import { nodeFactory, webhookNodeFactory, slackNodeFactory } from '@tests/fixtures/factories/node.factory'; 3 | 4 | // Mock better-sqlite3 5 | vi.mock('better-sqlite3'); 6 | 7 | describe('Test Infrastructure', () => { 8 | describe('Database Mock', () => { 9 | it('should create a mock database instance', async () => { 10 | const Database = (await import('better-sqlite3')).default; 11 | const db = new Database(':memory:'); 12 | 13 | expect(Database).toHaveBeenCalled(); 14 | expect(db).toBeDefined(); 15 | expect(db.prepare).toBeDefined(); 16 | expect(db.exec).toBeDefined(); 17 | expect(db.close).toBeDefined(); 18 | }); 19 | 20 | it('should handle basic CRUD operations', async () => { 21 | const { MockDatabase } = await import('@tests/unit/database/__mocks__/better-sqlite3'); 22 | const db = new MockDatabase(); 23 | 24 | // Test data seeding 25 | db._seedData('nodes', [ 26 | { id: '1', name: 'test-node', type: 'webhook' } 27 | ]); 28 | 29 | // Test SELECT 30 | const selectStmt = db.prepare('SELECT * FROM nodes'); 31 | const allNodes = selectStmt.all(); 32 | expect(allNodes).toHaveLength(1); 33 | expect(allNodes[0]).toEqual({ id: '1', name: 'test-node', type: 'webhook' }); 34 | 35 | // Test INSERT 36 | const insertStmt = db.prepare('INSERT INTO nodes (id, name, type) VALUES (?, ?, ?)'); 37 | const result = insertStmt.run({ id: '2', name: 'new-node', type: 'slack' }); 38 | expect(result.changes).toBe(1); 39 | 40 | // Verify insert worked 41 | const allNodesAfter = selectStmt.all(); 42 | expect(allNodesAfter).toHaveLength(2); 43 | }); 44 | }); 45 | 46 | describe('Node Factory', () => { 47 | it('should create a basic node definition', () => { 48 | const node = nodeFactory.build(); 49 | 50 | expect(node).toMatchObject({ 51 | name: expect.any(String), 52 | displayName: expect.any(String), 53 | description: expect.any(String), 54 | version: expect.any(Number), 55 | defaults: { 56 | name: expect.any(String) 57 | }, 58 | inputs: ['main'], 59 | outputs: ['main'], 60 | properties: expect.any(Array), 61 | credentials: [] 62 | }); 63 | }); 64 | 65 | it('should create a webhook node', () => { 66 | const webhook = webhookNodeFactory.build(); 67 | 68 | expect(webhook).toMatchObject({ 69 | name: 'webhook', 70 | displayName: 'Webhook', 71 | description: 'Starts the workflow when a webhook is called', 72 | group: ['trigger'], 73 | properties: expect.arrayContaining([ 74 | expect.objectContaining({ 75 | name: 'path', 76 | type: 'string', 77 | required: true 78 | }), 79 | expect.objectContaining({ 80 | name: 'method', 81 | type: 'options' 82 | }) 83 | ]) 84 | }); 85 | }); 86 | 87 | it('should create a slack node', () => { 88 | const slack = slackNodeFactory.build(); 89 | 90 | expect(slack).toMatchObject({ 91 | name: 'slack', 92 | displayName: 'Slack', 93 | description: 'Send messages to Slack', 94 | group: ['output'], 95 | credentials: [ 96 | { 97 | name: 'slackApi', 98 | required: true 99 | } 100 | ], 101 | properties: expect.arrayContaining([ 102 | expect.objectContaining({ 103 | name: 'resource', 104 | type: 'options' 105 | }), 106 | expect.objectContaining({ 107 | name: 'operation', 108 | type: 'options', 109 | displayOptions: { 110 | show: { 111 | resource: ['message'] 112 | } 113 | } 114 | }) 115 | ]) 116 | }); 117 | }); 118 | 119 | it('should allow overriding factory defaults', () => { 120 | const customNode = nodeFactory.build({ 121 | name: 'custom-node', 122 | displayName: 'Custom Node', 123 | version: 2 124 | }); 125 | 126 | expect(customNode.name).toBe('custom-node'); 127 | expect(customNode.displayName).toBe('Custom Node'); 128 | expect(customNode.version).toBe(2); 129 | }); 130 | 131 | it('should create multiple unique nodes', () => { 132 | const nodes = nodeFactory.buildList(5); 133 | 134 | expect(nodes).toHaveLength(5); 135 | const names = nodes.map(n => n.name); 136 | const uniqueNames = new Set(names); 137 | expect(uniqueNames.size).toBe(5); 138 | }); 139 | }); 140 | }); ``` -------------------------------------------------------------------------------- /scripts/test-user-id-persistence.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Test User ID Persistence 3 | * Verifies that user IDs are consistent across sessions and modes 4 | */ 5 | 6 | import { TelemetryConfigManager } from '../src/telemetry/config-manager'; 7 | import { hostname, platform, arch, homedir } from 'os'; 8 | import { createHash } from 'crypto'; 9 | 10 | console.log('=== User ID Persistence Test ===\n'); 11 | 12 | // Test 1: Verify deterministic ID generation 13 | console.log('Test 1: Deterministic ID Generation'); 14 | console.log('-----------------------------------'); 15 | 16 | const machineId = `${hostname()}-${platform()}-${arch()}-${homedir()}`; 17 | const expectedUserId = createHash('sha256') 18 | .update(machineId) 19 | .digest('hex') 20 | .substring(0, 16); 21 | 22 | console.log('Machine characteristics:'); 23 | console.log(' hostname:', hostname()); 24 | console.log(' platform:', platform()); 25 | console.log(' arch:', arch()); 26 | console.log(' homedir:', homedir()); 27 | console.log('\nGenerated machine ID:', machineId); 28 | console.log('Expected user ID:', expectedUserId); 29 | 30 | // Test 2: Load actual config 31 | console.log('\n\nTest 2: Actual Config Manager'); 32 | console.log('-----------------------------------'); 33 | 34 | const configManager = TelemetryConfigManager.getInstance(); 35 | const actualUserId = configManager.getUserId(); 36 | const config = configManager.loadConfig(); 37 | 38 | console.log('Actual user ID:', actualUserId); 39 | console.log('Config first run:', config.firstRun || 'Unknown'); 40 | console.log('Config version:', config.version || 'Unknown'); 41 | console.log('Telemetry enabled:', config.enabled); 42 | 43 | // Test 3: Verify consistency 44 | console.log('\n\nTest 3: Consistency Check'); 45 | console.log('-----------------------------------'); 46 | 47 | const match = actualUserId === expectedUserId; 48 | console.log('User IDs match:', match ? '✓ YES' : '✗ NO'); 49 | 50 | if (!match) { 51 | console.log('WARNING: User ID mismatch detected!'); 52 | console.log('This could indicate an implementation issue.'); 53 | } 54 | 55 | // Test 4: Multiple loads (simulate multiple sessions) 56 | console.log('\n\nTest 4: Multiple Session Simulation'); 57 | console.log('-----------------------------------'); 58 | 59 | const userId1 = configManager.getUserId(); 60 | const userId2 = TelemetryConfigManager.getInstance().getUserId(); 61 | const userId3 = configManager.getUserId(); 62 | 63 | console.log('Session 1 user ID:', userId1); 64 | console.log('Session 2 user ID:', userId2); 65 | console.log('Session 3 user ID:', userId3); 66 | 67 | const consistent = userId1 === userId2 && userId2 === userId3; 68 | console.log('All sessions consistent:', consistent ? '✓ YES' : '✗ NO'); 69 | 70 | // Test 5: Docker environment simulation 71 | console.log('\n\nTest 5: Docker Environment Check'); 72 | console.log('-----------------------------------'); 73 | 74 | const isDocker = process.env.IS_DOCKER === 'true'; 75 | console.log('Running in Docker:', isDocker); 76 | 77 | if (isDocker) { 78 | console.log('\n⚠️ DOCKER MODE DETECTED'); 79 | console.log('In Docker, user IDs may change across container recreations because:'); 80 | console.log(' 1. Container hostname changes each time'); 81 | console.log(' 2. Config file is not persisted (no volume mount)'); 82 | console.log(' 3. Each container gets a new ephemeral filesystem'); 83 | console.log('\nRecommendation: Mount ~/.n8n-mcp as a volume for persistent user IDs'); 84 | } 85 | 86 | // Test 6: Environment variable override check 87 | console.log('\n\nTest 6: Environment Variable Override'); 88 | console.log('-----------------------------------'); 89 | 90 | const telemetryDisabledVars = [ 91 | 'N8N_MCP_TELEMETRY_DISABLED', 92 | 'TELEMETRY_DISABLED', 93 | 'DISABLE_TELEMETRY' 94 | ]; 95 | 96 | telemetryDisabledVars.forEach(varName => { 97 | const value = process.env[varName]; 98 | if (value !== undefined) { 99 | console.log(`${varName}:`, value); 100 | } 101 | }); 102 | 103 | console.log('\nTelemetry status:', configManager.isEnabled() ? 'ENABLED' : 'DISABLED'); 104 | 105 | // Summary 106 | console.log('\n\n=== SUMMARY ==='); 107 | console.log('User ID:', actualUserId); 108 | console.log('Deterministic:', match ? 'YES ✓' : 'NO ✗'); 109 | console.log('Persistent across sessions:', consistent ? 'YES ✓' : 'NO ✗'); 110 | console.log('Telemetry enabled:', config.enabled ? 'YES' : 'NO'); 111 | console.log('Docker mode:', isDocker ? 'YES' : 'NO'); 112 | 113 | if (isDocker && !process.env.N8N_MCP_CONFIG_VOLUME) { 114 | console.log('\n⚠️ WARNING: Running in Docker without persistent volume!'); 115 | console.log('User IDs will change on container recreation.'); 116 | console.log('Mount /home/nodejs/.n8n-mcp to persist telemetry config.'); 117 | } 118 | 119 | console.log('\n'); 120 | ``` -------------------------------------------------------------------------------- /src/telemetry/startup-checkpoints.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Startup Checkpoint System 3 | * Defines checkpoints throughout the server initialization process 4 | * to identify where failures occur 5 | */ 6 | 7 | /** 8 | * Startup checkpoint constants 9 | * These checkpoints mark key stages in the server initialization process 10 | */ 11 | export const STARTUP_CHECKPOINTS = { 12 | /** Process has started, very first checkpoint */ 13 | PROCESS_STARTED: 'process_started', 14 | 15 | /** About to connect to database */ 16 | DATABASE_CONNECTING: 'database_connecting', 17 | 18 | /** Database connection successful */ 19 | DATABASE_CONNECTED: 'database_connected', 20 | 21 | /** About to check n8n API configuration (if applicable) */ 22 | N8N_API_CHECKING: 'n8n_api_checking', 23 | 24 | /** n8n API is configured and ready (if applicable) */ 25 | N8N_API_READY: 'n8n_api_ready', 26 | 27 | /** About to initialize telemetry system */ 28 | TELEMETRY_INITIALIZING: 'telemetry_initializing', 29 | 30 | /** Telemetry system is ready */ 31 | TELEMETRY_READY: 'telemetry_ready', 32 | 33 | /** About to start MCP handshake */ 34 | MCP_HANDSHAKE_STARTING: 'mcp_handshake_starting', 35 | 36 | /** MCP handshake completed successfully */ 37 | MCP_HANDSHAKE_COMPLETE: 'mcp_handshake_complete', 38 | 39 | /** Server is fully ready to handle requests */ 40 | SERVER_READY: 'server_ready', 41 | } as const; 42 | 43 | /** 44 | * Type for checkpoint names 45 | */ 46 | export type StartupCheckpoint = typeof STARTUP_CHECKPOINTS[keyof typeof STARTUP_CHECKPOINTS]; 47 | 48 | /** 49 | * Checkpoint data structure 50 | */ 51 | export interface CheckpointData { 52 | name: StartupCheckpoint; 53 | timestamp: number; 54 | success: boolean; 55 | error?: string; 56 | } 57 | 58 | /** 59 | * Get all checkpoint names in order 60 | */ 61 | export function getAllCheckpoints(): StartupCheckpoint[] { 62 | return Object.values(STARTUP_CHECKPOINTS); 63 | } 64 | 65 | /** 66 | * Find which checkpoint failed based on the list of passed checkpoints 67 | * Returns the first checkpoint that was not passed 68 | */ 69 | export function findFailedCheckpoint(passedCheckpoints: string[]): StartupCheckpoint { 70 | const allCheckpoints = getAllCheckpoints(); 71 | 72 | for (const checkpoint of allCheckpoints) { 73 | if (!passedCheckpoints.includes(checkpoint)) { 74 | return checkpoint; 75 | } 76 | } 77 | 78 | // If all checkpoints were passed, the failure must have occurred after SERVER_READY 79 | // This would be an unexpected post-initialization failure 80 | return STARTUP_CHECKPOINTS.SERVER_READY; 81 | } 82 | 83 | /** 84 | * Validate if a string is a valid checkpoint 85 | */ 86 | export function isValidCheckpoint(checkpoint: string): checkpoint is StartupCheckpoint { 87 | return getAllCheckpoints().includes(checkpoint as StartupCheckpoint); 88 | } 89 | 90 | /** 91 | * Get human-readable description for a checkpoint 92 | */ 93 | export function getCheckpointDescription(checkpoint: StartupCheckpoint): string { 94 | const descriptions: Record<StartupCheckpoint, string> = { 95 | [STARTUP_CHECKPOINTS.PROCESS_STARTED]: 'Process initialization started', 96 | [STARTUP_CHECKPOINTS.DATABASE_CONNECTING]: 'Connecting to database', 97 | [STARTUP_CHECKPOINTS.DATABASE_CONNECTED]: 'Database connection established', 98 | [STARTUP_CHECKPOINTS.N8N_API_CHECKING]: 'Checking n8n API configuration', 99 | [STARTUP_CHECKPOINTS.N8N_API_READY]: 'n8n API ready', 100 | [STARTUP_CHECKPOINTS.TELEMETRY_INITIALIZING]: 'Initializing telemetry system', 101 | [STARTUP_CHECKPOINTS.TELEMETRY_READY]: 'Telemetry system ready', 102 | [STARTUP_CHECKPOINTS.MCP_HANDSHAKE_STARTING]: 'Starting MCP protocol handshake', 103 | [STARTUP_CHECKPOINTS.MCP_HANDSHAKE_COMPLETE]: 'MCP handshake completed', 104 | [STARTUP_CHECKPOINTS.SERVER_READY]: 'Server fully initialized and ready', 105 | }; 106 | 107 | return descriptions[checkpoint] || 'Unknown checkpoint'; 108 | } 109 | 110 | /** 111 | * Get the next expected checkpoint after the given one 112 | * Returns null if this is the last checkpoint 113 | */ 114 | export function getNextCheckpoint(current: StartupCheckpoint): StartupCheckpoint | null { 115 | const allCheckpoints = getAllCheckpoints(); 116 | const currentIndex = allCheckpoints.indexOf(current); 117 | 118 | if (currentIndex === -1 || currentIndex === allCheckpoints.length - 1) { 119 | return null; 120 | } 121 | 122 | return allCheckpoints[currentIndex + 1]; 123 | } 124 | 125 | /** 126 | * Calculate completion percentage based on checkpoints passed 127 | */ 128 | export function getCompletionPercentage(passedCheckpoints: string[]): number { 129 | const totalCheckpoints = getAllCheckpoints().length; 130 | const passedCount = passedCheckpoints.length; 131 | 132 | return Math.round((passedCount / totalCheckpoints) * 100); 133 | } 134 | ``` -------------------------------------------------------------------------------- /tests/unit/services/expression-validator.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, vi, beforeEach } from 'vitest'; 2 | import { ExpressionValidator } from '@/services/expression-validator'; 3 | 4 | describe('ExpressionValidator', () => { 5 | const defaultContext = { 6 | availableNodes: [], 7 | currentNodeName: 'TestNode', 8 | isInLoop: false, 9 | hasInputData: true 10 | }; 11 | 12 | beforeEach(() => { 13 | vi.clearAllMocks(); 14 | }); 15 | 16 | describe('validateExpression', () => { 17 | it('should be a static method that validates expressions', () => { 18 | expect(typeof ExpressionValidator.validateExpression).toBe('function'); 19 | }); 20 | 21 | it('should return a validation result', () => { 22 | const result = ExpressionValidator.validateExpression('{{ $json.field }}', defaultContext); 23 | 24 | expect(result).toHaveProperty('valid'); 25 | expect(result).toHaveProperty('errors'); 26 | expect(result).toHaveProperty('warnings'); 27 | expect(result).toHaveProperty('usedVariables'); 28 | expect(result).toHaveProperty('usedNodes'); 29 | }); 30 | 31 | it('should validate expressions with proper syntax', () => { 32 | const validExpr = '{{ $json.field }}'; 33 | const result = ExpressionValidator.validateExpression(validExpr, defaultContext); 34 | 35 | expect(result).toBeDefined(); 36 | expect(Array.isArray(result.errors)).toBe(true); 37 | }); 38 | 39 | it('should detect malformed expressions', () => { 40 | const invalidExpr = '{{ $json.field'; // Missing closing braces 41 | const result = ExpressionValidator.validateExpression(invalidExpr, defaultContext); 42 | 43 | expect(result.errors.length).toBeGreaterThan(0); 44 | }); 45 | }); 46 | 47 | describe('validateNodeExpressions', () => { 48 | it('should validate all expressions in node parameters', () => { 49 | const parameters = { 50 | field1: '{{ $json.data }}', 51 | nested: { 52 | field2: 'regular text', 53 | field3: '{{ $node["Webhook"].json }}' 54 | } 55 | }; 56 | 57 | const result = ExpressionValidator.validateNodeExpressions(parameters, defaultContext); 58 | 59 | expect(result).toHaveProperty('valid'); 60 | expect(result).toHaveProperty('errors'); 61 | expect(result).toHaveProperty('warnings'); 62 | }); 63 | 64 | it('should collect errors from invalid expressions', () => { 65 | const parameters = { 66 | badExpr: '{{ $json.field', // Missing closing 67 | goodExpr: '{{ $json.field }}' 68 | }; 69 | 70 | const result = ExpressionValidator.validateNodeExpressions(parameters, defaultContext); 71 | 72 | expect(result.errors.length).toBeGreaterThan(0); 73 | }); 74 | }); 75 | 76 | describe('expression patterns', () => { 77 | it('should recognize n8n variable patterns', () => { 78 | const expressions = [ 79 | '{{ $json }}', 80 | '{{ $json.field }}', 81 | '{{ $node["NodeName"].json }}', 82 | '{{ $workflow.id }}', 83 | '{{ $now }}', 84 | '{{ $itemIndex }}' 85 | ]; 86 | 87 | expressions.forEach(expr => { 88 | const result = ExpressionValidator.validateExpression(expr, defaultContext); 89 | expect(result).toBeDefined(); 90 | }); 91 | }); 92 | }); 93 | 94 | describe('context validation', () => { 95 | it('should use available nodes from context', () => { 96 | const contextWithNodes = { 97 | ...defaultContext, 98 | availableNodes: ['Webhook', 'Function', 'Slack'] 99 | }; 100 | 101 | const expr = '{{ $node["Webhook"].json }}'; 102 | const result = ExpressionValidator.validateExpression(expr, contextWithNodes); 103 | 104 | expect(result.usedNodes.has('Webhook')).toBe(true); 105 | }); 106 | }); 107 | 108 | describe('edge cases', () => { 109 | it('should handle empty expressions', () => { 110 | const result = ExpressionValidator.validateExpression('{{ }}', defaultContext); 111 | // The implementation might consider empty expressions as valid 112 | expect(result).toBeDefined(); 113 | expect(Array.isArray(result.errors)).toBe(true); 114 | }); 115 | 116 | it('should handle non-expression text', () => { 117 | const result = ExpressionValidator.validateExpression('regular text without expressions', defaultContext); 118 | expect(result.valid).toBe(true); 119 | expect(result.errors).toHaveLength(0); 120 | }); 121 | 122 | it('should handle nested expressions', () => { 123 | const expr = '{{ $json[{{ $json.index }}] }}'; // Nested expressions not allowed 124 | const result = ExpressionValidator.validateExpression(expr, defaultContext); 125 | expect(result).toBeDefined(); 126 | }); 127 | }); 128 | }); ``` -------------------------------------------------------------------------------- /src/utils/bridge.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { INodeExecutionData, IDataObject } from 'n8n-workflow'; 2 | 3 | export class N8NMCPBridge { 4 | /** 5 | * Convert n8n workflow data to MCP tool arguments 6 | */ 7 | static n8nToMCPToolArgs(data: IDataObject): any { 8 | // Handle different data formats from n8n 9 | if (data.json) { 10 | return data.json; 11 | } 12 | 13 | // Remove n8n-specific metadata 14 | const { pairedItem, ...cleanData } = data; 15 | return cleanData; 16 | } 17 | 18 | /** 19 | * Convert MCP tool response to n8n execution data 20 | */ 21 | static mcpToN8NExecutionData(mcpResponse: any, itemIndex: number = 0): INodeExecutionData { 22 | // Handle MCP content array format 23 | if (mcpResponse.content && Array.isArray(mcpResponse.content)) { 24 | const textContent = mcpResponse.content 25 | .filter((c: any) => c.type === 'text') 26 | .map((c: any) => c.text) 27 | .join('\n'); 28 | 29 | try { 30 | // Try to parse as JSON if possible 31 | const parsed = JSON.parse(textContent); 32 | return { 33 | json: parsed, 34 | pairedItem: itemIndex, 35 | }; 36 | } catch { 37 | // Return as text if not JSON 38 | return { 39 | json: { result: textContent }, 40 | pairedItem: itemIndex, 41 | }; 42 | } 43 | } 44 | 45 | // Handle direct object response 46 | return { 47 | json: mcpResponse, 48 | pairedItem: itemIndex, 49 | }; 50 | } 51 | 52 | /** 53 | * Convert n8n workflow definition to MCP-compatible format 54 | */ 55 | static n8nWorkflowToMCP(workflow: any): any { 56 | return { 57 | id: workflow.id, 58 | name: workflow.name, 59 | description: workflow.description || '', 60 | nodes: workflow.nodes?.map((node: any) => ({ 61 | id: node.id, 62 | type: node.type, 63 | name: node.name, 64 | parameters: node.parameters, 65 | position: node.position, 66 | })), 67 | connections: workflow.connections, 68 | settings: workflow.settings, 69 | metadata: { 70 | createdAt: workflow.createdAt, 71 | updatedAt: workflow.updatedAt, 72 | active: workflow.active, 73 | }, 74 | }; 75 | } 76 | 77 | /** 78 | * Convert MCP workflow format to n8n-compatible format 79 | */ 80 | static mcpToN8NWorkflow(mcpWorkflow: any): any { 81 | return { 82 | name: mcpWorkflow.name, 83 | nodes: mcpWorkflow.nodes || [], 84 | connections: mcpWorkflow.connections || {}, 85 | settings: mcpWorkflow.settings || { 86 | executionOrder: 'v1', 87 | }, 88 | staticData: null, 89 | pinData: {}, 90 | }; 91 | } 92 | 93 | /** 94 | * Convert n8n execution data to MCP resource format 95 | */ 96 | static n8nExecutionToMCPResource(execution: any): any { 97 | return { 98 | uri: `execution://${execution.id}`, 99 | name: `Execution ${execution.id}`, 100 | description: `Workflow: ${execution.workflowData?.name || 'Unknown'}`, 101 | mimeType: 'application/json', 102 | data: { 103 | id: execution.id, 104 | workflowId: execution.workflowId, 105 | status: execution.finished ? 'completed' : execution.stoppedAt ? 'stopped' : 'running', 106 | mode: execution.mode, 107 | startedAt: execution.startedAt, 108 | stoppedAt: execution.stoppedAt, 109 | error: execution.data?.resultData?.error, 110 | executionData: execution.data, 111 | }, 112 | }; 113 | } 114 | 115 | /** 116 | * Convert MCP prompt arguments to n8n-compatible format 117 | */ 118 | static mcpPromptArgsToN8N(promptArgs: any): IDataObject { 119 | return { 120 | prompt: promptArgs.name || '', 121 | arguments: promptArgs.arguments || {}, 122 | messages: promptArgs.messages || [], 123 | }; 124 | } 125 | 126 | /** 127 | * Validate and sanitize data before conversion 128 | */ 129 | static sanitizeData(data: any): any { 130 | if (data === null || data === undefined) { 131 | return {}; 132 | } 133 | 134 | if (typeof data !== 'object') { 135 | return { value: data }; 136 | } 137 | 138 | // Remove circular references 139 | const seen = new WeakSet(); 140 | return JSON.parse(JSON.stringify(data, (_key, value) => { 141 | if (typeof value === 'object' && value !== null) { 142 | if (seen.has(value)) { 143 | return '[Circular]'; 144 | } 145 | seen.add(value); 146 | } 147 | return value; 148 | })); 149 | } 150 | 151 | /** 152 | * Extract error information for both n8n and MCP formats 153 | */ 154 | static formatError(error: any): any { 155 | return { 156 | message: error.message || 'Unknown error', 157 | type: error.name || 'Error', 158 | stack: error.stack, 159 | details: { 160 | code: error.code, 161 | statusCode: error.statusCode, 162 | data: error.data, 163 | }, 164 | }; 165 | } 166 | } ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/templates/search-templates.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolDocumentation } from '../types'; 2 | 3 | export const searchTemplatesDoc: ToolDocumentation = { 4 | name: 'search_templates', 5 | category: 'templates', 6 | essentials: { 7 | description: 'Search templates by name/description keywords. NOT for node types! For nodes use list_node_templates. Example: "chatbot".', 8 | keyParameters: ['query', 'limit', 'fields'], 9 | example: 'search_templates({query: "chatbot", fields: ["id", "name"]})', 10 | performance: 'Fast (<100ms) - FTS5 full-text search', 11 | tips: [ 12 | 'Searches template names and descriptions, NOT node types', 13 | 'Use keywords like "automation", "sync", "notification"', 14 | 'For node-specific search, use list_node_templates instead', 15 | 'Use fields parameter to get only specific data (reduces response by 70-90%)' 16 | ] 17 | }, 18 | full: { 19 | description: `Performs full-text search across workflow template names and descriptions. This tool is ideal for finding workflows based on their purpose or functionality rather than specific nodes used. It searches through the community library of 399+ templates using SQLite FTS5 for fast, fuzzy matching.`, 20 | parameters: { 21 | query: { 22 | type: 'string', 23 | required: true, 24 | description: 'Search query for template names/descriptions. NOT for node types! Examples: "chatbot", "automation", "social media", "webhook". For node-based search use list_node_templates instead.' 25 | }, 26 | fields: { 27 | type: 'array', 28 | required: false, 29 | description: 'Fields to include in response. Options: "id", "name", "description", "author", "nodes", "views", "created", "url", "metadata". Default: all fields. Example: ["id", "name"] for minimal response.' 30 | }, 31 | limit: { 32 | type: 'number', 33 | required: false, 34 | description: 'Maximum number of results. Default 20, max 100' 35 | } 36 | }, 37 | returns: `Returns an object containing: 38 | - templates: Array of matching templates sorted by relevance 39 | - id: Template ID for retrieval 40 | - name: Template name (with match highlights) 41 | - description: What the workflow does 42 | - author: Creator information 43 | - nodes: Array of all nodes used 44 | - views: Popularity metric 45 | - created: Creation date 46 | - url: Link to template 47 | - relevanceScore: Search match score 48 | - totalFound: Total matching templates 49 | - searchQuery: The processed search query 50 | - tip: Helpful hints if no results`, 51 | examples: [ 52 | 'search_templates({query: "chatbot"}) - Find chatbot and conversational AI workflows', 53 | 'search_templates({query: "email notification"}) - Find email alert workflows', 54 | 'search_templates({query: "data sync"}) - Find data synchronization workflows', 55 | 'search_templates({query: "webhook automation", limit: 30}) - Find webhook-based automations', 56 | 'search_templates({query: "social media scheduler"}) - Find social posting workflows', 57 | 'search_templates({query: "slack", fields: ["id", "name"]}) - Get only IDs and names of Slack templates', 58 | 'search_templates({query: "automation", fields: ["id", "name", "description"]}) - Get minimal info for automation templates' 59 | ], 60 | useCases: [ 61 | 'Find workflows by business purpose', 62 | 'Discover automations for specific use cases', 63 | 'Search by workflow functionality', 64 | 'Find templates by problem they solve', 65 | 'Explore workflows by industry or domain' 66 | ], 67 | performance: `Excellent performance with FTS5 indexing: 68 | - Full-text search: <50ms for most queries 69 | - Fuzzy matching enabled for typos 70 | - Relevance-based sorting included 71 | - Searches both title and description 72 | - Returns highlighted matches`, 73 | bestPractices: [ 74 | 'Use descriptive keywords about the workflow purpose', 75 | 'Try multiple related terms if first search has few results', 76 | 'Combine terms for more specific results', 77 | 'Check both name and description in results', 78 | 'Use quotes for exact phrase matching' 79 | ], 80 | pitfalls: [ 81 | 'Does NOT search by node types - use list_node_templates', 82 | 'Search is case-insensitive but not semantic', 83 | 'Very specific terms may return no results', 84 | 'Descriptions may be brief - check full template', 85 | 'Relevance scoring may not match your expectations' 86 | ], 87 | relatedTools: ['list_node_templates', 'get_templates_for_task', 'get_template', 'search_nodes'] 88 | } 89 | }; ``` -------------------------------------------------------------------------------- /tests/integration/n8n-api/workflows/delete-workflow.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Integration Tests: handleDeleteWorkflow 3 | * 4 | * Tests workflow deletion against a real n8n instance. 5 | * Covers successful deletion, error handling, and cleanup verification. 6 | */ 7 | 8 | import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest'; 9 | import { createTestContext, TestContext, createTestWorkflowName } from '../utils/test-context'; 10 | import { getTestN8nClient } from '../utils/n8n-client'; 11 | import { N8nApiClient } from '../../../../src/services/n8n-api-client'; 12 | import { SIMPLE_WEBHOOK_WORKFLOW } from '../utils/fixtures'; 13 | import { cleanupOrphanedWorkflows } from '../utils/cleanup-helpers'; 14 | import { createMcpContext } from '../utils/mcp-context'; 15 | import { InstanceContext } from '../../../../src/types/instance-context'; 16 | import { handleDeleteWorkflow } from '../../../../src/mcp/handlers-n8n-manager'; 17 | 18 | describe('Integration: handleDeleteWorkflow', () => { 19 | let context: TestContext; 20 | let client: N8nApiClient; 21 | let mcpContext: InstanceContext; 22 | 23 | beforeEach(() => { 24 | context = createTestContext(); 25 | client = getTestN8nClient(); 26 | mcpContext = createMcpContext(); 27 | }); 28 | 29 | afterEach(async () => { 30 | await context.cleanup(); 31 | }); 32 | 33 | afterAll(async () => { 34 | if (!process.env.CI) { 35 | await cleanupOrphanedWorkflows(); 36 | } 37 | }); 38 | 39 | // ====================================================================== 40 | // Successful Deletion 41 | // ====================================================================== 42 | 43 | describe('Successful Deletion', () => { 44 | it('should delete an existing workflow', async () => { 45 | // Create workflow 46 | const workflow = { 47 | ...SIMPLE_WEBHOOK_WORKFLOW, 48 | name: createTestWorkflowName('Delete - Success'), 49 | tags: ['mcp-integration-test'] 50 | }; 51 | 52 | const created = await client.createWorkflow(workflow); 53 | expect(created.id).toBeTruthy(); 54 | if (!created.id) throw new Error('Workflow ID is missing'); 55 | 56 | // Do NOT track workflow since we're testing deletion 57 | // context.trackWorkflow(created.id); 58 | 59 | // Delete using MCP handler 60 | const response = await handleDeleteWorkflow( 61 | { id: created.id }, 62 | mcpContext 63 | ); 64 | 65 | // Verify MCP response 66 | expect(response.success).toBe(true); 67 | expect(response.data).toBeDefined(); 68 | 69 | // Verify workflow is actually deleted 70 | await expect(async () => { 71 | await client.getWorkflow(created.id!); 72 | }).rejects.toThrow(); 73 | }); 74 | }); 75 | 76 | // ====================================================================== 77 | // Error Handling 78 | // ====================================================================== 79 | 80 | describe('Error Handling', () => { 81 | it('should return error for non-existent workflow ID', async () => { 82 | const response = await handleDeleteWorkflow( 83 | { id: '99999999' }, 84 | mcpContext 85 | ); 86 | 87 | expect(response.success).toBe(false); 88 | expect(response.error).toBeDefined(); 89 | }); 90 | }); 91 | 92 | // ====================================================================== 93 | // Cleanup Verification 94 | // ====================================================================== 95 | 96 | describe('Cleanup Verification', () => { 97 | it('should verify workflow is actually deleted from n8n', async () => { 98 | // Create workflow 99 | const workflow = { 100 | ...SIMPLE_WEBHOOK_WORKFLOW, 101 | name: createTestWorkflowName('Delete - Cleanup Check'), 102 | tags: ['mcp-integration-test'] 103 | }; 104 | 105 | const created = await client.createWorkflow(workflow); 106 | expect(created.id).toBeTruthy(); 107 | if (!created.id) throw new Error('Workflow ID is missing'); 108 | 109 | // Verify workflow exists 110 | const beforeDelete = await client.getWorkflow(created.id); 111 | expect(beforeDelete.id).toBe(created.id); 112 | 113 | // Delete workflow 114 | const deleteResponse = await handleDeleteWorkflow( 115 | { id: created.id }, 116 | mcpContext 117 | ); 118 | 119 | expect(deleteResponse.success).toBe(true); 120 | 121 | // Verify workflow no longer exists 122 | try { 123 | await client.getWorkflow(created.id); 124 | // If we reach here, workflow wasn't deleted 125 | throw new Error('Workflow should have been deleted but still exists'); 126 | } catch (error: any) { 127 | // Expected: workflow should not be found 128 | expect(error.message).toMatch(/not found|404/i); 129 | } 130 | }); 131 | }); 132 | }); 133 | ``` -------------------------------------------------------------------------------- /src/scripts/fetch-templates-robust.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | import { createDatabaseAdapter } from '../database/database-adapter'; 3 | import { TemplateRepository } from '../templates/template-repository'; 4 | import { TemplateFetcher } from '../templates/template-fetcher'; 5 | import * as fs from 'fs'; 6 | import * as path from 'path'; 7 | 8 | async function fetchTemplatesRobust() { 9 | console.log('🌐 Fetching n8n workflow templates (last year)...\n'); 10 | 11 | // Ensure data directory exists 12 | const dataDir = './data'; 13 | if (!fs.existsSync(dataDir)) { 14 | fs.mkdirSync(dataDir, { recursive: true }); 15 | } 16 | 17 | // Initialize database 18 | const db = await createDatabaseAdapter('./data/nodes.db'); 19 | 20 | // Drop existing templates table to ensure clean schema 21 | try { 22 | db.exec('DROP TABLE IF EXISTS templates'); 23 | db.exec('DROP TABLE IF EXISTS templates_fts'); 24 | console.log('🗑️ Dropped existing templates tables\n'); 25 | } catch (error) { 26 | // Ignore errors if tables don't exist 27 | } 28 | 29 | // Apply schema with updated constraint 30 | const schema = fs.readFileSync(path.join(__dirname, '../../src/database/schema.sql'), 'utf8'); 31 | db.exec(schema); 32 | 33 | // Create repository and fetcher 34 | const repository = new TemplateRepository(db); 35 | const fetcher = new TemplateFetcher(); 36 | 37 | // Progress tracking 38 | let lastMessage = ''; 39 | const startTime = Date.now(); 40 | 41 | try { 42 | // Fetch template list 43 | console.log('📋 Phase 1: Fetching template list from n8n.io API\n'); 44 | const templates = await fetcher.fetchTemplates((current, total) => { 45 | // Clear previous line 46 | if (lastMessage) { 47 | process.stdout.write('\r' + ' '.repeat(lastMessage.length) + '\r'); 48 | } 49 | 50 | const progress = Math.round((current / total) * 100); 51 | lastMessage = `📊 Fetching template list: ${current}/${total} (${progress}%)`; 52 | process.stdout.write(lastMessage); 53 | }); 54 | 55 | console.log('\n'); 56 | console.log(`✅ Found ${templates.length} templates from last year\n`); 57 | 58 | // Fetch details and save incrementally 59 | console.log('📥 Phase 2: Fetching details and saving to database\n'); 60 | let saved = 0; 61 | let errors = 0; 62 | 63 | for (let i = 0; i < templates.length; i++) { 64 | const template = templates[i]; 65 | 66 | try { 67 | // Clear previous line 68 | if (lastMessage) { 69 | process.stdout.write('\r' + ' '.repeat(lastMessage.length) + '\r'); 70 | } 71 | 72 | const progress = Math.round(((i + 1) / templates.length) * 100); 73 | lastMessage = `📊 Processing: ${i + 1}/${templates.length} (${progress}%) - Saved: ${saved}, Errors: ${errors}`; 74 | process.stdout.write(lastMessage); 75 | 76 | // Fetch detail 77 | const detail = await fetcher.fetchTemplateDetail(template.id); 78 | 79 | // Save immediately 80 | repository.saveTemplate(template, detail); 81 | saved++; 82 | 83 | // Rate limiting 84 | await new Promise(resolve => setTimeout(resolve, 200)); 85 | } catch (error: any) { 86 | errors++; 87 | console.error(`\n❌ Error processing template ${template.id} (${template.name}): ${error.message}`); 88 | // Continue with next template 89 | } 90 | } 91 | 92 | console.log('\n'); 93 | 94 | // Get stats 95 | const elapsed = Math.round((Date.now() - startTime) / 1000); 96 | const stats = await repository.getTemplateStats(); 97 | 98 | console.log('✅ Template fetch complete!\n'); 99 | console.log('📈 Statistics:'); 100 | console.log(` - Templates found: ${templates.length}`); 101 | console.log(` - Templates saved: ${saved}`); 102 | console.log(` - Errors: ${errors}`); 103 | console.log(` - Success rate: ${Math.round((saved / templates.length) * 100)}%`); 104 | console.log(` - Time elapsed: ${elapsed} seconds`); 105 | console.log(` - Average time per template: ${(elapsed / saved).toFixed(2)} seconds`); 106 | 107 | if (stats.topUsedNodes && stats.topUsedNodes.length > 0) { 108 | console.log('\n🔝 Top used nodes:'); 109 | stats.topUsedNodes.slice(0, 10).forEach((node: any, index: number) => { 110 | console.log(` ${index + 1}. ${node.node} (${node.count} templates)`); 111 | }); 112 | } 113 | 114 | } catch (error) { 115 | console.error('\n❌ Fatal error:', error); 116 | process.exit(1); 117 | } 118 | 119 | // Close database 120 | if ('close' in db && typeof db.close === 'function') { 121 | db.close(); 122 | } 123 | } 124 | 125 | // Run if called directly 126 | if (require.main === module) { 127 | fetchTemplatesRobust().catch(console.error); 128 | } 129 | 130 | export { fetchTemplatesRobust }; ``` -------------------------------------------------------------------------------- /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- ```yaml 1 | # .github/workflows/docker-build.yml 2 | name: Build and Push Docker Images 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | tags: 9 | - 'v*' 10 | paths-ignore: 11 | - '**.md' 12 | - '**.txt' 13 | - 'docs/**' 14 | - 'examples/**' 15 | - '.github/FUNDING.yml' 16 | - '.github/ISSUE_TEMPLATE/**' 17 | - '.github/pull_request_template.md' 18 | - '.gitignore' 19 | - 'LICENSE*' 20 | - 'ATTRIBUTION.md' 21 | - 'SECURITY.md' 22 | - 'CODE_OF_CONDUCT.md' 23 | pull_request: 24 | branches: 25 | - main 26 | paths-ignore: 27 | - '**.md' 28 | - '**.txt' 29 | - 'docs/**' 30 | - 'examples/**' 31 | - '.github/FUNDING.yml' 32 | - '.github/ISSUE_TEMPLATE/**' 33 | - '.github/pull_request_template.md' 34 | - '.gitignore' 35 | - 'LICENSE*' 36 | - 'ATTRIBUTION.md' 37 | - 'SECURITY.md' 38 | - 'CODE_OF_CONDUCT.md' 39 | workflow_dispatch: 40 | 41 | env: 42 | REGISTRY: ghcr.io 43 | IMAGE_NAME: ${{ github.repository }} 44 | 45 | jobs: 46 | build: 47 | name: Build Docker Image 48 | runs-on: ubuntu-latest 49 | permissions: 50 | contents: read 51 | packages: write 52 | 53 | steps: 54 | - name: Checkout repository 55 | uses: actions/checkout@v4 56 | with: 57 | lfs: true 58 | 59 | - name: Set up QEMU 60 | uses: docker/setup-qemu-action@v3 61 | 62 | - name: Set up Docker Buildx 63 | id: buildx 64 | uses: docker/setup-buildx-action@v3 65 | 66 | - name: Log in to GitHub Container Registry 67 | if: github.event_name != 'pull_request' 68 | uses: docker/login-action@v3 69 | with: 70 | registry: ${{ env.REGISTRY }} 71 | username: ${{ github.actor }} 72 | password: ${{ secrets.GITHUB_TOKEN }} 73 | 74 | - name: Extract metadata 75 | id: meta 76 | uses: docker/metadata-action@v5 77 | with: 78 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 79 | tags: | 80 | type=ref,event=branch 81 | type=ref,event=pr 82 | type=semver,pattern={{version}} 83 | type=semver,pattern={{major}}.{{minor}} 84 | type=semver,pattern={{major}} 85 | type=sha,format=short 86 | type=raw,value=latest,enable={{is_default_branch}} 87 | 88 | - name: Build and push Docker image 89 | uses: docker/build-push-action@v5 90 | with: 91 | context: . 92 | no-cache: true 93 | platforms: linux/amd64,linux/arm64 94 | push: ${{ github.event_name != 'pull_request' }} 95 | tags: ${{ steps.meta.outputs.tags }} 96 | labels: ${{ steps.meta.outputs.labels }} 97 | provenance: false 98 | 99 | build-railway: 100 | name: Build Railway Docker Image 101 | runs-on: ubuntu-latest 102 | permissions: 103 | contents: read 104 | packages: write 105 | 106 | steps: 107 | - name: Checkout repository 108 | uses: actions/checkout@v4 109 | with: 110 | lfs: true 111 | 112 | - name: Set up QEMU 113 | uses: docker/setup-qemu-action@v3 114 | 115 | - name: Set up Docker Buildx 116 | id: buildx 117 | uses: docker/setup-buildx-action@v3 118 | 119 | - name: Log in to GitHub Container Registry 120 | if: github.event_name != 'pull_request' 121 | uses: docker/login-action@v3 122 | with: 123 | registry: ${{ env.REGISTRY }} 124 | username: ${{ github.actor }} 125 | password: ${{ secrets.GITHUB_TOKEN }} 126 | 127 | - name: Extract metadata for Railway 128 | id: meta-railway 129 | uses: docker/metadata-action@v5 130 | with: 131 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-railway 132 | tags: | 133 | type=ref,event=branch 134 | type=ref,event=pr 135 | type=semver,pattern={{version}} 136 | type=semver,pattern={{major}}.{{minor}} 137 | type=semver,pattern={{major}} 138 | type=sha,format=short 139 | type=raw,value=latest,enable={{is_default_branch}} 140 | 141 | - name: Build and push Railway Docker image 142 | uses: docker/build-push-action@v5 143 | with: 144 | context: . 145 | file: ./Dockerfile.railway 146 | no-cache: true 147 | platforms: linux/amd64 148 | push: ${{ github.event_name != 'pull_request' }} 149 | tags: ${{ steps.meta-railway.outputs.tags }} 150 | labels: ${{ steps.meta-railway.outputs.labels }} 151 | provenance: false 152 | 153 | # Nginx build commented out until Phase 2 154 | # build-nginx: 155 | # name: Build nginx-enhanced Docker Image 156 | # runs-on: ubuntu-latest 157 | # permissions: 158 | # contents: read 159 | # packages: write ``` -------------------------------------------------------------------------------- /tests/fixtures/database/test-nodes.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "nodes": [ 3 | { 4 | "style": "programmatic", 5 | "nodeType": "nodes-base.httpRequest", 6 | "displayName": "HTTP Request", 7 | "description": "Makes HTTP requests and returns the response", 8 | "category": "Core Nodes", 9 | "properties": [ 10 | { 11 | "name": "url", 12 | "displayName": "URL", 13 | "type": "string", 14 | "required": true, 15 | "default": "" 16 | }, 17 | { 18 | "name": "method", 19 | "displayName": "Method", 20 | "type": "options", 21 | "options": [ 22 | { "name": "GET", "value": "GET" }, 23 | { "name": "POST", "value": "POST" }, 24 | { "name": "PUT", "value": "PUT" }, 25 | { "name": "DELETE", "value": "DELETE" } 26 | ], 27 | "default": "GET" 28 | } 29 | ], 30 | "credentials": [], 31 | "isAITool": true, 32 | "isTrigger": false, 33 | "isWebhook": false, 34 | "operations": [], 35 | "version": "1", 36 | "isVersioned": false, 37 | "packageName": "n8n-nodes-base", 38 | "documentation": "The HTTP Request node makes HTTP requests and returns the response data." 39 | }, 40 | { 41 | "style": "programmatic", 42 | "nodeType": "nodes-base.webhook", 43 | "displayName": "Webhook", 44 | "description": "Receives data from external services via webhooks", 45 | "category": "Core Nodes", 46 | "properties": [ 47 | { 48 | "name": "httpMethod", 49 | "displayName": "HTTP Method", 50 | "type": "options", 51 | "options": [ 52 | { "name": "GET", "value": "GET" }, 53 | { "name": "POST", "value": "POST" } 54 | ], 55 | "default": "POST" 56 | }, 57 | { 58 | "name": "path", 59 | "displayName": "Path", 60 | "type": "string", 61 | "default": "webhook" 62 | } 63 | ], 64 | "credentials": [], 65 | "isAITool": false, 66 | "isTrigger": true, 67 | "isWebhook": true, 68 | "operations": [], 69 | "version": "1", 70 | "isVersioned": false, 71 | "packageName": "n8n-nodes-base", 72 | "documentation": "The Webhook node creates an endpoint to receive data from external services." 73 | }, 74 | { 75 | "style": "declarative", 76 | "nodeType": "nodes-base.slack", 77 | "displayName": "Slack", 78 | "description": "Send messages and interact with Slack", 79 | "category": "Communication", 80 | "properties": [], 81 | "credentials": [ 82 | { 83 | "name": "slackApi", 84 | "required": true 85 | } 86 | ], 87 | "isAITool": true, 88 | "isTrigger": false, 89 | "isWebhook": false, 90 | "operations": [ 91 | { 92 | "name": "Message", 93 | "value": "message", 94 | "operations": [ 95 | { 96 | "name": "Send", 97 | "value": "send", 98 | "description": "Send a message to a channel or user" 99 | } 100 | ] 101 | } 102 | ], 103 | "version": "2.1", 104 | "isVersioned": true, 105 | "packageName": "n8n-nodes-base", 106 | "documentation": "The Slack node allows you to send messages and interact with Slack workspaces." 107 | } 108 | ], 109 | "templates": [ 110 | { 111 | "id": 1001, 112 | "name": "HTTP to Webhook", 113 | "description": "Fetch data from HTTP and send to webhook", 114 | "workflow": { 115 | "nodes": [ 116 | { 117 | "id": "1", 118 | "name": "HTTP Request", 119 | "type": "n8n-nodes-base.httpRequest", 120 | "position": [250, 300], 121 | "parameters": { 122 | "url": "https://api.example.com/data", 123 | "method": "GET" 124 | } 125 | }, 126 | { 127 | "id": "2", 128 | "name": "Webhook", 129 | "type": "n8n-nodes-base.webhook", 130 | "position": [450, 300], 131 | "parameters": { 132 | "path": "data-webhook", 133 | "httpMethod": "POST" 134 | } 135 | } 136 | ], 137 | "connections": { 138 | "HTTP Request": { 139 | "main": [[{ "node": "Webhook", "type": "main", "index": 0 }]] 140 | } 141 | } 142 | }, 143 | "nodes": [ 144 | { "id": 1, "name": "HTTP Request", "icon": "http" }, 145 | { "id": 2, "name": "Webhook", "icon": "webhook" } 146 | ], 147 | "categories": ["Data Processing"], 148 | "user": { 149 | "id": 1, 150 | "name": "Test User", 151 | "username": "testuser", 152 | "verified": false 153 | }, 154 | "views": 150, 155 | "createdAt": "2024-01-15T10:00:00Z", 156 | "updatedAt": "2024-01-20T15:30:00Z", 157 | "totalViews": 150 158 | } 159 | ] 160 | } ``` -------------------------------------------------------------------------------- /src/utils/node-type-utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Utility functions for working with n8n node types 3 | * Provides consistent normalization and transformation of node type strings 4 | */ 5 | 6 | /** 7 | * Normalize a node type to the standard short form 8 | * Handles both old-style (n8n-nodes-base.) and new-style (nodes-base.) prefixes 9 | * 10 | * @example 11 | * normalizeNodeType('n8n-nodes-base.httpRequest') // 'nodes-base.httpRequest' 12 | * normalizeNodeType('@n8n/n8n-nodes-langchain.openAi') // 'nodes-langchain.openAi' 13 | * normalizeNodeType('nodes-base.webhook') // 'nodes-base.webhook' (unchanged) 14 | */ 15 | export function normalizeNodeType(type: string): string { 16 | if (!type) return type; 17 | 18 | return type 19 | .replace(/^n8n-nodes-base\./, 'nodes-base.') 20 | .replace(/^@n8n\/n8n-nodes-langchain\./, 'nodes-langchain.'); 21 | } 22 | 23 | /** 24 | * Convert a short-form node type to the full package name 25 | * 26 | * @example 27 | * denormalizeNodeType('nodes-base.httpRequest', 'base') // 'n8n-nodes-base.httpRequest' 28 | * denormalizeNodeType('nodes-langchain.openAi', 'langchain') // '@n8n/n8n-nodes-langchain.openAi' 29 | */ 30 | export function denormalizeNodeType(type: string, packageType: 'base' | 'langchain'): string { 31 | if (!type) return type; 32 | 33 | if (packageType === 'base') { 34 | return type.replace(/^nodes-base\./, 'n8n-nodes-base.'); 35 | } 36 | 37 | return type.replace(/^nodes-langchain\./, '@n8n/n8n-nodes-langchain.'); 38 | } 39 | 40 | /** 41 | * Extract the node name from a full node type 42 | * 43 | * @example 44 | * extractNodeName('nodes-base.httpRequest') // 'httpRequest' 45 | * extractNodeName('n8n-nodes-base.webhook') // 'webhook' 46 | */ 47 | export function extractNodeName(type: string): string { 48 | if (!type) return ''; 49 | 50 | // First normalize the type 51 | const normalized = normalizeNodeType(type); 52 | 53 | // Extract everything after the last dot 54 | const parts = normalized.split('.'); 55 | return parts[parts.length - 1] || ''; 56 | } 57 | 58 | /** 59 | * Get the package prefix from a node type 60 | * 61 | * @example 62 | * getNodePackage('nodes-base.httpRequest') // 'nodes-base' 63 | * getNodePackage('nodes-langchain.openAi') // 'nodes-langchain' 64 | */ 65 | export function getNodePackage(type: string): string | null { 66 | if (!type || !type.includes('.')) return null; 67 | 68 | // First normalize the type 69 | const normalized = normalizeNodeType(type); 70 | 71 | // Extract everything before the first dot 72 | const parts = normalized.split('.'); 73 | return parts[0] || null; 74 | } 75 | 76 | /** 77 | * Check if a node type is from the base package 78 | */ 79 | export function isBaseNode(type: string): boolean { 80 | const normalized = normalizeNodeType(type); 81 | return normalized.startsWith('nodes-base.'); 82 | } 83 | 84 | /** 85 | * Check if a node type is from the langchain package 86 | */ 87 | export function isLangChainNode(type: string): boolean { 88 | const normalized = normalizeNodeType(type); 89 | return normalized.startsWith('nodes-langchain.'); 90 | } 91 | 92 | /** 93 | * Validate if a string looks like a valid node type 94 | * (has package prefix and node name) 95 | */ 96 | export function isValidNodeTypeFormat(type: string): boolean { 97 | if (!type || typeof type !== 'string') return false; 98 | 99 | // Must contain at least one dot 100 | if (!type.includes('.')) return false; 101 | 102 | const parts = type.split('.'); 103 | 104 | // Must have exactly 2 parts (package and node name) 105 | if (parts.length !== 2) return false; 106 | 107 | // Both parts must be non-empty 108 | return parts[0].length > 0 && parts[1].length > 0; 109 | } 110 | 111 | /** 112 | * Try multiple variations of a node type to find a match 113 | * Returns an array of variations to try in order 114 | * 115 | * @example 116 | * getNodeTypeVariations('httpRequest') 117 | * // ['nodes-base.httpRequest', 'n8n-nodes-base.httpRequest', 'nodes-langchain.httpRequest', ...] 118 | */ 119 | export function getNodeTypeVariations(type: string): string[] { 120 | const variations: string[] = []; 121 | 122 | // If it already has a package prefix, try normalized version first 123 | if (type.includes('.')) { 124 | variations.push(normalizeNodeType(type)); 125 | 126 | // Also try the denormalized versions 127 | const normalized = normalizeNodeType(type); 128 | if (normalized.startsWith('nodes-base.')) { 129 | variations.push(denormalizeNodeType(normalized, 'base')); 130 | } else if (normalized.startsWith('nodes-langchain.')) { 131 | variations.push(denormalizeNodeType(normalized, 'langchain')); 132 | } 133 | } else { 134 | // No package prefix, try common packages 135 | variations.push(`nodes-base.${type}`); 136 | variations.push(`n8n-nodes-base.${type}`); 137 | variations.push(`nodes-langchain.${type}`); 138 | variations.push(`@n8n/n8n-nodes-langchain.${type}`); 139 | } 140 | 141 | // Remove duplicates while preserving order 142 | return [...new Set(variations)]; 143 | } ``` -------------------------------------------------------------------------------- /tests/integration/n8n-api/system/health-check.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Integration Tests: handleHealthCheck 3 | * 4 | * Tests API health check against a real n8n instance. 5 | * Covers connectivity verification and feature availability. 6 | */ 7 | 8 | import { describe, it, expect, beforeEach } from 'vitest'; 9 | import { createMcpContext } from '../utils/mcp-context'; 10 | import { InstanceContext } from '../../../../src/types/instance-context'; 11 | import { handleHealthCheck } from '../../../../src/mcp/handlers-n8n-manager'; 12 | import { HealthCheckResponse } from '../utils/response-types'; 13 | 14 | describe('Integration: handleHealthCheck', () => { 15 | let mcpContext: InstanceContext; 16 | 17 | beforeEach(() => { 18 | mcpContext = createMcpContext(); 19 | }); 20 | 21 | // ====================================================================== 22 | // Successful Health Check 23 | // ====================================================================== 24 | 25 | describe('API Available', () => { 26 | it('should successfully check n8n API health', async () => { 27 | const response = await handleHealthCheck(mcpContext); 28 | 29 | expect(response.success).toBe(true); 30 | expect(response.data).toBeDefined(); 31 | 32 | const data = response.data as HealthCheckResponse; 33 | 34 | // Verify required fields 35 | expect(data).toHaveProperty('status'); 36 | expect(data).toHaveProperty('apiUrl'); 37 | expect(data).toHaveProperty('mcpVersion'); 38 | expect(data).toHaveProperty('versionCheck'); 39 | expect(data).toHaveProperty('performance'); 40 | expect(data).toHaveProperty('nextSteps'); 41 | 42 | // Status should be a string (e.g., "ok", "healthy") 43 | if (data.status) { 44 | expect(typeof data.status).toBe('string'); 45 | } 46 | 47 | // API URL should match configuration 48 | expect(data.apiUrl).toBeDefined(); 49 | expect(typeof data.apiUrl).toBe('string'); 50 | 51 | // MCP version should be defined 52 | expect(data.mcpVersion).toBeDefined(); 53 | expect(typeof data.mcpVersion).toBe('string'); 54 | 55 | // Version check should be present 56 | expect(data.versionCheck).toBeDefined(); 57 | expect(data.versionCheck).toHaveProperty('current'); 58 | expect(data.versionCheck).toHaveProperty('upToDate'); 59 | expect(typeof data.versionCheck.upToDate).toBe('boolean'); 60 | 61 | // Performance metrics should be present 62 | expect(data.performance).toBeDefined(); 63 | expect(data.performance).toHaveProperty('responseTimeMs'); 64 | expect(typeof data.performance.responseTimeMs).toBe('number'); 65 | expect(data.performance.responseTimeMs).toBeGreaterThan(0); 66 | 67 | // Next steps should be present 68 | expect(data.nextSteps).toBeDefined(); 69 | expect(Array.isArray(data.nextSteps)).toBe(true); 70 | }); 71 | 72 | it('should include feature availability information', async () => { 73 | const response = await handleHealthCheck(mcpContext); 74 | 75 | expect(response.success).toBe(true); 76 | const data = response.data as HealthCheckResponse; 77 | 78 | // Check for feature information 79 | // Note: Features may vary by n8n instance configuration 80 | if (data.features) { 81 | expect(typeof data.features).toBe('object'); 82 | } 83 | 84 | // Check for version information 85 | if (data.n8nVersion) { 86 | expect(typeof data.n8nVersion).toBe('string'); 87 | } 88 | 89 | if (data.supportedN8nVersion) { 90 | expect(typeof data.supportedN8nVersion).toBe('string'); 91 | } 92 | 93 | // Should include version note for AI agents 94 | if (data.versionNote) { 95 | expect(typeof data.versionNote).toBe('string'); 96 | expect(data.versionNote).toContain('version'); 97 | } 98 | }); 99 | }); 100 | 101 | // ====================================================================== 102 | // Response Format Verification 103 | // ====================================================================== 104 | 105 | describe('Response Format', () => { 106 | it('should return complete health check response structure', async () => { 107 | const response = await handleHealthCheck(mcpContext); 108 | 109 | expect(response.success).toBe(true); 110 | expect(response.data).toBeDefined(); 111 | 112 | const data = response.data as HealthCheckResponse; 113 | 114 | // Verify all expected fields are present 115 | const expectedFields = ['status', 'apiUrl', 'mcpVersion']; 116 | expectedFields.forEach(field => { 117 | expect(data).toHaveProperty(field); 118 | }); 119 | 120 | // Optional fields that may be present 121 | const optionalFields = ['instanceId', 'n8nVersion', 'features', 'supportedN8nVersion', 'versionNote']; 122 | optionalFields.forEach(field => { 123 | if (data[field] !== undefined) { 124 | expect(data[field]).not.toBeNull(); 125 | } 126 | }); 127 | }); 128 | }); 129 | }); 130 | ``` -------------------------------------------------------------------------------- /tests/integration/n8n-api/executions/delete-execution.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Integration Tests: handleDeleteExecution 3 | * 4 | * Tests execution deletion against a real n8n instance. 5 | * Covers successful deletion, error handling, and cleanup verification. 6 | */ 7 | 8 | import { describe, it, expect, beforeEach, beforeAll } from 'vitest'; 9 | import { createMcpContext } from '../utils/mcp-context'; 10 | import { InstanceContext } from '../../../../src/types/instance-context'; 11 | import { handleDeleteExecution, handleTriggerWebhookWorkflow, handleGetExecution } from '../../../../src/mcp/handlers-n8n-manager'; 12 | import { getN8nCredentials } from '../utils/credentials'; 13 | 14 | describe('Integration: handleDeleteExecution', () => { 15 | let mcpContext: InstanceContext; 16 | let webhookUrl: string; 17 | 18 | beforeEach(() => { 19 | mcpContext = createMcpContext(); 20 | }); 21 | 22 | beforeAll(() => { 23 | const creds = getN8nCredentials(); 24 | webhookUrl = creds.webhookUrls.get; 25 | }); 26 | 27 | // ====================================================================== 28 | // Successful Deletion 29 | // ====================================================================== 30 | 31 | describe('Successful Deletion', () => { 32 | it('should delete an execution successfully', async () => { 33 | // First, create an execution to delete 34 | const triggerResponse = await handleTriggerWebhookWorkflow( 35 | { 36 | webhookUrl, 37 | httpMethod: 'GET', 38 | waitForResponse: true 39 | }, 40 | mcpContext 41 | ); 42 | 43 | // Try to extract execution ID 44 | let executionId: string | undefined; 45 | if (triggerResponse.success && triggerResponse.data) { 46 | const responseData = triggerResponse.data as any; 47 | executionId = responseData.executionId || 48 | responseData.id || 49 | responseData.execution?.id || 50 | responseData.workflowData?.executionId; 51 | } 52 | 53 | if (!executionId) { 54 | console.warn('Could not extract execution ID for deletion test'); 55 | return; 56 | } 57 | 58 | // Delete the execution 59 | const response = await handleDeleteExecution( 60 | { id: executionId }, 61 | mcpContext 62 | ); 63 | 64 | expect(response.success).toBe(true); 65 | expect(response.data).toBeDefined(); 66 | }, 30000); 67 | 68 | it('should verify execution is actually deleted', async () => { 69 | // Create an execution 70 | const triggerResponse = await handleTriggerWebhookWorkflow( 71 | { 72 | webhookUrl, 73 | httpMethod: 'GET', 74 | waitForResponse: true 75 | }, 76 | mcpContext 77 | ); 78 | 79 | let executionId: string | undefined; 80 | if (triggerResponse.success && triggerResponse.data) { 81 | const responseData = triggerResponse.data as any; 82 | executionId = responseData.executionId || 83 | responseData.id || 84 | responseData.execution?.id || 85 | responseData.workflowData?.executionId; 86 | } 87 | 88 | if (!executionId) { 89 | console.warn('Could not extract execution ID for deletion verification test'); 90 | return; 91 | } 92 | 93 | // Delete it 94 | const deleteResponse = await handleDeleteExecution( 95 | { id: executionId }, 96 | mcpContext 97 | ); 98 | 99 | expect(deleteResponse.success).toBe(true); 100 | 101 | // Try to fetch the deleted execution 102 | const getResponse = await handleGetExecution( 103 | { id: executionId }, 104 | mcpContext 105 | ); 106 | 107 | // Should fail to find the deleted execution 108 | expect(getResponse.success).toBe(false); 109 | expect(getResponse.error).toBeDefined(); 110 | }, 30000); 111 | }); 112 | 113 | // ====================================================================== 114 | // Error Handling 115 | // ====================================================================== 116 | 117 | describe('Error Handling', () => { 118 | it('should handle non-existent execution ID', async () => { 119 | const response = await handleDeleteExecution( 120 | { id: '99999999' }, 121 | mcpContext 122 | ); 123 | 124 | expect(response.success).toBe(false); 125 | expect(response.error).toBeDefined(); 126 | }); 127 | 128 | it('should handle invalid execution ID format', async () => { 129 | const response = await handleDeleteExecution( 130 | { id: 'invalid-id-format' }, 131 | mcpContext 132 | ); 133 | 134 | expect(response.success).toBe(false); 135 | expect(response.error).toBeDefined(); 136 | }); 137 | 138 | it('should handle missing execution ID', async () => { 139 | const response = await handleDeleteExecution( 140 | {} as any, 141 | mcpContext 142 | ); 143 | 144 | expect(response.success).toBe(false); 145 | expect(response.error).toBeDefined(); 146 | }); 147 | }); 148 | }); 149 | ``` -------------------------------------------------------------------------------- /src/scripts/validate.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright (c) 2024 AiAdvisors Romuald Czlonkowski 4 | * Licensed under the Sustainable Use License v1.0 5 | */ 6 | import { createDatabaseAdapter } from '../database/database-adapter'; 7 | 8 | interface NodeRow { 9 | node_type: string; 10 | package_name: string; 11 | display_name: string; 12 | description?: string; 13 | category?: string; 14 | development_style?: string; 15 | is_ai_tool: number; 16 | is_trigger: number; 17 | is_webhook: number; 18 | is_versioned: number; 19 | version?: string; 20 | documentation?: string; 21 | properties_schema?: string; 22 | operations?: string; 23 | credentials_required?: string; 24 | updated_at: string; 25 | } 26 | 27 | async function validate() { 28 | const db = await createDatabaseAdapter('./data/nodes.db'); 29 | 30 | console.log('🔍 Validating critical nodes...\n'); 31 | 32 | const criticalChecks = [ 33 | { 34 | type: 'nodes-base.httpRequest', 35 | checks: { 36 | hasDocumentation: true, 37 | documentationContains: 'HTTP Request', 38 | style: 'programmatic' 39 | } 40 | }, 41 | { 42 | type: 'nodes-base.code', 43 | checks: { 44 | hasDocumentation: true, 45 | documentationContains: 'Code' 46 | } 47 | }, 48 | { 49 | type: 'nodes-base.slack', 50 | checks: { 51 | hasOperations: true, 52 | style: 'programmatic' 53 | } 54 | }, 55 | { 56 | type: 'nodes-langchain.agent', 57 | checks: { 58 | isAITool: false, // According to the database, it's not marked as AI tool 59 | packageName: '@n8n/n8n-nodes-langchain' 60 | } 61 | } 62 | ]; 63 | 64 | let passed = 0; 65 | let failed = 0; 66 | 67 | for (const check of criticalChecks) { 68 | const node = db.prepare('SELECT * FROM nodes WHERE node_type = ?').get(check.type) as NodeRow | undefined; 69 | 70 | if (!node) { 71 | console.log(`❌ ${check.type}: NOT FOUND`); 72 | failed++; 73 | continue; 74 | } 75 | 76 | let nodeOk = true; 77 | const issues: string[] = []; 78 | 79 | // Run checks 80 | if (check.checks.hasDocumentation && !node.documentation) { 81 | nodeOk = false; 82 | issues.push('missing documentation'); 83 | } 84 | 85 | if (check.checks.documentationContains && 86 | !node.documentation?.includes(check.checks.documentationContains)) { 87 | nodeOk = false; 88 | issues.push(`documentation doesn't contain "${check.checks.documentationContains}"`); 89 | } 90 | 91 | if (check.checks.style && node.development_style !== check.checks.style) { 92 | nodeOk = false; 93 | issues.push(`wrong style: ${node.development_style}`); 94 | } 95 | 96 | if (check.checks.hasOperations) { 97 | const operations = JSON.parse(node.operations || '[]'); 98 | if (!operations.length) { 99 | nodeOk = false; 100 | issues.push('no operations found'); 101 | } 102 | } 103 | 104 | if (check.checks.isAITool !== undefined && !!node.is_ai_tool !== check.checks.isAITool) { 105 | nodeOk = false; 106 | issues.push(`AI tool flag mismatch: expected ${check.checks.isAITool}, got ${!!node.is_ai_tool}`); 107 | } 108 | 109 | if ('isVersioned' in check.checks && check.checks.isVersioned && !node.is_versioned) { 110 | nodeOk = false; 111 | issues.push('not marked as versioned'); 112 | } 113 | 114 | if (check.checks.packageName && node.package_name !== check.checks.packageName) { 115 | nodeOk = false; 116 | issues.push(`wrong package: ${node.package_name}`); 117 | } 118 | 119 | if (nodeOk) { 120 | console.log(`✅ ${check.type}`); 121 | passed++; 122 | } else { 123 | console.log(`❌ ${check.type}: ${issues.join(', ')}`); 124 | failed++; 125 | } 126 | } 127 | 128 | console.log(`\n📊 Results: ${passed} passed, ${failed} failed`); 129 | 130 | // Additional statistics 131 | const stats = db.prepare(` 132 | SELECT 133 | COUNT(*) as total, 134 | SUM(is_ai_tool) as ai_tools, 135 | SUM(is_trigger) as triggers, 136 | SUM(is_versioned) as versioned, 137 | COUNT(DISTINCT package_name) as packages 138 | FROM nodes 139 | `).get() as any; 140 | 141 | console.log('\n📈 Database Statistics:'); 142 | console.log(` Total nodes: ${stats.total}`); 143 | console.log(` AI tools: ${stats.ai_tools}`); 144 | console.log(` Triggers: ${stats.triggers}`); 145 | console.log(` Versioned: ${stats.versioned}`); 146 | console.log(` Packages: ${stats.packages}`); 147 | 148 | // Check documentation coverage 149 | const docStats = db.prepare(` 150 | SELECT 151 | COUNT(*) as total, 152 | SUM(CASE WHEN documentation IS NOT NULL THEN 1 ELSE 0 END) as with_docs 153 | FROM nodes 154 | `).get() as any; 155 | 156 | console.log(`\n📚 Documentation Coverage:`); 157 | console.log(` Nodes with docs: ${docStats.with_docs}/${docStats.total} (${Math.round(docStats.with_docs / docStats.total * 100)}%)`); 158 | 159 | db.close(); 160 | process.exit(failed > 0 ? 1 : 0); 161 | } 162 | 163 | if (require.main === module) { 164 | validate().catch(console.error); 165 | } ```