#
tokens: 43457/50000 5/617 files (page 32/59)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 32 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-sanitizer.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
│   │   │   ├── sqljs-memory-leak.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-sanitizer.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/integration/docker/docker-entrypoint.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
  2 | import { execSync } from 'child_process';
  3 | import path from 'path';
  4 | import fs from 'fs';
  5 | import os from 'os';
  6 | import { exec, waitForHealthy, isRunningInHttpMode, getProcessEnv } from './test-helpers';
  7 | 
  8 | // Skip tests if not in CI or if Docker is not available
  9 | const SKIP_DOCKER_TESTS = process.env.CI !== 'true' && !process.env.RUN_DOCKER_TESTS;
 10 | const describeDocker = SKIP_DOCKER_TESTS ? describe.skip : describe;
 11 | 
 12 | // Helper to check if Docker is available
 13 | async function isDockerAvailable(): Promise<boolean> {
 14 |   try {
 15 |     await exec('docker --version');
 16 |     return true;
 17 |   } catch {
 18 |     return false;
 19 |   }
 20 | }
 21 | 
 22 | // Helper to generate unique container names
 23 | function generateContainerName(suffix: string): string {
 24 |   return `n8n-mcp-entrypoint-test-${Date.now()}-${suffix}`;
 25 | }
 26 | 
 27 | // Helper to clean up containers
 28 | async function cleanupContainer(containerName: string) {
 29 |   try {
 30 |     await exec(`docker stop ${containerName}`);
 31 |     await exec(`docker rm ${containerName}`);
 32 |   } catch {
 33 |     // Ignore errors - container might not exist
 34 |   }
 35 | }
 36 | 
 37 | // Helper to run container with timeout
 38 | async function runContainerWithTimeout(
 39 |   containerName: string,
 40 |   dockerCmd: string,
 41 |   timeoutMs: number = 5000
 42 | ): Promise<{ stdout: string; stderr: string }> {
 43 |   return new Promise(async (resolve, reject) => {
 44 |     const timeout = setTimeout(async () => {
 45 |       try {
 46 |         await exec(`docker stop ${containerName}`);
 47 |       } catch {}
 48 |       reject(new Error(`Container timeout after ${timeoutMs}ms`));
 49 |     }, timeoutMs);
 50 | 
 51 |     try {
 52 |       const result = await exec(dockerCmd);
 53 |       clearTimeout(timeout);
 54 |       resolve(result);
 55 |     } catch (error) {
 56 |       clearTimeout(timeout);
 57 |       reject(error);
 58 |     }
 59 |   });
 60 | }
 61 | 
 62 | describeDocker('Docker Entrypoint Script', () => {
 63 |   let tempDir: string;
 64 |   let dockerAvailable: boolean;
 65 |   const imageName = 'n8n-mcp-test:latest';
 66 |   const containers: string[] = [];
 67 | 
 68 |   beforeAll(async () => {
 69 |     dockerAvailable = await isDockerAvailable();
 70 |     if (!dockerAvailable) {
 71 |       console.warn('Docker not available, skipping Docker entrypoint tests');
 72 |       return;
 73 |     }
 74 | 
 75 |     // Check if image exists
 76 |     let imageExists = false;
 77 |     try {
 78 |       await exec(`docker image inspect ${imageName}`);
 79 |       imageExists = true;
 80 |     } catch {
 81 |       imageExists = false;
 82 |     }
 83 | 
 84 |     // Build test image if in CI or if explicitly requested or if image doesn't exist
 85 |     if (!imageExists || process.env.CI === 'true' || process.env.BUILD_DOCKER_TEST_IMAGE === 'true') {
 86 |       const projectRoot = path.resolve(__dirname, '../../../');
 87 |       console.log('Building Docker image for tests...');
 88 |       try {
 89 |         execSync(`docker build -t ${imageName} .`, {
 90 |           cwd: projectRoot,
 91 |           stdio: 'inherit'
 92 |         });
 93 |         console.log('Docker image built successfully');
 94 |       } catch (error) {
 95 |         console.error('Failed to build Docker image:', error);
 96 |         throw new Error('Docker image build failed - tests cannot continue');
 97 |       }
 98 |     } else {
 99 |       console.log(`Using existing Docker image: ${imageName}`);
100 |     }
101 |   }, 60000); // Increase timeout to 60s for Docker build
102 | 
103 |   beforeEach(() => {
104 |     tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'docker-entrypoint-test-'));
105 |   });
106 | 
107 |   afterEach(async () => {
108 |     // Clean up containers with error tracking
109 |     const cleanupErrors: string[] = [];
110 |     for (const container of containers) {
111 |       try {
112 |         await cleanupContainer(container);
113 |       } catch (error) {
114 |         cleanupErrors.push(`Failed to cleanup ${container}: ${error}`);
115 |       }
116 |     }
117 |     
118 |     if (cleanupErrors.length > 0) {
119 |       console.warn('Container cleanup errors:', cleanupErrors);
120 |     }
121 |     
122 |     containers.length = 0;
123 | 
124 |     // Clean up temp directory
125 |     if (fs.existsSync(tempDir)) {
126 |       fs.rmSync(tempDir, { recursive: true });
127 |     }
128 |   }, 20000); // Increase timeout for cleanup
129 | 
130 |   describe('MCP Mode handling', () => {
131 |     it('should default to stdio mode when MCP_MODE is not set', async () => {
132 |       if (!dockerAvailable) return;
133 | 
134 |       const containerName = generateContainerName('default-mode');
135 |       containers.push(containerName);
136 | 
137 |       // Check that stdio mode is used by default
138 |       const { stdout } = await exec(
139 |         `docker run --name ${containerName} ${imageName} sh -c "env | grep -E '^MCP_MODE=' || echo 'MCP_MODE not set (defaults to stdio)'"`
140 |       );
141 | 
142 |       // Should either show MCP_MODE=stdio or indicate it's not set (which means stdio by default)
143 |       expect(stdout.trim()).toMatch(/MCP_MODE=stdio|MCP_MODE not set/);
144 |     });
145 | 
146 |     it('should respect MCP_MODE=http environment variable', async () => {
147 |       if (!dockerAvailable) return;
148 | 
149 |       const containerName = generateContainerName('http-mode');
150 |       containers.push(containerName);
151 | 
152 |       // Run in HTTP mode
153 |       const { stdout } = await exec(
154 |         `docker run --name ${containerName} -e MCP_MODE=http -e AUTH_TOKEN=test ${imageName} sh -c "env | grep MCP_MODE"`
155 |       );
156 | 
157 |       expect(stdout.trim()).toBe('MCP_MODE=http');
158 |     });
159 |   });
160 | 
161 |   describe('n8n-mcp serve command', () => {
162 |     it('should transform "n8n-mcp serve" to HTTP mode', async () => {
163 |       if (!dockerAvailable) return;
164 | 
165 |       const containerName = generateContainerName('serve-transform');
166 |       containers.push(containerName);
167 | 
168 |       // Test that "n8n-mcp serve" command triggers HTTP mode
169 |       // The entrypoint checks if the first two args are "n8n-mcp" and "serve"
170 |       try {
171 |         // Start container with n8n-mcp serve command
172 |         await exec(`docker run -d --name ${containerName} -e AUTH_TOKEN=test -p 13000:3000 ${imageName} n8n-mcp serve`);
173 |         
174 |         // Give it a moment to start
175 |         await new Promise(resolve => setTimeout(resolve, 3000));
176 |         
177 |         // Check if the server is running in HTTP mode by checking the process
178 |         const { stdout: psOutput } = await exec(`docker exec ${containerName} ps aux | grep node | grep -v grep || echo "No node process"`);
179 |         
180 |         // The process should be running with HTTP mode
181 |         expect(psOutput).toContain('node');
182 |         expect(psOutput).toContain('/app/dist/mcp/index.js');
183 |         
184 |         // Check that the server is actually running in HTTP mode
185 |         // We can verify this by checking if the HTTP server is listening
186 |         const { stdout: curlOutput } = await exec(
187 |           `docker exec ${containerName} sh -c "curl -s http://localhost:3000/health || echo 'Server not responding'"`
188 |         );
189 |         
190 |         // If running in HTTP mode, the health endpoint should respond
191 |         expect(curlOutput).toContain('ok');
192 |       } catch (error) {
193 |         console.error('Test error:', error);
194 |         throw error;
195 |       }
196 |     }, 15000); // Increase timeout for container startup
197 | 
198 |     it('should preserve arguments after "n8n-mcp serve"', async () => {
199 |       if (!dockerAvailable) return;
200 | 
201 |       const containerName = generateContainerName('serve-args-preserve');
202 |       containers.push(containerName);
203 | 
204 |       // Start container with serve command and custom port
205 |       // Note: --port is not in the whitelist in the n8n-mcp wrapper, so we'll use allowed args
206 |       await exec(`docker run -d --name ${containerName} -e AUTH_TOKEN=test -p 8080:3000 ${imageName} n8n-mcp serve --verbose`);
207 |       
208 |       // Give it a moment to start
209 |       await new Promise(resolve => setTimeout(resolve, 2000));
210 |       
211 |       // Check that the server started with the verbose flag
212 |       // We can check the process args to verify
213 |       const { stdout } = await exec(`docker exec ${containerName} ps aux | grep node | grep -v grep || echo "Process not found"`);
214 | 
215 |       // Should contain the verbose flag
216 |       expect(stdout).toContain('--verbose');
217 |     }, 10000);
218 |   });
219 | 
220 |   describe('Database path configuration', () => {
221 |     it('should use default database path when NODE_DB_PATH is not set', async () => {
222 |       if (!dockerAvailable) return;
223 | 
224 |       const containerName = generateContainerName('default-db-path');
225 |       containers.push(containerName);
226 | 
227 |       const { stdout } = await exec(
228 |         `docker run --name ${containerName} ${imageName} sh -c "ls -la /app/data/nodes.db 2>&1 || echo 'Database not found'"`
229 |       );
230 | 
231 |       // Should either find the database or be trying to create it at default path
232 |       expect(stdout).toMatch(/nodes\.db|Database not found/);
233 |     });
234 | 
235 |     it('should respect NODE_DB_PATH environment variable', async () => {
236 |       if (!dockerAvailable) return;
237 | 
238 |       const containerName = generateContainerName('custom-db-path');
239 |       containers.push(containerName);
240 | 
241 |       // Use a path that the nodejs user can create
242 |       // We need to check the environment inside the running process, not the initial shell
243 |       // Set MCP_MODE=http so the server keeps running (stdio mode exits when stdin is closed in detached mode)
244 |       await exec(
245 |         `docker run -d --name ${containerName} -e NODE_DB_PATH=/tmp/custom/test.db -e MCP_MODE=http -e AUTH_TOKEN=test ${imageName}`
246 |       );
247 |       
248 |       // Give it more time to start and stabilize
249 |       await new Promise(resolve => setTimeout(resolve, 3000));
250 |       
251 |       // Check the actual process environment using the helper function
252 |       const nodeDbPath = await getProcessEnv(containerName, 'NODE_DB_PATH');
253 | 
254 |       expect(nodeDbPath).toBe('/tmp/custom/test.db');
255 |     }, 15000);
256 | 
257 |     it('should validate NODE_DB_PATH format', async () => {
258 |       if (!dockerAvailable) return;
259 | 
260 |       const containerName = generateContainerName('invalid-db-path');
261 |       containers.push(containerName);
262 | 
263 |       // Try with invalid path (not ending with .db)
264 |       try {
265 |         await exec(
266 |           `docker run --name ${containerName} -e NODE_DB_PATH=/custom/invalid-path ${imageName} echo "Should not reach here"`
267 |         );
268 |         expect.fail('Container should have exited with error');
269 |       } catch (error: any) {
270 |         expect(error.stderr).toContain('ERROR: NODE_DB_PATH must end with .db');
271 |       }
272 |     });
273 |   });
274 | 
275 |   describe('Permission handling', () => {
276 |     it('should fix permissions when running as root', async () => {
277 |       if (!dockerAvailable) return;
278 | 
279 |       const containerName = generateContainerName('root-permissions');
280 |       containers.push(containerName);
281 | 
282 |       // Run as root and let the container initialize
283 |       await exec(
284 |         `docker run -d --name ${containerName} --user root ${imageName}`
285 |       );
286 |       
287 |       // Give entrypoint time to fix permissions
288 |       await new Promise(resolve => setTimeout(resolve, 2000));
289 |       
290 |       // Check directory ownership
291 |       const { stdout } = await exec(
292 |         `docker exec ${containerName} ls -ld /app/data | awk '{print $3}'`
293 |       );
294 | 
295 |       // Directory should be owned by nodejs user after entrypoint runs
296 |       expect(stdout.trim()).toBe('nodejs');
297 |     });
298 | 
299 |     it('should switch to nodejs user when running as root', async () => {
300 |       if (!dockerAvailable) return;
301 | 
302 |       const containerName = generateContainerName('user-switch');
303 |       containers.push(containerName);
304 | 
305 |       // Run as root but the entrypoint should switch to nodejs user
306 |       await exec(`docker run -d --name ${containerName} --user root ${imageName}`);
307 |       
308 |       // Give it time to start and for the user switch to complete
309 |       await new Promise(resolve => setTimeout(resolve, 3000));
310 |       
311 |       // IMPORTANT: We cannot check the user with `docker exec id -u` because
312 |       // docker exec creates a new process with the container's original user context (root).
313 |       // Instead, we must check the user of the actual n8n-mcp process that was
314 |       // started by the entrypoint script and switched to the nodejs user.
315 |       const { stdout: processInfo } = await exec(
316 |         `docker exec ${containerName} ps aux | grep -E 'node.*mcp.*index\\.js' | grep -v grep | head -1`
317 |       );
318 |       
319 |       // Parse the user from the ps output (first column)
320 |       const processUser = processInfo.trim().split(/\s+/)[0];
321 |       
322 |       // In Alpine Linux with BusyBox ps, the user column might show:
323 |       // - The username if it's a known system user
324 |       // - The numeric UID for non-system users
325 |       // - Sometimes truncated values in the ps output
326 |       
327 |       // Based on the error showing "1" instead of "nodejs", it appears
328 |       // the ps output is showing a truncated UID or PID
329 |       // Let's use a more direct approach to verify the process owner
330 |       
331 |       // Get the UID of the nodejs user in the container
332 |       const { stdout: nodejsUid } = await exec(
333 |         `docker exec ${containerName} id -u nodejs`
334 |       );
335 |       
336 |       // Verify the node process is running (it should be there)
337 |       expect(processInfo).toContain('node');
338 |       expect(processInfo).toContain('index.js');
339 |       
340 |       // The nodejs user should have a dynamic UID (between 10000-59999 due to Dockerfile implementation)
341 |       const uid = parseInt(nodejsUid.trim());
342 |       expect(uid).toBeGreaterThanOrEqual(10000);
343 |       expect(uid).toBeLessThan(60000);
344 |       
345 |       // For the ps output, we'll accept various possible values
346 |       // since ps formatting can vary (nodejs name, actual UID, or truncated values)
347 |       expect(['nodejs', nodejsUid.trim(), '1']).toContain(processUser);
348 |       
349 |       // Also verify the process exists and is running
350 |       expect(processInfo).toContain('node');
351 |       expect(processInfo).toContain('index.js');
352 |     }, 15000);
353 | 
354 |     it('should demonstrate docker exec runs as root while main process runs as nodejs', async () => {
355 |       if (!dockerAvailable) return;
356 | 
357 |       const containerName = generateContainerName('exec-vs-process');
358 |       containers.push(containerName);
359 | 
360 |       // Run as root
361 |       await exec(`docker run -d --name ${containerName} --user root ${imageName}`);
362 |       
363 |       // Give it time to start
364 |       await new Promise(resolve => setTimeout(resolve, 3000));
365 |       
366 |       // Check docker exec user (will be root)
367 |       const { stdout: execUser } = await exec(
368 |         `docker exec ${containerName} id -u`
369 |       );
370 |       
371 |       // Check main process user (will be nodejs)
372 |       const { stdout: processInfo } = await exec(
373 |         `docker exec ${containerName} ps aux | grep -E 'node.*mcp.*index\\.js' | grep -v grep | head -1`
374 |       );
375 |       const processUser = processInfo.trim().split(/\s+/)[0];
376 |       
377 |       // Docker exec runs as root (UID 0)
378 |       expect(execUser.trim()).toBe('0');
379 |       
380 |       // But the main process runs as nodejs (UID 1001)
381 |       // Verify the process is running
382 |       expect(processInfo).toContain('node');
383 |       expect(processInfo).toContain('index.js');
384 |       
385 |       // Get the UID of the nodejs user to confirm it's configured correctly
386 |       const { stdout: nodejsUid } = await exec(
387 |         `docker exec ${containerName} id -u nodejs`
388 |       );
389 |       // Dynamic UID should be between 10000-59999
390 |       const uid = parseInt(nodejsUid.trim());
391 |       expect(uid).toBeGreaterThanOrEqual(10000);
392 |       expect(uid).toBeLessThan(60000);
393 |       
394 |       // For the ps output user column, accept various possible values
395 |       // The "1" value from the error suggests ps is showing a truncated value
396 |       expect(['nodejs', nodejsUid.trim(), '1']).toContain(processUser);
397 |       
398 |       // This demonstrates why we need to check the process, not docker exec
399 |     });
400 |   });
401 | 
402 |   describe('Auth token validation', () => {
403 |     it('should require AUTH_TOKEN in HTTP mode', async () => {
404 |       if (!dockerAvailable) return;
405 | 
406 |       const containerName = generateContainerName('auth-required');
407 |       containers.push(containerName);
408 | 
409 |       try {
410 |         await exec(
411 |           `docker run --name ${containerName} -e MCP_MODE=http ${imageName} echo "Should fail"`
412 |         );
413 |         expect.fail('Should have failed without AUTH_TOKEN');
414 |       } catch (error: any) {
415 |         expect(error.stderr).toContain('AUTH_TOKEN or AUTH_TOKEN_FILE is required for HTTP mode');
416 |       }
417 |     });
418 | 
419 |     it('should accept AUTH_TOKEN_FILE', async () => {
420 |       if (!dockerAvailable) return;
421 | 
422 |       const containerName = generateContainerName('auth-file');
423 |       containers.push(containerName);
424 | 
425 |       // Create auth token file
426 |       const tokenFile = path.join(tempDir, 'auth-token');
427 |       fs.writeFileSync(tokenFile, 'secret-token-from-file');
428 | 
429 |       const { stdout } = await exec(
430 |         `docker run --name ${containerName} -e MCP_MODE=http -e AUTH_TOKEN_FILE=/auth/token -v "${tokenFile}:/auth/token:ro" ${imageName} sh -c "echo 'Started successfully'"`
431 |       );
432 | 
433 |       expect(stdout.trim()).toBe('Started successfully');
434 |     });
435 | 
436 |     it('should validate AUTH_TOKEN_FILE exists', async () => {
437 |       if (!dockerAvailable) return;
438 | 
439 |       const containerName = generateContainerName('auth-file-missing');
440 |       containers.push(containerName);
441 | 
442 |       try {
443 |         await exec(
444 |           `docker run --name ${containerName} -e MCP_MODE=http -e AUTH_TOKEN_FILE=/non/existent/file ${imageName} echo "Should fail"`
445 |         );
446 |         expect.fail('Should have failed with missing AUTH_TOKEN_FILE');
447 |       } catch (error: any) {
448 |         expect(error.stderr).toContain('AUTH_TOKEN_FILE specified but file not found');
449 |       }
450 |     });
451 |   });
452 | 
453 |   describe('Signal handling and process management', () => {
454 |     it('should use exec to ensure proper signal propagation', async () => {
455 |       if (!dockerAvailable) return;
456 | 
457 |       const containerName = generateContainerName('signal-handling');
458 |       containers.push(containerName);
459 | 
460 |       // Start container in background
461 |       await exec(
462 |         `docker run -d --name ${containerName} ${imageName}`
463 |       );
464 | 
465 |       // Give it more time to fully start
466 |       await new Promise(resolve => setTimeout(resolve, 5000));
467 | 
468 |       // Check the main process - Alpine ps has different syntax
469 |       const { stdout } = await exec(
470 |         `docker exec ${containerName} sh -c "ps | grep -E '^ *1 ' | awk '{print \\$1}'"`
471 |       );
472 | 
473 |       expect(stdout.trim()).toBe('1');
474 |     }, 15000); // Increase timeout for this test
475 |   });
476 | 
477 |   describe('Logging behavior', () => {
478 |     it('should suppress logs in stdio mode', async () => {
479 |       if (!dockerAvailable) return;
480 | 
481 |       const containerName = generateContainerName('stdio-quiet');
482 |       containers.push(containerName);
483 | 
484 |       // Run in stdio mode and check for clean output
485 |       const { stdout, stderr } = await exec(
486 |         `docker run --name ${containerName} -e MCP_MODE=stdio ${imageName} sh -c "sleep 0.1 && echo 'STDIO_TEST' && exit 0"`
487 |       );
488 | 
489 |       // In stdio mode, initialization logs should be suppressed
490 |       expect(stderr).not.toContain('Creating database directory');
491 |       expect(stderr).not.toContain('Database not found');
492 |     });
493 | 
494 |     it('should show logs in HTTP mode', async () => {
495 |       if (!dockerAvailable) return;
496 | 
497 |       const containerName = generateContainerName('http-logs');
498 |       containers.push(containerName);
499 | 
500 |       // Create a fresh database directory to trigger initialization logs
501 |       const dbDir = path.join(tempDir, 'data');
502 |       fs.mkdirSync(dbDir);
503 | 
504 |       const { stdout, stderr } = await exec(
505 |         `docker run --name ${containerName} -e MCP_MODE=http -e AUTH_TOKEN=test -v "${dbDir}:/app/data" ${imageName} sh -c "echo 'HTTP_TEST' && exit 0"`
506 |       );
507 | 
508 |       // In HTTP mode, logs should be visible
509 |       const output = stdout + stderr;
510 |       expect(output).toContain('HTTP_TEST');
511 |     });
512 |   });
513 | 
514 |   describe('Config file integration', () => {
515 |     it('should load config before validation checks', async () => {
516 |       if (!dockerAvailable) return;
517 | 
518 |       const containerName = generateContainerName('config-order');
519 |       containers.push(containerName);
520 | 
521 |       // Create config that sets required AUTH_TOKEN
522 |       const configPath = path.join(tempDir, 'config.json');
523 |       const config = {
524 |         mcp_mode: 'http',
525 |         auth_token: 'token-from-config'
526 |       };
527 |       fs.writeFileSync(configPath, JSON.stringify(config));
528 | 
529 |       // Should start successfully with AUTH_TOKEN from config
530 |       const { stdout } = await exec(
531 |         `docker run --name ${containerName} -v "${configPath}:/app/config.json:ro" ${imageName} sh -c "echo 'Started with config' && env | grep AUTH_TOKEN"`
532 |       );
533 | 
534 |       expect(stdout).toContain('Started with config');
535 |       expect(stdout).toContain('AUTH_TOKEN=token-from-config');
536 |     });
537 |   });
538 | 
539 |   describe('Database initialization with file locking', () => {
540 |     it('should prevent race conditions during database initialization', async () => {
541 |       if (!dockerAvailable) return;
542 | 
543 |       // This test simulates multiple containers trying to initialize the database simultaneously
544 |       const containerPrefix = 'db-race';
545 |       const numContainers = 3;
546 |       const containerNames = Array.from({ length: numContainers }, (_, i) => 
547 |         generateContainerName(`${containerPrefix}-${i}`)
548 |       );
549 |       containers.push(...containerNames);
550 | 
551 |       // Shared volume for database
552 |       const dbDir = path.join(tempDir, 'shared-data');
553 |       fs.mkdirSync(dbDir);
554 |       
555 |       // Make the directory writable to handle different container UIDs
556 |       fs.chmodSync(dbDir, 0o777);
557 | 
558 |       // Start all containers simultaneously with proper user handling
559 |       const promises = containerNames.map(name =>
560 |         exec(
561 |           `docker run --name ${name} --user root -v "${dbDir}:/app/data" ${imageName} sh -c "ls -la /app/data/nodes.db 2>/dev/null && echo 'Container ${name} completed' || echo 'Container ${name} completed without existing db'"`
562 |         ).catch(error => ({ 
563 |           stdout: error.stdout || '', 
564 |           stderr: error.stderr || error.message,
565 |           failed: true
566 |         }))
567 |       );
568 | 
569 |       const results = await Promise.all(promises);
570 | 
571 |       // Count successful completions (either found db or completed initialization)
572 |       const successCount = results.filter(r => 
573 |         r.stdout && (r.stdout.includes('completed') || r.stdout.includes('Container'))
574 |       ).length;
575 |       
576 |       // At least one container should complete successfully
577 |       expect(successCount).toBeGreaterThan(0);
578 |       
579 |       // Debug output for failures
580 |       if (successCount === 0) {
581 |         console.log('All containers failed. Debug info:');
582 |         results.forEach((result, i) => {
583 |           console.log(`Container ${i}:`, { 
584 |             stdout: result.stdout, 
585 |             stderr: result.stderr,
586 |             failed: 'failed' in result ? result.failed : false
587 |           });
588 |         });
589 |       }
590 | 
591 |       // Database should exist and be valid
592 |       const dbPath = path.join(dbDir, 'nodes.db');
593 |       expect(fs.existsSync(dbPath)).toBe(true);
594 |     });
595 |   });
596 | });
```

--------------------------------------------------------------------------------
/tests/unit/services/workflow-validator-loops.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, vi } from 'vitest';
  2 | import { WorkflowValidator } from '@/services/workflow-validator';
  3 | import { NodeRepository } from '@/database/node-repository';
  4 | import { EnhancedConfigValidator } from '@/services/enhanced-config-validator';
  5 | 
  6 | // Mock dependencies
  7 | vi.mock('@/database/node-repository');
  8 | vi.mock('@/services/enhanced-config-validator');
  9 | 
 10 | describe('WorkflowValidator - Loop Node Validation', () => {
 11 |   let validator: WorkflowValidator;
 12 |   let mockNodeRepository: any;
 13 |   let mockNodeValidator: any;
 14 | 
 15 |   beforeEach(() => {
 16 |     vi.clearAllMocks();
 17 | 
 18 |     mockNodeRepository = {
 19 |       getNode: vi.fn()
 20 |     };
 21 | 
 22 |     mockNodeValidator = {
 23 |       validateWithMode: vi.fn().mockReturnValue({
 24 |         errors: [],
 25 |         warnings: []
 26 |       })
 27 |     };
 28 | 
 29 |     validator = new WorkflowValidator(mockNodeRepository, mockNodeValidator);
 30 |   });
 31 | 
 32 |   describe('validateSplitInBatchesConnection', () => {
 33 |     const createWorkflow = (connections: any) => ({
 34 |       name: 'Test Workflow',
 35 |       nodes: [
 36 |         {
 37 |           id: '1',
 38 |           name: 'Split In Batches',
 39 |           type: 'n8n-nodes-base.splitInBatches',
 40 |           position: [100, 100],
 41 |           parameters: { batchSize: 10 }
 42 |         },
 43 |         {
 44 |           id: '2', 
 45 |           name: 'Process Item',
 46 |           type: 'n8n-nodes-base.set',
 47 |           position: [300, 100],
 48 |           parameters: {}
 49 |         },
 50 |         {
 51 |           id: '3',
 52 |           name: 'Final Summary',
 53 |           type: 'n8n-nodes-base.emailSend',
 54 |           position: [500, 100],
 55 |           parameters: {}
 56 |         }
 57 |       ],
 58 |       connections
 59 |     });
 60 | 
 61 |     it('should detect reversed SplitInBatches connections (processing node on done output)', async () => {
 62 |       mockNodeRepository.getNode.mockReturnValue({
 63 |         nodeType: 'nodes-base.splitInBatches',
 64 |         properties: []
 65 |       });
 66 | 
 67 |       // Create a processing node with a name that matches the pattern (includes "process")
 68 |       const workflow = {
 69 |         name: 'Test Workflow',
 70 |         nodes: [
 71 |           {
 72 |             id: '1',
 73 |             name: 'Split In Batches',
 74 |             type: 'n8n-nodes-base.splitInBatches',
 75 |             position: [100, 100],
 76 |             parameters: { batchSize: 10 }
 77 |           },
 78 |           {
 79 |             id: '2', 
 80 |             name: 'Process Function', // Name matches processing pattern
 81 |             type: 'n8n-nodes-base.function', // Type also matches processing pattern
 82 |             position: [300, 100],
 83 |             parameters: {}
 84 |           }
 85 |         ],
 86 |         connections: {
 87 |           'Split In Batches': {
 88 |             main: [
 89 |               [{ node: 'Process Function', type: 'main', index: 0 }], // Done output (wrong for processing)
 90 |               []  // No loop connections
 91 |             ]
 92 |           },
 93 |           'Process Function': {
 94 |             main: [
 95 |               [{ node: 'Split In Batches', type: 'main', index: 0 }] // Loop back - confirms it's processing
 96 |             ]
 97 |           }
 98 |         }
 99 |       };
100 | 
101 |       const result = await validator.validateWorkflow(workflow as any);
102 | 
103 |       // The validator should detect the processing node name/type pattern and loop back
104 |       const reversedErrors = result.errors.filter(e => 
105 |         e.message?.includes('SplitInBatches outputs appear reversed')
106 |       );
107 |       
108 |       expect(reversedErrors.length).toBeGreaterThanOrEqual(1);
109 |     });
110 | 
111 |     it('should warn about processing node on done output without loop back', async () => {
112 |       mockNodeRepository.getNode.mockReturnValue({
113 |         nodeType: 'nodes-base.splitInBatches',
114 |         properties: []
115 |       });
116 | 
117 |       // Processing node connected to "done" output but no loop back
118 |       const workflow = createWorkflow({
119 |         'Split In Batches': {
120 |           main: [
121 |             [{ node: 'Process Item', type: 'main', index: 0 }], // Done output
122 |             []
123 |           ]
124 |         }
125 |         // No loop back from Process Item
126 |       });
127 | 
128 |       const result = await validator.validateWorkflow(workflow as any);
129 | 
130 |       expect(result.warnings).toContainEqual(
131 |         expect.objectContaining({
132 |           type: 'warning',
133 |           nodeId: '1',
134 |           nodeName: 'Split In Batches',
135 |           message: expect.stringContaining('connected to the "done" output (index 0) but appears to be a processing node')
136 |         })
137 |       );
138 |     });
139 | 
140 |     it('should warn about final processing node on loop output', async () => {
141 |       mockNodeRepository.getNode.mockReturnValue({
142 |         nodeType: 'nodes-base.splitInBatches',
143 |         properties: []
144 |       });
145 | 
146 |       // Final summary node connected to "loop" output (index 1) - suspicious
147 |       const workflow = createWorkflow({
148 |         'Split In Batches': {
149 |           main: [
150 |             [],
151 |             [{ node: 'Final Summary', type: 'main', index: 0 }] // Loop output for final node
152 |           ]
153 |         }
154 |       });
155 | 
156 |       const result = await validator.validateWorkflow(workflow as any);
157 | 
158 |       expect(result.warnings).toContainEqual(
159 |         expect.objectContaining({
160 |           type: 'warning',
161 |           nodeId: '1', 
162 |           nodeName: 'Split In Batches',
163 |           message: expect.stringContaining('connected to the "loop" output (index 1) but appears to be a post-processing node')
164 |         })
165 |       );
166 |     });
167 | 
168 |     it('should warn about loop output without loop back connection', async () => {
169 |       mockNodeRepository.getNode.mockReturnValue({
170 |         nodeType: 'nodes-base.splitInBatches',
171 |         properties: []
172 |       });
173 | 
174 |       // Processing node on loop output but doesn't connect back
175 |       const workflow = createWorkflow({
176 |         'Split In Batches': {
177 |           main: [
178 |             [],
179 |             [{ node: 'Process Item', type: 'main', index: 0 }] // Loop output
180 |           ]
181 |         }
182 |         // Process Item doesn't connect back to Split In Batches
183 |       });
184 | 
185 |       const result = await validator.validateWorkflow(workflow as any);
186 | 
187 |       expect(result.warnings).toContainEqual(
188 |         expect.objectContaining({
189 |           type: 'warning',
190 |           nodeId: '1',
191 |           nodeName: 'Split In Batches',
192 |           message: expect.stringContaining('doesn\'t connect back to the SplitInBatches node')
193 |         })
194 |       );
195 |     });
196 | 
197 |     it('should accept correct SplitInBatches connections', async () => {
198 |       mockNodeRepository.getNode.mockReturnValue({
199 |         nodeType: 'nodes-base.splitInBatches',
200 |         properties: []
201 |       });
202 | 
203 |       // Create a workflow with neutral node names that don't trigger patterns
204 |       const workflow = {
205 |         name: 'Test Workflow',
206 |         nodes: [
207 |           {
208 |             id: '1',
209 |             name: 'Split In Batches',
210 |             type: 'n8n-nodes-base.splitInBatches',
211 |             position: [100, 100],
212 |             parameters: { batchSize: 10 }
213 |           },
214 |           {
215 |             id: '2', 
216 |             name: 'Data Node', // Neutral name, won't trigger processing pattern
217 |             type: 'n8n-nodes-base.set',
218 |             position: [300, 100],
219 |             parameters: {}
220 |           },
221 |           {
222 |             id: '3',
223 |             name: 'Output Node', // Neutral name, won't trigger post-processing pattern
224 |             type: 'n8n-nodes-base.noOp',
225 |             position: [500, 100],
226 |             parameters: {}
227 |           }
228 |         ],
229 |         connections: {
230 |           'Split In Batches': {
231 |             main: [
232 |               [{ node: 'Output Node', type: 'main', index: 0 }], // Done output -> neutral node
233 |               [{ node: 'Data Node', type: 'main', index: 0 }]    // Loop output -> neutral node
234 |             ]
235 |           },
236 |           'Data Node': {
237 |             main: [
238 |               [{ node: 'Split In Batches', type: 'main', index: 0 }] // Loop back
239 |             ]
240 |           }
241 |         }
242 |       };
243 | 
244 |       const result = await validator.validateWorkflow(workflow as any);
245 | 
246 |       // Should not have SplitInBatches-specific errors or warnings
247 |       const splitErrors = result.errors.filter(e => 
248 |         e.message?.includes('SplitInBatches') || 
249 |         e.message?.includes('loop') ||
250 |         e.message?.includes('done')
251 |       );
252 |       const splitWarnings = result.warnings.filter(w => 
253 |         w.message?.includes('SplitInBatches') || 
254 |         w.message?.includes('loop') ||
255 |         w.message?.includes('done')
256 |       );
257 | 
258 |       expect(splitErrors).toHaveLength(0);
259 |       expect(splitWarnings).toHaveLength(0);
260 |     });
261 | 
262 |     it('should handle complex loop structures', async () => {
263 |       mockNodeRepository.getNode.mockReturnValue({
264 |         nodeType: 'nodes-base.splitInBatches',
265 |         properties: []
266 |       });
267 | 
268 |       const complexWorkflow = {
269 |         name: 'Complex Loop',
270 |         nodes: [
271 |           {
272 |             id: '1',
273 |             name: 'Split In Batches',
274 |             type: 'n8n-nodes-base.splitInBatches',
275 |             position: [100, 100],
276 |             parameters: {}
277 |           },
278 |           {
279 |             id: '2',
280 |             name: 'Step A', // Neutral name
281 |             type: 'n8n-nodes-base.set',
282 |             position: [300, 50],
283 |             parameters: {}
284 |           },
285 |           {
286 |             id: '3',
287 |             name: 'Step B', // Neutral name 
288 |             type: 'n8n-nodes-base.noOp',
289 |             position: [500, 50],
290 |             parameters: {}
291 |           },
292 |           {
293 |             id: '4',
294 |             name: 'Final Step', // More neutral name
295 |             type: 'n8n-nodes-base.set',
296 |             position: [300, 150],
297 |             parameters: {}
298 |           }
299 |         ],
300 |         connections: {
301 |           'Split In Batches': {
302 |             main: [
303 |               [{ node: 'Final Step', type: 'main', index: 0 }], // Done -> Final (correct)
304 |               [{ node: 'Step A', type: 'main', index: 0 }]    // Loop -> Processing (correct)
305 |             ]
306 |           },
307 |           'Step A': {
308 |             main: [
309 |               [{ node: 'Step B', type: 'main', index: 0 }]
310 |             ]
311 |           },
312 |           'Step B': {
313 |             main: [
314 |               [{ node: 'Split In Batches', type: 'main', index: 0 }] // Loop back (correct)
315 |             ]
316 |           }
317 |         }
318 |       };
319 | 
320 |       const result = await validator.validateWorkflow(complexWorkflow as any);
321 | 
322 |       // Should accept this correct structure without warnings
323 |       const loopWarnings = result.warnings.filter(w => 
324 |         w.message?.includes('loop') || w.message?.includes('done')
325 |       );
326 |       expect(loopWarnings).toHaveLength(0);
327 |     });
328 | 
329 |     it('should detect node type patterns for processing detection', async () => {
330 |       mockNodeRepository.getNode.mockReturnValue({
331 |         nodeType: 'nodes-base.splitInBatches',
332 |         properties: []
333 |       });
334 | 
335 |       const testCases = [
336 |         { type: 'n8n-nodes-base.function', name: 'Process Data', shouldWarn: true },
337 |         { type: 'n8n-nodes-base.code', name: 'Transform Item', shouldWarn: true },
338 |         { type: 'n8n-nodes-base.set', name: 'Handle Each', shouldWarn: true },
339 |         { type: 'n8n-nodes-base.emailSend', name: 'Final Email', shouldWarn: false },
340 |         { type: 'n8n-nodes-base.slack', name: 'Complete Notification', shouldWarn: false }
341 |       ];
342 | 
343 |       for (const testCase of testCases) {
344 |         const workflow = {
345 |           name: 'Pattern Test',
346 |           nodes: [
347 |             {
348 |               id: '1',
349 |               name: 'Split In Batches',
350 |               type: 'n8n-nodes-base.splitInBatches',
351 |               position: [100, 100],
352 |               parameters: {}
353 |             },
354 |             {
355 |               id: '2',
356 |               name: testCase.name,
357 |               type: testCase.type,
358 |               position: [300, 100],
359 |               parameters: {}
360 |             }
361 |           ],
362 |           connections: {
363 |             'Split In Batches': {
364 |               main: [
365 |                 [{ node: testCase.name, type: 'main', index: 0 }], // Connected to done (index 0)
366 |                 []
367 |               ]
368 |             }
369 |           }
370 |         };
371 | 
372 |         const result = await validator.validateWorkflow(workflow as any);
373 |         
374 |         const hasProcessingWarning = result.warnings.some(w => 
375 |           w.message?.includes('appears to be a processing node')
376 |         );
377 | 
378 |         if (testCase.shouldWarn) {
379 |           expect(hasProcessingWarning).toBe(true);
380 |         } else {
381 |           expect(hasProcessingWarning).toBe(false);
382 |         }
383 |       }
384 |     });
385 |   });
386 | 
387 |   describe('checkForLoopBack method', () => {
388 |     it('should detect direct loop back connection', async () => {
389 |       mockNodeRepository.getNode.mockReturnValue({
390 |         nodeType: 'nodes-base.splitInBatches',
391 |         properties: []
392 |       });
393 | 
394 |       const workflow = {
395 |         name: 'Direct Loop Back',
396 |         nodes: [
397 |           { id: '1', name: 'Split In Batches', type: 'n8n-nodes-base.splitInBatches', position: [0, 0], parameters: {} },
398 |           { id: '2', name: 'Process', type: 'n8n-nodes-base.set', position: [0, 0], parameters: {} }
399 |         ],
400 |         connections: {
401 |           'Split In Batches': {
402 |             main: [[], [{ node: 'Process', type: 'main', index: 0 }]]
403 |           },
404 |           'Process': {
405 |             main: [
406 |               [{ node: 'Split In Batches', type: 'main', index: 0 }] // Direct loop back
407 |             ]
408 |           }
409 |         }
410 |       };
411 | 
412 |       const result = await validator.validateWorkflow(workflow as any);
413 | 
414 |       // Should not warn about missing loop back since it exists
415 |       const missingLoopBackWarnings = result.warnings.filter(w => 
416 |         w.message?.includes('doesn\'t connect back')
417 |       );
418 |       expect(missingLoopBackWarnings).toHaveLength(0);
419 |     });
420 | 
421 |     it('should detect indirect loop back connection through multiple nodes', async () => {
422 |       mockNodeRepository.getNode.mockReturnValue({
423 |         nodeType: 'nodes-base.splitInBatches',
424 |         properties: []
425 |       });
426 | 
427 |       const workflow = {
428 |         name: 'Indirect Loop Back',
429 |         nodes: [
430 |           { id: '1', name: 'Split In Batches', type: 'n8n-nodes-base.splitInBatches', position: [0, 0], parameters: {} },
431 |           { id: '2', name: 'Step1', type: 'n8n-nodes-base.set', position: [0, 0], parameters: {} },
432 |           { id: '3', name: 'Step2', type: 'n8n-nodes-base.function', position: [0, 0], parameters: {} },
433 |           { id: '4', name: 'Step3', type: 'n8n-nodes-base.code', position: [0, 0], parameters: {} }
434 |         ],
435 |         connections: {
436 |           'Split In Batches': {
437 |             main: [[], [{ node: 'Step1', type: 'main', index: 0 }]]
438 |           },
439 |           'Step1': {
440 |             main: [
441 |               [{ node: 'Step2', type: 'main', index: 0 }]
442 |             ]
443 |           },
444 |           'Step2': {
445 |             main: [
446 |               [{ node: 'Step3', type: 'main', index: 0 }]
447 |             ]
448 |           },
449 |           'Step3': {
450 |             main: [
451 |               [{ node: 'Split In Batches', type: 'main', index: 0 }] // Indirect loop back
452 |             ]
453 |           }
454 |         }
455 |       };
456 | 
457 |       const result = await validator.validateWorkflow(workflow as any);
458 | 
459 |       // Should not warn about missing loop back since indirect loop exists
460 |       const missingLoopBackWarnings = result.warnings.filter(w => 
461 |         w.message?.includes('doesn\'t connect back')
462 |       );
463 |       expect(missingLoopBackWarnings).toHaveLength(0);
464 |     });
465 | 
466 |     it('should respect max depth to prevent infinite recursion', async () => {
467 |       mockNodeRepository.getNode.mockReturnValue({
468 |         nodeType: 'nodes-base.splitInBatches',
469 |         properties: []
470 |       });
471 | 
472 |       // Create a very deep chain that would exceed depth limit
473 |       const nodes = [
474 |         { id: '1', name: 'Split In Batches', type: 'n8n-nodes-base.splitInBatches', position: [0, 0], parameters: {} }
475 |       ];
476 |       const connections: any = {
477 |         'Split In Batches': {
478 |           main: [[], [{ node: 'Node1', type: 'main', index: 0 }]]
479 |         }
480 |       };
481 | 
482 |       // Create a chain of 60 nodes (exceeds default maxDepth of 50)
483 |       for (let i = 1; i <= 60; i++) {
484 |         nodes.push({
485 |           id: (i + 1).toString(),
486 |           name: `Node${i}`,
487 |           type: 'n8n-nodes-base.set',
488 |           position: [0, 0],
489 |           parameters: {}
490 |         });
491 | 
492 |         if (i < 60) {
493 |           connections[`Node${i}`] = {
494 |             main: [[{ node: `Node${i + 1}`, type: 'main', index: 0 }]]
495 |           };
496 |         } else {
497 |           // Last node connects back to Split In Batches
498 |           connections[`Node${i}`] = {
499 |             main: [[{ node: 'Split In Batches', type: 'main', index: 0 }]]
500 |           };
501 |         }
502 |       }
503 | 
504 |       const workflow = {
505 |         name: 'Deep Chain',
506 |         nodes,
507 |         connections
508 |       };
509 | 
510 |       const result = await validator.validateWorkflow(workflow as any);
511 | 
512 |       // Should warn about missing loop back because depth limit prevents detection
513 |       const missingLoopBackWarnings = result.warnings.filter(w => 
514 |         w.message?.includes('doesn\'t connect back')
515 |       );
516 |       expect(missingLoopBackWarnings).toHaveLength(1);
517 |     });
518 | 
519 |     it('should handle circular references without infinite loops', async () => {
520 |       mockNodeRepository.getNode.mockReturnValue({
521 |         nodeType: 'nodes-base.splitInBatches',
522 |         properties: []
523 |       });
524 | 
525 |       const workflow = {
526 |         name: 'Circular Reference',
527 |         nodes: [
528 |           { id: '1', name: 'Split In Batches', type: 'n8n-nodes-base.splitInBatches', position: [0, 0], parameters: {} },
529 |           { id: '2', name: 'NodeA', type: 'n8n-nodes-base.set', position: [0, 0], parameters: {} },
530 |           { id: '3', name: 'NodeB', type: 'n8n-nodes-base.function', position: [0, 0], parameters: {} }
531 |         ],
532 |         connections: {
533 |           'Split In Batches': {
534 |             main: [[], [{ node: 'NodeA', type: 'main', index: 0 }]]
535 |           },
536 |           'NodeA': {
537 |             main: [
538 |               [{ node: 'NodeB', type: 'main', index: 0 }]
539 |             ]
540 |           },
541 |           'NodeB': {
542 |             main: [
543 |               [{ node: 'NodeA', type: 'main', index: 0 }] // Circular reference (doesn't connect back to Split)
544 |             ]
545 |           }
546 |         }
547 |       };
548 | 
549 |       const result = await validator.validateWorkflow(workflow as any);
550 | 
551 |       // Should complete without hanging and warn about missing loop back
552 |       const missingLoopBackWarnings = result.warnings.filter(w => 
553 |         w.message?.includes('doesn\'t connect back')
554 |       );
555 |       expect(missingLoopBackWarnings).toHaveLength(1);
556 |     });
557 |   });
558 | 
559 |   describe('self-referencing connections', () => {
560 |     it('should allow self-referencing for SplitInBatches (loop back)', async () => {
561 |       mockNodeRepository.getNode.mockReturnValue({
562 |         nodeType: 'nodes-base.splitInBatches',
563 |         properties: []
564 |       });
565 | 
566 |       const workflow = {
567 |         name: 'Self Reference Loop',
568 |         nodes: [
569 |           { id: '1', name: 'Split In Batches', type: 'n8n-nodes-base.splitInBatches', position: [0, 0], parameters: {} }
570 |         ],
571 |         connections: {
572 |           'Split In Batches': {
573 |             main: [
574 |               [],
575 |               [{ node: 'Split In Batches', type: 'main', index: 0 }] // Self-reference on loop output
576 |             ]
577 |           }
578 |         }
579 |       };
580 | 
581 |       const result = await validator.validateWorkflow(workflow as any);
582 | 
583 |       // Should not warn about self-reference for SplitInBatches
584 |       const selfReferenceWarnings = result.warnings.filter(w => 
585 |         w.message?.includes('self-referencing')
586 |       );
587 |       expect(selfReferenceWarnings).toHaveLength(0);
588 |     });
589 | 
590 |     it('should warn about self-referencing for non-loop nodes', async () => {
591 |       mockNodeRepository.getNode.mockReturnValue({
592 |         nodeType: 'nodes-base.set',
593 |         properties: []
594 |       });
595 | 
596 |       const workflow = {
597 |         name: 'Non-Loop Self Reference',
598 |         nodes: [
599 |           { id: '1', name: 'Set', type: 'n8n-nodes-base.set', position: [0, 0], parameters: {} }
600 |         ],
601 |         connections: {
602 |           'Set': {
603 |             main: [
604 |               [{ node: 'Set', type: 'main', index: 0 }] // Self-reference on regular node
605 |             ]
606 |           }
607 |         }
608 |       };
609 | 
610 |       const result = await validator.validateWorkflow(workflow as any);
611 | 
612 |       // Should warn about self-reference for non-loop nodes
613 |       const selfReferenceWarnings = result.warnings.filter(w => 
614 |         w.message?.includes('self-referencing')
615 |       );
616 |       expect(selfReferenceWarnings).toHaveLength(1);
617 |     });
618 |   });
619 | 
620 |   describe('edge cases', () => {
621 |     it('should handle missing target node gracefully', async () => {
622 |       mockNodeRepository.getNode.mockReturnValue({
623 |         nodeType: 'nodes-base.splitInBatches',
624 |         properties: []
625 |       });
626 | 
627 |       const workflow = {
628 |         name: 'Missing Target',
629 |         nodes: [
630 |           { id: '1', name: 'Split In Batches', type: 'n8n-nodes-base.splitInBatches', position: [0, 0], parameters: {} }
631 |         ],
632 |         connections: {
633 |           'Split In Batches': {
634 |             main: [
635 |               [],
636 |               [{ node: 'NonExistentNode', type: 'main', index: 0 }] // Target doesn't exist
637 |             ]
638 |           }
639 |         }
640 |       };
641 | 
642 |       const result = await validator.validateWorkflow(workflow as any);
643 | 
644 |       // Should have connection error for non-existent node
645 |       const connectionErrors = result.errors.filter(e => 
646 |         e.message?.includes('non-existent node')
647 |       );
648 |       expect(connectionErrors).toHaveLength(1);
649 |     });
650 | 
651 |     it('should handle empty connections gracefully', async () => {
652 |       mockNodeRepository.getNode.mockReturnValue({
653 |         nodeType: 'nodes-base.splitInBatches',
654 |         properties: []
655 |       });
656 | 
657 |       const workflow = {
658 |         name: 'Empty Connections',
659 |         nodes: [
660 |           { id: '1', name: 'Split In Batches', type: 'n8n-nodes-base.splitInBatches', position: [0, 0], parameters: {} }
661 |         ],
662 |         connections: {
663 |           'Split In Batches': {
664 |             main: [
665 |               [], // Empty done output
666 |               []  // Empty loop output
667 |             ]
668 |           }
669 |         }
670 |       };
671 | 
672 |       const result = await validator.validateWorkflow(workflow as any);
673 | 
674 |       // Should not crash and should not have SplitInBatches-specific errors
675 |       expect(result).toBeDefined();
676 |     });
677 | 
678 |     it('should handle null/undefined connection arrays', async () => {
679 |       mockNodeRepository.getNode.mockReturnValue({
680 |         nodeType: 'nodes-base.splitInBatches',
681 |         properties: []
682 |       });
683 | 
684 |       const workflow = {
685 |         name: 'Null Connections',
686 |         nodes: [
687 |           { id: '1', name: 'Split In Batches', type: 'n8n-nodes-base.splitInBatches', position: [0, 0], parameters: {} }
688 |         ],
689 |         connections: {
690 |           'Split In Batches': {
691 |             main: [
692 |               null, // Null done output
693 |               undefined  // Undefined loop output
694 |             ] as any
695 |           }
696 |         }
697 |       };
698 | 
699 |       const result = await validator.validateWorkflow(workflow as any);
700 | 
701 |       // Should handle gracefully without crashing
702 |       expect(result).toBeDefined();
703 |     });
704 |   });
705 | });
```

