This is page 8 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 -------------------------------------------------------------------------------- /tests/error-handler.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, vi } from 'vitest'; 2 | import { 3 | MCPError, 4 | N8NConnectionError, 5 | AuthenticationError, 6 | ValidationError, 7 | ToolNotFoundError, 8 | ResourceNotFoundError, 9 | handleError, 10 | withErrorHandling, 11 | } from '../src/utils/error-handler'; 12 | import { logger } from '../src/utils/logger'; 13 | 14 | // Mock the logger 15 | vi.mock('../src/utils/logger', () => ({ 16 | logger: { 17 | error: vi.fn(), 18 | }, 19 | })); 20 | 21 | describe('Error Classes', () => { 22 | describe('MCPError', () => { 23 | it('should create error with all properties', () => { 24 | const error = new MCPError('Test error', 'TEST_CODE', 400, { field: 'value' }); 25 | 26 | expect(error.message).toBe('Test error'); 27 | expect(error.code).toBe('TEST_CODE'); 28 | expect(error.statusCode).toBe(400); 29 | expect(error.data).toEqual({ field: 'value' }); 30 | expect(error.name).toBe('MCPError'); 31 | }); 32 | }); 33 | 34 | describe('N8NConnectionError', () => { 35 | it('should create connection error with correct code', () => { 36 | const error = new N8NConnectionError('Connection failed'); 37 | 38 | expect(error.message).toBe('Connection failed'); 39 | expect(error.code).toBe('N8N_CONNECTION_ERROR'); 40 | expect(error.statusCode).toBe(503); 41 | expect(error.name).toBe('N8NConnectionError'); 42 | }); 43 | }); 44 | 45 | describe('AuthenticationError', () => { 46 | it('should create auth error with default message', () => { 47 | const error = new AuthenticationError(); 48 | 49 | expect(error.message).toBe('Authentication failed'); 50 | expect(error.code).toBe('AUTH_ERROR'); 51 | expect(error.statusCode).toBe(401); 52 | }); 53 | 54 | it('should accept custom message', () => { 55 | const error = new AuthenticationError('Invalid token'); 56 | expect(error.message).toBe('Invalid token'); 57 | }); 58 | }); 59 | 60 | describe('ValidationError', () => { 61 | it('should create validation error', () => { 62 | const error = new ValidationError('Invalid input', { field: 'email' }); 63 | 64 | expect(error.message).toBe('Invalid input'); 65 | expect(error.code).toBe('VALIDATION_ERROR'); 66 | expect(error.statusCode).toBe(400); 67 | expect(error.data).toEqual({ field: 'email' }); 68 | }); 69 | }); 70 | 71 | describe('ToolNotFoundError', () => { 72 | it('should create tool not found error', () => { 73 | const error = new ToolNotFoundError('myTool'); 74 | 75 | expect(error.message).toBe("Tool 'myTool' not found"); 76 | expect(error.code).toBe('TOOL_NOT_FOUND'); 77 | expect(error.statusCode).toBe(404); 78 | }); 79 | }); 80 | 81 | describe('ResourceNotFoundError', () => { 82 | it('should create resource not found error', () => { 83 | const error = new ResourceNotFoundError('workflow://123'); 84 | 85 | expect(error.message).toBe("Resource 'workflow://123' not found"); 86 | expect(error.code).toBe('RESOURCE_NOT_FOUND'); 87 | expect(error.statusCode).toBe(404); 88 | }); 89 | }); 90 | }); 91 | 92 | describe('handleError', () => { 93 | it('should return MCPError instances as-is', () => { 94 | const mcpError = new ValidationError('Test'); 95 | const result = handleError(mcpError); 96 | 97 | expect(result).toBe(mcpError); 98 | }); 99 | 100 | it('should handle HTTP 401 errors', () => { 101 | const httpError = { 102 | response: { status: 401, data: { message: 'Unauthorized' } }, 103 | }; 104 | 105 | const result = handleError(httpError); 106 | 107 | expect(result).toBeInstanceOf(AuthenticationError); 108 | expect(result.message).toBe('Unauthorized'); 109 | }); 110 | 111 | it('should handle HTTP 404 errors', () => { 112 | const httpError = { 113 | response: { status: 404, data: { message: 'Not found' } }, 114 | }; 115 | 116 | const result = handleError(httpError); 117 | 118 | expect(result.code).toBe('NOT_FOUND'); 119 | expect(result.statusCode).toBe(404); 120 | }); 121 | 122 | it('should handle HTTP 5xx errors', () => { 123 | const httpError = { 124 | response: { status: 503, data: { message: 'Service unavailable' } }, 125 | }; 126 | 127 | const result = handleError(httpError); 128 | 129 | expect(result).toBeInstanceOf(N8NConnectionError); 130 | }); 131 | 132 | it('should handle connection refused errors', () => { 133 | const connError = { code: 'ECONNREFUSED' }; 134 | 135 | const result = handleError(connError); 136 | 137 | expect(result).toBeInstanceOf(N8NConnectionError); 138 | expect(result.message).toBe('Cannot connect to n8n API'); 139 | }); 140 | 141 | it('should handle generic errors', () => { 142 | const error = new Error('Something went wrong'); 143 | 144 | const result = handleError(error); 145 | 146 | expect(result.message).toBe('Something went wrong'); 147 | expect(result.code).toBe('UNKNOWN_ERROR'); 148 | expect(result.statusCode).toBe(500); 149 | }); 150 | 151 | it('should handle errors without message', () => { 152 | const error = {}; 153 | 154 | const result = handleError(error); 155 | 156 | expect(result.message).toBe('An unexpected error occurred'); 157 | }); 158 | }); 159 | 160 | describe('withErrorHandling', () => { 161 | it('should execute operation successfully', async () => { 162 | const operation = vi.fn().mockResolvedValue('success'); 163 | 164 | const result = await withErrorHandling(operation, 'test operation'); 165 | 166 | expect(result).toBe('success'); 167 | expect(logger.error).not.toHaveBeenCalled(); 168 | }); 169 | 170 | it('should handle and log errors', async () => { 171 | const error = new Error('Operation failed'); 172 | const operation = vi.fn().mockRejectedValue(error); 173 | 174 | await expect(withErrorHandling(operation, 'test operation')).rejects.toThrow(); 175 | 176 | expect(logger.error).toHaveBeenCalledWith('Error in test operation:', error); 177 | }); 178 | 179 | it('should transform errors using handleError', async () => { 180 | const error = { code: 'ECONNREFUSED' }; 181 | const operation = vi.fn().mockRejectedValue(error); 182 | 183 | try { 184 | await withErrorHandling(operation, 'test operation'); 185 | } catch (err) { 186 | expect(err).toBeInstanceOf(N8NConnectionError); 187 | } 188 | }); 189 | }); ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/templates/search-templates-by-metadata.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolDocumentation } from '../types'; 2 | 3 | export const searchTemplatesByMetadataDoc: ToolDocumentation = { 4 | name: 'search_templates_by_metadata', 5 | category: 'templates', 6 | essentials: { 7 | description: 'Search templates using AI-generated metadata filters. Find templates by complexity, setup time, required services, or target audience. Enables smart template discovery beyond simple text search.', 8 | keyParameters: ['category', 'complexity', 'maxSetupMinutes', 'targetAudience'], 9 | example: 'search_templates_by_metadata({complexity: "simple", maxSetupMinutes: 30})', 10 | performance: 'Fast (<100ms) - JSON extraction queries', 11 | tips: [ 12 | 'All filters are optional - combine them for precise results', 13 | 'Use getAvailableCategories() to see valid category values', 14 | 'Complexity levels: simple, medium, complex', 15 | 'Setup time is in minutes (5-480 range)' 16 | ] 17 | }, 18 | full: { 19 | description: `Advanced template search using AI-generated metadata. Each template has been analyzed by GPT-4 to extract structured information about its purpose, complexity, setup requirements, and target users. This enables intelligent filtering beyond simple keyword matching, helping you find templates that match your specific needs, skill level, and available time.`, 20 | parameters: { 21 | category: { 22 | type: 'string', 23 | required: false, 24 | description: 'Filter by category like "automation", "integration", "data processing", "communication". Use template service getAvailableCategories() for full list.' 25 | }, 26 | complexity: { 27 | type: 'string (enum)', 28 | required: false, 29 | description: 'Filter by implementation complexity: "simple" (beginner-friendly), "medium" (some experience needed), or "complex" (advanced features)' 30 | }, 31 | maxSetupMinutes: { 32 | type: 'number', 33 | required: false, 34 | description: 'Maximum acceptable setup time in minutes (5-480). Find templates you can implement within your time budget.' 35 | }, 36 | minSetupMinutes: { 37 | type: 'number', 38 | required: false, 39 | description: 'Minimum setup time in minutes (5-480). Find more substantial templates that offer comprehensive solutions.' 40 | }, 41 | requiredService: { 42 | type: 'string', 43 | required: false, 44 | description: 'Filter by required external service like "openai", "slack", "google", "shopify". Ensures you have necessary accounts/APIs.' 45 | }, 46 | targetAudience: { 47 | type: 'string', 48 | required: false, 49 | description: 'Filter by intended users: "developers", "marketers", "analysts", "operations", "sales". Find templates for your role.' 50 | }, 51 | limit: { 52 | type: 'number', 53 | required: false, 54 | description: 'Maximum results to return. Default 20, max 100.' 55 | }, 56 | offset: { 57 | type: 'number', 58 | required: false, 59 | description: 'Pagination offset for results. Default 0.' 60 | } 61 | }, 62 | returns: `Returns an object containing: 63 | - items: Array of matching templates with full metadata 64 | - id: Template ID 65 | - name: Template name 66 | - description: Purpose and functionality 67 | - author: Creator details 68 | - nodes: Array of nodes used 69 | - views: Popularity count 70 | - metadata: AI-generated structured data 71 | - categories: Primary use categories 72 | - complexity: Difficulty level 73 | - use_cases: Specific applications 74 | - estimated_setup_minutes: Time to implement 75 | - required_services: External dependencies 76 | - key_features: Main capabilities 77 | - target_audience: Intended users 78 | - total: Total matching templates 79 | - filters: Applied filter criteria 80 | - filterSummary: Human-readable filter description 81 | - availableCategories: Suggested categories if no results 82 | - availableAudiences: Suggested audiences if no results 83 | - tip: Contextual guidance`, 84 | examples: [ 85 | 'search_templates_by_metadata({complexity: "simple"}) - Find beginner-friendly templates', 86 | 'search_templates_by_metadata({category: "automation", maxSetupMinutes: 30}) - Quick automation templates', 87 | 'search_templates_by_metadata({targetAudience: "marketers"}) - Marketing-focused workflows', 88 | 'search_templates_by_metadata({requiredService: "openai", complexity: "medium"}) - AI templates with moderate complexity', 89 | 'search_templates_by_metadata({minSetupMinutes: 60, category: "integration"}) - Comprehensive integration solutions' 90 | ], 91 | useCases: [ 92 | 'Finding beginner-friendly templates by setting complexity:"simple"', 93 | 'Discovering templates you can implement quickly with maxSetupMinutes:30', 94 | 'Finding role-specific workflows with targetAudience filter', 95 | 'Identifying templates that need specific APIs with requiredService filter', 96 | 'Combining multiple filters for precise template discovery' 97 | ], 98 | performance: 'Fast (<100ms) - Uses SQLite JSON extraction on pre-generated metadata. 97.5% coverage (2,534/2,598 templates).', 99 | bestPractices: [ 100 | 'Start with broad filters and narrow down based on results', 101 | 'Use getAvailableCategories() to discover valid category values', 102 | 'Combine complexity and setup time for skill-appropriate templates', 103 | 'Check required services before selecting templates to ensure you have necessary accounts' 104 | ], 105 | pitfalls: [ 106 | 'Not all templates have metadata (97.5% coverage)', 107 | 'Setup time estimates assume basic n8n familiarity', 108 | 'Categories/audiences use partial matching - be specific', 109 | 'Metadata is AI-generated and may occasionally be imprecise' 110 | ], 111 | relatedTools: [ 112 | 'list_templates', 113 | 'search_templates', 114 | 'list_node_templates', 115 | 'get_templates_for_task' 116 | ] 117 | } 118 | }; ``` -------------------------------------------------------------------------------- /src/utils/node-utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Normalizes node type from n8n export format to database format 3 | * 4 | * Examples: 5 | * - 'n8n-nodes-base.httpRequest' → 'nodes-base.httpRequest' 6 | * - '@n8n/n8n-nodes-langchain.agent' → 'nodes-langchain.agent' 7 | * - 'n8n-nodes-langchain.chatTrigger' → 'nodes-langchain.chatTrigger' 8 | * - 'nodes-base.slack' → 'nodes-base.slack' (unchanged) 9 | * 10 | * @param nodeType The node type to normalize 11 | * @returns The normalized node type 12 | */ 13 | export function normalizeNodeType(nodeType: string): string { 14 | // Handle n8n-nodes-base -> nodes-base 15 | if (nodeType.startsWith('n8n-nodes-base.')) { 16 | return nodeType.replace('n8n-nodes-base.', 'nodes-base.'); 17 | } 18 | 19 | // Handle @n8n/n8n-nodes-langchain -> nodes-langchain 20 | if (nodeType.startsWith('@n8n/n8n-nodes-langchain.')) { 21 | return nodeType.replace('@n8n/n8n-nodes-langchain.', 'nodes-langchain.'); 22 | } 23 | 24 | // Handle n8n-nodes-langchain -> nodes-langchain (without @n8n/ prefix) 25 | if (nodeType.startsWith('n8n-nodes-langchain.')) { 26 | return nodeType.replace('n8n-nodes-langchain.', 'nodes-langchain.'); 27 | } 28 | 29 | // Return unchanged if already normalized or unknown format 30 | return nodeType; 31 | } 32 | 33 | /** 34 | * Gets alternative node type formats to try for lookups 35 | * 36 | * @param nodeType The original node type 37 | * @returns Array of alternative formats to try 38 | */ 39 | export function getNodeTypeAlternatives(nodeType: string): string[] { 40 | // Defensive: validate input to prevent TypeError when nodeType is undefined/null/empty 41 | if (!nodeType || typeof nodeType !== 'string' || nodeType.trim() === '') { 42 | return []; 43 | } 44 | 45 | const alternatives: string[] = []; 46 | 47 | // Add lowercase version 48 | alternatives.push(nodeType.toLowerCase()); 49 | 50 | // If it has a prefix, try case variations on the node name part 51 | if (nodeType.includes('.')) { 52 | const [prefix, nodeName] = nodeType.split('.'); 53 | 54 | // Try different case variations for the node name 55 | if (nodeName && nodeName.toLowerCase() !== nodeName) { 56 | alternatives.push(`${prefix}.${nodeName.toLowerCase()}`); 57 | } 58 | 59 | // For camelCase names like "chatTrigger", also try with capital first letter variations 60 | // e.g., "chattrigger" -> "chatTrigger" 61 | if (nodeName && nodeName.toLowerCase() === nodeName && nodeName.length > 1) { 62 | // Try to detect common patterns and create camelCase version 63 | const camelCaseVariants = generateCamelCaseVariants(nodeName); 64 | camelCaseVariants.forEach(variant => { 65 | alternatives.push(`${prefix}.${variant}`); 66 | }); 67 | } 68 | } 69 | 70 | // If it's just a bare node name, try with common prefixes 71 | if (!nodeType.includes('.')) { 72 | alternatives.push(`nodes-base.${nodeType}`); 73 | alternatives.push(`nodes-langchain.${nodeType}`); 74 | 75 | // Also try camelCase variants for bare names 76 | const camelCaseVariants = generateCamelCaseVariants(nodeType); 77 | camelCaseVariants.forEach(variant => { 78 | alternatives.push(`nodes-base.${variant}`); 79 | alternatives.push(`nodes-langchain.${variant}`); 80 | }); 81 | } 82 | 83 | // Normalize all alternatives and combine with originals 84 | const normalizedAlternatives = alternatives.map(alt => normalizeNodeType(alt)); 85 | 86 | // Combine original alternatives with normalized ones and remove duplicates 87 | return [...new Set([...alternatives, ...normalizedAlternatives])]; 88 | } 89 | 90 | /** 91 | * Generate camelCase variants for a lowercase string 92 | * @param str The lowercase string 93 | * @returns Array of possible camelCase variants 94 | */ 95 | function generateCamelCaseVariants(str: string): string[] { 96 | const variants: string[] = []; 97 | 98 | // Common patterns for n8n nodes 99 | const patterns = [ 100 | // Pattern: wordTrigger (e.g., chatTrigger, webhookTrigger) 101 | /^(.+)(trigger|node|request|response)$/i, 102 | // Pattern: httpRequest, mysqlDatabase 103 | /^(http|mysql|postgres|mongo|redis|mqtt|smtp|imap|ftp|ssh|api)(.+)$/i, 104 | // Pattern: googleSheets, microsoftTeams 105 | /^(google|microsoft|amazon|slack|discord|telegram)(.+)$/i, 106 | ]; 107 | 108 | for (const pattern of patterns) { 109 | const match = str.toLowerCase().match(pattern); 110 | if (match) { 111 | const [, first, second] = match; 112 | // Capitalize the second part 113 | variants.push(first.toLowerCase() + second.charAt(0).toUpperCase() + second.slice(1).toLowerCase()); 114 | } 115 | } 116 | 117 | // Generic camelCase: capitalize after common word boundaries 118 | if (variants.length === 0) { 119 | // Try splitting on common boundaries and capitalizing 120 | const words = str.split(/[-_\s]+/); 121 | if (words.length > 1) { 122 | const camelCase = words[0].toLowerCase() + words.slice(1).map(w => 123 | w.charAt(0).toUpperCase() + w.slice(1).toLowerCase() 124 | ).join(''); 125 | variants.push(camelCase); 126 | } 127 | } 128 | 129 | return variants; 130 | } 131 | 132 | /** 133 | * Constructs the workflow node type from package name and normalized node type 134 | * This creates the format that n8n expects in workflow definitions 135 | * 136 | * Examples: 137 | * - ('n8n-nodes-base', 'nodes-base.webhook') → 'n8n-nodes-base.webhook' 138 | * - ('@n8n/n8n-nodes-langchain', 'nodes-langchain.agent') → '@n8n/n8n-nodes-langchain.agent' 139 | * 140 | * @param packageName The package name from the database 141 | * @param nodeType The normalized node type from the database 142 | * @returns The workflow node type for use in n8n workflows 143 | */ 144 | export function getWorkflowNodeType(packageName: string, nodeType: string): string { 145 | // Extract just the node name from the normalized type 146 | const nodeName = nodeType.split('.').pop() || nodeType; 147 | 148 | // Construct the full workflow type based on package 149 | if (packageName === 'n8n-nodes-base') { 150 | return `n8n-nodes-base.${nodeName}`; 151 | } else if (packageName === '@n8n/n8n-nodes-langchain') { 152 | return `@n8n/n8n-nodes-langchain.${nodeName}`; 153 | } 154 | 155 | // Fallback for unknown packages - return as is 156 | return nodeType; 157 | } ``` -------------------------------------------------------------------------------- /docker/parse-config.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | /** 3 | * Parse JSON config file and output shell-safe export commands 4 | * Only outputs variables that aren't already set in environment 5 | * 6 | * Security: Uses safe quoting without any shell execution 7 | */ 8 | 9 | const fs = require('fs'); 10 | 11 | // Debug logging support 12 | const DEBUG = process.env.DEBUG_CONFIG === 'true'; 13 | 14 | function debugLog(message) { 15 | if (DEBUG) { 16 | process.stderr.write(`[parse-config] ${message}\n`); 17 | } 18 | } 19 | 20 | const configPath = process.argv[2] || '/app/config.json'; 21 | debugLog(`Using config path: ${configPath}`); 22 | 23 | // Dangerous environment variables that should never be set 24 | const DANGEROUS_VARS = new Set([ 25 | 'PATH', 'LD_PRELOAD', 'LD_LIBRARY_PATH', 'LD_AUDIT', 26 | 'BASH_ENV', 'ENV', 'CDPATH', 'IFS', 'PS1', 'PS2', 'PS3', 'PS4', 27 | 'SHELL', 'BASH_FUNC', 'SHELLOPTS', 'GLOBIGNORE', 28 | 'PERL5LIB', 'PYTHONPATH', 'NODE_PATH', 'RUBYLIB' 29 | ]); 30 | 31 | /** 32 | * Sanitize a key name for use as environment variable 33 | * Converts to uppercase and replaces invalid chars with underscore 34 | */ 35 | function sanitizeKey(key) { 36 | // Convert to string and handle edge cases 37 | const keyStr = String(key || '').trim(); 38 | 39 | if (!keyStr) { 40 | return 'EMPTY_KEY'; 41 | } 42 | 43 | // Special handling for NODE_DB_PATH to preserve exact casing 44 | if (keyStr === 'NODE_DB_PATH') { 45 | return 'NODE_DB_PATH'; 46 | } 47 | 48 | const sanitized = keyStr 49 | .toUpperCase() 50 | .replace(/[^A-Z0-9]+/g, '_') 51 | .replace(/^_+|_+$/g, '') // Trim underscores 52 | .replace(/^(\d)/, '_$1'); // Prefix with _ if starts with number 53 | 54 | // If sanitization results in empty string, use a default 55 | return sanitized || 'EMPTY_KEY'; 56 | } 57 | 58 | /** 59 | * Safely quote a string for shell use 60 | * This follows POSIX shell quoting rules 61 | */ 62 | function shellQuote(str) { 63 | // Remove null bytes which are not allowed in environment variables 64 | str = str.replace(/\x00/g, ''); 65 | 66 | // Always use single quotes for consistency and safety 67 | // Single quotes protect everything except other single quotes 68 | return "'" + str.replace(/'/g, "'\"'\"'") + "'"; 69 | } 70 | 71 | try { 72 | if (!fs.existsSync(configPath)) { 73 | debugLog(`Config file not found at: ${configPath}`); 74 | process.exit(0); // Silent exit if no config file 75 | } 76 | 77 | let configContent; 78 | let config; 79 | 80 | try { 81 | configContent = fs.readFileSync(configPath, 'utf8'); 82 | debugLog(`Read config file, size: ${configContent.length} bytes`); 83 | } catch (readError) { 84 | // Silent exit on read errors 85 | debugLog(`Error reading config: ${readError.message}`); 86 | process.exit(0); 87 | } 88 | 89 | try { 90 | config = JSON.parse(configContent); 91 | debugLog(`Parsed config with ${Object.keys(config).length} top-level keys`); 92 | } catch (parseError) { 93 | // Silent exit on invalid JSON 94 | debugLog(`Error parsing JSON: ${parseError.message}`); 95 | process.exit(0); 96 | } 97 | 98 | // Validate config is an object 99 | if (typeof config !== 'object' || config === null || Array.isArray(config)) { 100 | // Silent exit on invalid config structure 101 | process.exit(0); 102 | } 103 | 104 | // Convert nested objects to flat environment variables 105 | const flattenConfig = (obj, prefix = '', depth = 0) => { 106 | const result = {}; 107 | 108 | // Prevent infinite recursion 109 | if (depth > 10) { 110 | return result; 111 | } 112 | 113 | for (const [key, value] of Object.entries(obj)) { 114 | const sanitizedKey = sanitizeKey(key); 115 | 116 | // Skip if sanitization resulted in EMPTY_KEY (indicating invalid key) 117 | if (sanitizedKey === 'EMPTY_KEY') { 118 | debugLog(`Skipping key '${key}': invalid key name`); 119 | continue; 120 | } 121 | 122 | const envKey = prefix ? `${prefix}_${sanitizedKey}` : sanitizedKey; 123 | 124 | // Skip if key is too long 125 | if (envKey.length > 255) { 126 | debugLog(`Skipping key '${envKey}': too long (${envKey.length} chars)`); 127 | continue; 128 | } 129 | 130 | if (typeof value === 'object' && value !== null && !Array.isArray(value)) { 131 | // Recursively flatten nested objects 132 | Object.assign(result, flattenConfig(value, envKey, depth + 1)); 133 | } else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { 134 | // Only include if not already set in environment 135 | if (!process.env[envKey]) { 136 | let stringValue = String(value); 137 | 138 | // Handle special JavaScript number values 139 | if (typeof value === 'number') { 140 | if (!isFinite(value)) { 141 | if (value === Infinity) { 142 | stringValue = 'Infinity'; 143 | } else if (value === -Infinity) { 144 | stringValue = '-Infinity'; 145 | } else if (isNaN(value)) { 146 | stringValue = 'NaN'; 147 | } 148 | } 149 | } 150 | 151 | // Skip if value is too long 152 | if (stringValue.length <= 32768) { 153 | result[envKey] = stringValue; 154 | } 155 | } 156 | } 157 | } 158 | 159 | return result; 160 | }; 161 | 162 | // Output shell-safe export commands 163 | const flattened = flattenConfig(config); 164 | const exports = []; 165 | 166 | for (const [key, value] of Object.entries(flattened)) { 167 | // Validate key name (alphanumeric and underscore only) 168 | if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) { 169 | continue; // Skip invalid variable names 170 | } 171 | 172 | // Skip dangerous variables 173 | if (DANGEROUS_VARS.has(key) || key.startsWith('BASH_FUNC_')) { 174 | debugLog(`Warning: Ignoring dangerous variable: ${key}`); 175 | process.stderr.write(`Warning: Ignoring dangerous variable: ${key}\n`); 176 | continue; 177 | } 178 | 179 | // Safely quote the value 180 | const quotedValue = shellQuote(value); 181 | exports.push(`export ${key}=${quotedValue}`); 182 | } 183 | 184 | // Use process.stdout.write to ensure output goes to stdout 185 | if (exports.length > 0) { 186 | process.stdout.write(exports.join('\n') + '\n'); 187 | } 188 | 189 | } catch (error) { 190 | // Silent fail - don't break the container startup 191 | process.exit(0); 192 | } ``` -------------------------------------------------------------------------------- /src/mcp/handlers-workflow-diff.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * MCP Handler for Partial Workflow Updates 3 | * Handles diff-based workflow modifications 4 | */ 5 | 6 | import { z } from 'zod'; 7 | import { McpToolResponse } from '../types/n8n-api'; 8 | import { WorkflowDiffRequest, WorkflowDiffOperation } from '../types/workflow-diff'; 9 | import { WorkflowDiffEngine } from '../services/workflow-diff-engine'; 10 | import { getN8nApiClient } from './handlers-n8n-manager'; 11 | import { N8nApiError, getUserFriendlyErrorMessage } from '../utils/n8n-errors'; 12 | import { logger } from '../utils/logger'; 13 | import { InstanceContext } from '../types/instance-context'; 14 | 15 | // Zod schema for the diff request 16 | const workflowDiffSchema = z.object({ 17 | id: z.string(), 18 | operations: z.array(z.object({ 19 | type: z.string(), 20 | description: z.string().optional(), 21 | // Node operations 22 | node: z.any().optional(), 23 | nodeId: z.string().optional(), 24 | nodeName: z.string().optional(), 25 | updates: z.any().optional(), 26 | position: z.tuple([z.number(), z.number()]).optional(), 27 | // Connection operations 28 | source: z.string().optional(), 29 | target: z.string().optional(), 30 | from: z.string().optional(), // For rewireConnection 31 | to: z.string().optional(), // For rewireConnection 32 | sourceOutput: z.string().optional(), 33 | targetInput: z.string().optional(), 34 | sourceIndex: z.number().optional(), 35 | targetIndex: z.number().optional(), 36 | // Smart parameters (Phase 1 UX improvement) 37 | branch: z.enum(['true', 'false']).optional(), 38 | case: z.number().optional(), 39 | ignoreErrors: z.boolean().optional(), 40 | // Connection cleanup operations 41 | dryRun: z.boolean().optional(), 42 | connections: z.any().optional(), 43 | // Metadata operations 44 | settings: z.any().optional(), 45 | name: z.string().optional(), 46 | tag: z.string().optional(), 47 | })), 48 | validateOnly: z.boolean().optional(), 49 | continueOnError: z.boolean().optional(), 50 | }); 51 | 52 | export async function handleUpdatePartialWorkflow(args: unknown, context?: InstanceContext): Promise<McpToolResponse> { 53 | try { 54 | // Debug logging (only in debug mode) 55 | if (process.env.DEBUG_MCP === 'true') { 56 | logger.debug('Workflow diff request received', { 57 | argsType: typeof args, 58 | hasWorkflowId: args && typeof args === 'object' && 'workflowId' in args, 59 | operationCount: args && typeof args === 'object' && 'operations' in args ? 60 | (args as any).operations?.length : 0 61 | }); 62 | } 63 | 64 | // Validate input 65 | const input = workflowDiffSchema.parse(args); 66 | 67 | // Get API client 68 | const client = getN8nApiClient(context); 69 | if (!client) { 70 | return { 71 | success: false, 72 | error: 'n8n API not configured. Please set N8N_API_URL and N8N_API_KEY environment variables.' 73 | }; 74 | } 75 | 76 | // Fetch current workflow 77 | let workflow; 78 | try { 79 | workflow = await client.getWorkflow(input.id); 80 | } catch (error) { 81 | if (error instanceof N8nApiError) { 82 | return { 83 | success: false, 84 | error: getUserFriendlyErrorMessage(error), 85 | code: error.code 86 | }; 87 | } 88 | throw error; 89 | } 90 | 91 | // Apply diff operations 92 | const diffEngine = new WorkflowDiffEngine(); 93 | const diffRequest = input as WorkflowDiffRequest; 94 | const diffResult = await diffEngine.applyDiff(workflow, diffRequest); 95 | 96 | // Check if this is a complete failure or partial success in continueOnError mode 97 | if (!diffResult.success) { 98 | // In continueOnError mode, partial success is still valuable 99 | if (diffRequest.continueOnError && diffResult.workflow && diffResult.operationsApplied && diffResult.operationsApplied > 0) { 100 | logger.info(`continueOnError mode: Applying ${diffResult.operationsApplied} successful operations despite ${diffResult.failed?.length || 0} failures`); 101 | // Continue to update workflow with partial changes 102 | } else { 103 | // Complete failure - return error 104 | return { 105 | success: false, 106 | error: 'Failed to apply diff operations', 107 | details: { 108 | errors: diffResult.errors, 109 | operationsApplied: diffResult.operationsApplied, 110 | applied: diffResult.applied, 111 | failed: diffResult.failed 112 | } 113 | }; 114 | } 115 | } 116 | 117 | // If validateOnly, return validation result 118 | if (input.validateOnly) { 119 | return { 120 | success: true, 121 | message: diffResult.message, 122 | data: { 123 | valid: true, 124 | operationsToApply: input.operations.length 125 | } 126 | }; 127 | } 128 | 129 | // Update workflow via API 130 | try { 131 | const updatedWorkflow = await client.updateWorkflow(input.id, diffResult.workflow!); 132 | 133 | return { 134 | success: true, 135 | data: updatedWorkflow, 136 | message: `Workflow "${updatedWorkflow.name}" updated successfully. Applied ${diffResult.operationsApplied} operations.`, 137 | details: { 138 | operationsApplied: diffResult.operationsApplied, 139 | workflowId: updatedWorkflow.id, 140 | workflowName: updatedWorkflow.name, 141 | applied: diffResult.applied, 142 | failed: diffResult.failed, 143 | errors: diffResult.errors 144 | } 145 | }; 146 | } catch (error) { 147 | if (error instanceof N8nApiError) { 148 | return { 149 | success: false, 150 | error: getUserFriendlyErrorMessage(error), 151 | code: error.code, 152 | details: error.details as Record<string, unknown> | undefined 153 | }; 154 | } 155 | throw error; 156 | } 157 | } catch (error) { 158 | if (error instanceof z.ZodError) { 159 | return { 160 | success: false, 161 | error: 'Invalid input', 162 | details: { errors: error.errors } 163 | }; 164 | } 165 | 166 | logger.error('Failed to update partial workflow', error); 167 | return { 168 | success: false, 169 | error: error instanceof Error ? error.message : 'Unknown error occurred' 170 | }; 171 | } 172 | } 173 | 174 | ``` -------------------------------------------------------------------------------- /tests/test-enhanced-integration.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | const { DocumentationFetcher } = require('../dist/utils/documentation-fetcher'); 4 | const { NodeDocumentationService } = require('../dist/services/node-documentation-service'); 5 | 6 | async function testEnhancedIntegration() { 7 | console.log('🧪 Testing Enhanced Documentation Integration...\n'); 8 | 9 | // Test 1: DocumentationFetcher backward compatibility 10 | console.log('1️⃣ Testing DocumentationFetcher backward compatibility...'); 11 | const docFetcher = new DocumentationFetcher(); 12 | 13 | try { 14 | // Test getNodeDocumentation (backward compatible method) 15 | const simpleDoc = await docFetcher.getNodeDocumentation('n8n-nodes-base.slack'); 16 | if (simpleDoc) { 17 | console.log(' ✅ Simple documentation format works'); 18 | console.log(` - Has markdown: ${!!simpleDoc.markdown}`); 19 | console.log(` - Has URL: ${!!simpleDoc.url}`); 20 | console.log(` - Has examples: ${simpleDoc.examples?.length || 0}`); 21 | } 22 | 23 | // Test getEnhancedNodeDocumentation (new method) 24 | const enhancedDoc = await docFetcher.getEnhancedNodeDocumentation('n8n-nodes-base.slack'); 25 | if (enhancedDoc) { 26 | console.log(' ✅ Enhanced documentation format works'); 27 | console.log(` - Title: ${enhancedDoc.title || 'N/A'}`); 28 | console.log(` - Operations: ${enhancedDoc.operations?.length || 0}`); 29 | console.log(` - API Methods: ${enhancedDoc.apiMethods?.length || 0}`); 30 | console.log(` - Examples: ${enhancedDoc.examples?.length || 0}`); 31 | console.log(` - Templates: ${enhancedDoc.templates?.length || 0}`); 32 | console.log(` - Related Resources: ${enhancedDoc.relatedResources?.length || 0}`); 33 | } 34 | } catch (error) { 35 | console.error(' ❌ DocumentationFetcher test failed:', error.message); 36 | } 37 | 38 | // Test 2: NodeDocumentationService with enhanced fields 39 | console.log('\n2️⃣ Testing NodeDocumentationService enhanced schema...'); 40 | const docService = new NodeDocumentationService('data/test-enhanced-docs.db'); 41 | 42 | try { 43 | // Store a test node with enhanced documentation 44 | const testNode = { 45 | nodeType: 'test.enhanced-node', 46 | name: 'enhanced-node', 47 | displayName: 'Enhanced Test Node', 48 | description: 'A test node with enhanced documentation', 49 | sourceCode: 'const testCode = "example";', 50 | packageName: 'test-package', 51 | documentation: '# Test Documentation', 52 | documentationUrl: 'https://example.com/docs', 53 | documentationTitle: 'Enhanced Test Node Documentation', 54 | operations: [ 55 | { 56 | resource: 'Message', 57 | operation: 'Send', 58 | description: 'Send a message' 59 | } 60 | ], 61 | apiMethods: [ 62 | { 63 | resource: 'Message', 64 | operation: 'Send', 65 | apiMethod: 'chat.postMessage', 66 | apiUrl: 'https://api.slack.com/methods/chat.postMessage' 67 | } 68 | ], 69 | documentationExamples: [ 70 | { 71 | title: 'Send Message Example', 72 | type: 'json', 73 | code: '{"text": "Hello World"}' 74 | } 75 | ], 76 | templates: [ 77 | { 78 | name: 'Basic Message Template', 79 | description: 'Simple message sending template' 80 | } 81 | ], 82 | relatedResources: [ 83 | { 84 | title: 'API Documentation', 85 | url: 'https://api.slack.com', 86 | type: 'api' 87 | } 88 | ], 89 | requiredScopes: ['chat:write'], 90 | hasCredentials: true, 91 | isTrigger: false, 92 | isWebhook: false 93 | }; 94 | 95 | await docService.storeNode(testNode); 96 | console.log(' ✅ Stored node with enhanced documentation'); 97 | 98 | // Retrieve and verify 99 | const retrieved = await docService.getNodeInfo('test.enhanced-node'); 100 | if (retrieved) { 101 | console.log(' ✅ Retrieved node with enhanced fields:'); 102 | console.log(` - Has operations: ${!!retrieved.operations}`); 103 | console.log(` - Has API methods: ${!!retrieved.apiMethods}`); 104 | console.log(` - Has documentation examples: ${!!retrieved.documentationExamples}`); 105 | console.log(` - Has templates: ${!!retrieved.templates}`); 106 | console.log(` - Has related resources: ${!!retrieved.relatedResources}`); 107 | console.log(` - Has required scopes: ${!!retrieved.requiredScopes}`); 108 | } 109 | 110 | // Test search 111 | const searchResults = await docService.searchNodes({ query: 'enhanced' }); 112 | console.log(` ✅ Search found ${searchResults.length} results`); 113 | 114 | } catch (error) { 115 | console.error(' ❌ NodeDocumentationService test failed:', error.message); 116 | } finally { 117 | docService.close(); 118 | } 119 | 120 | // Test 3: MCP Server integration 121 | console.log('\n3️⃣ Testing MCP Server integration...'); 122 | try { 123 | const { N8NMCPServer } = require('../dist/mcp/server'); 124 | console.log(' ✅ MCP Server loads with enhanced documentation support'); 125 | 126 | // Check if new tools are available 127 | const { n8nTools } = require('../dist/mcp/tools'); 128 | const enhancedTools = [ 129 | 'get_node_documentation', 130 | 'search_node_documentation', 131 | 'get_node_operations', 132 | 'get_node_examples' 133 | ]; 134 | 135 | const hasAllTools = enhancedTools.every(toolName => 136 | n8nTools.some(tool => tool.name === toolName) 137 | ); 138 | 139 | if (hasAllTools) { 140 | console.log(' ✅ All enhanced documentation tools are available'); 141 | enhancedTools.forEach(toolName => { 142 | const tool = n8nTools.find(t => t.name === toolName); 143 | console.log(` - ${toolName}: ${tool.description}`); 144 | }); 145 | } else { 146 | console.log(' ⚠️ Some enhanced tools are missing'); 147 | } 148 | 149 | } catch (error) { 150 | console.error(' ❌ MCP Server integration test failed:', error.message); 151 | } 152 | 153 | console.log('\n✨ Enhanced documentation integration tests completed!'); 154 | 155 | // Cleanup 156 | await docFetcher.cleanup(); 157 | } 158 | 159 | // Run tests 160 | testEnhancedIntegration().catch(error => { 161 | console.error('Fatal error:', error); 162 | process.exit(1); 163 | }); ``` -------------------------------------------------------------------------------- /tests/integration/n8n-api/utils/credentials.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Integration Test Credentials Management 3 | * 4 | * Provides environment-aware credential loading for integration tests. 5 | * - Local development: Reads from .env file 6 | * - CI/GitHub Actions: Uses GitHub secrets from process.env 7 | */ 8 | 9 | import dotenv from 'dotenv'; 10 | import path from 'path'; 11 | 12 | // Load .env file for local development 13 | dotenv.config({ path: path.resolve(process.cwd(), '.env') }); 14 | 15 | export interface N8nTestCredentials { 16 | url: string; 17 | apiKey: string; 18 | webhookUrls: { 19 | get: string; 20 | post: string; 21 | put: string; 22 | delete: string; 23 | }; 24 | cleanup: { 25 | enabled: boolean; 26 | tag: string; 27 | namePrefix: string; 28 | }; 29 | } 30 | 31 | /** 32 | * Get n8n credentials for integration tests 33 | * 34 | * Automatically detects environment (local vs CI) and loads 35 | * credentials from the appropriate source. 36 | * 37 | * @returns N8nTestCredentials 38 | * @throws Error if required credentials are missing 39 | */ 40 | export function getN8nCredentials(): N8nTestCredentials { 41 | if (process.env.CI) { 42 | // CI: Use GitHub secrets - validate required variables first 43 | const url = process.env.N8N_API_URL; 44 | const apiKey = process.env.N8N_API_KEY; 45 | 46 | if (!url || !apiKey) { 47 | throw new Error( 48 | 'Missing required CI credentials:\n' + 49 | ` N8N_API_URL: ${url ? 'set' : 'MISSING'}\n` + 50 | ` N8N_API_KEY: ${apiKey ? 'set' : 'MISSING'}\n` + 51 | 'Please configure GitHub secrets for integration tests.' 52 | ); 53 | } 54 | 55 | return { 56 | url, 57 | apiKey, 58 | webhookUrls: { 59 | get: process.env.N8N_TEST_WEBHOOK_GET_URL || '', 60 | post: process.env.N8N_TEST_WEBHOOK_POST_URL || '', 61 | put: process.env.N8N_TEST_WEBHOOK_PUT_URL || '', 62 | delete: process.env.N8N_TEST_WEBHOOK_DELETE_URL || '' 63 | }, 64 | cleanup: { 65 | enabled: true, 66 | tag: 'mcp-integration-test', 67 | namePrefix: '[MCP-TEST]' 68 | } 69 | }; 70 | } else { 71 | // Local: Use .env file - validate required variables first 72 | const url = process.env.N8N_API_URL; 73 | const apiKey = process.env.N8N_API_KEY; 74 | 75 | if (!url || !apiKey) { 76 | throw new Error( 77 | 'Missing required credentials in .env:\n' + 78 | ` N8N_API_URL: ${url ? 'set' : 'MISSING'}\n` + 79 | ` N8N_API_KEY: ${apiKey ? 'set' : 'MISSING'}\n\n` + 80 | 'Please add these to your .env file.\n' + 81 | 'See .env.example for configuration details.' 82 | ); 83 | } 84 | 85 | return { 86 | url, 87 | apiKey, 88 | webhookUrls: { 89 | get: process.env.N8N_TEST_WEBHOOK_GET_URL || '', 90 | post: process.env.N8N_TEST_WEBHOOK_POST_URL || '', 91 | put: process.env.N8N_TEST_WEBHOOK_PUT_URL || '', 92 | delete: process.env.N8N_TEST_WEBHOOK_DELETE_URL || '' 93 | }, 94 | cleanup: { 95 | enabled: process.env.N8N_TEST_CLEANUP_ENABLED !== 'false', 96 | tag: process.env.N8N_TEST_TAG || 'mcp-integration-test', 97 | namePrefix: process.env.N8N_TEST_NAME_PREFIX || '[MCP-TEST]' 98 | } 99 | }; 100 | } 101 | } 102 | 103 | /** 104 | * Validate that required credentials are present 105 | * 106 | * @param creds - Credentials to validate 107 | * @throws Error if required credentials are missing 108 | */ 109 | export function validateCredentials(creds: N8nTestCredentials): void { 110 | const missing: string[] = []; 111 | 112 | if (!creds.url) { 113 | missing.push(process.env.CI ? 'N8N_URL' : 'N8N_API_URL'); 114 | } 115 | if (!creds.apiKey) { 116 | missing.push('N8N_API_KEY'); 117 | } 118 | 119 | if (missing.length > 0) { 120 | throw new Error( 121 | `Missing required n8n credentials: ${missing.join(', ')}\n\n` + 122 | `Please set the following environment variables:\n` + 123 | missing.map(v => ` ${v}`).join('\n') + '\n\n' + 124 | `See .env.example for configuration details.` 125 | ); 126 | } 127 | } 128 | 129 | /** 130 | * Validate that webhook URLs are configured 131 | * 132 | * @param creds - Credentials to validate 133 | * @throws Error with setup instructions if webhook URLs are missing 134 | */ 135 | export function validateWebhookUrls(creds: N8nTestCredentials): void { 136 | const missing: string[] = []; 137 | 138 | if (!creds.webhookUrls.get) missing.push('GET'); 139 | if (!creds.webhookUrls.post) missing.push('POST'); 140 | if (!creds.webhookUrls.put) missing.push('PUT'); 141 | if (!creds.webhookUrls.delete) missing.push('DELETE'); 142 | 143 | if (missing.length > 0) { 144 | const envVars = missing.map(m => `N8N_TEST_WEBHOOK_${m}_URL`); 145 | 146 | throw new Error( 147 | `Missing webhook URLs for HTTP methods: ${missing.join(', ')}\n\n` + 148 | `Webhook testing requires pre-activated workflows in n8n.\n` + 149 | `n8n API doesn't support workflow activation, so these must be created manually.\n\n` + 150 | `Setup Instructions:\n` + 151 | `1. Create ${missing.length} workflow(s) in your n8n instance\n` + 152 | `2. Each workflow should have a single Webhook node\n` + 153 | `3. Configure webhook paths:\n` + 154 | missing.map(m => ` - ${m}: mcp-test-${m.toLowerCase()}`).join('\n') + '\n' + 155 | `4. ACTIVATE each workflow in n8n UI\n` + 156 | `5. Set the following environment variables with full webhook URLs:\n` + 157 | envVars.map(v => ` ${v}=<full-webhook-url>`).join('\n') + '\n\n' + 158 | `Example: N8N_TEST_WEBHOOK_GET_URL=https://n8n-test.n8n-mcp.com/webhook/mcp-test-get\n\n` + 159 | `See docs/local/integration-testing-plan.md for detailed instructions.` 160 | ); 161 | } 162 | } 163 | 164 | /** 165 | * Check if credentials are configured (non-throwing version) 166 | * 167 | * @returns true if basic credentials are available 168 | */ 169 | export function hasCredentials(): boolean { 170 | try { 171 | const creds = getN8nCredentials(); 172 | return !!(creds.url && creds.apiKey); 173 | } catch { 174 | return false; 175 | } 176 | } 177 | 178 | /** 179 | * Check if webhook URLs are configured (non-throwing version) 180 | * 181 | * @returns true if all webhook URLs are available 182 | */ 183 | export function hasWebhookUrls(): boolean { 184 | try { 185 | const creds = getN8nCredentials(); 186 | return !!( 187 | creds.webhookUrls.get && 188 | creds.webhookUrls.post && 189 | creds.webhookUrls.put && 190 | creds.webhookUrls.delete 191 | ); 192 | } catch { 193 | return false; 194 | } 195 | } 196 | ``` -------------------------------------------------------------------------------- /src/utils/npm-version-checker.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * NPM Version Checker Utility 3 | * 4 | * Checks if the current n8n-mcp version is outdated by comparing 5 | * against the latest version published on npm. 6 | */ 7 | 8 | import { logger } from './logger'; 9 | 10 | /** 11 | * NPM Registry Response structure 12 | * Based on npm registry JSON format for package metadata 13 | */ 14 | interface NpmRegistryResponse { 15 | version: string; 16 | [key: string]: unknown; 17 | } 18 | 19 | export interface VersionCheckResult { 20 | currentVersion: string; 21 | latestVersion: string | null; 22 | isOutdated: boolean; 23 | updateAvailable: boolean; 24 | error: string | null; 25 | checkedAt: Date; 26 | updateCommand?: string; 27 | } 28 | 29 | // Cache for version check to avoid excessive npm requests 30 | let versionCheckCache: VersionCheckResult | null = null; 31 | let lastCheckTime: number = 0; 32 | const CACHE_TTL_MS = 1 * 60 * 60 * 1000; // 1 hour cache 33 | 34 | /** 35 | * Check if current version is outdated compared to npm registry 36 | * Uses caching to avoid excessive npm API calls 37 | * 38 | * @param forceRefresh - Force a fresh check, bypassing cache 39 | * @returns Version check result 40 | */ 41 | export async function checkNpmVersion(forceRefresh: boolean = false): Promise<VersionCheckResult> { 42 | const now = Date.now(); 43 | 44 | // Return cached result if available and not expired 45 | if (!forceRefresh && versionCheckCache && (now - lastCheckTime) < CACHE_TTL_MS) { 46 | logger.debug('Returning cached npm version check result'); 47 | return versionCheckCache; 48 | } 49 | 50 | // Get current version from package.json 51 | const packageJson = require('../../package.json'); 52 | const currentVersion = packageJson.version; 53 | 54 | try { 55 | // Fetch latest version from npm registry 56 | const response = await fetch('https://registry.npmjs.org/n8n-mcp/latest', { 57 | headers: { 58 | 'Accept': 'application/json', 59 | }, 60 | signal: AbortSignal.timeout(5000) // 5 second timeout 61 | }); 62 | 63 | if (!response.ok) { 64 | logger.warn('Failed to fetch npm version info', { 65 | status: response.status, 66 | statusText: response.statusText 67 | }); 68 | 69 | const result: VersionCheckResult = { 70 | currentVersion, 71 | latestVersion: null, 72 | isOutdated: false, 73 | updateAvailable: false, 74 | error: `npm registry returned ${response.status}`, 75 | checkedAt: new Date() 76 | }; 77 | 78 | versionCheckCache = result; 79 | lastCheckTime = now; 80 | return result; 81 | } 82 | 83 | // Parse and validate JSON response 84 | let data: unknown; 85 | try { 86 | data = await response.json(); 87 | } catch (error) { 88 | throw new Error('Failed to parse npm registry response as JSON'); 89 | } 90 | 91 | // Validate response structure 92 | if (!data || typeof data !== 'object' || !('version' in data)) { 93 | throw new Error('Invalid response format from npm registry'); 94 | } 95 | 96 | const registryData = data as NpmRegistryResponse; 97 | const latestVersion = registryData.version; 98 | 99 | // Validate version format (semver: x.y.z or x.y.z-prerelease) 100 | if (!latestVersion || !/^\d+\.\d+\.\d+/.test(latestVersion)) { 101 | throw new Error(`Invalid version format from npm registry: ${latestVersion}`); 102 | } 103 | 104 | // Compare versions 105 | const isOutdated = compareVersions(currentVersion, latestVersion) < 0; 106 | 107 | const result: VersionCheckResult = { 108 | currentVersion, 109 | latestVersion, 110 | isOutdated, 111 | updateAvailable: isOutdated, 112 | error: null, 113 | checkedAt: new Date(), 114 | updateCommand: isOutdated ? `npm install -g n8n-mcp@${latestVersion}` : undefined 115 | }; 116 | 117 | // Cache the result 118 | versionCheckCache = result; 119 | lastCheckTime = now; 120 | 121 | logger.debug('npm version check completed', { 122 | current: currentVersion, 123 | latest: latestVersion, 124 | outdated: isOutdated 125 | }); 126 | 127 | return result; 128 | 129 | } catch (error) { 130 | logger.warn('Error checking npm version', { 131 | error: error instanceof Error ? error.message : String(error) 132 | }); 133 | 134 | const result: VersionCheckResult = { 135 | currentVersion, 136 | latestVersion: null, 137 | isOutdated: false, 138 | updateAvailable: false, 139 | error: error instanceof Error ? error.message : 'Unknown error', 140 | checkedAt: new Date() 141 | }; 142 | 143 | // Cache error result to avoid rapid retry 144 | versionCheckCache = result; 145 | lastCheckTime = now; 146 | 147 | return result; 148 | } 149 | } 150 | 151 | /** 152 | * Compare two semantic version strings 153 | * Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2 154 | * 155 | * @param v1 - First version (e.g., "1.2.3") 156 | * @param v2 - Second version (e.g., "1.3.0") 157 | * @returns Comparison result 158 | */ 159 | export function compareVersions(v1: string, v2: string): number { 160 | // Remove 'v' prefix if present 161 | const clean1 = v1.replace(/^v/, ''); 162 | const clean2 = v2.replace(/^v/, ''); 163 | 164 | // Split into parts and convert to numbers 165 | const parts1 = clean1.split('.').map(n => parseInt(n, 10) || 0); 166 | const parts2 = clean2.split('.').map(n => parseInt(n, 10) || 0); 167 | 168 | // Compare each part 169 | for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { 170 | const p1 = parts1[i] || 0; 171 | const p2 = parts2[i] || 0; 172 | 173 | if (p1 < p2) return -1; 174 | if (p1 > p2) return 1; 175 | } 176 | 177 | return 0; // Versions are equal 178 | } 179 | 180 | /** 181 | * Clear the version check cache (useful for testing) 182 | */ 183 | export function clearVersionCheckCache(): void { 184 | versionCheckCache = null; 185 | lastCheckTime = 0; 186 | } 187 | 188 | /** 189 | * Format version check result as a user-friendly message 190 | * 191 | * @param result - Version check result 192 | * @returns Formatted message 193 | */ 194 | export function formatVersionMessage(result: VersionCheckResult): string { 195 | if (result.error) { 196 | return `Version check failed: ${result.error}. Current version: ${result.currentVersion}`; 197 | } 198 | 199 | if (!result.latestVersion) { 200 | return `Current version: ${result.currentVersion} (latest version unknown)`; 201 | } 202 | 203 | if (result.isOutdated) { 204 | return `⚠️ Update available! Current: ${result.currentVersion} → Latest: ${result.latestVersion}`; 205 | } 206 | 207 | return `✓ You're up to date! Current version: ${result.currentVersion}`; 208 | } 209 | ``` -------------------------------------------------------------------------------- /scripts/test-ai-validation-debug.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | /** 3 | * Debug test for AI validation issues 4 | * Reproduces the bugs found by n8n-mcp-tester 5 | */ 6 | 7 | import { validateAISpecificNodes, buildReverseConnectionMap } from '../src/services/ai-node-validator'; 8 | import type { WorkflowJson } from '../src/services/ai-tool-validators'; 9 | import { NodeTypeNormalizer } from '../src/utils/node-type-normalizer'; 10 | 11 | console.log('=== AI Validation Debug Tests ===\n'); 12 | 13 | // Test 1: AI Agent with NO language model connection 14 | console.log('Test 1: Missing Language Model Detection'); 15 | const workflow1: WorkflowJson = { 16 | name: 'Test Missing LM', 17 | nodes: [ 18 | { 19 | id: 'ai-agent-1', 20 | name: 'AI Agent', 21 | type: '@n8n/n8n-nodes-langchain.agent', 22 | position: [500, 300], 23 | parameters: { 24 | promptType: 'define', 25 | text: 'You are a helpful assistant' 26 | }, 27 | typeVersion: 1.7 28 | } 29 | ], 30 | connections: { 31 | // NO connections - AI Agent is isolated 32 | } 33 | }; 34 | 35 | console.log('Workflow:', JSON.stringify(workflow1, null, 2)); 36 | 37 | const reverseMap1 = buildReverseConnectionMap(workflow1); 38 | console.log('\nReverse connection map for AI Agent:'); 39 | console.log('Entries:', Array.from(reverseMap1.entries())); 40 | console.log('AI Agent connections:', reverseMap1.get('AI Agent')); 41 | 42 | // Check node normalization 43 | const normalizedType1 = NodeTypeNormalizer.normalizeToFullForm(workflow1.nodes[0].type); 44 | console.log(`\nNode type: ${workflow1.nodes[0].type}`); 45 | console.log(`Normalized type: ${normalizedType1}`); 46 | console.log(`Match check: ${normalizedType1 === '@n8n/n8n-nodes-langchain.agent'}`); 47 | 48 | const issues1 = validateAISpecificNodes(workflow1); 49 | console.log('\nValidation issues:'); 50 | console.log(JSON.stringify(issues1, null, 2)); 51 | 52 | const hasMissingLMError = issues1.some( 53 | i => i.severity === 'error' && i.code === 'MISSING_LANGUAGE_MODEL' 54 | ); 55 | console.log(`\n✓ Has MISSING_LANGUAGE_MODEL error: ${hasMissingLMError}`); 56 | console.log(`✗ Expected: true, Got: ${hasMissingLMError}`); 57 | 58 | // Test 2: AI Agent WITH language model connection 59 | console.log('\n\n' + '='.repeat(60)); 60 | console.log('Test 2: AI Agent WITH Language Model (Should be valid)'); 61 | const workflow2: WorkflowJson = { 62 | name: 'Test With LM', 63 | nodes: [ 64 | { 65 | id: 'openai-1', 66 | name: 'OpenAI Chat Model', 67 | type: '@n8n/n8n-nodes-langchain.lmChatOpenAi', 68 | position: [200, 300], 69 | parameters: { 70 | modelName: 'gpt-4' 71 | }, 72 | typeVersion: 1 73 | }, 74 | { 75 | id: 'ai-agent-1', 76 | name: 'AI Agent', 77 | type: '@n8n/n8n-nodes-langchain.agent', 78 | position: [500, 300], 79 | parameters: { 80 | promptType: 'define', 81 | text: 'You are a helpful assistant' 82 | }, 83 | typeVersion: 1.7 84 | } 85 | ], 86 | connections: { 87 | 'OpenAI Chat Model': { 88 | ai_languageModel: [ 89 | [ 90 | { 91 | node: 'AI Agent', 92 | type: 'ai_languageModel', 93 | index: 0 94 | } 95 | ] 96 | ] 97 | } 98 | } 99 | }; 100 | 101 | console.log('\nConnections:', JSON.stringify(workflow2.connections, null, 2)); 102 | 103 | const reverseMap2 = buildReverseConnectionMap(workflow2); 104 | console.log('\nReverse connection map for AI Agent:'); 105 | console.log('AI Agent connections:', reverseMap2.get('AI Agent')); 106 | 107 | const issues2 = validateAISpecificNodes(workflow2); 108 | console.log('\nValidation issues:'); 109 | console.log(JSON.stringify(issues2, null, 2)); 110 | 111 | const hasMissingLMError2 = issues2.some( 112 | i => i.severity === 'error' && i.code === 'MISSING_LANGUAGE_MODEL' 113 | ); 114 | console.log(`\n✓ Should NOT have MISSING_LANGUAGE_MODEL error: ${!hasMissingLMError2}`); 115 | console.log(`Expected: false, Got: ${hasMissingLMError2}`); 116 | 117 | // Test 3: AI Agent with tools but no language model 118 | console.log('\n\n' + '='.repeat(60)); 119 | console.log('Test 3: AI Agent with Tools but NO Language Model'); 120 | const workflow3: WorkflowJson = { 121 | name: 'Test Tools No LM', 122 | nodes: [ 123 | { 124 | id: 'http-tool-1', 125 | name: 'HTTP Request Tool', 126 | type: '@n8n/n8n-nodes-langchain.toolHttpRequest', 127 | position: [200, 300], 128 | parameters: { 129 | toolDescription: 'Calls an API', 130 | url: 'https://api.example.com' 131 | }, 132 | typeVersion: 1.1 133 | }, 134 | { 135 | id: 'ai-agent-1', 136 | name: 'AI Agent', 137 | type: '@n8n/n8n-nodes-langchain.agent', 138 | position: [500, 300], 139 | parameters: { 140 | promptType: 'define', 141 | text: 'You are a helpful assistant' 142 | }, 143 | typeVersion: 1.7 144 | } 145 | ], 146 | connections: { 147 | 'HTTP Request Tool': { 148 | ai_tool: [ 149 | [ 150 | { 151 | node: 'AI Agent', 152 | type: 'ai_tool', 153 | index: 0 154 | } 155 | ] 156 | ] 157 | } 158 | } 159 | }; 160 | 161 | console.log('\nConnections:', JSON.stringify(workflow3.connections, null, 2)); 162 | 163 | const reverseMap3 = buildReverseConnectionMap(workflow3); 164 | console.log('\nReverse connection map for AI Agent:'); 165 | const aiAgentConns = reverseMap3.get('AI Agent'); 166 | console.log('AI Agent connections:', aiAgentConns); 167 | console.log('Connection types:', aiAgentConns?.map(c => c.type)); 168 | 169 | const issues3 = validateAISpecificNodes(workflow3); 170 | console.log('\nValidation issues:'); 171 | console.log(JSON.stringify(issues3, null, 2)); 172 | 173 | const hasMissingLMError3 = issues3.some( 174 | i => i.severity === 'error' && i.code === 'MISSING_LANGUAGE_MODEL' 175 | ); 176 | const hasNoToolsInfo3 = issues3.some( 177 | i => i.severity === 'info' && i.message.includes('no ai_tool connections') 178 | ); 179 | 180 | console.log(`\n✓ Should have MISSING_LANGUAGE_MODEL error: ${hasMissingLMError3}`); 181 | console.log(`Expected: true, Got: ${hasMissingLMError3}`); 182 | console.log(`✗ Should NOT have "no tools" info: ${!hasNoToolsInfo3}`); 183 | console.log(`Expected: false, Got: ${hasNoToolsInfo3}`); 184 | 185 | console.log('\n' + '='.repeat(60)); 186 | console.log('Summary:'); 187 | console.log(`Test 1 (No LM): ${hasMissingLMError ? 'PASS ✓' : 'FAIL ✗'}`); 188 | console.log(`Test 2 (With LM): ${!hasMissingLMError2 ? 'PASS ✓' : 'FAIL ✗'}`); 189 | console.log(`Test 3 (Tools, No LM): ${hasMissingLMError3 && !hasNoToolsInfo3 ? 'PASS ✓' : 'FAIL ✗'}`); 190 | ``` -------------------------------------------------------------------------------- /scripts/update-and-publish-prep.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | # Comprehensive script to update n8n dependencies, run tests, and prepare for npm publish 3 | # Based on MEMORY_N8N_UPDATE.md but enhanced with test suite and publish preparation 4 | 5 | set -e 6 | 7 | # Color codes for output 8 | RED='\033[0;31m' 9 | GREEN='\033[0;32m' 10 | YELLOW='\033[1;33m' 11 | BLUE='\033[0;34m' 12 | NC='\033[0m' # No Color 13 | 14 | echo -e "${BLUE}🚀 n8n Update and Publish Preparation Script${NC}" 15 | echo "==============================================" 16 | echo "" 17 | 18 | # 1. Check current branch 19 | CURRENT_BRANCH=$(git branch --show-current) 20 | if [ "$CURRENT_BRANCH" != "main" ]; then 21 | echo -e "${YELLOW}⚠️ Warning: Not on main branch (current: $CURRENT_BRANCH)${NC}" 22 | echo "It's recommended to run this on the main branch." 23 | read -p "Continue anyway? (y/N) " -n 1 -r 24 | echo 25 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 26 | exit 1 27 | fi 28 | fi 29 | 30 | # 2. Check for uncommitted changes 31 | if ! git diff-index --quiet HEAD --; then 32 | echo -e "${RED}❌ Error: You have uncommitted changes${NC}" 33 | echo "Please commit or stash your changes before updating." 34 | exit 1 35 | fi 36 | 37 | # 3. Get current versions for comparison 38 | echo -e "${BLUE}📊 Current versions:${NC}" 39 | CURRENT_N8N=$(node -e "console.log(require('./package.json').dependencies['n8n'])" 2>/dev/null || echo "not installed") 40 | CURRENT_PROJECT=$(node -e "console.log(require('./package.json').version)") 41 | echo "- n8n: $CURRENT_N8N" 42 | echo "- n8n-mcp: $CURRENT_PROJECT" 43 | echo "" 44 | 45 | # 4. Check for updates first 46 | echo -e "${BLUE}🔍 Checking for n8n updates...${NC}" 47 | npm run update:n8n:check 48 | 49 | echo "" 50 | read -p "Do you want to proceed with the update? (y/N) " -n 1 -r 51 | echo 52 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 53 | echo "Update cancelled." 54 | exit 0 55 | fi 56 | 57 | # 5. Update n8n dependencies 58 | echo "" 59 | echo -e "${BLUE}📦 Updating n8n dependencies...${NC}" 60 | npm run update:n8n 61 | 62 | # 6. Run the test suite 63 | echo "" 64 | echo -e "${BLUE}🧪 Running comprehensive test suite (1,182 tests)...${NC}" 65 | npm test 66 | if [ $? -ne 0 ]; then 67 | echo -e "${RED}❌ Tests failed! Please fix failing tests before proceeding.${NC}" 68 | exit 1 69 | fi 70 | echo -e "${GREEN}✅ All tests passed!${NC}" 71 | 72 | # 7. Run validation 73 | echo "" 74 | echo -e "${BLUE}✔️ Validating critical nodes...${NC}" 75 | npm run validate 76 | 77 | # 8. Build the project 78 | echo "" 79 | echo -e "${BLUE}🔨 Building project...${NC}" 80 | npm run build 81 | 82 | # 9. Bump version 83 | echo "" 84 | echo -e "${BLUE}📌 Bumping version...${NC}" 85 | # Get new n8n version 86 | NEW_N8N=$(node -e "console.log(require('./package.json').dependencies['n8n'])") 87 | # Bump patch version 88 | npm version patch --no-git-tag-version 89 | 90 | # Get new project version 91 | NEW_PROJECT=$(node -e "console.log(require('./package.json').version)") 92 | 93 | # 10. Update n8n version badge in README 94 | echo "" 95 | echo -e "${BLUE}📝 Updating n8n version badge...${NC}" 96 | sed -i.bak "s/n8n-v[0-9.]*/n8n-$NEW_N8N/" README.md && rm README.md.bak 97 | 98 | # 11. Sync runtime version (this also updates the version badge in README) 99 | echo "" 100 | echo -e "${BLUE}🔄 Syncing runtime version and updating version badge...${NC}" 101 | npm run sync:runtime-version 102 | 103 | # 12. Get update details for commit message 104 | echo "" 105 | echo -e "${BLUE}📊 Gathering update information...${NC}" 106 | # Get all n8n package versions 107 | N8N_CORE=$(node -e "console.log(require('./package.json').dependencies['n8n-core'])") 108 | N8N_WORKFLOW=$(node -e "console.log(require('./package.json').dependencies['n8n-workflow'])") 109 | N8N_LANGCHAIN=$(node -e "console.log(require('./package.json').dependencies['@n8n/n8n-nodes-langchain'])") 110 | 111 | # Get node count from database 112 | NODE_COUNT=$(node -e " 113 | const Database = require('better-sqlite3'); 114 | const db = new Database('./data/nodes.db', { readonly: true }); 115 | const count = db.prepare('SELECT COUNT(*) as count FROM nodes').get().count; 116 | console.log(count); 117 | db.close(); 118 | " 2>/dev/null || echo "unknown") 119 | 120 | # Check if templates were sanitized 121 | TEMPLATES_SANITIZED=false 122 | if [ -f "./data/nodes.db" ]; then 123 | TEMPLATE_COUNT=$(node -e " 124 | const Database = require('better-sqlite3'); 125 | const db = new Database('./data/nodes.db', { readonly: true }); 126 | const count = db.prepare('SELECT COUNT(*) as count FROM templates').get().count; 127 | console.log(count); 128 | db.close(); 129 | " 2>/dev/null || echo "0") 130 | if [ "$TEMPLATE_COUNT" != "0" ]; then 131 | TEMPLATES_SANITIZED=true 132 | fi 133 | fi 134 | 135 | # 13. Create commit message 136 | echo "" 137 | echo -e "${BLUE}📝 Creating commit...${NC}" 138 | COMMIT_MSG="chore: update n8n to $NEW_N8N and bump version to $NEW_PROJECT 139 | 140 | - Updated n8n to $NEW_N8N 141 | - Updated n8n-core to $N8N_CORE 142 | - Updated n8n-workflow to $N8N_WORKFLOW 143 | - Updated @n8n/n8n-nodes-langchain to $N8N_LANGCHAIN 144 | - Rebuilt node database with $NODE_COUNT nodes" 145 | 146 | if [ "$TEMPLATES_SANITIZED" = true ]; then 147 | COMMIT_MSG="$COMMIT_MSG 148 | - Sanitized $TEMPLATE_COUNT workflow templates" 149 | fi 150 | 151 | COMMIT_MSG="$COMMIT_MSG 152 | - All 1,182 tests passing (933 unit, 249 integration) 153 | - All validation tests passing 154 | - Built and prepared for npm publish 155 | 156 | 🤖 Generated with [Claude Code](https://claude.ai/code) 157 | 158 | Co-Authored-By: Claude <[email protected]>" 159 | 160 | # 14. Stage all changes 161 | git add -A 162 | 163 | # 15. Show what will be committed 164 | echo "" 165 | echo -e "${BLUE}📋 Changes to be committed:${NC}" 166 | git status --short 167 | 168 | # 16. Commit changes 169 | git commit -m "$COMMIT_MSG" 170 | 171 | # 17. Summary 172 | echo "" 173 | echo -e "${GREEN}✅ Update completed successfully!${NC}" 174 | echo "" 175 | echo -e "${BLUE}Summary:${NC}" 176 | echo "- Updated n8n from $CURRENT_N8N to $NEW_N8N" 177 | echo "- Bumped version from $CURRENT_PROJECT to $NEW_PROJECT" 178 | echo "- All 1,182 tests passed" 179 | echo "- Project built and ready for npm publish" 180 | echo "" 181 | echo -e "${YELLOW}Next steps:${NC}" 182 | echo "1. Push to GitHub:" 183 | echo -e " ${GREEN}git push origin $CURRENT_BRANCH${NC}" 184 | echo "" 185 | echo "2. Create a GitHub release (after push):" 186 | echo -e " ${GREEN}gh release create v$NEW_PROJECT --title \"v$NEW_PROJECT\" --notes \"Updated n8n to $NEW_N8N\"${NC}" 187 | echo "" 188 | echo "3. Publish to npm:" 189 | echo -e " ${GREEN}npm run prepare:publish${NC}" 190 | echo " Then follow the instructions to publish with OTP" 191 | echo "" 192 | echo -e "${BLUE}🎉 Done!${NC}" ``` -------------------------------------------------------------------------------- /scripts/test-node-type-validation.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env tsx 2 | 3 | /** 4 | * Test script for node type validation 5 | * Tests the improvements to catch invalid node types like "nodes-base.webhook" 6 | */ 7 | 8 | import { WorkflowValidator } from '../src/services/workflow-validator'; 9 | import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator'; 10 | import { NodeRepository } from '../src/database/node-repository'; 11 | import { createDatabaseAdapter } from '../src/database/database-adapter'; 12 | import { validateWorkflowStructure } from '../src/services/n8n-validation'; 13 | import { Logger } from '../src/utils/logger'; 14 | 15 | const logger = new Logger({ prefix: '[TestNodeTypeValidation]' }); 16 | 17 | async function testValidation() { 18 | const adapter = await createDatabaseAdapter('./data/nodes.db'); 19 | const repository = new NodeRepository(adapter); 20 | const validator = new WorkflowValidator(repository, EnhancedConfigValidator); 21 | 22 | logger.info('Testing node type validation...\n'); 23 | 24 | // Test 1: The exact broken workflow from Claude Desktop 25 | const brokenWorkflowFromLogs = { 26 | "nodes": [ 27 | { 28 | "parameters": {}, 29 | "id": "webhook_node", 30 | "name": "Webhook", 31 | "type": "nodes-base.webhook", // WRONG! Missing n8n- prefix 32 | "typeVersion": 2, 33 | "position": [260, 300] as [number, number] 34 | } 35 | ], 36 | "connections": {}, 37 | "pinData": {}, 38 | "meta": { 39 | "instanceId": "74e11c77e266f2c77f6408eb6c88e3fec63c9a5d8c4a3a2ea4c135c542012d6b" 40 | } 41 | }; 42 | 43 | logger.info('Test 1: Invalid node type "nodes-base.webhook" (missing n8n- prefix)'); 44 | const result1 = await validator.validateWorkflow(brokenWorkflowFromLogs as any); 45 | 46 | logger.info('Validation result:'); 47 | logger.info(`Valid: ${result1.valid}`); 48 | logger.info(`Errors: ${result1.errors.length}`); 49 | result1.errors.forEach(err => { 50 | if (typeof err === 'string') { 51 | logger.error(` - ${err}`); 52 | } else if (err && typeof err === 'object' && 'message' in err) { 53 | logger.error(` - ${err.message}`); 54 | } 55 | }); 56 | 57 | // Check if the specific error about nodes-base.webhook was caught 58 | const hasNodeBaseError = result1.errors.some(err => 59 | err && typeof err === 'object' && 'message' in err && 60 | err.message.includes('nodes-base.webhook') && 61 | err.message.includes('n8n-nodes-base.webhook') 62 | ); 63 | logger.info(`Caught nodes-base.webhook error: ${hasNodeBaseError ? 'YES ✅' : 'NO ❌'}`); 64 | 65 | // Test 2: Node type without any prefix 66 | const noPrefixWorkflow = { 67 | "name": "Test Workflow", 68 | "nodes": [ 69 | { 70 | "id": "webhook-1", 71 | "name": "My Webhook", 72 | "type": "webhook", // WRONG! No package prefix 73 | "typeVersion": 2, 74 | "position": [250, 300] as [number, number], 75 | "parameters": {} 76 | }, 77 | { 78 | "id": "set-1", 79 | "name": "Set Data", 80 | "type": "set", // WRONG! No package prefix 81 | "typeVersion": 3.4, 82 | "position": [450, 300] as [number, number], 83 | "parameters": {} 84 | } 85 | ], 86 | "connections": { 87 | "My Webhook": { 88 | "main": [[{ 89 | "node": "Set Data", 90 | "type": "main", 91 | "index": 0 92 | }]] 93 | } 94 | } 95 | }; 96 | 97 | logger.info('\nTest 2: Node types without package prefix ("webhook", "set")'); 98 | const result2 = await validator.validateWorkflow(noPrefixWorkflow as any); 99 | 100 | logger.info('Validation result:'); 101 | logger.info(`Valid: ${result2.valid}`); 102 | logger.info(`Errors: ${result2.errors.length}`); 103 | result2.errors.forEach(err => { 104 | if (typeof err === 'string') { 105 | logger.error(` - ${err}`); 106 | } else if (err && typeof err === 'object' && 'message' in err) { 107 | logger.error(` - ${err.message}`); 108 | } 109 | }); 110 | 111 | // Test 3: Completely invalid node type 112 | const invalidNodeWorkflow = { 113 | "name": "Test Workflow", 114 | "nodes": [ 115 | { 116 | "id": "fake-1", 117 | "name": "Fake Node", 118 | "type": "n8n-nodes-base.fakeNodeThatDoesNotExist", 119 | "typeVersion": 1, 120 | "position": [250, 300] as [number, number], 121 | "parameters": {} 122 | } 123 | ], 124 | "connections": {} 125 | }; 126 | 127 | logger.info('\nTest 3: Completely invalid node type'); 128 | const result3 = await validator.validateWorkflow(invalidNodeWorkflow as any); 129 | 130 | logger.info('Validation result:'); 131 | logger.info(`Valid: ${result3.valid}`); 132 | logger.info(`Errors: ${result3.errors.length}`); 133 | result3.errors.forEach(err => { 134 | if (typeof err === 'string') { 135 | logger.error(` - ${err}`); 136 | } else if (err && typeof err === 'object' && 'message' in err) { 137 | logger.error(` - ${err.message}`); 138 | } 139 | }); 140 | 141 | // Test 4: Using n8n-validation.ts function 142 | logger.info('\nTest 4: Testing n8n-validation.ts with invalid node types'); 143 | 144 | const errors = validateWorkflowStructure(brokenWorkflowFromLogs as any); 145 | logger.info('Validation errors:'); 146 | errors.forEach(err => logger.error(` - ${err}`)); 147 | 148 | // Test 5: Valid workflow (should pass) 149 | const validWorkflow = { 150 | "name": "Valid Webhook Workflow", 151 | "nodes": [ 152 | { 153 | "id": "webhook-1", 154 | "name": "Webhook", 155 | "type": "n8n-nodes-base.webhook", // CORRECT! 156 | "typeVersion": 2, 157 | "position": [250, 300] as [number, number], 158 | "parameters": { 159 | "path": "my-webhook", 160 | "responseMode": "onReceived", 161 | "responseData": "allEntries" 162 | } 163 | } 164 | ], 165 | "connections": {} 166 | }; 167 | 168 | logger.info('\nTest 5: Valid workflow with correct node type'); 169 | const result5 = await validator.validateWorkflow(validWorkflow as any); 170 | 171 | logger.info('Validation result:'); 172 | logger.info(`Valid: ${result5.valid}`); 173 | logger.info(`Errors: ${result5.errors.length}`); 174 | logger.info(`Warnings: ${result5.warnings.length}`); 175 | result5.warnings.forEach(warn => { 176 | if (warn && typeof warn === 'object' && 'message' in warn) { 177 | logger.warn(` - ${warn.message}`); 178 | } 179 | }); 180 | 181 | adapter.close(); 182 | } 183 | 184 | testValidation().catch(err => { 185 | logger.error('Test failed:', err); 186 | process.exit(1); 187 | }); ``` -------------------------------------------------------------------------------- /scripts/test-code-node-enhancements.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env npx tsx 2 | 3 | /** 4 | * Test script for Code node enhancements 5 | * Tests: 6 | * 1. Code node documentation in tools_documentation 7 | * 2. Enhanced validation for Code nodes 8 | * 3. Code node examples 9 | * 4. Code node task templates 10 | */ 11 | 12 | import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator.js'; 13 | import { ExampleGenerator } from '../src/services/example-generator.js'; 14 | import { TaskTemplates } from '../src/services/task-templates.js'; 15 | import { getToolDocumentation } from '../src/mcp/tools-documentation.js'; 16 | 17 | console.log('🧪 Testing Code Node Enhancements\n'); 18 | 19 | // Test 1: Code node documentation 20 | console.log('1️⃣ Testing Code Node Documentation'); 21 | console.log('====================================='); 22 | const codeNodeDocs = getToolDocumentation('code_node_guide', 'essentials'); 23 | console.log('✅ Code node documentation available'); 24 | console.log('First 500 chars:', codeNodeDocs.substring(0, 500) + '...\n'); 25 | 26 | // Test 2: Code node validation 27 | console.log('2️⃣ Testing Code Node Validation'); 28 | console.log('====================================='); 29 | 30 | // Test cases 31 | const validationTests = [ 32 | { 33 | name: 'Empty code', 34 | config: { 35 | language: 'javaScript', 36 | jsCode: '' 37 | } 38 | }, 39 | { 40 | name: 'No return statement', 41 | config: { 42 | language: 'javaScript', 43 | jsCode: 'const data = items;' 44 | } 45 | }, 46 | { 47 | name: 'Invalid return format', 48 | config: { 49 | language: 'javaScript', 50 | jsCode: 'return "hello";' 51 | } 52 | }, 53 | { 54 | name: 'Valid code', 55 | config: { 56 | language: 'javaScript', 57 | jsCode: 'return [{json: {result: "success"}}];' 58 | } 59 | }, 60 | { 61 | name: 'Python with external library', 62 | config: { 63 | language: 'python', 64 | pythonCode: 'import pandas as pd\nreturn [{"json": {"result": "fail"}}]' 65 | } 66 | }, 67 | { 68 | name: 'Code with $json in wrong mode', 69 | config: { 70 | language: 'javaScript', 71 | jsCode: 'const value = $json.field;\nreturn [{json: {value}}];' 72 | } 73 | }, 74 | { 75 | name: 'Code with security issue', 76 | config: { 77 | language: 'javaScript', 78 | jsCode: 'const result = eval(item.json.code);\nreturn [{json: {result}}];' 79 | } 80 | } 81 | ]; 82 | 83 | for (const test of validationTests) { 84 | console.log(`\nTest: ${test.name}`); 85 | const result = EnhancedConfigValidator.validateWithMode( 86 | 'nodes-base.code', 87 | test.config, 88 | [ 89 | { name: 'language', type: 'options', options: ['javaScript', 'python'] }, 90 | { name: 'jsCode', type: 'string' }, 91 | { name: 'pythonCode', type: 'string' }, 92 | { name: 'mode', type: 'options', options: ['runOnceForAllItems', 'runOnceForEachItem'] } 93 | ], 94 | 'operation', 95 | 'ai-friendly' 96 | ); 97 | 98 | console.log(` Valid: ${result.valid}`); 99 | if (result.errors.length > 0) { 100 | console.log(` Errors: ${result.errors.map(e => e.message).join(', ')}`); 101 | } 102 | if (result.warnings.length > 0) { 103 | console.log(` Warnings: ${result.warnings.map(w => w.message).join(', ')}`); 104 | } 105 | if (result.suggestions.length > 0) { 106 | console.log(` Suggestions: ${result.suggestions.join(', ')}`); 107 | } 108 | } 109 | 110 | // Test 3: Code node examples 111 | console.log('\n\n3️⃣ Testing Code Node Examples'); 112 | console.log('====================================='); 113 | 114 | const codeExamples = ExampleGenerator.getExamples('nodes-base.code'); 115 | console.log('Available examples:', Object.keys(codeExamples)); 116 | console.log('\nMinimal example:'); 117 | console.log(JSON.stringify(codeExamples.minimal, null, 2)); 118 | console.log('\nCommon example preview:'); 119 | console.log(codeExamples.common?.jsCode?.substring(0, 200) + '...'); 120 | 121 | // Test 4: Code node task templates 122 | console.log('\n\n4️⃣ Testing Code Node Task Templates'); 123 | console.log('====================================='); 124 | 125 | const codeNodeTasks = [ 126 | 'transform_data', 127 | 'custom_ai_tool', 128 | 'aggregate_data', 129 | 'batch_process_with_api', 130 | 'error_safe_transform', 131 | 'async_data_processing', 132 | 'python_data_analysis' 133 | ]; 134 | 135 | for (const taskName of codeNodeTasks) { 136 | const template = TaskTemplates.getTemplate(taskName); 137 | if (template) { 138 | console.log(`\n✅ ${taskName}:`); 139 | console.log(` Description: ${template.description}`); 140 | console.log(` Language: ${template.configuration.language || 'javaScript'}`); 141 | console.log(` Code preview: ${template.configuration.jsCode?.substring(0, 100) || template.configuration.pythonCode?.substring(0, 100)}...`); 142 | } else { 143 | console.log(`\n❌ ${taskName}: Template not found`); 144 | } 145 | } 146 | 147 | // Test 5: Validate a complex Code node configuration 148 | console.log('\n\n5️⃣ Testing Complex Code Node Validation'); 149 | console.log('=========================================='); 150 | 151 | const complexCode = { 152 | language: 'javaScript', 153 | mode: 'runOnceForEachItem', 154 | jsCode: `// Complex validation test 155 | try { 156 | const email = $json.email; 157 | const response = await $helpers.httpRequest({ 158 | method: 'POST', 159 | url: 'https://api.example.com/validate', 160 | body: { email } 161 | }); 162 | 163 | return [{ 164 | json: { 165 | ...response, 166 | validated: true 167 | } 168 | }]; 169 | } catch (error) { 170 | return [{ 171 | json: { 172 | error: error.message, 173 | validated: false 174 | } 175 | }]; 176 | }`, 177 | onError: 'continueRegularOutput', 178 | retryOnFail: true, 179 | maxTries: 3 180 | }; 181 | 182 | const complexResult = EnhancedConfigValidator.validateWithMode( 183 | 'nodes-base.code', 184 | complexCode, 185 | [ 186 | { name: 'language', type: 'options', options: ['javaScript', 'python'] }, 187 | { name: 'jsCode', type: 'string' }, 188 | { name: 'mode', type: 'options', options: ['runOnceForAllItems', 'runOnceForEachItem'] }, 189 | { name: 'onError', type: 'options' }, 190 | { name: 'retryOnFail', type: 'boolean' }, 191 | { name: 'maxTries', type: 'number' } 192 | ], 193 | 'operation', 194 | 'strict' 195 | ); 196 | 197 | console.log('Complex code validation:'); 198 | console.log(` Valid: ${complexResult.valid}`); 199 | console.log(` Errors: ${complexResult.errors.length}`); 200 | console.log(` Warnings: ${complexResult.warnings.length}`); 201 | console.log(` Suggestions: ${complexResult.suggestions.length}`); 202 | 203 | console.log('\n✅ All Code node enhancement tests completed!'); ``` -------------------------------------------------------------------------------- /tests/unit/database/database-adapter-unit.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, vi } from 'vitest'; 2 | 3 | // Mock logger 4 | vi.mock('../../../src/utils/logger', () => ({ 5 | logger: { 6 | info: vi.fn(), 7 | warn: vi.fn(), 8 | error: vi.fn(), 9 | debug: vi.fn() 10 | } 11 | })); 12 | 13 | describe('Database Adapter - Unit Tests', () => { 14 | describe('DatabaseAdapter Interface', () => { 15 | it('should define interface when adapter is created', () => { 16 | // This is a type test - ensuring the interface is correctly defined 17 | type DatabaseAdapter = { 18 | prepare: (sql: string) => any; 19 | exec: (sql: string) => void; 20 | close: () => void; 21 | pragma: (key: string, value?: any) => any; 22 | readonly inTransaction: boolean; 23 | transaction: <T>(fn: () => T) => T; 24 | checkFTS5Support: () => boolean; 25 | }; 26 | 27 | // Type assertion to ensure interface matches 28 | const mockAdapter: DatabaseAdapter = { 29 | prepare: vi.fn(), 30 | exec: vi.fn(), 31 | close: vi.fn(), 32 | pragma: vi.fn(), 33 | inTransaction: false, 34 | transaction: vi.fn((fn) => fn()), 35 | checkFTS5Support: vi.fn(() => true) 36 | }; 37 | 38 | expect(mockAdapter).toBeDefined(); 39 | expect(mockAdapter.prepare).toBeDefined(); 40 | expect(mockAdapter.exec).toBeDefined(); 41 | expect(mockAdapter.close).toBeDefined(); 42 | expect(mockAdapter.pragma).toBeDefined(); 43 | expect(mockAdapter.transaction).toBeDefined(); 44 | expect(mockAdapter.checkFTS5Support).toBeDefined(); 45 | }); 46 | }); 47 | 48 | describe('PreparedStatement Interface', () => { 49 | it('should define interface when statement is prepared', () => { 50 | // Type test for PreparedStatement 51 | type PreparedStatement = { 52 | run: (...params: any[]) => { changes: number; lastInsertRowid: number | bigint }; 53 | get: (...params: any[]) => any; 54 | all: (...params: any[]) => any[]; 55 | iterate: (...params: any[]) => IterableIterator<any>; 56 | pluck: (toggle?: boolean) => PreparedStatement; 57 | expand: (toggle?: boolean) => PreparedStatement; 58 | raw: (toggle?: boolean) => PreparedStatement; 59 | columns: () => any[]; 60 | bind: (...params: any[]) => PreparedStatement; 61 | }; 62 | 63 | const mockStmt: PreparedStatement = { 64 | run: vi.fn(() => ({ changes: 1, lastInsertRowid: 1 })), 65 | get: vi.fn(), 66 | all: vi.fn(() => []), 67 | iterate: vi.fn(function* () {}), 68 | pluck: vi.fn(function(this: any) { return this; }), 69 | expand: vi.fn(function(this: any) { return this; }), 70 | raw: vi.fn(function(this: any) { return this; }), 71 | columns: vi.fn(() => []), 72 | bind: vi.fn(function(this: any) { return this; }) 73 | }; 74 | 75 | expect(mockStmt).toBeDefined(); 76 | expect(mockStmt.run).toBeDefined(); 77 | expect(mockStmt.get).toBeDefined(); 78 | expect(mockStmt.all).toBeDefined(); 79 | expect(mockStmt.iterate).toBeDefined(); 80 | expect(mockStmt.pluck).toBeDefined(); 81 | expect(mockStmt.expand).toBeDefined(); 82 | expect(mockStmt.raw).toBeDefined(); 83 | expect(mockStmt.columns).toBeDefined(); 84 | expect(mockStmt.bind).toBeDefined(); 85 | }); 86 | }); 87 | 88 | describe('FTS5 Support Detection', () => { 89 | it('should detect support when FTS5 module is available', () => { 90 | const mockDb = { 91 | exec: vi.fn() 92 | }; 93 | 94 | // Function to test FTS5 support detection logic 95 | const checkFTS5Support = (db: any): boolean => { 96 | try { 97 | db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS test_fts5 USING fts5(content);"); 98 | db.exec("DROP TABLE IF EXISTS test_fts5;"); 99 | return true; 100 | } catch (error) { 101 | return false; 102 | } 103 | }; 104 | 105 | // Test when FTS5 is supported 106 | expect(checkFTS5Support(mockDb)).toBe(true); 107 | expect(mockDb.exec).toHaveBeenCalledWith( 108 | "CREATE VIRTUAL TABLE IF NOT EXISTS test_fts5 USING fts5(content);" 109 | ); 110 | 111 | // Test when FTS5 is not supported 112 | mockDb.exec.mockImplementation(() => { 113 | throw new Error('no such module: fts5'); 114 | }); 115 | 116 | expect(checkFTS5Support(mockDb)).toBe(false); 117 | }); 118 | }); 119 | 120 | describe('Transaction Handling', () => { 121 | it('should handle commit and rollback when transaction is executed', () => { 122 | // Test transaction wrapper logic 123 | const mockDb = { 124 | exec: vi.fn(), 125 | inTransaction: false 126 | }; 127 | 128 | const transaction = <T>(db: any, fn: () => T): T => { 129 | try { 130 | db.exec('BEGIN'); 131 | db.inTransaction = true; 132 | const result = fn(); 133 | db.exec('COMMIT'); 134 | db.inTransaction = false; 135 | return result; 136 | } catch (error) { 137 | db.exec('ROLLBACK'); 138 | db.inTransaction = false; 139 | throw error; 140 | } 141 | }; 142 | 143 | // Test successful transaction 144 | const result = transaction(mockDb, () => 'success'); 145 | expect(result).toBe('success'); 146 | expect(mockDb.exec).toHaveBeenCalledWith('BEGIN'); 147 | expect(mockDb.exec).toHaveBeenCalledWith('COMMIT'); 148 | expect(mockDb.inTransaction).toBe(false); 149 | 150 | // Reset mocks 151 | mockDb.exec.mockClear(); 152 | 153 | // Test failed transaction 154 | expect(() => { 155 | transaction(mockDb, () => { 156 | throw new Error('transaction error'); 157 | }); 158 | }).toThrow('transaction error'); 159 | 160 | expect(mockDb.exec).toHaveBeenCalledWith('BEGIN'); 161 | expect(mockDb.exec).toHaveBeenCalledWith('ROLLBACK'); 162 | expect(mockDb.inTransaction).toBe(false); 163 | }); 164 | }); 165 | 166 | describe('Pragma Handling', () => { 167 | it('should return values when pragma commands are executed', () => { 168 | const mockDb = { 169 | pragma: vi.fn((key: string, value?: any) => { 170 | if (key === 'journal_mode' && value === 'WAL') { 171 | return 'wal'; 172 | } 173 | return null; 174 | }) 175 | }; 176 | 177 | expect(mockDb.pragma('journal_mode', 'WAL')).toBe('wal'); 178 | expect(mockDb.pragma('other_key')).toBe(null); 179 | }); 180 | }); 181 | }); ``` -------------------------------------------------------------------------------- /.github/workflows/benchmark-pr.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Benchmark PR Comparison 2 | on: 3 | pull_request: 4 | branches: [main] 5 | paths-ignore: 6 | - '**.md' 7 | - '**.txt' 8 | - 'docs/**' 9 | - 'examples/**' 10 | - '.github/FUNDING.yml' 11 | - '.github/ISSUE_TEMPLATE/**' 12 | - '.github/pull_request_template.md' 13 | - '.gitignore' 14 | - 'LICENSE*' 15 | - 'ATTRIBUTION.md' 16 | - 'SECURITY.md' 17 | - 'CODE_OF_CONDUCT.md' 18 | 19 | permissions: 20 | pull-requests: write 21 | contents: read 22 | statuses: write 23 | 24 | jobs: 25 | benchmark-comparison: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout PR branch 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 32 | 33 | - name: Setup Node.js 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: 20 37 | cache: 'npm' 38 | 39 | - name: Install dependencies 40 | run: npm ci 41 | 42 | # Run benchmarks on current branch 43 | - name: Run current benchmarks 44 | run: npm run benchmark:ci 45 | 46 | - name: Save current results 47 | run: cp benchmark-results.json benchmark-current.json 48 | 49 | # Checkout and run benchmarks on base branch 50 | - name: Checkout base branch 51 | run: | 52 | git checkout ${{ github.event.pull_request.base.sha }} 53 | git status 54 | 55 | - name: Install base dependencies 56 | run: npm ci 57 | 58 | - name: Run baseline benchmarks 59 | run: npm run benchmark:ci 60 | continue-on-error: true 61 | 62 | - name: Save baseline results 63 | run: | 64 | if [ -f benchmark-results.json ]; then 65 | cp benchmark-results.json benchmark-baseline.json 66 | else 67 | echo '{"files":[]}' > benchmark-baseline.json 68 | fi 69 | 70 | # Compare results 71 | - name: Checkout PR branch again 72 | run: git checkout ${{ github.event.pull_request.head.sha }} 73 | 74 | - name: Compare benchmarks 75 | id: compare 76 | run: | 77 | node scripts/compare-benchmarks.js benchmark-current.json benchmark-baseline.json || echo "REGRESSION=true" >> $GITHUB_OUTPUT 78 | 79 | # Upload comparison artifacts 80 | - name: Upload benchmark comparison 81 | if: always() 82 | uses: actions/upload-artifact@v4 83 | with: 84 | name: benchmark-comparison-${{ github.run_number }} 85 | path: | 86 | benchmark-current.json 87 | benchmark-baseline.json 88 | benchmark-comparison.json 89 | benchmark-comparison.md 90 | retention-days: 30 91 | 92 | # Post comparison to PR 93 | - name: Post benchmark comparison to PR 94 | if: always() 95 | uses: actions/github-script@v7 96 | continue-on-error: true 97 | with: 98 | script: | 99 | try { 100 | const fs = require('fs'); 101 | let comment = '## ⚡ Benchmark Comparison\n\n'; 102 | 103 | try { 104 | if (fs.existsSync('benchmark-comparison.md')) { 105 | const comparison = fs.readFileSync('benchmark-comparison.md', 'utf8'); 106 | comment += comparison; 107 | } else { 108 | comment += 'Benchmark comparison could not be generated.'; 109 | } 110 | } catch (error) { 111 | comment += `Error reading benchmark comparison: ${error.message}`; 112 | } 113 | 114 | comment += '\n\n---\n'; 115 | comment += `*[View full benchmark results](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})*`; 116 | 117 | // Find existing comment 118 | const { data: comments } = await github.rest.issues.listComments({ 119 | owner: context.repo.owner, 120 | repo: context.repo.repo, 121 | issue_number: context.issue.number, 122 | }); 123 | 124 | const botComment = comments.find(comment => 125 | comment.user.type === 'Bot' && 126 | comment.body.includes('## ⚡ Benchmark Comparison') 127 | ); 128 | 129 | if (botComment) { 130 | await github.rest.issues.updateComment({ 131 | owner: context.repo.owner, 132 | repo: context.repo.repo, 133 | comment_id: botComment.id, 134 | body: comment 135 | }); 136 | } else { 137 | await github.rest.issues.createComment({ 138 | owner: context.repo.owner, 139 | repo: context.repo.repo, 140 | issue_number: context.issue.number, 141 | body: comment 142 | }); 143 | } 144 | } catch (error) { 145 | console.error('Failed to create/update PR comment:', error.message); 146 | console.log('This is likely due to insufficient permissions for external PRs.'); 147 | console.log('Benchmark comparison has been saved to artifacts instead.'); 148 | } 149 | 150 | # Add status check 151 | - name: Set benchmark status 152 | if: always() 153 | uses: actions/github-script@v7 154 | continue-on-error: true 155 | with: 156 | script: | 157 | try { 158 | const hasRegression = '${{ steps.compare.outputs.REGRESSION }}' === 'true'; 159 | const state = hasRegression ? 'failure' : 'success'; 160 | const description = hasRegression 161 | ? 'Performance regressions detected' 162 | : 'No performance regressions'; 163 | 164 | await github.rest.repos.createCommitStatus({ 165 | owner: context.repo.owner, 166 | repo: context.repo.repo, 167 | sha: context.sha, 168 | state: state, 169 | target_url: `https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}`, 170 | description: description, 171 | context: 'benchmarks/regression-check' 172 | }); 173 | } catch (error) { 174 | console.error('Failed to create commit status:', error.message); 175 | console.log('This is likely due to insufficient permissions for external PRs.'); 176 | } ``` -------------------------------------------------------------------------------- /tests/test-slack-node-complete.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | const { NodeDocumentationService } = require('../dist/services/node-documentation-service'); 4 | const { EnhancedDocumentationFetcher } = require('../dist/utils/documentation-fetcher'); 5 | const { NodeSourceExtractor } = require('../dist/utils/node-source-extractor'); 6 | const path = require('path'); 7 | 8 | async function testSlackNode() { 9 | console.log('🧪 Testing Slack Node Complete Information Extraction\n'); 10 | 11 | const dbPath = path.join(__dirname, '../data/test-slack.db'); 12 | const service = new NodeDocumentationService(dbPath); 13 | const fetcher = new EnhancedDocumentationFetcher(); 14 | const extractor = new NodeSourceExtractor(); 15 | 16 | try { 17 | console.log('📚 Fetching Slack node documentation...'); 18 | const docs = await fetcher.getEnhancedNodeDocumentation('n8n-nodes-base.Slack'); 19 | 20 | console.log('\n✅ Documentation Structure:'); 21 | console.log(`- Title: ${docs.title}`); 22 | console.log(`- Has markdown: ${docs.markdown?.length > 0 ? 'Yes' : 'No'} (${docs.markdown?.length || 0} chars)`); 23 | console.log(`- Operations: ${docs.operations?.length || 0}`); 24 | console.log(`- API Methods: ${docs.apiMethods?.length || 0}`); 25 | console.log(`- Examples: ${docs.examples?.length || 0}`); 26 | console.log(`- Templates: ${docs.templates?.length || 0}`); 27 | console.log(`- Related Resources: ${docs.relatedResources?.length || 0}`); 28 | console.log(`- Required Scopes: ${docs.requiredScopes?.length || 0}`); 29 | 30 | console.log('\n📋 Operations by Resource:'); 31 | const resourceMap = new Map(); 32 | if (docs.operations) { 33 | docs.operations.forEach(op => { 34 | if (!resourceMap.has(op.resource)) { 35 | resourceMap.set(op.resource, []); 36 | } 37 | resourceMap.get(op.resource).push(op); 38 | }); 39 | } 40 | 41 | for (const [resource, ops] of resourceMap) { 42 | console.log(`\n ${resource}:`); 43 | ops.forEach(op => { 44 | console.log(` - ${op.operation}: ${op.description}`); 45 | }); 46 | } 47 | 48 | console.log('\n🔌 Sample API Methods:'); 49 | if (docs.apiMethods) { 50 | docs.apiMethods.slice(0, 5).forEach(method => { 51 | console.log(` - ${method.operation} → ${method.apiMethod}`); 52 | }); 53 | } 54 | 55 | console.log('\n💻 Extracting Slack node source code...'); 56 | const sourceInfo = await extractor.extractNodeSource('n8n-nodes-base.Slack'); 57 | 58 | console.log('\n✅ Source Code Extraction:'); 59 | console.log(`- Has source code: ${sourceInfo.sourceCode ? 'Yes' : 'No'} (${sourceInfo.sourceCode?.length || 0} chars)`); 60 | console.log(`- Has credential code: ${sourceInfo.credentialCode ? 'Yes' : 'No'} (${sourceInfo.credentialCode?.length || 0} chars)`); 61 | console.log(`- Package name: ${sourceInfo.packageInfo?.name}`); 62 | console.log(`- Package version: ${sourceInfo.packageInfo?.version}`); 63 | 64 | // Store in database 65 | console.log('\n💾 Storing in database...'); 66 | await service.storeNode({ 67 | nodeType: 'n8n-nodes-base.Slack', 68 | name: 'Slack', 69 | displayName: 'Slack', 70 | description: 'Send and receive messages, manage channels, and more', 71 | category: 'Communication', 72 | documentationUrl: docs?.url || 'https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.slack/', 73 | documentationMarkdown: docs?.markdown, 74 | documentationTitle: docs?.title, 75 | operations: docs?.operations, 76 | apiMethods: docs?.apiMethods, 77 | documentationExamples: docs?.examples, 78 | templates: docs?.templates, 79 | relatedResources: docs?.relatedResources, 80 | requiredScopes: docs?.requiredScopes, 81 | sourceCode: sourceInfo.sourceCode || '', 82 | credentialCode: sourceInfo.credentialCode, 83 | packageName: sourceInfo.packageInfo?.name || 'n8n-nodes-base', 84 | version: sourceInfo.packageInfo?.version, 85 | hasCredentials: true, 86 | isTrigger: false, 87 | isWebhook: false 88 | }); 89 | 90 | // Retrieve and verify 91 | console.log('\n🔍 Retrieving from database...'); 92 | const storedNode = await service.getNodeInfo('n8n-nodes-base.Slack'); 93 | 94 | console.log('\n✅ Verification Results:'); 95 | console.log(`- Node found: ${storedNode ? 'Yes' : 'No'}`); 96 | if (storedNode) { 97 | console.log(`- Has operations: ${storedNode.operations?.length > 0 ? 'Yes' : 'No'} (${storedNode.operations?.length || 0})`); 98 | console.log(`- Has API methods: ${storedNode.apiMethods?.length > 0 ? 'Yes' : 'No'} (${storedNode.apiMethods?.length || 0})`); 99 | console.log(`- Has examples: ${storedNode.documentationExamples?.length > 0 ? 'Yes' : 'No'} (${storedNode.documentationExamples?.length || 0})`); 100 | console.log(`- Has source code: ${storedNode.sourceCode ? 'Yes' : 'No'}`); 101 | console.log(`- Has credential code: ${storedNode.credentialCode ? 'Yes' : 'No'}`); 102 | } 103 | 104 | // Test search 105 | console.log('\n🔍 Testing search...'); 106 | const searchResults = await service.searchNodes('message send'); 107 | const slackInResults = searchResults.some(r => r.nodeType === 'n8n-nodes-base.Slack'); 108 | console.log(`- Slack found in search results: ${slackInResults ? 'Yes' : 'No'}`); 109 | 110 | console.log('\n✅ Complete Information Test Summary:'); 111 | const hasCompleteInfo = 112 | storedNode && 113 | storedNode.operations?.length > 0 && 114 | storedNode.apiMethods?.length > 0 && 115 | storedNode.sourceCode && 116 | storedNode.documentationMarkdown; 117 | 118 | console.log(`- Has complete information: ${hasCompleteInfo ? '✅ YES' : '❌ NO'}`); 119 | 120 | if (!hasCompleteInfo) { 121 | console.log('\n❌ Missing Information:'); 122 | if (!storedNode) console.log(' - Node not stored properly'); 123 | if (!storedNode?.operations?.length) console.log(' - No operations extracted'); 124 | if (!storedNode?.apiMethods?.length) console.log(' - No API methods extracted'); 125 | if (!storedNode?.sourceCode) console.log(' - No source code extracted'); 126 | if (!storedNode?.documentationMarkdown) console.log(' - No documentation extracted'); 127 | } 128 | 129 | } catch (error) { 130 | console.error('❌ Test failed:', error); 131 | } finally { 132 | await service.close(); 133 | } 134 | } 135 | 136 | // Run the test 137 | testSlackNode().catch(console.error); ``` -------------------------------------------------------------------------------- /src/scripts/test-protocol-negotiation.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | /** 3 | * Test Protocol Version Negotiation 4 | * 5 | * This script tests the protocol version negotiation logic with different client scenarios. 6 | */ 7 | 8 | import { 9 | negotiateProtocolVersion, 10 | isN8nClient, 11 | STANDARD_PROTOCOL_VERSION, 12 | N8N_PROTOCOL_VERSION 13 | } from '../utils/protocol-version'; 14 | 15 | interface TestCase { 16 | name: string; 17 | clientVersion?: string; 18 | clientInfo?: any; 19 | userAgent?: string; 20 | headers?: Record<string, string>; 21 | expectedVersion: string; 22 | expectedIsN8nClient: boolean; 23 | } 24 | 25 | const testCases: TestCase[] = [ 26 | { 27 | name: 'Standard MCP client (Claude Desktop)', 28 | clientVersion: '2025-03-26', 29 | clientInfo: { name: 'Claude Desktop', version: '1.0.0' }, 30 | expectedVersion: '2025-03-26', 31 | expectedIsN8nClient: false 32 | }, 33 | { 34 | name: 'n8n client with specific client info', 35 | clientVersion: '2025-03-26', 36 | clientInfo: { name: 'n8n', version: '1.0.0' }, 37 | expectedVersion: N8N_PROTOCOL_VERSION, 38 | expectedIsN8nClient: true 39 | }, 40 | { 41 | name: 'LangChain client', 42 | clientVersion: '2025-03-26', 43 | clientInfo: { name: 'langchain-js', version: '0.1.0' }, 44 | expectedVersion: N8N_PROTOCOL_VERSION, 45 | expectedIsN8nClient: true 46 | }, 47 | { 48 | name: 'n8n client via user agent', 49 | clientVersion: '2025-03-26', 50 | userAgent: 'n8n/1.0.0', 51 | expectedVersion: N8N_PROTOCOL_VERSION, 52 | expectedIsN8nClient: true 53 | }, 54 | { 55 | name: 'n8n mode environment variable', 56 | clientVersion: '2025-03-26', 57 | expectedVersion: N8N_PROTOCOL_VERSION, 58 | expectedIsN8nClient: true 59 | }, 60 | { 61 | name: 'Client requesting older version', 62 | clientVersion: '2024-06-25', 63 | clientInfo: { name: 'Some Client', version: '1.0.0' }, 64 | expectedVersion: '2024-06-25', 65 | expectedIsN8nClient: false 66 | }, 67 | { 68 | name: 'Client requesting unsupported version', 69 | clientVersion: '2020-01-01', 70 | clientInfo: { name: 'Old Client', version: '1.0.0' }, 71 | expectedVersion: STANDARD_PROTOCOL_VERSION, 72 | expectedIsN8nClient: false 73 | }, 74 | { 75 | name: 'No client info provided', 76 | expectedVersion: STANDARD_PROTOCOL_VERSION, 77 | expectedIsN8nClient: false 78 | }, 79 | { 80 | name: 'n8n headers detection', 81 | clientVersion: '2025-03-26', 82 | headers: { 'x-n8n-version': '1.0.0' }, 83 | expectedVersion: N8N_PROTOCOL_VERSION, 84 | expectedIsN8nClient: true 85 | } 86 | ]; 87 | 88 | async function runTests(): Promise<void> { 89 | console.log('🧪 Testing Protocol Version Negotiation\n'); 90 | 91 | let passed = 0; 92 | let failed = 0; 93 | 94 | // Set N8N_MODE for the environment variable test 95 | const originalN8nMode = process.env.N8N_MODE; 96 | 97 | for (const testCase of testCases) { 98 | try { 99 | // Set N8N_MODE for specific test 100 | if (testCase.name.includes('environment variable')) { 101 | process.env.N8N_MODE = 'true'; 102 | } else { 103 | delete process.env.N8N_MODE; 104 | } 105 | 106 | // Test isN8nClient function 107 | const detectedAsN8n = isN8nClient(testCase.clientInfo, testCase.userAgent, testCase.headers); 108 | 109 | // Test negotiateProtocolVersion function 110 | const result = negotiateProtocolVersion( 111 | testCase.clientVersion, 112 | testCase.clientInfo, 113 | testCase.userAgent, 114 | testCase.headers 115 | ); 116 | 117 | // Check results 118 | const versionCorrect = result.version === testCase.expectedVersion; 119 | const n8nDetectionCorrect = result.isN8nClient === testCase.expectedIsN8nClient; 120 | const isN8nFunctionCorrect = detectedAsN8n === testCase.expectedIsN8nClient; 121 | 122 | if (versionCorrect && n8nDetectionCorrect && isN8nFunctionCorrect) { 123 | console.log(`✅ ${testCase.name}`); 124 | console.log(` Version: ${result.version}, n8n client: ${result.isN8nClient}`); 125 | console.log(` Reasoning: ${result.reasoning}\n`); 126 | passed++; 127 | } else { 128 | console.log(`❌ ${testCase.name}`); 129 | console.log(` Expected: version=${testCase.expectedVersion}, isN8n=${testCase.expectedIsN8nClient}`); 130 | console.log(` Got: version=${result.version}, isN8n=${result.isN8nClient}`); 131 | console.log(` isN8nClient function: ${detectedAsN8n} (expected: ${testCase.expectedIsN8nClient})`); 132 | console.log(` Reasoning: ${result.reasoning}\n`); 133 | failed++; 134 | } 135 | 136 | } catch (error) { 137 | console.log(`💥 ${testCase.name} - ERROR`); 138 | console.log(` ${error instanceof Error ? error.message : String(error)}\n`); 139 | failed++; 140 | } 141 | } 142 | 143 | // Restore original N8N_MODE 144 | if (originalN8nMode) { 145 | process.env.N8N_MODE = originalN8nMode; 146 | } else { 147 | delete process.env.N8N_MODE; 148 | } 149 | 150 | // Summary 151 | console.log(`\n📊 Test Results:`); 152 | console.log(` ✅ Passed: ${passed}`); 153 | console.log(` ❌ Failed: ${failed}`); 154 | console.log(` Total: ${passed + failed}`); 155 | 156 | if (failed > 0) { 157 | console.log(`\n❌ Some tests failed!`); 158 | process.exit(1); 159 | } else { 160 | console.log(`\n🎉 All tests passed!`); 161 | } 162 | } 163 | 164 | // Additional integration test 165 | async function testIntegration(): Promise<void> { 166 | console.log('\n🔧 Integration Test - MCP Server Protocol Negotiation\n'); 167 | 168 | // This would normally test the actual MCP server, but we'll just verify 169 | // the negotiation logic works in typical scenarios 170 | 171 | const scenarios = [ 172 | { 173 | name: 'Claude Desktop connecting', 174 | clientInfo: { name: 'Claude Desktop', version: '1.0.0' }, 175 | clientVersion: '2025-03-26' 176 | }, 177 | { 178 | name: 'n8n connecting via HTTP', 179 | headers: { 'user-agent': 'n8n/1.52.0' }, 180 | clientVersion: '2025-03-26' 181 | } 182 | ]; 183 | 184 | for (const scenario of scenarios) { 185 | const result = negotiateProtocolVersion( 186 | scenario.clientVersion, 187 | scenario.clientInfo, 188 | scenario.headers?.['user-agent'], 189 | scenario.headers 190 | ); 191 | 192 | console.log(`🔍 ${scenario.name}:`); 193 | console.log(` Negotiated version: ${result.version}`); 194 | console.log(` Is n8n client: ${result.isN8nClient}`); 195 | console.log(` Reasoning: ${result.reasoning}\n`); 196 | } 197 | } 198 | 199 | if (require.main === module) { 200 | runTests() 201 | .then(() => testIntegration()) 202 | .catch(error => { 203 | console.error('Test execution failed:', error); 204 | process.exit(1); 205 | }); 206 | } ``` -------------------------------------------------------------------------------- /tests/test-enhanced-documentation.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | const { EnhancedDocumentationFetcher } = require('../dist/utils/enhanced-documentation-fetcher'); 4 | const { EnhancedSQLiteStorageService } = require('../dist/services/enhanced-sqlite-storage-service'); 5 | const { NodeSourceExtractor } = require('../dist/utils/node-source-extractor'); 6 | 7 | async function testEnhancedDocumentation() { 8 | console.log('=== Testing Enhanced Documentation Fetcher ===\n'); 9 | 10 | const fetcher = new EnhancedDocumentationFetcher(); 11 | const storage = new EnhancedSQLiteStorageService('./data/test-enhanced.db'); 12 | const extractor = new NodeSourceExtractor(); 13 | 14 | try { 15 | // Test 1: Fetch and parse Slack node documentation 16 | console.log('1. Testing Slack node documentation parsing...'); 17 | const slackDoc = await fetcher.getEnhancedNodeDocumentation('n8n-nodes-base.slack'); 18 | 19 | if (slackDoc) { 20 | console.log('\n✓ Slack Documentation Found:'); 21 | console.log(` - Title: ${slackDoc.title}`); 22 | console.log(` - Description: ${slackDoc.description}`); 23 | console.log(` - URL: ${slackDoc.url}`); 24 | console.log(` - Operations: ${slackDoc.operations?.length || 0} found`); 25 | console.log(` - API Methods: ${slackDoc.apiMethods?.length || 0} found`); 26 | console.log(` - Examples: ${slackDoc.examples?.length || 0} found`); 27 | console.log(` - Required Scopes: ${slackDoc.requiredScopes?.length || 0} found`); 28 | 29 | // Show sample operations 30 | if (slackDoc.operations && slackDoc.operations.length > 0) { 31 | console.log('\n Sample Operations:'); 32 | slackDoc.operations.slice(0, 5).forEach(op => { 33 | console.log(` - ${op.resource}.${op.operation}: ${op.description}`); 34 | }); 35 | } 36 | 37 | // Show sample API mappings 38 | if (slackDoc.apiMethods && slackDoc.apiMethods.length > 0) { 39 | console.log('\n Sample API Mappings:'); 40 | slackDoc.apiMethods.slice(0, 5).forEach(api => { 41 | console.log(` - ${api.resource}.${api.operation} → ${api.apiMethod}`); 42 | }); 43 | } 44 | } else { 45 | console.log('✗ Slack documentation not found'); 46 | } 47 | 48 | // Test 2: Test with If node (core node) 49 | console.log('\n\n2. Testing If node documentation parsing...'); 50 | const ifDoc = await fetcher.getEnhancedNodeDocumentation('n8n-nodes-base.if'); 51 | 52 | if (ifDoc) { 53 | console.log('\n✓ If Documentation Found:'); 54 | console.log(` - Title: ${ifDoc.title}`); 55 | console.log(` - Description: ${ifDoc.description}`); 56 | console.log(` - Examples: ${ifDoc.examples?.length || 0} found`); 57 | console.log(` - Related Resources: ${ifDoc.relatedResources?.length || 0} found`); 58 | } 59 | 60 | // Test 3: Store node with documentation 61 | console.log('\n\n3. Testing node storage with documentation...'); 62 | 63 | // Extract a node 64 | const nodeInfo = await extractor.extractNodeSource('n8n-nodes-base.slack'); 65 | if (nodeInfo) { 66 | const storedNode = await storage.storeNodeWithDocumentation(nodeInfo); 67 | 68 | console.log('\n✓ Node stored successfully:'); 69 | console.log(` - Node Type: ${storedNode.nodeType}`); 70 | console.log(` - Has Documentation: ${!!storedNode.documentationMarkdown}`); 71 | console.log(` - Operations: ${storedNode.operationCount}`); 72 | console.log(` - API Methods: ${storedNode.apiMethodCount}`); 73 | console.log(` - Examples: ${storedNode.exampleCount}`); 74 | console.log(` - Resources: ${storedNode.resourceCount}`); 75 | console.log(` - Scopes: ${storedNode.scopeCount}`); 76 | 77 | // Get detailed operations 78 | const operations = await storage.getNodeOperations(storedNode.id); 79 | if (operations.length > 0) { 80 | console.log('\n Stored Operations (first 5):'); 81 | operations.slice(0, 5).forEach(op => { 82 | console.log(` - ${op.resource}.${op.operation}: ${op.description}`); 83 | }); 84 | } 85 | 86 | // Get examples 87 | const examples = await storage.getNodeExamples(storedNode.id); 88 | if (examples.length > 0) { 89 | console.log('\n Stored Examples:'); 90 | examples.forEach(ex => { 91 | console.log(` - ${ex.title || 'Untitled'} (${ex.type}): ${ex.code.length} chars`); 92 | }); 93 | } 94 | } 95 | 96 | // Test 4: Search with enhanced FTS 97 | console.log('\n\n4. Testing enhanced search...'); 98 | 99 | const searchResults = await storage.searchNodes({ query: 'slack message' }); 100 | console.log(`\n✓ Search Results for "slack message": ${searchResults.length} nodes found`); 101 | 102 | if (searchResults.length > 0) { 103 | console.log(' First result:'); 104 | const result = searchResults[0]; 105 | console.log(` - ${result.displayName || result.name} (${result.nodeType})`); 106 | console.log(` - Documentation: ${result.documentationTitle || 'No title'}`); 107 | } 108 | 109 | // Test 5: Get statistics 110 | console.log('\n\n5. Getting enhanced statistics...'); 111 | const stats = await storage.getEnhancedStatistics(); 112 | 113 | console.log('\n✓ Enhanced Statistics:'); 114 | console.log(` - Total Nodes: ${stats.totalNodes}`); 115 | console.log(` - Nodes with Documentation: ${stats.nodesWithDocumentation}`); 116 | console.log(` - Documentation Coverage: ${stats.documentationCoverage}%`); 117 | console.log(` - Total Operations: ${stats.totalOperations}`); 118 | console.log(` - Total API Methods: ${stats.totalApiMethods}`); 119 | console.log(` - Total Examples: ${stats.totalExamples}`); 120 | console.log(` - Total Resources: ${stats.totalResources}`); 121 | console.log(` - Total Scopes: ${stats.totalScopes}`); 122 | 123 | if (stats.topDocumentedNodes && stats.topDocumentedNodes.length > 0) { 124 | console.log('\n Top Documented Nodes:'); 125 | stats.topDocumentedNodes.slice(0, 3).forEach(node => { 126 | console.log(` - ${node.display_name || node.name}: ${node.operation_count} operations, ${node.example_count} examples`); 127 | }); 128 | } 129 | 130 | } catch (error) { 131 | console.error('Error during testing:', error); 132 | } finally { 133 | // Cleanup 134 | storage.close(); 135 | await fetcher.cleanup(); 136 | console.log('\n\n✓ Test completed and cleaned up'); 137 | } 138 | } 139 | 140 | // Run the test 141 | testEnhancedDocumentation().catch(console.error); ``` -------------------------------------------------------------------------------- /src/telemetry/telemetry-error.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Telemetry Error Classes 3 | * Custom error types for telemetry system with enhanced tracking 4 | */ 5 | 6 | import { TelemetryErrorType, TelemetryErrorContext } from './telemetry-types'; 7 | import { logger } from '../utils/logger'; 8 | 9 | // Re-export types for convenience 10 | export { TelemetryErrorType, TelemetryErrorContext } from './telemetry-types'; 11 | 12 | export class TelemetryError extends Error { 13 | public readonly type: TelemetryErrorType; 14 | public readonly context?: Record<string, any>; 15 | public readonly timestamp: number; 16 | public readonly retryable: boolean; 17 | 18 | constructor( 19 | type: TelemetryErrorType, 20 | message: string, 21 | context?: Record<string, any>, 22 | retryable: boolean = false 23 | ) { 24 | super(message); 25 | this.name = 'TelemetryError'; 26 | this.type = type; 27 | this.context = context; 28 | this.timestamp = Date.now(); 29 | this.retryable = retryable; 30 | 31 | // Ensure proper prototype chain 32 | Object.setPrototypeOf(this, TelemetryError.prototype); 33 | } 34 | 35 | /** 36 | * Convert error to context object 37 | */ 38 | toContext(): TelemetryErrorContext { 39 | return { 40 | type: this.type, 41 | message: this.message, 42 | context: this.context, 43 | timestamp: this.timestamp, 44 | retryable: this.retryable 45 | }; 46 | } 47 | 48 | /** 49 | * Log the error with appropriate level 50 | */ 51 | log(): void { 52 | const logContext = { 53 | type: this.type, 54 | message: this.message, 55 | ...this.context 56 | }; 57 | 58 | if (this.retryable) { 59 | logger.debug('Retryable telemetry error:', logContext); 60 | } else { 61 | logger.debug('Non-retryable telemetry error:', logContext); 62 | } 63 | } 64 | } 65 | 66 | /** 67 | * Circuit Breaker for handling repeated failures 68 | */ 69 | export class TelemetryCircuitBreaker { 70 | private failureCount: number = 0; 71 | private lastFailureTime: number = 0; 72 | private state: 'closed' | 'open' | 'half-open' = 'closed'; 73 | 74 | private readonly failureThreshold: number; 75 | private readonly resetTimeout: number; 76 | private readonly halfOpenRequests: number; 77 | private halfOpenCount: number = 0; 78 | 79 | constructor( 80 | failureThreshold: number = 5, 81 | resetTimeout: number = 60000, // 1 minute 82 | halfOpenRequests: number = 3 83 | ) { 84 | this.failureThreshold = failureThreshold; 85 | this.resetTimeout = resetTimeout; 86 | this.halfOpenRequests = halfOpenRequests; 87 | } 88 | 89 | /** 90 | * Check if requests should be allowed 91 | */ 92 | shouldAllow(): boolean { 93 | const now = Date.now(); 94 | 95 | switch (this.state) { 96 | case 'closed': 97 | return true; 98 | 99 | case 'open': 100 | // Check if enough time has passed to try half-open 101 | if (now - this.lastFailureTime > this.resetTimeout) { 102 | this.state = 'half-open'; 103 | this.halfOpenCount = 0; 104 | logger.debug('Circuit breaker transitioning to half-open'); 105 | return true; 106 | } 107 | return false; 108 | 109 | case 'half-open': 110 | // Allow limited requests in half-open state 111 | if (this.halfOpenCount < this.halfOpenRequests) { 112 | this.halfOpenCount++; 113 | return true; 114 | } 115 | return false; 116 | 117 | default: 118 | return false; 119 | } 120 | } 121 | 122 | /** 123 | * Record a success 124 | */ 125 | recordSuccess(): void { 126 | if (this.state === 'half-open') { 127 | // If we've had enough successful requests, close the circuit 128 | if (this.halfOpenCount >= this.halfOpenRequests) { 129 | this.state = 'closed'; 130 | this.failureCount = 0; 131 | logger.debug('Circuit breaker closed after successful recovery'); 132 | } 133 | } else if (this.state === 'closed') { 134 | // Reset failure count on success 135 | this.failureCount = 0; 136 | } 137 | } 138 | 139 | /** 140 | * Record a failure 141 | */ 142 | recordFailure(error?: Error): void { 143 | this.failureCount++; 144 | this.lastFailureTime = Date.now(); 145 | 146 | if (this.state === 'half-open') { 147 | // Immediately open on failure in half-open state 148 | this.state = 'open'; 149 | logger.debug('Circuit breaker opened from half-open state', { error: error?.message }); 150 | } else if (this.state === 'closed' && this.failureCount >= this.failureThreshold) { 151 | // Open circuit after threshold reached 152 | this.state = 'open'; 153 | logger.debug( 154 | `Circuit breaker opened after ${this.failureCount} failures`, 155 | { error: error?.message } 156 | ); 157 | } 158 | } 159 | 160 | /** 161 | * Get current state 162 | */ 163 | getState(): { state: string; failureCount: number; canRetry: boolean } { 164 | return { 165 | state: this.state, 166 | failureCount: this.failureCount, 167 | canRetry: this.shouldAllow() 168 | }; 169 | } 170 | 171 | /** 172 | * Force reset the circuit breaker 173 | */ 174 | reset(): void { 175 | this.state = 'closed'; 176 | this.failureCount = 0; 177 | this.lastFailureTime = 0; 178 | this.halfOpenCount = 0; 179 | } 180 | } 181 | 182 | /** 183 | * Error aggregator for tracking error patterns 184 | */ 185 | export class TelemetryErrorAggregator { 186 | private errors: Map<TelemetryErrorType, number> = new Map(); 187 | private errorDetails: TelemetryErrorContext[] = []; 188 | private readonly maxDetails: number = 100; 189 | 190 | /** 191 | * Record an error 192 | */ 193 | record(error: TelemetryError): void { 194 | // Increment counter for this error type 195 | const count = this.errors.get(error.type) || 0; 196 | this.errors.set(error.type, count + 1); 197 | 198 | // Store error details (limited) 199 | this.errorDetails.push(error.toContext()); 200 | if (this.errorDetails.length > this.maxDetails) { 201 | this.errorDetails.shift(); 202 | } 203 | } 204 | 205 | /** 206 | * Get error statistics 207 | */ 208 | getStats(): { 209 | totalErrors: number; 210 | errorsByType: Record<string, number>; 211 | mostCommonError?: string; 212 | recentErrors: TelemetryErrorContext[]; 213 | } { 214 | const errorsByType: Record<string, number> = {}; 215 | let totalErrors = 0; 216 | let mostCommonError: string | undefined; 217 | let maxCount = 0; 218 | 219 | for (const [type, count] of this.errors.entries()) { 220 | errorsByType[type] = count; 221 | totalErrors += count; 222 | 223 | if (count > maxCount) { 224 | maxCount = count; 225 | mostCommonError = type; 226 | } 227 | } 228 | 229 | return { 230 | totalErrors, 231 | errorsByType, 232 | mostCommonError, 233 | recentErrors: this.errorDetails.slice(-10) // Last 10 errors 234 | }; 235 | } 236 | 237 | /** 238 | * Clear error history 239 | */ 240 | reset(): void { 241 | this.errors.clear(); 242 | this.errorDetails = []; 243 | } 244 | } ``` -------------------------------------------------------------------------------- /.claude/agents/test-automator.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | name: test-automator 3 | description: Use this agent when you need to create comprehensive test suites, improve test coverage, or set up test automation infrastructure. This includes writing unit tests with mocks, integration tests with test containers, E2E tests with Playwright/Cypress, configuring CI/CD pipelines for testing, or establishing test data management strategies. The agent should be used proactively when new features are implemented without tests or when test coverage needs improvement.\n\nExamples:\n- <example>\n Context: The user has just implemented a new API endpoint without tests.\n user: "I've added a new user registration endpoint to our API"\n assistant: "I see you've added a new endpoint. Let me use the test-automator agent to create a comprehensive test suite for it."\n <commentary>\n Since new functionality was added without tests, proactively use the test-automator agent to ensure proper test coverage.\n </commentary>\n </example>\n- <example>\n Context: The user explicitly asks for test creation.\n user: "Can you write tests for the payment processing module?"\n assistant: "I'll use the test-automator agent to create a comprehensive test suite for the payment processing module."\n <commentary>\n The user directly requested tests, so use the test-automator agent to handle this task.\n </commentary>\n </example>\n- <example>\n Context: The user mentions issues with test reliability.\n user: "Our tests keep failing randomly in CI"\n assistant: "I'll use the test-automator agent to analyze and fix the flaky tests, ensuring they run deterministically."\n <commentary>\n Test reliability issues require the test-automator agent's expertise in creating deterministic tests.\n </commentary>\n </example> 4 | --- 5 | 6 | You are a test automation specialist with deep expertise in comprehensive testing strategies across multiple frameworks and languages. Your mission is to create robust, maintainable test suites that provide confidence in code quality while enabling rapid development cycles. 7 | 8 | ## Core Responsibilities 9 | 10 | You will design and implement test suites following the test pyramid principle: 11 | - **Unit Tests (70%)**: Fast, isolated tests with extensive mocking and stubbing 12 | - **Integration Tests (20%)**: Tests verifying component interactions, using test containers when needed 13 | - **E2E Tests (10%)**: Critical user journey tests using Playwright, Cypress, or similar tools 14 | 15 | ## Testing Philosophy 16 | 17 | 1. **Test Behavior, Not Implementation**: Focus on what the code does, not how it does it. Tests should survive refactoring. 18 | 2. **Arrange-Act-Assert Pattern**: Structure every test clearly with setup, execution, and verification phases. 19 | 3. **Deterministic Execution**: Eliminate flakiness through proper async handling, explicit waits, and controlled test data. 20 | 4. **Fast Feedback**: Optimize for quick test execution through parallelization and efficient test design. 21 | 5. **Meaningful Test Names**: Use descriptive names that explain what is being tested and expected behavior. 22 | 23 | ## Implementation Guidelines 24 | 25 | ### Unit Testing 26 | - Create focused tests for individual functions/methods 27 | - Mock all external dependencies (databases, APIs, file systems) 28 | - Use factories or builders for test data creation 29 | - Include edge cases: null values, empty collections, boundary conditions 30 | - Aim for high code coverage but prioritize critical paths 31 | 32 | ### Integration Testing 33 | - Test real interactions between components 34 | - Use test containers for databases and external services 35 | - Verify data persistence and retrieval 36 | - Test transaction boundaries and rollback scenarios 37 | - Include error handling and recovery tests 38 | 39 | ### E2E Testing 40 | - Focus on critical user journeys only 41 | - Use page object pattern for maintainability 42 | - Implement proper wait strategies (no arbitrary sleeps) 43 | - Create reusable test utilities and helpers 44 | - Include accessibility checks where applicable 45 | 46 | ### Test Data Management 47 | - Create factories or fixtures for consistent test data 48 | - Use builders for complex object creation 49 | - Implement data cleanup strategies 50 | - Separate test data from production data 51 | - Version control test data schemas 52 | 53 | ### CI/CD Integration 54 | - Configure parallel test execution 55 | - Set up test result reporting and artifacts 56 | - Implement test retry strategies for network-dependent tests 57 | - Create test environment provisioning 58 | - Configure coverage thresholds and reporting 59 | 60 | ## Output Requirements 61 | 62 | You will provide: 63 | 1. **Complete test files** with all necessary imports and setup 64 | 2. **Mock implementations** for external dependencies 65 | 3. **Test data factories** or fixtures as separate modules 66 | 4. **CI pipeline configuration** (GitHub Actions, GitLab CI, Jenkins, etc.) 67 | 5. **Coverage configuration** files and scripts 68 | 6. **E2E test scenarios** with page objects and utilities 69 | 7. **Documentation** explaining test structure and running instructions 70 | 71 | ## Framework Selection 72 | 73 | Choose appropriate frameworks based on the technology stack: 74 | - **JavaScript/TypeScript**: Jest, Vitest, Mocha + Chai, Playwright, Cypress 75 | - **Python**: pytest, unittest, pytest-mock, factory_boy 76 | - **Java**: JUnit 5, Mockito, TestContainers, REST Assured 77 | - **Go**: testing package, testify, gomock 78 | - **Ruby**: RSpec, Minitest, FactoryBot 79 | 80 | ## Quality Checks 81 | 82 | Before finalizing any test suite, verify: 83 | - All tests pass consistently (run multiple times) 84 | - No hardcoded values or environment dependencies 85 | - Proper teardown and cleanup 86 | - Clear assertion messages for failures 87 | - Appropriate use of beforeEach/afterEach hooks 88 | - No test interdependencies 89 | - Reasonable execution time 90 | 91 | ## Special Considerations 92 | 93 | - For async code, ensure proper promise handling and async/await usage 94 | - For UI tests, implement proper element waiting strategies 95 | - For API tests, validate both response structure and data 96 | - For performance-critical code, include benchmark tests 97 | - For security-sensitive code, include security-focused test cases 98 | 99 | When encountering existing tests, analyze them first to understand patterns and conventions before adding new ones. Always strive for consistency with the existing test architecture while improving where possible. 100 | ``` -------------------------------------------------------------------------------- /docs/INSTALLATION.md: -------------------------------------------------------------------------------- ```markdown 1 | # Installation Guide 2 | 3 | This guide covers all installation methods for n8n-MCP. 4 | 5 | ## Table of Contents 6 | 7 | - [Quick Start](#quick-start) 8 | - [Docker Installation](#docker-installation) 9 | - [Manual Installation](#manual-installation) 10 | - [Development Setup](#development-setup) 11 | - [Troubleshooting](#troubleshooting) 12 | 13 | ## Quick Start 14 | 15 | The fastest way to get n8n-MCP running: 16 | 17 | ```bash 18 | # Using Docker (recommended) 19 | cat > .env << EOF 20 | AUTH_TOKEN=$(openssl rand -base64 32) 21 | USE_FIXED_HTTP=true 22 | EOF 23 | docker compose up -d 24 | ``` 25 | 26 | ## Docker Installation 27 | 28 | ### Prerequisites 29 | 30 | - Docker Engine (install via package manager or Docker Desktop) 31 | - Docker Compose V2 (included with modern Docker installations) 32 | 33 | ### Method 1: Using Pre-built Images 34 | 35 | 1. **Create a project directory:** 36 | ```bash 37 | mkdir n8n-mcp && cd n8n-mcp 38 | ``` 39 | 40 | 2. **Create docker-compose.yml:** 41 | ```yaml 42 | version: '3.8' 43 | 44 | services: 45 | n8n-mcp: 46 | image: ghcr.io/czlonkowski/n8n-mcp:latest 47 | container_name: n8n-mcp 48 | restart: unless-stopped 49 | 50 | environment: 51 | MCP_MODE: ${MCP_MODE:-http} 52 | USE_FIXED_HTTP: ${USE_FIXED_HTTP:-true} 53 | AUTH_TOKEN: ${AUTH_TOKEN:?AUTH_TOKEN is required} 54 | NODE_ENV: ${NODE_ENV:-production} 55 | LOG_LEVEL: ${LOG_LEVEL:-info} 56 | PORT: ${PORT:-3000} 57 | 58 | volumes: 59 | - n8n-mcp-data:/app/data 60 | 61 | ports: 62 | - "${PORT:-3000}:3000" 63 | 64 | healthcheck: 65 | test: ["CMD", "curl", "-f", "http://127.0.0.1:3000/health"] 66 | interval: 30s 67 | timeout: 10s 68 | retries: 3 69 | 70 | volumes: 71 | n8n-mcp-data: 72 | driver: local 73 | ``` 74 | 75 | 3. **Create .env file:** 76 | ```bash 77 | echo "AUTH_TOKEN=$(openssl rand -base64 32)" > .env 78 | ``` 79 | 80 | 4. **Start the container:** 81 | ```bash 82 | docker compose up -d 83 | ``` 84 | 85 | 5. **Verify installation:** 86 | ```bash 87 | curl http://localhost:3000/health 88 | ``` 89 | 90 | ### Method 2: Building from Source 91 | 92 | 1. **Clone the repository:** 93 | ```bash 94 | git clone https://github.com/czlonkowski/n8n-mcp.git 95 | cd n8n-mcp 96 | ``` 97 | 98 | 2. **Build the image:** 99 | ```bash 100 | docker build -t n8n-mcp:local . 101 | ``` 102 | 103 | 3. **Run with docker-compose:** 104 | ```bash 105 | docker compose up -d 106 | ``` 107 | 108 | ### Docker Management Commands 109 | 110 | ```bash 111 | # View logs 112 | docker compose logs -f 113 | 114 | # Stop the container 115 | docker compose stop 116 | 117 | # Remove container and volumes 118 | docker compose down -v 119 | 120 | # Update to latest image 121 | docker compose pull 122 | docker compose up -d 123 | 124 | # Execute commands inside container 125 | docker compose exec n8n-mcp npm run validate 126 | 127 | # Backup database 128 | docker cp n8n-mcp:/app/data/nodes.db ./nodes-backup.db 129 | ``` 130 | 131 | ## Manual Installation 132 | 133 | ### Prerequisites 134 | 135 | - Node.js v16+ (v20+ recommended) 136 | - npm or yarn 137 | - Git 138 | 139 | ### Step-by-Step Installation 140 | 141 | 1. **Clone the repository:** 142 | ```bash 143 | git clone https://github.com/czlonkowski/n8n-mcp.git 144 | cd n8n-mcp 145 | ``` 146 | 147 | 2. **Clone n8n documentation (optional but recommended):** 148 | ```bash 149 | git clone https://github.com/n8n-io/n8n-docs.git ../n8n-docs 150 | ``` 151 | 152 | 3. **Install dependencies:** 153 | ```bash 154 | npm install 155 | ``` 156 | 157 | 4. **Build the project:** 158 | ```bash 159 | npm run build 160 | ``` 161 | 162 | 5. **Initialize the database:** 163 | ```bash 164 | npm run rebuild 165 | ``` 166 | 167 | 6. **Validate installation:** 168 | ```bash 169 | npm run test-nodes 170 | ``` 171 | 172 | ### Running the Server 173 | 174 | #### stdio Mode (for Claude Desktop) 175 | ```bash 176 | npm start 177 | ``` 178 | 179 | #### HTTP Mode (for remote access) 180 | ```bash 181 | npm run start:http 182 | ``` 183 | 184 | ### Environment Configuration 185 | 186 | Create a `.env` file in the project root: 187 | 188 | ```env 189 | # Server configuration 190 | MCP_MODE=http # or stdio 191 | PORT=3000 192 | HOST=0.0.0.0 193 | NODE_ENV=production 194 | LOG_LEVEL=info 195 | 196 | # Authentication (required for HTTP mode) 197 | AUTH_TOKEN=your-secure-token-here 198 | 199 | # Database 200 | NODE_DB_PATH=./data/nodes.db 201 | REBUILD_ON_START=false 202 | ``` 203 | 204 | ## Development Setup 205 | 206 | ### Prerequisites 207 | 208 | - All manual installation prerequisites 209 | - TypeScript knowledge 210 | - Familiarity with MCP protocol 211 | 212 | ### Setup Steps 213 | 214 | 1. **Clone and install:** 215 | ```bash 216 | git clone https://github.com/czlonkowski/n8n-mcp.git 217 | cd n8n-mcp 218 | npm install 219 | ``` 220 | 221 | 2. **Set up development environment:** 222 | ```bash 223 | cp .env.example .env 224 | # Edit .env with your settings 225 | ``` 226 | 227 | 3. **Development commands:** 228 | ```bash 229 | # Run in development mode with auto-reload 230 | npm run dev 231 | 232 | # Run tests 233 | npm test 234 | 235 | # Type checking 236 | npm run typecheck 237 | 238 | # Linting 239 | npm run lint 240 | ``` 241 | 242 | ### Docker Development 243 | 244 | 1. **Use docker-compose override:** 245 | ```bash 246 | cp docker-compose.override.yml.example docker-compose.override.yml 247 | ``` 248 | 249 | 2. **Edit override for development:** 250 | ```yaml 251 | version: '3.8' 252 | 253 | services: 254 | n8n-mcp: 255 | build: . 256 | environment: 257 | NODE_ENV: development 258 | LOG_LEVEL: debug 259 | volumes: 260 | - ./src:/app/src:ro 261 | - ./dist:/app/dist 262 | ``` 263 | 264 | 3. **Run with live reload:** 265 | ```bash 266 | docker compose up --build 267 | ``` 268 | 269 | ## Troubleshooting 270 | 271 | ### Common Issues 272 | 273 | #### Port Already in Use 274 | ```bash 275 | # Find process using port 3000 276 | lsof -i :3000 277 | 278 | # Use a different port 279 | PORT=3001 docker compose up -d 280 | ``` 281 | 282 | #### Database Initialization Failed 283 | ```bash 284 | # For Docker 285 | docker compose exec n8n-mcp npm run rebuild 286 | 287 | # For manual installation 288 | npm run rebuild 289 | ``` 290 | 291 | #### Permission Denied Errors 292 | ```bash 293 | # Fix permissions (Linux/macOS) 294 | sudo chown -R $(whoami) ./data 295 | 296 | # For Docker volumes 297 | docker compose exec n8n-mcp chown -R nodejs:nodejs /app/data 298 | ``` 299 | 300 | #### Node Version Mismatch 301 | The project includes automatic fallback to sql.js for compatibility. If you still have issues: 302 | ```bash 303 | # Check Node version 304 | node --version 305 | 306 | # Use nvm to switch versions 307 | nvm use 20 308 | ``` 309 | 310 | ### Getting Help 311 | 312 | 1. Check the logs: 313 | - Docker: `docker compose logs` 314 | - Manual: Check console output or `LOG_LEVEL=debug npm start` 315 | 316 | 2. Validate the database: 317 | ```bash 318 | npm run validate 319 | ``` 320 | 321 | 3. Run tests: 322 | ```bash 323 | npm test 324 | ``` 325 | 326 | 4. Report issues: 327 | - GitHub Issues: https://github.com/czlonkowski/n8n-mcp/issues 328 | - Include logs and environment details 329 | 330 | ## Next Steps 331 | 332 | After installation, configure Claude Desktop to use n8n-MCP: 333 | - See [Claude Desktop Setup Guide](./README_CLAUDE_SETUP.md) 334 | - For remote deployments, see [HTTP Deployment Guide](./HTTP_DEPLOYMENT.md) 335 | - For Docker details, see [Docker README](../DOCKER_README.md) ``` -------------------------------------------------------------------------------- /deploy/quick-deploy-n8n.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | # Quick deployment script for n8n + n8n-mcp stack 3 | 4 | set -e 5 | 6 | # Colors for output 7 | RED='\033[0;31m' 8 | GREEN='\033[0;32m' 9 | YELLOW='\033[1;33m' 10 | NC='\033[0m' # No Color 11 | 12 | # Default values 13 | COMPOSE_FILE="docker-compose.n8n.yml" 14 | ENV_FILE=".env" 15 | ENV_EXAMPLE=".env.n8n.example" 16 | 17 | # Function to print colored output 18 | print_info() { 19 | echo -e "${GREEN}[INFO]${NC} $1" 20 | } 21 | 22 | print_warn() { 23 | echo -e "${YELLOW}[WARN]${NC} $1" 24 | } 25 | 26 | print_error() { 27 | echo -e "${RED}[ERROR]${NC} $1" 28 | } 29 | 30 | # Function to generate random token 31 | generate_token() { 32 | openssl rand -hex 32 33 | } 34 | 35 | # Function to check prerequisites 36 | check_prerequisites() { 37 | print_info "Checking prerequisites..." 38 | 39 | # Check Docker 40 | if ! command -v docker &> /dev/null; then 41 | print_error "Docker is not installed. Please install Docker first." 42 | exit 1 43 | fi 44 | 45 | # Check Docker Compose 46 | if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then 47 | print_error "Docker Compose is not installed. Please install Docker Compose first." 48 | exit 1 49 | fi 50 | 51 | # Check openssl for token generation 52 | if ! command -v openssl &> /dev/null; then 53 | print_error "OpenSSL is not installed. Please install OpenSSL first." 54 | exit 1 55 | fi 56 | 57 | print_info "All prerequisites are installed." 58 | } 59 | 60 | # Function to setup environment 61 | setup_environment() { 62 | print_info "Setting up environment..." 63 | 64 | # Check if .env exists 65 | if [ -f "$ENV_FILE" ]; then 66 | print_warn ".env file already exists. Backing up to .env.backup" 67 | cp "$ENV_FILE" ".env.backup" 68 | fi 69 | 70 | # Copy example env file 71 | if [ -f "$ENV_EXAMPLE" ]; then 72 | cp "$ENV_EXAMPLE" "$ENV_FILE" 73 | print_info "Created .env file from example" 74 | else 75 | print_error ".env.n8n.example file not found!" 76 | exit 1 77 | fi 78 | 79 | # Generate encryption key 80 | ENCRYPTION_KEY=$(generate_token) 81 | if [[ "$OSTYPE" == "darwin"* ]]; then 82 | sed -i '' "s/N8N_ENCRYPTION_KEY=/N8N_ENCRYPTION_KEY=$ENCRYPTION_KEY/" "$ENV_FILE" 83 | else 84 | sed -i "s/N8N_ENCRYPTION_KEY=/N8N_ENCRYPTION_KEY=$ENCRYPTION_KEY/" "$ENV_FILE" 85 | fi 86 | print_info "Generated n8n encryption key" 87 | 88 | # Generate MCP auth token 89 | MCP_TOKEN=$(generate_token) 90 | if [[ "$OSTYPE" == "darwin"* ]]; then 91 | sed -i '' "s/MCP_AUTH_TOKEN=/MCP_AUTH_TOKEN=$MCP_TOKEN/" "$ENV_FILE" 92 | else 93 | sed -i "s/MCP_AUTH_TOKEN=/MCP_AUTH_TOKEN=$MCP_TOKEN/" "$ENV_FILE" 94 | fi 95 | print_info "Generated MCP authentication token" 96 | 97 | print_warn "Please update the following in .env file:" 98 | print_warn " - N8N_BASIC_AUTH_PASSWORD (current: changeme)" 99 | print_warn " - N8N_API_KEY (get from n8n UI after first start)" 100 | } 101 | 102 | # Function to build images 103 | build_images() { 104 | print_info "Building n8n-mcp image..." 105 | 106 | if docker compose version &> /dev/null; then 107 | docker compose -f "$COMPOSE_FILE" build 108 | else 109 | docker-compose -f "$COMPOSE_FILE" build 110 | fi 111 | 112 | print_info "Image built successfully" 113 | } 114 | 115 | # Function to start services 116 | start_services() { 117 | print_info "Starting services..." 118 | 119 | if docker compose version &> /dev/null; then 120 | docker compose -f "$COMPOSE_FILE" up -d 121 | else 122 | docker-compose -f "$COMPOSE_FILE" up -d 123 | fi 124 | 125 | print_info "Services started" 126 | } 127 | 128 | # Function to show status 129 | show_status() { 130 | print_info "Checking service status..." 131 | 132 | if docker compose version &> /dev/null; then 133 | docker compose -f "$COMPOSE_FILE" ps 134 | else 135 | docker-compose -f "$COMPOSE_FILE" ps 136 | fi 137 | 138 | echo "" 139 | print_info "Services are starting up. This may take a minute..." 140 | print_info "n8n will be available at: http://localhost:5678" 141 | print_info "n8n-mcp will be available at: http://localhost:3000" 142 | echo "" 143 | print_warn "Next steps:" 144 | print_warn "1. Access n8n at http://localhost:5678" 145 | print_warn "2. Log in with admin/changeme (or your custom password)" 146 | print_warn "3. Go to Settings > n8n API > Create API Key" 147 | print_warn "4. Update N8N_API_KEY in .env file" 148 | print_warn "5. Restart n8n-mcp: docker-compose -f $COMPOSE_FILE restart n8n-mcp" 149 | } 150 | 151 | # Function to stop services 152 | stop_services() { 153 | print_info "Stopping services..." 154 | 155 | if docker compose version &> /dev/null; then 156 | docker compose -f "$COMPOSE_FILE" down 157 | else 158 | docker-compose -f "$COMPOSE_FILE" down 159 | fi 160 | 161 | print_info "Services stopped" 162 | } 163 | 164 | # Function to view logs 165 | view_logs() { 166 | SERVICE=$1 167 | 168 | if [ -z "$SERVICE" ]; then 169 | if docker compose version &> /dev/null; then 170 | docker compose -f "$COMPOSE_FILE" logs -f 171 | else 172 | docker-compose -f "$COMPOSE_FILE" logs -f 173 | fi 174 | else 175 | if docker compose version &> /dev/null; then 176 | docker compose -f "$COMPOSE_FILE" logs -f "$SERVICE" 177 | else 178 | docker-compose -f "$COMPOSE_FILE" logs -f "$SERVICE" 179 | fi 180 | fi 181 | } 182 | 183 | # Main script 184 | case "${1:-help}" in 185 | setup) 186 | check_prerequisites 187 | setup_environment 188 | build_images 189 | start_services 190 | show_status 191 | ;; 192 | start) 193 | start_services 194 | show_status 195 | ;; 196 | stop) 197 | stop_services 198 | ;; 199 | restart) 200 | stop_services 201 | start_services 202 | show_status 203 | ;; 204 | status) 205 | show_status 206 | ;; 207 | logs) 208 | view_logs "${2}" 209 | ;; 210 | build) 211 | build_images 212 | ;; 213 | *) 214 | echo "n8n-mcp Quick Deploy Script" 215 | echo "" 216 | echo "Usage: $0 {setup|start|stop|restart|status|logs|build}" 217 | echo "" 218 | echo "Commands:" 219 | echo " setup - Initial setup: create .env, build images, and start services" 220 | echo " start - Start all services" 221 | echo " stop - Stop all services" 222 | echo " restart - Restart all services" 223 | echo " status - Show service status" 224 | echo " logs - View logs (optionally specify service: logs n8n-mcp)" 225 | echo " build - Build/rebuild images" 226 | echo "" 227 | echo "Examples:" 228 | echo " $0 setup # First time setup" 229 | echo " $0 logs n8n-mcp # View n8n-mcp logs" 230 | echo " $0 restart # Restart all services" 231 | ;; 232 | esac ```