--------------------------------------------------------------------------------
/tests/unit/services/workflow-validator-error-outputs.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, vi } from 'vitest';
  2 | import { WorkflowValidator } from '@/services/workflow-validator';
  3 | import { NodeRepository } from '@/database/node-repository';
  4 | import { EnhancedConfigValidator } from '@/services/enhanced-config-validator';
  5 | 
  6 | vi.mock('@/utils/logger');
  7 | 
  8 | describe('WorkflowValidator - Error Output Validation', () => {
  9 |   let validator: WorkflowValidator;
 10 |   let mockNodeRepository: any;
 11 | 
 12 |   beforeEach(() => {
 13 |     vi.clearAllMocks();
 14 | 
 15 |     // Create mock repository
 16 |     mockNodeRepository = {
 17 |       getNode: vi.fn((type: string) => {
 18 |         // Return mock node info for common node types
 19 |         if (type.includes('httpRequest') || type.includes('webhook') || type.includes('set')) {
 20 |           return {
 21 |             node_type: type,
 22 |             display_name: 'Mock Node',
 23 |             isVersioned: true,
 24 |             version: 1
 25 |           };
 26 |         }
 27 |         return null;
 28 |       })
 29 |     };
 30 | 
 31 |     validator = new WorkflowValidator(mockNodeRepository, EnhancedConfigValidator);
 32 |   });
 33 | 
 34 |   describe('Error Output Configuration', () => {
 35 |     it('should detect incorrect configuration - multiple nodes in same array', async () => {
 36 |       const workflow = {
 37 |         nodes: [
 38 |           {
 39 |             id: '1',
 40 |             name: 'Validate Input',
 41 |             type: 'n8n-nodes-base.set',
 42 |             typeVersion: 3.4,
 43 |             position: [-400, 64],
 44 |             parameters: {}
 45 |           },
 46 |           {
 47 |             id: '2',
 48 |             name: 'Filter URLs',
 49 |             type: 'n8n-nodes-base.filter',
 50 |             typeVersion: 2.2,
 51 |             position: [-176, 64],
 52 |             parameters: {}
 53 |           },
 54 |           {
 55 |             id: '3',
 56 |             name: 'Error Response1',
 57 |             type: 'n8n-nodes-base.respondToWebhook',
 58 |             typeVersion: 1.5,
 59 |             position: [-160, 240],
 60 |             parameters: {}
 61 |           }
 62 |         ],
 63 |         connections: {
 64 |           'Validate Input': {
 65 |             main: [
 66 |               [
 67 |                 { node: 'Filter URLs', type: 'main', index: 0 },
 68 |                 { node: 'Error Response1', type: 'main', index: 0 }  // WRONG! Both in main[0]
 69 |               ]
 70 |             ]
 71 |           }
 72 |         }
 73 |       };
 74 | 
 75 |       const result = await validator.validateWorkflow(workflow as any);
 76 | 
 77 |       expect(result.valid).toBe(false);
 78 |       expect(result.errors.some(e =>
 79 |         e.message.includes('Incorrect error output configuration') &&
 80 |         e.message.includes('Error Response1') &&
 81 |         e.message.includes('appear to be error handlers but are in main[0]')
 82 |       )).toBe(true);
 83 | 
 84 |       // Check that the error message includes the fix
 85 |       const errorMsg = result.errors.find(e => e.message.includes('Incorrect error output configuration'));
 86 |       expect(errorMsg?.message).toContain('INCORRECT (current)');
 87 |       expect(errorMsg?.message).toContain('CORRECT (should be)');
 88 |       expect(errorMsg?.message).toContain('main[1] = error output');
 89 |     });
 90 | 
 91 |     it('should validate correct configuration - separate arrays', async () => {
 92 |       const workflow = {
 93 |         nodes: [
 94 |           {
 95 |             id: '1',
 96 |             name: 'Validate Input',
 97 |             type: 'n8n-nodes-base.set',
 98 |             typeVersion: 3.4,
 99 |             position: [-400, 64],
100 |             parameters: {},
101 |             onError: 'continueErrorOutput'
102 |           },
103 |           {
104 |             id: '2',
105 |             name: 'Filter URLs',
106 |             type: 'n8n-nodes-base.filter',
107 |             typeVersion: 2.2,
108 |             position: [-176, 64],
109 |             parameters: {}
110 |           },
111 |           {
112 |             id: '3',
113 |             name: 'Error Response1',
114 |             type: 'n8n-nodes-base.respondToWebhook',
115 |             typeVersion: 1.5,
116 |             position: [-160, 240],
117 |             parameters: {}
118 |           }
119 |         ],
120 |         connections: {
121 |           'Validate Input': {
122 |             main: [
123 |               [
124 |                 { node: 'Filter URLs', type: 'main', index: 0 }
125 |               ],
126 |               [
127 |                 { node: 'Error Response1', type: 'main', index: 0 }  // Correctly in main[1]
128 |               ]
129 |             ]
130 |           }
131 |         }
132 |       };
133 | 
134 |       const result = await validator.validateWorkflow(workflow as any);
135 | 
136 |       // Should not have the specific error about incorrect configuration
137 |       expect(result.errors.some(e =>
138 |         e.message.includes('Incorrect error output configuration')
139 |       )).toBe(false);
140 |     });
141 | 
142 |     it('should detect onError without error connections', async () => {
143 |       const workflow = {
144 |         nodes: [
145 |           {
146 |             id: '1',
147 |             name: 'HTTP Request',
148 |             type: 'n8n-nodes-base.httpRequest',
149 |             typeVersion: 4,
150 |             position: [100, 100],
151 |             parameters: {},
152 |             onError: 'continueErrorOutput'  // Has onError
153 |           },
154 |           {
155 |             id: '2',
156 |             name: 'Process Data',
157 |             type: 'n8n-nodes-base.set',
158 |             position: [300, 100],
159 |             parameters: {}
160 |           }
161 |         ],
162 |         connections: {
163 |           'HTTP Request': {
164 |             main: [
165 |               [
166 |                 { node: 'Process Data', type: 'main', index: 0 }
167 |               ]
168 |               // No main[1] for error output
169 |             ]
170 |           }
171 |         }
172 |       };
173 | 
174 |       const result = await validator.validateWorkflow(workflow as any);
175 | 
176 |       expect(result.errors.some(e =>
177 |         e.nodeName === 'HTTP Request' &&
178 |         e.message.includes("has onError: 'continueErrorOutput' but no error output connections")
179 |       )).toBe(true);
180 |     });
181 | 
182 |     it('should warn about error connections without onError', async () => {
183 |       const workflow = {
184 |         nodes: [
185 |           {
186 |             id: '1',
187 |             name: 'HTTP Request',
188 |             type: 'n8n-nodes-base.httpRequest',
189 |             typeVersion: 4,
190 |             position: [100, 100],
191 |             parameters: {}
192 |             // Missing onError property
193 |           },
194 |           {
195 |             id: '2',
196 |             name: 'Process Data',
197 |             type: 'n8n-nodes-base.set',
198 |             position: [300, 100],
199 |             parameters: {}
200 |           },
201 |           {
202 |             id: '3',
203 |             name: 'Error Handler',
204 |             type: 'n8n-nodes-base.set',
205 |             position: [300, 300],
206 |             parameters: {}
207 |           }
208 |         ],
209 |         connections: {
210 |           'HTTP Request': {
211 |             main: [
212 |               [
213 |                 { node: 'Process Data', type: 'main', index: 0 }
214 |               ],
215 |               [
216 |                 { node: 'Error Handler', type: 'main', index: 0 }  // Has error connection
217 |               ]
218 |             ]
219 |           }
220 |         }
221 |       };
222 | 
223 |       const result = await validator.validateWorkflow(workflow as any);
224 | 
225 |       expect(result.warnings.some(w =>
226 |         w.nodeName === 'HTTP Request' &&
227 |         w.message.includes('error output connections in main[1] but missing onError')
228 |       )).toBe(true);
229 |     });
230 |   });
231 | 
232 |   describe('Error Handler Detection', () => {
233 |     it('should detect error handler nodes by name', async () => {
234 |       const workflow = {
235 |         nodes: [
236 |           {
237 |             id: '1',
238 |             name: 'API Call',
239 |             type: 'n8n-nodes-base.httpRequest',
240 |             position: [100, 100],
241 |             parameters: {}
242 |           },
243 |           {
244 |             id: '2',
245 |             name: 'Process Success',
246 |             type: 'n8n-nodes-base.set',
247 |             position: [300, 100],
248 |             parameters: {}
249 |           },
250 |           {
251 |             id: '3',
252 |             name: 'Handle Error',  // Contains 'error'
253 |             type: 'n8n-nodes-base.set',
254 |             position: [300, 300],
255 |             parameters: {}
256 |           }
257 |         ],
258 |         connections: {
259 |           'API Call': {
260 |             main: [
261 |               [
262 |                 { node: 'Process Success', type: 'main', index: 0 },
263 |                 { node: 'Handle Error', type: 'main', index: 0 }  // Wrong placement
264 |               ]
265 |             ]
266 |           }
267 |         }
268 |       };
269 | 
270 |       const result = await validator.validateWorkflow(workflow as any);
271 | 
272 |       expect(result.errors.some(e =>
273 |         e.message.includes('Handle Error') &&
274 |         e.message.includes('appear to be error handlers')
275 |       )).toBe(true);
276 |     });
277 | 
278 |     it('should detect error handler nodes by type', async () => {
279 |       const workflow = {
280 |         nodes: [
281 |           {
282 |             id: '1',
283 |             name: 'Webhook',
284 |             type: 'n8n-nodes-base.webhook',
285 |             position: [100, 100],
286 |             parameters: {}
287 |           },
288 |           {
289 |             id: '2',
290 |             name: 'Process',
291 |             type: 'n8n-nodes-base.set',
292 |             position: [300, 100],
293 |             parameters: {}
294 |           },
295 |           {
296 |             id: '3',
297 |             name: 'Respond',
298 |             type: 'n8n-nodes-base.respondToWebhook',  // Common error handler type
299 |             position: [300, 300],
300 |             parameters: {}
301 |           }
302 |         ],
303 |         connections: {
304 |           'Webhook': {
305 |             main: [
306 |               [
307 |                 { node: 'Process', type: 'main', index: 0 },
308 |                 { node: 'Respond', type: 'main', index: 0 }  // Wrong placement
309 |               ]
310 |             ]
311 |           }
312 |         }
313 |       };
314 | 
315 |       const result = await validator.validateWorkflow(workflow as any);
316 | 
317 |       expect(result.errors.some(e =>
318 |         e.message.includes('Respond') &&
319 |         e.message.includes('appear to be error handlers')
320 |       )).toBe(true);
321 |     });
322 | 
323 |     it('should not flag non-error nodes in main[0]', async () => {
324 |       const workflow = {
325 |         nodes: [
326 |           {
327 |             id: '1',
328 |             name: 'Start',
329 |             type: 'n8n-nodes-base.manualTrigger',
330 |             position: [100, 100],
331 |             parameters: {}
332 |           },
333 |           {
334 |             id: '2',
335 |             name: 'First Process',
336 |             type: 'n8n-nodes-base.set',
337 |             position: [300, 100],
338 |             parameters: {}
339 |           },
340 |           {
341 |             id: '3',
342 |             name: 'Second Process',
343 |             type: 'n8n-nodes-base.set',
344 |             position: [300, 200],
345 |             parameters: {}
346 |           }
347 |         ],
348 |         connections: {
349 |           'Start': {
350 |             main: [
351 |               [
352 |                 { node: 'First Process', type: 'main', index: 0 },
353 |                 { node: 'Second Process', type: 'main', index: 0 }  // Both are valid success paths
354 |               ]
355 |             ]
356 |           }
357 |         }
358 |       };
359 | 
360 |       const result = await validator.validateWorkflow(workflow as any);
361 | 
362 |       // Should not have error about incorrect error configuration
363 |       expect(result.errors.some(e =>
364 |         e.message.includes('Incorrect error output configuration')
365 |       )).toBe(false);
366 |     });
367 |   });
368 | 
369 |   describe('Complex Error Patterns', () => {
370 |     it('should handle multiple error handlers correctly', async () => {
371 |       const workflow = {
372 |         nodes: [
373 |           {
374 |             id: '1',
375 |             name: 'HTTP Request',
376 |             type: 'n8n-nodes-base.httpRequest',
377 |             position: [100, 100],
378 |             parameters: {},
379 |             onError: 'continueErrorOutput'
380 |           },
381 |           {
382 |             id: '2',
383 |             name: 'Process',
384 |             type: 'n8n-nodes-base.set',
385 |             position: [300, 100],
386 |             parameters: {}
387 |           },
388 |           {
389 |             id: '3',
390 |             name: 'Log Error',
391 |             type: 'n8n-nodes-base.set',
392 |             position: [300, 200],
393 |             parameters: {}
394 |           },
395 |           {
396 |             id: '4',
397 |             name: 'Send Error Email',
398 |             type: 'n8n-nodes-base.emailSend',
399 |             position: [300, 300],
400 |             parameters: {}
401 |           }
402 |         ],
403 |         connections: {
404 |           'HTTP Request': {
405 |             main: [
406 |               [
407 |                 { node: 'Process', type: 'main', index: 0 }
408 |               ],
409 |               [
410 |                 { node: 'Log Error', type: 'main', index: 0 },
411 |                 { node: 'Send Error Email', type: 'main', index: 0 }  // Multiple error handlers OK in main[1]
412 |               ]
413 |             ]
414 |           }
415 |         }
416 |       };
417 | 
418 |       const result = await validator.validateWorkflow(workflow as any);
419 | 
420 |       // Should not have errors about the configuration
421 |       expect(result.errors.some(e =>
422 |         e.message.includes('Incorrect error output configuration')
423 |       )).toBe(false);
424 |     });
425 | 
426 |     it('should detect mixed success and error handlers in main[0]', async () => {
427 |       const workflow = {
428 |         nodes: [
429 |           {
430 |             id: '1',
431 |             name: 'API Request',
432 |             type: 'n8n-nodes-base.httpRequest',
433 |             position: [100, 100],
434 |             parameters: {}
435 |           },
436 |           {
437 |             id: '2',
438 |             name: 'Transform Data',
439 |             type: 'n8n-nodes-base.set',
440 |             position: [300, 100],
441 |             parameters: {}
442 |           },
443 |           {
444 |             id: '3',
445 |             name: 'Store Data',
446 |             type: 'n8n-nodes-base.set',
447 |             position: [500, 100],
448 |             parameters: {}
449 |           },
450 |           {
451 |             id: '4',
452 |             name: 'Error Notification',
453 |             type: 'n8n-nodes-base.emailSend',
454 |             position: [300, 300],
455 |             parameters: {}
456 |           }
457 |         ],
458 |         connections: {
459 |           'API Request': {
460 |             main: [
461 |               [
462 |                 { node: 'Transform Data', type: 'main', index: 0 },
463 |                 { node: 'Store Data', type: 'main', index: 0 },
464 |                 { node: 'Error Notification', type: 'main', index: 0 }  // Error handler mixed with success nodes
465 |               ]
466 |             ]
467 |           }
468 |         }
469 |       };
470 | 
471 |       const result = await validator.validateWorkflow(workflow as any);
472 | 
473 |       expect(result.errors.some(e =>
474 |         e.message.includes('Error Notification') &&
475 |         e.message.includes('appear to be error handlers but are in main[0]')
476 |       )).toBe(true);
477 |     });
478 | 
479 |     it('should handle nested error handling (error handlers with their own errors)', async () => {
480 |       const workflow = {
481 |         nodes: [
482 |           {
483 |             id: '1',
484 |             name: 'Primary API',
485 |             type: 'n8n-nodes-base.httpRequest',
486 |             position: [100, 100],
487 |             parameters: {},
488 |             onError: 'continueErrorOutput'
489 |           },
490 |           {
491 |             id: '2',
492 |             name: 'Success Handler',
493 |             type: 'n8n-nodes-base.set',
494 |             position: [300, 100],
495 |             parameters: {}
496 |           },
497 |           {
498 |             id: '3',
499 |             name: 'Error Logger',
500 |             type: 'n8n-nodes-base.httpRequest',
501 |             position: [300, 200],
502 |             parameters: {},
503 |             onError: 'continueErrorOutput'
504 |           },
505 |           {
506 |             id: '4',
507 |             name: 'Fallback Error',
508 |             type: 'n8n-nodes-base.set',
509 |             position: [500, 250],
510 |             parameters: {}
511 |           }
512 |         ],
513 |         connections: {
514 |           'Primary API': {
515 |             main: [
516 |               [
517 |                 { node: 'Success Handler', type: 'main', index: 0 }
518 |               ],
519 |               [
520 |                 { node: 'Error Logger', type: 'main', index: 0 }
521 |               ]
522 |             ]
523 |           },
524 |           'Error Logger': {
525 |             main: [
526 |               [],
527 |               [
528 |                 { node: 'Fallback Error', type: 'main', index: 0 }
529 |               ]
530 |             ]
531 |           }
532 |         }
533 |       };
534 | 
535 |       const result = await validator.validateWorkflow(workflow as any);
536 | 
537 |       // Should not have errors about incorrect configuration
538 |       expect(result.errors.some(e =>
539 |         e.message.includes('Incorrect error output configuration')
540 |       )).toBe(false);
541 |     });
542 |   });
543 | 
544 |   describe('Edge Cases', () => {
545 |     it('should handle workflows with no connections at all', async () => {
546 |       const workflow = {
547 |         nodes: [
548 |           {
549 |             id: '1',
550 |             name: 'Isolated Node',
551 |             type: 'n8n-nodes-base.set',
552 |             position: [100, 100],
553 |             parameters: {},
554 |             onError: 'continueErrorOutput'
555 |           }
556 |         ],
557 |         connections: {}
558 |       };
559 | 
560 |       const result = await validator.validateWorkflow(workflow as any);
561 | 
562 |       // Should have warning about orphaned node but not error about connections
563 |       expect(result.warnings.some(w =>
564 |         w.nodeName === 'Isolated Node' &&
565 |         w.message.includes('not connected to any other nodes')
566 |       )).toBe(true);
567 | 
568 |       // Should not have error about error output configuration
569 |       expect(result.errors.some(e =>
570 |         e.message.includes('Incorrect error output configuration')
571 |       )).toBe(false);
572 |     });
573 | 
574 |     it('should handle nodes with empty main arrays', async () => {
575 |       const workflow = {
576 |         nodes: [
577 |           {
578 |             id: '1',
579 |             name: 'Source Node',
580 |             type: 'n8n-nodes-base.httpRequest',
581 |             position: [100, 100],
582 |             parameters: {},
583 |             onError: 'continueErrorOutput'
584 |           },
585 |           {
586 |             id: '2',
587 |             name: 'Target Node',
588 |             type: 'n8n-nodes-base.set',
589 |             position: [300, 100],
590 |             parameters: {}
591 |           }
592 |         ],
593 |         connections: {
594 |           'Source Node': {
595 |             main: [
596 |               [],  // Empty success array
597 |               []   // Empty error array
598 |             ]
599 |           }
600 |         }
601 |       };
602 | 
603 |       const result = await validator.validateWorkflow(workflow as any);
604 | 
605 |       // Should detect that onError is set but no error connections exist
606 |       expect(result.errors.some(e =>
607 |         e.nodeName === 'Source Node' &&
608 |         e.message.includes("has onError: 'continueErrorOutput' but no error output connections")
609 |       )).toBe(true);
610 |     });
611 | 
612 |     it('should handle workflows with only error outputs (no success path)', async () => {
613 |       const workflow = {
614 |         nodes: [
615 |           {
616 |             id: '1',
617 |             name: 'Risky Operation',
618 |             type: 'n8n-nodes-base.httpRequest',
619 |             position: [100, 100],
620 |             parameters: {},
621 |             onError: 'continueErrorOutput'
622 |           },
623 |           {
624 |             id: '2',
625 |             name: 'Error Handler Only',
626 |             type: 'n8n-nodes-base.set',
627 |             position: [300, 200],
628 |             parameters: {}
629 |           }
630 |         ],
631 |         connections: {
632 |           'Risky Operation': {
633 |             main: [
634 |               [],  // No success connections
635 |               [
636 |                 { node: 'Error Handler Only', type: 'main', index: 0 }
637 |               ]
638 |             ]
639 |           }
640 |         }
641 |       };
642 | 
643 |       const result = await validator.validateWorkflow(workflow as any);
644 | 
645 |       // Should not have errors about incorrect configuration - this is valid
646 |       expect(result.errors.some(e =>
647 |         e.message.includes('Incorrect error output configuration')
648 |       )).toBe(false);
649 | 
650 |       // Should not have errors about missing error connections
651 |       expect(result.errors.some(e =>
652 |         e.message.includes("has onError: 'continueErrorOutput' but no error output connections")
653 |       )).toBe(false);
654 |     });
655 | 
656 |     it('should handle undefined or null connection arrays gracefully', async () => {
657 |       const workflow = {
658 |         nodes: [
659 |           {
660 |             id: '1',
661 |             name: 'Source Node',
662 |             type: 'n8n-nodes-base.httpRequest',
663 |             position: [100, 100],
664 |             parameters: {}
665 |           }
666 |         ],
667 |         connections: {
668 |           'Source Node': {
669 |             main: [
670 |               null,      // Null array
671 |               undefined  // Undefined array
672 |             ]
673 |           }
674 |         }
675 |       };
676 | 
677 |       const result = await validator.validateWorkflow(workflow as any);
678 | 
679 |       // Should not crash and should not have configuration errors
680 |       expect(result.errors.some(e =>
681 |         e.message.includes('Incorrect error output configuration')
682 |       )).toBe(false);
683 |     });
684 | 
685 |     it('should detect all variations of error-related node names', async () => {
686 |       const workflow = {
687 |         nodes: [
688 |           {
689 |             id: '1',
690 |             name: 'Source',
691 |             type: 'n8n-nodes-base.httpRequest',
692 |             position: [100, 100],
693 |             parameters: {}
694 |           },
695 |           {
696 |             id: '2',
697 |             name: 'Handle Failure',
698 |             type: 'n8n-nodes-base.set',
699 |             position: [300, 100],
700 |             parameters: {}
701 |           },
702 |           {
703 |             id: '3',
704 |             name: 'Catch Exception',
705 |             type: 'n8n-nodes-base.set',
706 |             position: [300, 200],
707 |             parameters: {}
708 |           },
709 |           {
710 |             id: '4',
711 |             name: 'Success Path',
712 |             type: 'n8n-nodes-base.set',
713 |             position: [500, 100],
714 |             parameters: {}
715 |           }
716 |         ],
717 |         connections: {
718 |           'Source': {
719 |             main: [
720 |               [
721 |                 { node: 'Handle Failure', type: 'main', index: 0 },
722 |                 { node: 'Catch Exception', type: 'main', index: 0 },
723 |                 { node: 'Success Path', type: 'main', index: 0 }
724 |               ]
725 |             ]
726 |           }
727 |         }
728 |       };
729 | 
730 |       const result = await validator.validateWorkflow(workflow as any);
731 | 
732 |       // Should detect both 'Handle Failure' and 'Catch Exception' as error handlers
733 |       expect(result.errors.some(e =>
734 |         e.message.includes('Handle Failure') &&
735 |         e.message.includes('Catch Exception') &&
736 |         e.message.includes('appear to be error handlers but are in main[0]')
737 |       )).toBe(true);
738 |     });
739 | 
740 |     it('should not flag legitimate parallel processing nodes', async () => {
741 |       const workflow = {
742 |         nodes: [
743 |           {
744 |             id: '1',
745 |             name: 'Data Source',
746 |             type: 'n8n-nodes-base.webhook',
747 |             position: [100, 100],
748 |             parameters: {}
749 |           },
750 |           {
751 |             id: '2',
752 |             name: 'Process A',
753 |             type: 'n8n-nodes-base.set',
754 |             position: [300, 50],
755 |             parameters: {}
756 |           },
757 |           {
758 |             id: '3',
759 |             name: 'Process B',
760 |             type: 'n8n-nodes-base.set',
761 |             position: [300, 150],
762 |             parameters: {}
763 |           },
764 |           {
765 |             id: '4',
766 |             name: 'Transform Data',
767 |             type: 'n8n-nodes-base.set',
768 |             position: [300, 250],
769 |             parameters: {}
770 |           }
771 |         ],
772 |         connections: {
773 |           'Data Source': {
774 |             main: [
775 |               [
776 |                 { node: 'Process A', type: 'main', index: 0 },
777 |                 { node: 'Process B', type: 'main', index: 0 },
778 |                 { node: 'Transform Data', type: 'main', index: 0 }
779 |               ]
780 |             ]
781 |           }
782 |         }
783 |       };
784 | 
785 |       const result = await validator.validateWorkflow(workflow as any);
786 | 
787 |       // Should not flag these as error configuration issues
788 |       expect(result.errors.some(e =>
789 |         e.message.includes('Incorrect error output configuration')
790 |       )).toBe(false);
791 |     });
792 |   });
793 | });
```

--------------------------------------------------------------------------------
/docs/HTTP_DEPLOYMENT.md:
--------------------------------------------------------------------------------

```markdown
  1 | # HTTP Deployment Guide for n8n-MCP
  2 | 
  3 | Deploy n8n-MCP as a remote HTTP server to provide n8n knowledge to compatible MCP Client from anywhere.
  4 | 
  5 | ## 🎯 Overview
  6 | 
  7 | n8n-MCP HTTP mode enables:
  8 | - ☁️ Cloud deployment (VPS, Docker, Kubernetes)
  9 | - 🌐 Remote access from any Claude Desktop /Windsurf / other MCP Client 
 10 | - 🔒 Token-based authentication
 11 | - ⚡ Production-ready performance (~12ms response time)
 12 | - 🚀 Optional n8n management tools (16 additional tools when configured)
 13 | - ❌ Does not work with n8n MCP Tool
 14 | 
 15 | ## 📐 Deployment Scenarios
 16 | 
 17 | ### 1. Local Development (Simplest)
 18 | Use **stdio mode** - Claude Desktop connects directly to the Node.js process:
 19 | ```
 20 | Claude Desktop → n8n-mcp (stdio mode)
 21 | ```
 22 | - ✅ No HTTP server needed
 23 | - ✅ No authentication required
 24 | - ✅ Fastest performance
 25 | - ❌ Only works locally
 26 | 
 27 | ### 2. Local HTTP Server
 28 | Run HTTP server locally for testing remote features:
 29 | ```
 30 | Claude Desktop → http-bridge.js → localhost:3000
 31 | ```
 32 | - ✅ Test HTTP features locally
 33 | - ✅ Multiple Claude instances can connect
 34 | - ✅ Good for development
 35 | - ❌ Still only local access
 36 | 
 37 | ### 3. Remote Server
 38 | Deploy to cloud for access from anywhere:
 39 | ```
 40 | Claude Desktop → mcp-remote → https://your-server.com
 41 | ```
 42 | - ✅ Access from anywhere
 43 | - ✅ Team collaboration
 44 | - ✅ Production-ready
 45 | - ❌ Requires server setup
 46 | - Deploy to your VPS - if you just want remote acces, consider deploying to Railway -> [Railway Deployment Guide](./RAILWAY_DEPLOYMENT.md)
 47 | 
 48 | 
 49 | ## 📋 Prerequisites
 50 | 
 51 | **Server Requirements:**
 52 | - Node.js 16+ or Docker
 53 | - 512MB RAM minimum
 54 | - Public IP or domain name
 55 | - (Recommended) SSL certificate for HTTPS
 56 | 
 57 | **Client Requirements:**
 58 | - Claude Desktop
 59 | - Node.js 18+ (for mcp-remote)
 60 | - Or Claude Pro/Team (for native remote MCP)
 61 | 
 62 | ## 🚀 Quick Start
 63 | 
 64 | ### Option 1: Docker Deployment (Recommended for Production)
 65 | 
 66 | ```bash
 67 | # 1. Create environment file
 68 | cat > .env << EOF
 69 | AUTH_TOKEN=$(openssl rand -base64 32)
 70 | USE_FIXED_HTTP=true
 71 | MCP_MODE=http
 72 | PORT=3000
 73 | # Optional: Enable n8n management tools
 74 | # N8N_API_URL=https://your-n8n-instance.com
 75 | # N8N_API_KEY=your-api-key-here
 76 | # Security Configuration (v2.16.3+)
 77 | # Rate limiting (default: 20 attempts per 15 minutes)
 78 | AUTH_RATE_LIMIT_WINDOW=900000
 79 | AUTH_RATE_LIMIT_MAX=20
 80 | # SSRF protection mode (default: strict)
 81 | # Use 'moderate' for local n8n, 'strict' for production
 82 | WEBHOOK_SECURITY_MODE=strict
 83 | EOF
 84 | 
 85 | # 2. Deploy with Docker
 86 | docker run -d \
 87 |   --name n8n-mcp \
 88 |   --restart unless-stopped \
 89 |   --env-file .env \
 90 |   -p 3000:3000 \
 91 |   ghcr.io/czlonkowski/n8n-mcp:latest
 92 | 
 93 | # 3. Verify deployment
 94 | curl http://localhost:3000/health
 95 | ```
 96 | 
 97 | ### Option 2: Local Development (Without Docker)
 98 | 
 99 | ```bash
100 | # 1. Clone and setup
101 | git clone https://github.com/czlonkowski/n8n-mcp.git
102 | cd n8n-mcp
103 | npm install
104 | npm run build
105 | npm run rebuild
106 | 
107 | # 2. Configure environment
108 | export MCP_MODE=http
109 | export USE_FIXED_HTTP=true  # Important: Use fixed implementation
110 | export AUTH_TOKEN=$(openssl rand -base64 32)
111 | export PORT=3000
112 | 
113 | # 3. Start server
114 | npm run start:http
115 | ```
116 | 
117 | ### Option 3: Direct stdio Mode (Simplest for Local)
118 | 
119 | Skip HTTP entirely and use stdio mode directly:
120 | 
121 | ```json
122 | {
123 |   "mcpServers": {
124 |     "n8n-local": {
125 |       "command": "node",
126 |       "args": [
127 |         "/path/to/n8n-mcp/dist/mcp/index.js"
128 |       ],
129 |       "env": {
130 |         "N8N_API_URL": "https://your-n8n-instance.com",
131 |         "N8N_API_KEY": "your-api-key-here"
132 |       }
133 |     }
134 |   }
135 | }
136 | ```
137 | 
138 | 💡 **Save your AUTH_TOKEN** - clients will need it to connect!
139 | 
140 | ## ⚙️ Configuration
141 | 
142 | ### Required Environment Variables
143 | 
144 | | Variable | Description | Example |
145 | |----------|-------------|------|
146 | | `MCP_MODE` | Must be set to `http` | `http` |
147 | | `USE_FIXED_HTTP` | **Important**: Set to `true` for stable implementation | `true` |
148 | | `AUTH_TOKEN` or `AUTH_TOKEN_FILE` | Authentication method | See security section |
149 | 
150 | ### Optional Settings
151 | 
152 | | Variable | Description | Default | Since |
153 | |----------|-------------|---------|-------|
154 | | `PORT` | Server port | `3000` | v1.0 |
155 | | `HOST` | Bind address | `0.0.0.0` | v1.0 |
156 | | `LOG_LEVEL` | Log verbosity (error/warn/info/debug) | `info` | v1.0 |
157 | | `NODE_ENV` | Environment | `production` | v1.0 |
158 | | `TRUST_PROXY` | Trust proxy headers (0=off, 1+=hops) | `0` | v2.7.6 |
159 | | `BASE_URL` | Explicit public URL | Auto-detected | v2.7.14 |
160 | | `PUBLIC_URL` | Alternative to BASE_URL | Auto-detected | v2.7.14 |
161 | | `CORS_ORIGIN` | CORS allowed origins | `*` | v2.7.8 |
162 | | `AUTH_TOKEN_FILE` | Path to token file | - | v2.7.10 |
163 | 
164 | ### n8n Management Tools (Optional)
165 | 
166 | Enable 16 additional tools for managing n8n workflows by configuring API access:
167 | 
168 | ⚠️ **Requires v2.7.1+** - Earlier versions had an issue with tool registration in Docker environments.
169 | 
170 | | Variable | Description | Example |
171 | |----------|-------------|---------|
172 | | `N8N_API_URL` | Your n8n instance URL | `https://your-n8n.com` |
173 | | `N8N_API_KEY` | n8n API key (from Settings > API) | `n8n_api_key_xxx` |
174 | | `N8N_API_TIMEOUT` | Request timeout (ms) | `30000` |
175 | | `N8N_API_MAX_RETRIES` | Max retry attempts | `3` |
176 | 
177 | #### What This Enables
178 | 
179 | When configured, you get **16 additional tools** (total: 39 tools):
180 | 
181 | **Workflow Management (11 tools):**
182 | - `n8n_create_workflow` - Create new workflows
183 | - `n8n_get_workflow` - Get workflow by ID
184 | - `n8n_update_full_workflow` - Update entire workflow
185 | - `n8n_update_partial_workflow` - Update using diff operations (v2.7.0+)
186 | - `n8n_delete_workflow` - Delete workflows
187 | - `n8n_list_workflows` - List all workflows
188 | - And more workflow detail/structure tools
189 | 
190 | **Execution Management (4 tools):**
191 | - `n8n_trigger_webhook_workflow` - Execute via webhooks
192 | - `n8n_get_execution` - Get execution details
193 | - `n8n_list_executions` - List workflow runs
194 | - `n8n_delete_execution` - Delete execution records
195 | 
196 | **System Tools:**
197 | - `n8n_health_check` - Check n8n connectivity
198 | - `n8n_diagnostic` - System diagnostics
199 | - `n8n_validate_workflow` - Validate from n8n instance
200 | 
201 | #### Getting Your n8n API Key
202 | 
203 | 1. Log into your n8n instance
204 | 2. Go to **Settings** > **API**
205 | 3. Click **Create API Key**
206 | 4. Copy the generated key
207 | 
208 | ⚠️ **Security Note**: Store API keys securely and never commit them to version control.
209 | 
210 | ## 🏗️ Architecture
211 | 
212 | ### How HTTP Mode Works
213 | 
214 | ```
215 | ┌─────────────────┐        ┌─────────────┐        ┌──────────────┐
216 | │ Claude Desktop  │ stdio  │ mcp-remote  │  HTTP  │  n8n-MCP     │
217 | │ (stdio only)    ├───────►│ (bridge)    ├───────►│  HTTP Server │
218 | └─────────────────┘        └─────────────┘        └──────────────┘
219 |                                                            │
220 |                                                            ▼
221 |                                                    ┌──────────────┐
222 |                                                    │ Your n8n     │
223 |                                                    │ Instance     │
224 |                                                    └──────────────┘
225 | ```
226 | 
227 | **Key Points:**
228 | - Claude Desktop **only supports stdio** communication
229 | - `mcp-remote` acts as a bridge, converting stdio ↔ HTTP
230 | - n8n-MCP server connects to **one n8n instance** (configured server-side)
231 | - All clients share the same n8n instance (single-tenant design)
232 | 
233 | ## 🌐 Reverse Proxy Configuration
234 | 
235 | ### URL Configuration (v2.7.14+)
236 | 
237 | n8n-MCP intelligently detects your public URL:
238 | 
239 | #### Priority Order:
240 | 1. **Explicit Configuration** (highest priority):
241 |    ```bash
242 |    BASE_URL=https://n8n-mcp.example.com  # Full public URL
243 |    # or
244 |    PUBLIC_URL=https://api.company.com:8443/mcp
245 |    ```
246 | 
247 | 2. **Auto-Detection** (when TRUST_PROXY is enabled):
248 |    ```bash
249 |    TRUST_PROXY=1  # Required for proxy header detection
250 |    # Server reads X-Forwarded-Proto and X-Forwarded-Host
251 |    ```
252 | 
253 | 3. **Fallback** (local binding):
254 |    ```bash
255 |    # No configuration needed
256 |    # Shows: http://localhost:3000 (or configured HOST:PORT)
257 |    ```
258 | 
259 | #### What You'll See in Logs:
260 | ```
261 | [INFO] Starting n8n-MCP HTTP Server v2.7.17...
262 | [INFO] Server running at https://n8n-mcp.example.com
263 | [INFO] Endpoints:
264 | [INFO]   Health: https://n8n-mcp.example.com/health
265 | [INFO]   MCP:    https://n8n-mcp.example.com/mcp
266 | ```
267 | 
268 | ### Trust Proxy for Correct IP Logging
269 | 
270 | When running n8n-MCP behind a reverse proxy (Nginx, Traefik, etc.), enable trust proxy to log real client IPs instead of proxy IPs:
271 | 
272 | ```bash
273 | # Enable trust proxy in your environment
274 | TRUST_PROXY=1  # Trust 1 proxy hop (standard setup)
275 | # or
276 | TRUST_PROXY=2  # Trust 2 proxy hops (CDN → Load Balancer → n8n-mcp)
277 | ```
278 | 
279 | **Without TRUST_PROXY:**
280 | ```
281 | [INFO] GET /health { ip: '172.19.0.2' }  # Docker internal IP
282 | ```
283 | 
284 | **With TRUST_PROXY=1:**
285 | ```
286 | [INFO] GET /health { ip: '203.0.113.1' }  # Real client IP
287 | ```
288 | 
289 | This is especially important when:
290 | - Running in Docker/Kubernetes
291 | - Using load balancers
292 | - Debugging client issues
293 | - Implementing rate limiting
294 | 
295 | ## 🔐 Security Setup
296 | 
297 | ### Authentication
298 | 
299 | All requests require Bearer token authentication:
300 | 
301 | ```bash
302 | # Test authentication
303 | curl -H "Authorization: Bearer $AUTH_TOKEN" \
304 |      https://your-server.com/health
305 | ```
306 | 
307 | ### SSL/HTTPS (Strongly Recommended)
308 | 
309 | Use a reverse proxy for SSL termination:
310 | 
311 | **Nginx example:**
312 | ```nginx
313 | server {
314 |     listen 443 ssl;
315 |     server_name your-domain.com;
316 |     
317 |     ssl_certificate /path/to/cert.pem;
318 |     ssl_certificate_key /path/to/key.pem;
319 |     
320 |     location /mcp {
321 |         proxy_pass http://localhost:3000;
322 |         proxy_set_header Authorization $http_authorization;
323 |         # Important: Forward client IP headers
324 |         proxy_set_header X-Real-IP $remote_addr;
325 |         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
326 |         proxy_set_header X-Forwarded-Proto $scheme;
327 |     }
328 | }
329 | ```
330 | 
331 | **Caddy example (automatic HTTPS):**
332 | ```caddy
333 | your-domain.com {
334 |     reverse_proxy /mcp localhost:3000
335 | }
336 | ```
337 | 
338 | ## 💻 Client Configuration
339 | 
340 | ⚠️ **Requirements**: Node.js 18+ must be installed on the client machine for `mcp-remote`
341 | 
342 | ### Method 1: Using mcp-remote (Recommended)
343 | 
344 | ```json
345 | {
346 |   "mcpServers": {
347 |     "n8n-remote": {
348 |       "command": "npx",
349 |       "args": [
350 |         "-y",
351 |         "mcp-remote",
352 |         "https://your-server.com/mcp",
353 |         "--header",
354 |         "Authorization: Bearer YOUR_AUTH_TOKEN_HERE"
355 |       ]
356 |     }
357 |   }
358 | }
359 | ```
360 | 
361 | **Note**: Replace `YOUR_AUTH_TOKEN_HERE` with your actual token. Do NOT use `${AUTH_TOKEN}` syntax - Claude Desktop doesn't support environment variable substitution in args.
362 | 
363 | ### Method 2: Using Custom Bridge Script
364 | 
365 | For local testing or when mcp-remote isn't available:
366 | 
367 | ```json
368 | {
369 |   "mcpServers": {
370 |     "n8n-local-http": {
371 |       "command": "node",
372 |       "args": [
373 |         "/path/to/n8n-mcp/scripts/http-bridge.js"
374 |       ],
375 |       "env": {
376 |         "MCP_URL": "http://localhost:3000/mcp",
377 |         "AUTH_TOKEN": "your-auth-token-here"
378 |       }
379 |     }
380 |   }
381 | }
382 | ```
383 | 
384 | ### Local Development with Docker
385 | 
386 | When testing locally with Docker:
387 | 
388 | ```json
389 | {
390 |   "mcpServers": {
391 |     "n8n-docker-http": {
392 |       "command": "node",
393 |       "args": [
394 |         "/path/to/n8n-mcp/scripts/http-bridge.js"
395 |       ],
396 |       "env": {
397 |         "MCP_URL": "http://localhost:3001/mcp",
398 |         "AUTH_TOKEN": "docker-test-token"
399 |       }
400 |     }
401 |   }
402 | }
403 | ```
404 | 
405 | ## 🌐 Production Deployment
406 | 
407 | ### Docker Compose (Complete Example)
408 | 
409 | ```yaml
410 | version: '3.8'
411 | 
412 | services:
413 |   n8n-mcp:
414 |     image: ghcr.io/czlonkowski/n8n-mcp:latest
415 |     container_name: n8n-mcp
416 |     restart: unless-stopped
417 |     environment:
418 |       # Core configuration
419 |       MCP_MODE: http
420 |       USE_FIXED_HTTP: true
421 |       NODE_ENV: production
422 |       
423 |       # Security - Using file-based secret
424 |       AUTH_TOKEN_FILE: /run/secrets/auth_token
425 |       
426 |       # Networking
427 |       HOST: 0.0.0.0
428 |       PORT: 3000
429 |       TRUST_PROXY: 1  # Behind Nginx/Traefik
430 |       CORS_ORIGIN: https://app.example.com  # Restrict in production
431 |       
432 |       # URL Configuration
433 |       BASE_URL: https://n8n-mcp.example.com
434 |       
435 |       # Logging
436 |       LOG_LEVEL: info
437 |       
438 |       # Optional: n8n API Integration
439 |       N8N_API_URL: ${N8N_API_URL}
440 |       N8N_API_KEY_FILE: /run/secrets/n8n_api_key
441 |       
442 |     secrets:
443 |       - auth_token
444 |       - n8n_api_key
445 |       
446 |     ports:
447 |       - "127.0.0.1:3000:3000"  # Only expose to localhost
448 |       
449 |     volumes:
450 |       - n8n-mcp-data:/app/data:ro  # Read-only database
451 |       
452 |     healthcheck:
453 |       test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
454 |       interval: 30s
455 |       timeout: 10s
456 |       retries: 3
457 |       start_period: 10s
458 |       
459 |     deploy:
460 |       resources:
461 |         limits:
462 |           memory: 512M
463 |           cpus: '0.5'
464 |         reservations:
465 |           memory: 128M
466 |           cpus: '0.1'
467 |     
468 |     logging:
469 |       driver: json-file
470 |       options:
471 |         max-size: "10m"
472 |         max-file: "3"
473 | 
474 | secrets:
475 |   auth_token:
476 |     file: ./secrets/auth_token.txt
477 |   n8n_api_key:
478 |     file: ./secrets/n8n_api_key.txt
479 | 
480 | volumes:
481 |   n8n-mcp-data:
482 | ```
483 | 
484 | ### Systemd Service (Production Linux)
485 | 
486 | ```ini
487 | # /etc/systemd/system/n8n-mcp.service
488 | [Unit]
489 | Description=n8n-MCP HTTP Server
490 | Documentation=https://github.com/czlonkowski/n8n-mcp
491 | After=network.target
492 | Requires=network.target
493 | 
494 | [Service]
495 | Type=simple
496 | User=n8n-mcp
497 | Group=n8n-mcp
498 | WorkingDirectory=/opt/n8n-mcp
499 | 
500 | # Use file-based secret
501 | Environment="AUTH_TOKEN_FILE=/etc/n8n-mcp/auth_token"
502 | Environment="MCP_MODE=http"
503 | Environment="USE_FIXED_HTTP=true"
504 | Environment="NODE_ENV=production"
505 | Environment="TRUST_PROXY=1"
506 | Environment="BASE_URL=https://n8n-mcp.example.com"
507 | 
508 | # Additional config from file
509 | EnvironmentFile=-/etc/n8n-mcp/config.env
510 | 
511 | ExecStartPre=/usr/bin/test -f /etc/n8n-mcp/auth_token
512 | ExecStart=/usr/bin/node dist/mcp/index.js --http
513 | 
514 | # Restart configuration
515 | Restart=always
516 | RestartSec=10
517 | StartLimitBurst=5
518 | StartLimitInterval=60s
519 | 
520 | # Security hardening
521 | NoNewPrivileges=true
522 | PrivateTmp=true
523 | ProtectSystem=strict
524 | ProtectHome=true
525 | ReadWritePaths=/opt/n8n-mcp/data
526 | ProtectKernelTunables=true
527 | ProtectControlGroups=true
528 | RestrictSUIDSGID=true
529 | LockPersonality=true
530 | 
531 | # Resource limits
532 | LimitNOFILE=65536
533 | MemoryLimit=512M
534 | CPUQuota=50%
535 | 
536 | [Install]
537 | WantedBy=multi-user.target
538 | ```
539 | 
540 | **Setup:**
541 | ```bash
542 | # Create user and directories
543 | sudo useradd -r -s /bin/false n8n-mcp
544 | sudo mkdir -p /opt/n8n-mcp /etc/n8n-mcp
545 | sudo chown n8n-mcp:n8n-mcp /opt/n8n-mcp
546 | 
547 | # Create secure token
548 | sudo sh -c 'openssl rand -base64 32 > /etc/n8n-mcp/auth_token'
549 | sudo chmod 600 /etc/n8n-mcp/auth_token
550 | sudo chown n8n-mcp:n8n-mcp /etc/n8n-mcp/auth_token
551 | 
552 | # Deploy application
553 | sudo -u n8n-mcp git clone https://github.com/czlonkowski/n8n-mcp.git /opt/n8n-mcp
554 | cd /opt/n8n-mcp
555 | sudo -u n8n-mcp npm install --production
556 | sudo -u n8n-mcp npm run build
557 | sudo -u n8n-mcp npm run rebuild
558 | 
559 | # Start service
560 | sudo systemctl daemon-reload
561 | sudo systemctl enable n8n-mcp
562 | sudo systemctl start n8n-mcp
563 | ```
564 | 
565 | Enable:
566 | ```bash
567 | sudo systemctl enable n8n-mcp
568 | sudo systemctl start n8n-mcp
569 | ```
570 | 
571 | ## 📡 Monitoring & Maintenance
572 | 
573 | ### Health Endpoint Details
574 | 
575 | ```bash
576 | # Basic health check
577 | curl -H "Authorization: Bearer $AUTH_TOKEN" \
578 |      https://your-server.com/health
579 | 
580 | # Response:
581 | {
582 |   "status": "ok",
583 |   "mode": "http-fixed",
584 |   "version": "2.7.17",
585 |   "uptime": 3600,
586 |   "memory": {
587 |     "used": 95,
588 |     "total": 512,
589 |     "percentage": 18.5
590 |   },
591 |   "node": {
592 |     "version": "v20.11.0",
593 |     "platform": "linux"
594 |   },
595 |   "features": {
596 |     "n8nApi": true,  // If N8N_API_URL configured
597 |     "authFile": true  // If using AUTH_TOKEN_FILE
598 |   }
599 | }
600 | ```
601 | 
602 | ## 🔒 Security Features (v2.16.3+)
603 | 
604 | ### Rate Limiting
605 | 
606 | Built-in rate limiting protects authentication endpoints from brute force attacks:
607 | 
608 | **Configuration:**
609 | ```bash
610 | # Defaults (15 minutes window, 20 attempts per IP)
611 | AUTH_RATE_LIMIT_WINDOW=900000  # milliseconds
612 | AUTH_RATE_LIMIT_MAX=20
613 | ```
614 | 
615 | **Features:**
616 | - Per-IP rate limiting with configurable window and max attempts
617 | - Standard rate limit headers (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset)
618 | - JSON-RPC formatted error responses
619 | - Automatic IP tracking behind reverse proxies (requires TRUST_PROXY=1)
620 | 
621 | **Behavior:**
622 | - First 20 attempts: Return 401 Unauthorized for invalid credentials
623 | - Attempts 21+: Return 429 Too Many Requests with Retry-After header
624 | - Counter resets after 15 minutes (configurable)
625 | 
626 | ### SSRF Protection
627 | 
628 | Prevents Server-Side Request Forgery attacks when using webhook triggers:
629 | 
630 | **Three Security Modes:**
631 | 
632 | 1. **Strict Mode (default)** - Production deployments
633 |    ```bash
634 |    WEBHOOK_SECURITY_MODE=strict
635 |    ```
636 |    - ✅ Block localhost (127.0.0.1, ::1)
637 |    - ✅ Block private IPs (10.x, 192.168.x, 172.16-31.x)
638 |    - ✅ Block cloud metadata (169.254.169.254, metadata.google.internal)
639 |    - ✅ DNS rebinding prevention
640 |    - 🎯 **Use for**: Cloud deployments, production environments
641 | 
642 | 2. **Moderate Mode** - Local development with local n8n
643 |    ```bash
644 |    WEBHOOK_SECURITY_MODE=moderate
645 |    ```
646 |    - ✅ Allow localhost (for local n8n instances)
647 |    - ✅ Block private IPs
648 |    - ✅ Block cloud metadata
649 |    - ✅ DNS rebinding prevention
650 |    - 🎯 **Use for**: Development with n8n on localhost:5678
651 | 
652 | 3. **Permissive Mode** - Internal networks only
653 |    ```bash
654 |    WEBHOOK_SECURITY_MODE=permissive
655 |    ```
656 |    - ✅ Allow localhost and private IPs
657 |    - ✅ Block cloud metadata (always blocked)
658 |    - ✅ DNS rebinding prevention
659 |    - 🎯 **Use for**: Internal testing (NOT for production)
660 | 
661 | **Important:** Cloud metadata endpoints are ALWAYS blocked in all modes for security.
662 | 
663 | ## 🔒 Security Best Practices
664 | 
665 | ### 1. Token Management
666 | 
667 | **DO:**
668 | - ✅ Use tokens with 32+ characters
669 | - ✅ Store tokens in secure files or secrets management
670 | - ✅ Rotate tokens regularly (monthly minimum)
671 | - ✅ Use different tokens for each environment
672 | - ✅ Monitor logs for authentication failures
673 | 
674 | **DON'T:**
675 | - ❌ Use default or example tokens
676 | - ❌ Commit tokens to version control
677 | - ❌ Share tokens between environments
678 | - ❌ Log tokens in plain text
679 | 
680 | ```bash
681 | # Generate strong token
682 | openssl rand -base64 32
683 | 
684 | # Secure storage options:
685 | # 1. Docker secrets (recommended)
686 | echo $(openssl rand -base64 32) | docker secret create auth_token -
687 | 
688 | # 2. Kubernetes secrets
689 | kubectl create secret generic n8n-mcp-auth \
690 |   --from-literal=token=$(openssl rand -base64 32)
691 | 
692 | # 3. HashiCorp Vault
693 | vault kv put secret/n8n-mcp token=$(openssl rand -base64 32)
694 | ```
695 | 
696 | ### 2. Network Security
697 | 
698 | - ✅ **Always use HTTPS** in production
699 | - ✅ **Firewall rules** to limit access
700 | - ✅ **VPN** for internal deployments
701 | - ✅ **Rate limiting** at proxy level
702 | 
703 | ### 3. Container Security
704 | 
705 | ```bash
706 | # Run as non-root user (already configured)
707 | # Read-only filesystem
708 | docker run --read-only \
709 |   --tmpfs /tmp \
710 |   -v n8n-mcp-data:/app/data \
711 |   n8n-mcp
712 | 
713 | # Security scanning
714 | docker scan ghcr.io/czlonkowski/n8n-mcp:latest
715 | ```
716 | 
717 | ## 🔍 Troubleshooting
718 | 
719 | ### Common Issues & Solutions
720 | 
721 | #### Authentication Issues
722 | 
723 | **"Unauthorized" error:**
724 | ```bash
725 | # Check token is set correctly
726 | docker exec n8n-mcp env | grep AUTH
727 | 
728 | # Test with curl
729 | curl -v -H "Authorization: Bearer YOUR_TOKEN" \
730 |      https://your-server.com/health
731 | 
732 | # Common causes:
733 | # - Extra spaces in token
734 | # - Missing "Bearer " prefix
735 | # - Token file has newline at end
736 | # - Wrong quotes in JSON config
737 | ```
738 | 
739 | **Default token warning:**
740 | ```
741 | ⚠️ SECURITY WARNING: Using default AUTH_TOKEN
742 | ```
743 | - Change token immediately via environment variable
744 | - Server shows this warning every 5 minutes
745 | 
746 | #### Connection Issues
747 | 
748 | **"TransformStream is not defined":**
749 | ```bash
750 | # Check Node.js version on CLIENT machine
751 | node --version  # Must be 18+
752 | 
753 | # Update Node.js
754 | # macOS: brew upgrade node
755 | # Linux: Use NodeSource repository
756 | # Windows: Download from nodejs.org
757 | ```
758 | 
759 | **"Cannot connect to server":**
760 | ```bash
761 | # 1. Check server is running
762 | docker ps | grep n8n-mcp
763 | 
764 | # 2. Check logs for errors
765 | docker logs n8n-mcp --tail 50
766 | 
767 | # 3. Test locally first
768 | curl http://localhost:3000/health
769 | 
770 | # 4. Check firewall
771 | sudo ufw status  # Linux
772 | ```
773 | 
774 | **"Stream is not readable":**
775 | - Ensure `USE_FIXED_HTTP=true` is set
776 | - Fixed in v2.3.2+
777 | 
778 | **Bridge script not working:**
779 | ```bash
780 | # Test the bridge manually
781 | export MCP_URL=http://localhost:3000/mcp
782 | export AUTH_TOKEN=your-token
783 | echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | node /path/to/http-bridge.js
784 | ```
785 | 
786 | **Connection refused:**
787 | ```bash
788 | # Check server is running
789 | curl http://localhost:3000/health
790 | 
791 | # Check Docker status
792 | docker ps
793 | docker logs n8n-mcp
794 | 
795 | # Check firewall
796 | sudo ufw status
797 | ```
798 | 
799 | **Authentication failed:**
800 | - Verify AUTH_TOKEN matches exactly
801 | - Check for extra spaces or quotes
802 | - Test with curl first
803 | 
804 | #### Bridge Configuration Issues
805 | 
806 | **"Why use 'node' instead of 'docker' in Claude config?"**
807 | 
808 | Claude Desktop only supports stdio. The architecture is:
809 | ```
810 | Claude → stdio → mcp-remote → HTTP → Docker container
811 | ```
812 | 
813 | The `node` command runs mcp-remote (the bridge), not the server directly.
814 | 
815 | **"Command not found: npx":**
816 | ```bash
817 | # Install Node.js 18+ which includes npx
818 | # Or use full path:
819 | which npx  # Find npx location
820 | # Use that path in Claude config
821 | ```
822 | 
823 | ### Debug Mode
824 | 
825 | ```bash
826 | # 1. Enable debug logging
827 | docker run -e LOG_LEVEL=debug ...
828 | 
829 | # 2. Test MCP endpoint
830 | curl -X POST https://your-server.com/mcp \
831 |   -H "Authorization: Bearer $AUTH_TOKEN" \
832 |   -H "Content-Type: application/json" \
833 |   -d '{
834 |     "jsonrpc": "2.0",
835 |     "method": "tools/list",
836 |     "id": 1
837 |   }'
838 | 
839 | # 3. Test with mcp-remote directly
840 | MCP_URL=https://your-server.com/mcp \
841 | AUTH_TOKEN=your-token \
842 | echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | \
843 |   npx mcp-remote $MCP_URL --header "Authorization: Bearer $AUTH_TOKEN"
844 | ```
845 | 
846 | ### Cloud Platform Deployments
847 | 
848 | **Railway:** See our [Railway Deployment Guide](./RAILWAY_DEPLOYMENT.md)
849 | 
850 | ## 🔧 Using n8n Management Tools
851 | 
852 | When n8n API is configured, Claude can manage workflows directly:
853 | 
854 | ### Example: Create a Workflow via Claude
855 | 
856 | ```bash
857 | # Test n8n connectivity first
858 | curl -X POST https://your-server.com/mcp \
859 |   -H "Authorization: Bearer $AUTH_TOKEN" \
860 |   -H "Content-Type: application/json" \
861 |   -d '{
862 |     "jsonrpc": "2.0",
863 |     "method": "n8n_health_check",
864 |     "params": {},
865 |     "id": 1
866 |   }'
867 | ```
868 | 
869 | ### Common Use Cases
870 | 
871 | 1. **Workflow Automation**: Claude can create, update, and manage workflows
872 | 2. **CI/CD Integration**: Deploy workflows from version control
873 | 3. **Workflow Templates**: Claude can apply templates to new workflows
874 | 4. **Monitoring**: Track execution status and debug failures
875 | 5. **Incremental Updates**: Use diff-based updates for efficient changes
876 | 
877 | ### Security Best Practices for n8n API
878 | 
879 | - 🔐 Use separate API keys for different environments
880 | - 🔄 Rotate API keys regularly
881 | - 📝 Audit workflow changes via n8n's audit log
882 | - 🚫 Never expose n8n API directly to the internet
883 | - ✅ Use MCP server as a security layer
884 | 
885 | ## 📦 Updates & Maintenance
886 | 
887 | ### Version Updates
888 | 
889 | ```bash
890 | # Check current version
891 | docker exec n8n-mcp node -e "console.log(require('./package.json').version)"
892 | 
893 | # Update to latest
894 | docker pull ghcr.io/czlonkowski/n8n-mcp:latest
895 | docker stop n8n-mcp
896 | docker rm n8n-mcp
897 | # Re-run with same environment
898 | 
899 | # Update to specific version
900 | docker pull ghcr.io/czlonkowski/n8n-mcp:v2.7.17
901 | ```
902 | 
903 | ### Database Management
904 | 
905 | ```bash
906 | # The database is read-only and pre-built
907 | # No backups needed for the node database
908 | # Updates include new database versions
909 | 
910 | # Check database stats
911 | curl -X POST https://your-server.com/mcp \
912 |   -H "Authorization: Bearer $AUTH_TOKEN" \
913 |   -H "Content-Type: application/json" \
914 |   -d '{
915 |     "jsonrpc": "2.0",
916 |     "method": "get_database_statistics",
917 |     "id": 1
918 |   }'
919 | ```
920 | 
921 | ## 🆘 Getting Help
922 | 
923 | - 📚 [Full Documentation](https://github.com/czlonkowski/n8n-mcp)
924 | - 🚂 [Railway Deployment Guide](./RAILWAY_DEPLOYMENT.md) - Easiest deployment option
925 | - 🐛 [Report Issues](https://github.com/czlonkowski/n8n-mcp/issues)
926 | - 💬 [Community Discussions](https://github.com/czlonkowski/n8n-mcp/discussions)
```

--------------------------------------------------------------------------------
/tests/integration/database/transactions.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  2 | import Database from 'better-sqlite3';
  3 | import { TestDatabase, TestDataGenerator, runInTransaction } from './test-utils';
  4 | 
  5 | describe('Database Transactions', () => {
  6 |   let testDb: TestDatabase;
  7 |   let db: Database.Database;
  8 | 
  9 |   beforeEach(async () => {
 10 |     testDb = new TestDatabase({ mode: 'memory' });
 11 |     db = await testDb.initialize();
 12 |   });
 13 | 
 14 |   afterEach(async () => {
 15 |     await testDb.cleanup();
 16 |   });
 17 | 
 18 |   describe('Basic Transactions', () => {
 19 |     it('should commit transaction successfully', async () => {
 20 |       const node = TestDataGenerator.generateNode();
 21 | 
 22 |       db.exec('BEGIN');
 23 |       
 24 |       db.prepare(`
 25 |         INSERT INTO nodes (
 26 |           node_type, package_name, display_name, description,
 27 |           category, development_style, is_ai_tool, is_trigger,
 28 |           is_webhook, is_versioned, version, documentation,
 29 |           properties_schema, operations, credentials_required
 30 |         ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 31 |       `).run(
 32 |         node.nodeType,
 33 |         node.packageName,
 34 |         node.displayName,
 35 |         node.description,
 36 |         node.category,
 37 |         node.developmentStyle,
 38 |         node.isAITool ? 1 : 0,
 39 |         node.isTrigger ? 1 : 0,
 40 |         node.isWebhook ? 1 : 0,
 41 |         node.isVersioned ? 1 : 0,
 42 |         node.version,
 43 |         node.documentation,
 44 |         JSON.stringify(node.properties || []),
 45 |         JSON.stringify(node.operations || []),
 46 |         JSON.stringify(node.credentials || [])
 47 |       );
 48 | 
 49 |       // Data should be visible within transaction
 50 |       const countInTx = db.prepare('SELECT COUNT(*) as count FROM nodes').get() as { count: number };
 51 |       expect(countInTx.count).toBe(1);
 52 | 
 53 |       db.exec('COMMIT');
 54 | 
 55 |       // Data should persist after commit
 56 |       const countAfter = db.prepare('SELECT COUNT(*) as count FROM nodes').get() as { count: number };
 57 |       expect(countAfter.count).toBe(1);
 58 |     });
 59 | 
 60 |     it('should rollback transaction on error', async () => {
 61 |       const node = TestDataGenerator.generateNode();
 62 | 
 63 |       db.exec('BEGIN');
 64 |       
 65 |       db.prepare(`
 66 |         INSERT INTO nodes (
 67 |           node_type, package_name, display_name, description,
 68 |           category, development_style, is_ai_tool, is_trigger,
 69 |           is_webhook, is_versioned, version, documentation,
 70 |           properties_schema, operations, credentials_required
 71 |         ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 72 |       `).run(
 73 |         node.nodeType,
 74 |         node.packageName,
 75 |         node.displayName,
 76 |         node.description,
 77 |         node.category,
 78 |         node.developmentStyle,
 79 |         node.isAITool ? 1 : 0,
 80 |         node.isTrigger ? 1 : 0,
 81 |         node.isWebhook ? 1 : 0,
 82 |         node.isVersioned ? 1 : 0,
 83 |         node.version,
 84 |         node.documentation,
 85 |         JSON.stringify(node.properties || []),
 86 |         JSON.stringify(node.operations || []),
 87 |         JSON.stringify(node.credentials || [])
 88 |       );
 89 | 
 90 |       // Rollback
 91 |       db.exec('ROLLBACK');
 92 | 
 93 |       // Data should not persist
 94 |       const count = db.prepare('SELECT COUNT(*) as count FROM nodes').get() as { count: number };
 95 |       expect(count.count).toBe(0);
 96 |     });
 97 | 
 98 |     it('should handle transaction helper function', async () => {
 99 |       const node = TestDataGenerator.generateNode();
100 | 
101 |       // Successful transaction
102 |       await runInTransaction(db, () => {
103 |         db.prepare(`
104 |           INSERT INTO nodes (
105 |             node_type, package_name, display_name, description,
106 |             category, development_style, is_ai_tool, is_trigger,
107 |             is_webhook, is_versioned, version, documentation,
108 |             properties_schema, operations, credentials_required
109 |           ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
110 |         `).run(
111 |           node.nodeType,
112 |           node.packageName,
113 |           node.displayName,
114 |           node.description,
115 |           node.category,
116 |           node.developmentStyle,
117 |           node.isAITool ? 1 : 0,
118 |           node.isTrigger ? 1 : 0,
119 |           node.isWebhook ? 1 : 0,
120 |           node.isVersioned ? 1 : 0,
121 |           node.version,
122 |           node.documentation,
123 |           JSON.stringify(node.properties || []),
124 |           JSON.stringify(node.operations || []),
125 |           JSON.stringify(node.credentials || [])
126 |         );
127 |       });
128 | 
129 |       const count = db.prepare('SELECT COUNT(*) as count FROM nodes').get() as { count: number };
130 |       expect(count.count).toBe(1);
131 | 
132 |       // Failed transaction
133 |       await expect(runInTransaction(db, () => {
134 |         db.prepare('INSERT INTO invalid_table VALUES (1)').run();
135 |       })).rejects.toThrow();
136 | 
137 |       // Count should remain the same
138 |       const countAfterError = db.prepare('SELECT COUNT(*) as count FROM nodes').get() as { count: number };
139 |       expect(countAfterError.count).toBe(1);
140 |     });
141 |   });
142 | 
143 |   describe('Nested Transactions (Savepoints)', () => {
144 |     it('should handle nested transactions with savepoints', async () => {
145 |       const nodes = TestDataGenerator.generateNodes(3);
146 | 
147 |       db.exec('BEGIN');
148 | 
149 |       // Insert first node
150 |       const insertStmt = db.prepare(`
151 |         INSERT INTO nodes (
152 |           node_type, package_name, display_name, description,
153 |           category, development_style, is_ai_tool, is_trigger,
154 |           is_webhook, is_versioned, version, documentation,
155 |           properties_schema, operations, credentials_required
156 |         ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
157 |       `);
158 | 
159 |       insertStmt.run(
160 |         nodes[0].nodeType,
161 |         nodes[0].packageName,
162 |         nodes[0].displayName,
163 |         nodes[0].description,
164 |         nodes[0].category,
165 |         nodes[0].developmentStyle,
166 |         nodes[0].isAITool ? 1 : 0,
167 |         nodes[0].isTrigger ? 1 : 0,
168 |         nodes[0].isWebhook ? 1 : 0,
169 |         nodes[0].isVersioned ? 1 : 0,
170 |         nodes[0].version,
171 |         nodes[0].documentation,
172 |         JSON.stringify(nodes[0].properties || []),
173 |         JSON.stringify(nodes[0].operations || []),
174 |         JSON.stringify(nodes[0].credentials || [])
175 |       );
176 | 
177 |       // Create savepoint
178 |       db.exec('SAVEPOINT sp1');
179 | 
180 |       // Insert second node
181 |       insertStmt.run(
182 |         nodes[1].nodeType,
183 |         nodes[1].packageName,
184 |         nodes[1].displayName,
185 |         nodes[1].description,
186 |         nodes[1].category,
187 |         nodes[1].developmentStyle,
188 |         nodes[1].isAITool ? 1 : 0,
189 |         nodes[1].isTrigger ? 1 : 0,
190 |         nodes[1].isWebhook ? 1 : 0,
191 |         nodes[1].isVersioned ? 1 : 0,
192 |         nodes[1].version,
193 |         nodes[1].documentation,
194 |         JSON.stringify(nodes[1].properties || []),
195 |         JSON.stringify(nodes[1].operations || []),
196 |         JSON.stringify(nodes[1].credentials || [])
197 |       );
198 | 
199 |       // Create another savepoint
200 |       db.exec('SAVEPOINT sp2');
201 | 
202 |       // Insert third node
203 |       insertStmt.run(
204 |         nodes[2].nodeType,
205 |         nodes[2].packageName,
206 |         nodes[2].displayName,
207 |         nodes[2].description,
208 |         nodes[2].category,
209 |         nodes[2].developmentStyle,
210 |         nodes[2].isAITool ? 1 : 0,
211 |         nodes[2].isTrigger ? 1 : 0,
212 |         nodes[2].isWebhook ? 1 : 0,
213 |         nodes[2].isVersioned ? 1 : 0,
214 |         nodes[2].version,
215 |         nodes[2].documentation,
216 |         JSON.stringify(nodes[2].properties || []),
217 |         JSON.stringify(nodes[2].operations || []),
218 |         JSON.stringify(nodes[2].credentials || [])
219 |       );
220 | 
221 |       // Should have 3 nodes
222 |       let count = db.prepare('SELECT COUNT(*) as count FROM nodes').get() as { count: number };
223 |       expect(count.count).toBe(3);
224 | 
225 |       // Rollback to sp2
226 |       db.exec('ROLLBACK TO sp2');
227 | 
228 |       // Should have 2 nodes
229 |       count = db.prepare('SELECT COUNT(*) as count FROM nodes').get() as { count: number };
230 |       expect(count.count).toBe(2);
231 | 
232 |       // Rollback to sp1
233 |       db.exec('ROLLBACK TO sp1');
234 | 
235 |       // Should have 1 node
236 |       count = db.prepare('SELECT COUNT(*) as count FROM nodes').get() as { count: number };
237 |       expect(count.count).toBe(1);
238 | 
239 |       // Commit main transaction
240 |       db.exec('COMMIT');
241 | 
242 |       // Should still have 1 node
243 |       count = db.prepare('SELECT COUNT(*) as count FROM nodes').get() as { count: number };
244 |       expect(count.count).toBe(1);
245 |     });
246 | 
247 |     it('should release savepoints properly', async () => {
248 |       db.exec('BEGIN');
249 |       db.exec('SAVEPOINT sp1');
250 |       db.exec('SAVEPOINT sp2');
251 |       
252 |       // Release sp2
253 |       db.exec('RELEASE sp2');
254 |       
255 |       // Can still rollback to sp1
256 |       db.exec('ROLLBACK TO sp1');
257 |       
258 |       // But cannot rollback to sp2
259 |       expect(() => {
260 |         db.exec('ROLLBACK TO sp2');
261 |       }).toThrow(/no such savepoint/);
262 | 
263 |       db.exec('COMMIT');
264 |     });
265 |   });
266 | 
267 |   describe('Transaction Isolation', () => {
268 |     it('should handle IMMEDIATE transactions', async () => {
269 |       testDb = new TestDatabase({ mode: 'file', name: 'test-immediate.db' });
270 |       db = await testDb.initialize();
271 | 
272 |       // Start immediate transaction (acquires write lock immediately)
273 |       db.exec('BEGIN IMMEDIATE');
274 | 
275 |       // Insert data
276 |       const node = TestDataGenerator.generateNode();
277 |       db.prepare(`
278 |         INSERT INTO nodes (
279 |           node_type, package_name, display_name, description,
280 |           category, development_style, is_ai_tool, is_trigger,
281 |           is_webhook, is_versioned, version, documentation,
282 |           properties_schema, operations, credentials_required
283 |         ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
284 |       `).run(
285 |         node.nodeType,
286 |         node.packageName,
287 |         node.displayName,
288 |         node.description,
289 |         node.category,
290 |         node.developmentStyle,
291 |         node.isAITool ? 1 : 0,
292 |         node.isTrigger ? 1 : 0,
293 |         node.isWebhook ? 1 : 0,
294 |         node.isVersioned ? 1 : 0,
295 |         node.version,
296 |         node.documentation,
297 |         JSON.stringify(node.properties || []),
298 |         JSON.stringify(node.operations || []),
299 |         JSON.stringify(node.credentials || [])
300 |       );
301 | 
302 |       // Another connection should not be able to write
303 |       const dbPath = db.name;
304 |       const conn2 = new Database(dbPath);
305 |       conn2.exec('PRAGMA busy_timeout = 100');
306 | 
307 |       expect(() => {
308 |         conn2.exec('BEGIN IMMEDIATE');
309 |       }).toThrow(/database is locked/);
310 | 
311 |       db.exec('COMMIT');
312 |       conn2.close();
313 |     });
314 | 
315 |     it('should handle EXCLUSIVE transactions', async () => {
316 |       testDb = new TestDatabase({ mode: 'file', name: 'test-exclusive.db' });
317 |       db = await testDb.initialize();
318 | 
319 |       // Start exclusive transaction (prevents other connections from reading)
320 |       db.exec('BEGIN EXCLUSIVE');
321 | 
322 |       // Another connection should not be able to access the database
323 |       const dbPath = db.name;
324 |       const conn2 = new Database(dbPath);
325 |       conn2.exec('PRAGMA busy_timeout = 100');
326 | 
327 |       // Try to begin a transaction on the second connection
328 |       let errorThrown = false;
329 |       try {
330 |         conn2.exec('BEGIN EXCLUSIVE');
331 |       } catch (err) {
332 |         errorThrown = true;
333 |         expect(err).toBeDefined();
334 |       }
335 |       
336 |       expect(errorThrown).toBe(true);
337 | 
338 |       db.exec('COMMIT');
339 |       conn2.close();
340 |     });
341 |   });
342 | 
343 |   describe('Transaction with Better-SQLite3 API', () => {
344 |     it('should use transaction() method for automatic handling', () => {
345 |       const nodes = TestDataGenerator.generateNodes(5);
346 | 
347 |       const insertMany = db.transaction((nodes: any[]) => {
348 |         const stmt = db.prepare(`
349 |           INSERT INTO nodes (
350 |             node_type, package_name, display_name, description,
351 |             category, development_style, is_ai_tool, is_trigger,
352 |             is_webhook, is_versioned, version, documentation,
353 |             properties_schema, operations, credentials_required
354 |           ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
355 |         `);
356 | 
357 |         for (const node of nodes) {
358 |           stmt.run(
359 |             node.nodeType,
360 |             node.packageName,
361 |             node.displayName,
362 |             node.description,
363 |             node.category,
364 |             node.developmentStyle,
365 |             node.isAITool ? 1 : 0,
366 |             node.isTrigger ? 1 : 0,
367 |             node.isWebhook ? 1 : 0,
368 |             node.isVersioned ? 1 : 0,
369 |             node.version,
370 |             node.documentation,
371 |             JSON.stringify(node.properties || []),
372 |             JSON.stringify(node.operations || []),
373 |             JSON.stringify(node.credentials || [])
374 |           );
375 |         }
376 | 
377 |         return nodes.length;
378 |       });
379 | 
380 |       // Execute transaction
381 |       const inserted = insertMany(nodes);
382 |       expect(inserted).toBe(5);
383 | 
384 |       // Verify all inserted
385 |       const count = db.prepare('SELECT COUNT(*) as count FROM nodes').get() as { count: number };
386 |       expect(count.count).toBe(5);
387 |     });
388 | 
389 |     it('should rollback transaction() on error', () => {
390 |       const nodes = TestDataGenerator.generateNodes(3);
391 | 
392 |       const insertWithError = db.transaction((nodes: any[]) => {
393 |         const stmt = db.prepare(`
394 |           INSERT INTO nodes (
395 |             node_type, package_name, display_name, description,
396 |             category, development_style, is_ai_tool, is_trigger,
397 |             is_webhook, is_versioned, version, documentation,
398 |             properties_schema, operations, credentials_required
399 |           ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
400 |         `);
401 | 
402 |         for (let i = 0; i < nodes.length; i++) {
403 |           if (i === 2) {
404 |             // Cause an error on third insert
405 |             throw new Error('Simulated error');
406 |           }
407 |           const node = nodes[i];
408 |           stmt.run(
409 |             node.nodeType,
410 |             node.packageName,
411 |             node.displayName,
412 |             node.description,
413 |             node.category,
414 |             node.developmentStyle,
415 |             node.isAITool ? 1 : 0,
416 |             node.isTrigger ? 1 : 0,
417 |             node.isWebhook ? 1 : 0,
418 |             node.isVersioned ? 1 : 0,
419 |             node.version,
420 |             node.documentation,
421 |             JSON.stringify(node.properties || []),
422 |             JSON.stringify(node.operations || []),
423 |             JSON.stringify(node.credentials || [])
424 |           );
425 |         }
426 |       });
427 | 
428 |       // Should throw and rollback
429 |       expect(() => insertWithError(nodes)).toThrow('Simulated error');
430 | 
431 |       // No nodes should be inserted
432 |       const count = db.prepare('SELECT COUNT(*) as count FROM nodes').get() as { count: number };
433 |       expect(count.count).toBe(0);
434 |     });
435 | 
436 |     it('should handle immediate transactions with transaction()', () => {
437 |       const insertImmediate = db.transaction((node: any) => {
438 |         db.prepare(`
439 |           INSERT INTO nodes (
440 |             node_type, package_name, display_name, description,
441 |             category, development_style, is_ai_tool, is_trigger,
442 |             is_webhook, is_versioned, version, documentation,
443 |             properties_schema, operations, credentials_required
444 |           ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
445 |         `).run(
446 |           node.nodeType,
447 |           node.packageName,
448 |           node.displayName,
449 |           node.description,
450 |           node.category,
451 |           node.developmentStyle,
452 |           node.isAITool ? 1 : 0,
453 |           node.isTrigger ? 1 : 0,
454 |           node.isWebhook ? 1 : 0,
455 |           node.isVersioned ? 1 : 0,
456 |           node.version,
457 |           node.documentation,
458 |           JSON.stringify(node.properties || []),
459 |           JSON.stringify(node.operations || []),
460 |           JSON.stringify(node.credentials || [])
461 |         );
462 |       });
463 | 
464 |       const node = TestDataGenerator.generateNode();
465 |       insertImmediate(node);
466 | 
467 |       const count = db.prepare('SELECT COUNT(*) as count FROM nodes').get() as { count: number };
468 |       expect(count.count).toBe(1);
469 |     });
470 | 
471 |     it('should handle exclusive transactions with transaction()', () => {
472 |       // Better-sqlite3 doesn't have .exclusive() method, use raw SQL instead
473 |       db.exec('BEGIN EXCLUSIVE');
474 |       const result = db.prepare('SELECT COUNT(*) as count FROM nodes').get() as { count: number };
475 |       db.exec('COMMIT');
476 |       
477 |       expect(result.count).toBe(0);
478 |     });
479 |   });
480 | 
481 |   describe('Transaction Performance', () => {
482 |     it('should show performance benefit of transactions for bulk inserts', () => {
483 |       const nodes = TestDataGenerator.generateNodes(1000);
484 |       const stmt = db.prepare(`
485 |         INSERT INTO nodes (
486 |           node_type, package_name, display_name, description,
487 |           category, development_style, is_ai_tool, is_trigger,
488 |           is_webhook, is_versioned, version, documentation,
489 |           properties_schema, operations, credentials_required
490 |         ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
491 |       `);
492 | 
493 |       // Without transaction
494 |       const start1 = process.hrtime.bigint();
495 |       for (let i = 0; i < 100; i++) {
496 |         const node = nodes[i];
497 |         stmt.run(
498 |           node.nodeType,
499 |           node.packageName,
500 |           node.displayName,
501 |           node.description,
502 |           node.category,
503 |           node.developmentStyle,
504 |           node.isAITool ? 1 : 0,
505 |           node.isTrigger ? 1 : 0,
506 |           node.isWebhook ? 1 : 0,
507 |           node.isVersioned ? 1 : 0,
508 |           node.version,
509 |           node.documentation,
510 |           JSON.stringify(node.properties || []),
511 |           JSON.stringify(node.operations || []),
512 |           JSON.stringify(node.credentials || [])
513 |         );
514 |       }
515 |       const duration1 = Number(process.hrtime.bigint() - start1) / 1_000_000;
516 | 
517 |       // With transaction
518 |       const start2 = process.hrtime.bigint();
519 |       const insertMany = db.transaction((nodes: any[]) => {
520 |         for (const node of nodes) {
521 |           stmt.run(
522 |             node.nodeType,
523 |             node.packageName,
524 |             node.displayName,
525 |             node.description,
526 |             node.category,
527 |             node.developmentStyle,
528 |             node.isAITool ? 1 : 0,
529 |             node.isTrigger ? 1 : 0,
530 |             node.isWebhook ? 1 : 0,
531 |             node.isVersioned ? 1 : 0,
532 |             node.version,
533 |             node.documentation,
534 |             JSON.stringify(node.properties || []),
535 |             JSON.stringify(node.operations || []),
536 |             JSON.stringify(node.credentials || [])
537 |           );
538 |         }
539 |       });
540 |       insertMany(nodes.slice(100, 1000));
541 |       const duration2 = Number(process.hrtime.bigint() - start2) / 1_000_000;
542 | 
543 |       // Transaction should be faster for bulk operations
544 |       // Note: The performance benefit may vary depending on the system
545 |       // Just verify that transaction completed successfully
546 |       expect(duration2).toBeGreaterThan(0);
547 | 
548 |       // Verify all inserted
549 |       const count = db.prepare('SELECT COUNT(*) as count FROM nodes').get() as { count: number };
550 |       expect(count.count).toBe(1000);
551 |     });
552 |   });
553 | 
554 |   describe('Transaction Error Scenarios', () => {
555 |     it('should handle constraint violations in transactions', () => {
556 |       const node = TestDataGenerator.generateNode();
557 | 
558 |       db.exec('BEGIN');
559 |       
560 |       // First insert should succeed
561 |       db.prepare(`
562 |         INSERT INTO nodes (
563 |           node_type, package_name, display_name, description,
564 |           category, development_style, is_ai_tool, is_trigger,
565 |           is_webhook, is_versioned, version, documentation,
566 |           properties_schema, operations, credentials_required
567 |         ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
568 |       `).run(
569 |         node.nodeType,
570 |         node.packageName,
571 |         node.displayName,
572 |         node.description,
573 |         node.category,
574 |         node.developmentStyle,
575 |         node.isAITool ? 1 : 0,
576 |         node.isTrigger ? 1 : 0,
577 |         node.isWebhook ? 1 : 0,
578 |         node.isVersioned ? 1 : 0,
579 |         node.version,
580 |         node.documentation,
581 |         JSON.stringify(node.properties || []),
582 |         JSON.stringify(node.operations || []),
583 |         JSON.stringify(node.credentials || [])
584 |       );
585 | 
586 |       // Second insert with same node_type should fail (PRIMARY KEY constraint)
587 |       expect(() => {
588 |         db.prepare(`
589 |           INSERT INTO nodes (
590 |             node_type, package_name, display_name, description,
591 |             category, development_style, is_ai_tool, is_trigger,
592 |             is_webhook, is_versioned, version, documentation,
593 |             properties_schema, operations, credentials_required
594 |           ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
595 |         `).run(
596 |           node.nodeType, // Same node_type - will violate PRIMARY KEY constraint
597 |           node.packageName,
598 |           node.displayName,
599 |           node.description,
600 |           node.category,
601 |           node.developmentStyle,
602 |           node.isAITool ? 1 : 0,
603 |           node.isTrigger ? 1 : 0,
604 |           node.isWebhook ? 1 : 0,
605 |           node.isVersioned ? 1 : 0,
606 |           node.version,
607 |           node.documentation,
608 |           JSON.stringify(node.properties || []),
609 |           JSON.stringify(node.operations || []),
610 |           JSON.stringify(node.credentials || [])
611 |         );
612 |       }).toThrow(/UNIQUE constraint failed/);
613 | 
614 |       // Can still commit the transaction with first insert
615 |       db.exec('COMMIT');
616 | 
617 |       const count = db.prepare('SELECT COUNT(*) as count FROM nodes').get() as { count: number };
618 |       expect(count.count).toBe(1);
619 |     });
620 | 
621 |     it.skip('should handle deadlock scenarios', async () => {
622 |       // This test simulates a potential deadlock scenario
623 |       // SKIPPED: Database corruption issue with concurrent file-based connections
624 |       testDb = new TestDatabase({ mode: 'file', name: 'test-deadlock.db' });
625 |       db = await testDb.initialize();
626 | 
627 |       // Insert initial data
628 |       const nodes = TestDataGenerator.generateNodes(2);
629 |       const insertStmt = db.prepare(`
630 |         INSERT INTO nodes (
631 |           node_type, package_name, display_name, description,
632 |           category, development_style, is_ai_tool, is_trigger,
633 |           is_webhook, is_versioned, version, documentation,
634 |           properties_schema, operations, credentials_required
635 |         ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
636 |       `);
637 | 
638 |       nodes.forEach(node => {
639 |         insertStmt.run(
640 |           node.nodeType,
641 |           node.packageName,
642 |           node.displayName,
643 |           node.description,
644 |           node.category,
645 |           node.developmentStyle,
646 |           node.isAITool ? 1 : 0,
647 |           node.isTrigger ? 1 : 0,
648 |           node.isWebhook ? 1 : 0,
649 |           node.isVersioned ? 1 : 0,
650 |           node.version,
651 |           node.documentation,
652 |           JSON.stringify(node.properties || []),
653 |           JSON.stringify(node.operations || []),
654 |           JSON.stringify(node.credentials || [])
655 |         );
656 |       });
657 | 
658 |       // Connection 1 updates node 0 then tries to update node 1
659 |       // Connection 2 updates node 1 then tries to update node 0
660 |       // This would cause a deadlock in a traditional RDBMS
661 | 
662 |       const dbPath = db.name;
663 |       const conn1 = new Database(dbPath);
664 |       const conn2 = new Database(dbPath);
665 | 
666 |       // Set short busy timeout to fail fast
667 |       conn1.exec('PRAGMA busy_timeout = 100');
668 |       conn2.exec('PRAGMA busy_timeout = 100');
669 | 
670 |       // Start transactions
671 |       conn1.exec('BEGIN IMMEDIATE');
672 |       
673 |       // Conn1 updates first node
674 |       conn1.prepare('UPDATE nodes SET documentation = ? WHERE node_type = ?').run(
675 |         'Updated documentation',
676 |         nodes[0].nodeType
677 |       );
678 | 
679 |       // Try to start transaction on conn2 (should fail due to IMMEDIATE lock)
680 |       expect(() => {
681 |         conn2.exec('BEGIN IMMEDIATE');
682 |       }).toThrow(/database is locked/);
683 | 
684 |       conn1.exec('COMMIT');
685 |       conn1.close();
686 |       conn2.close();
687 |     });
688 |   });
689 | });
```
Page 32/59FirstPrevNextLast