#
tokens: 47304/50000 10/617 files (page 18/46)
lines: off (toggle) GitHub
raw markdown copy
This is page 18 of 46. Use http://codebase.md/czlonkowski/n8n-mcp?lines=false&page={x} to view the full context.

# Directory Structure

```
├── _config.yml
├── .claude
│   └── agents
│       ├── code-reviewer.md
│       ├── context-manager.md
│       ├── debugger.md
│       ├── deployment-engineer.md
│       ├── mcp-backend-engineer.md
│       ├── n8n-mcp-tester.md
│       ├── technical-researcher.md
│       └── test-automator.md
├── .dockerignore
├── .env.docker
├── .env.example
├── .env.n8n.example
├── .env.test
├── .env.test.example
├── .github
│   ├── ABOUT.md
│   ├── BENCHMARK_THRESHOLDS.md
│   ├── FUNDING.yml
│   ├── gh-pages.yml
│   ├── secret_scanning.yml
│   └── workflows
│       ├── benchmark-pr.yml
│       ├── benchmark.yml
│       ├── docker-build-fast.yml
│       ├── docker-build-n8n.yml
│       ├── docker-build.yml
│       ├── release.yml
│       ├── test.yml
│       └── update-n8n-deps.yml
├── .gitignore
├── .npmignore
├── ATTRIBUTION.md
├── CHANGELOG.md
├── CLAUDE.md
├── codecov.yml
├── coverage.json
├── data
│   ├── .gitkeep
│   ├── nodes.db
│   ├── nodes.db-shm
│   ├── nodes.db-wal
│   └── templates.db
├── deploy
│   └── quick-deploy-n8n.sh
├── docker
│   ├── docker-entrypoint.sh
│   ├── n8n-mcp
│   ├── parse-config.js
│   └── README.md
├── docker-compose.buildkit.yml
├── docker-compose.extract.yml
├── docker-compose.n8n.yml
├── docker-compose.override.yml.example
├── docker-compose.test-n8n.yml
├── docker-compose.yml
├── Dockerfile
├── Dockerfile.railway
├── Dockerfile.test
├── docs
│   ├── AUTOMATED_RELEASES.md
│   ├── BENCHMARKS.md
│   ├── CHANGELOG.md
│   ├── CLAUDE_CODE_SETUP.md
│   ├── CLAUDE_INTERVIEW.md
│   ├── CODECOV_SETUP.md
│   ├── CODEX_SETUP.md
│   ├── CURSOR_SETUP.md
│   ├── DEPENDENCY_UPDATES.md
│   ├── DOCKER_README.md
│   ├── DOCKER_TROUBLESHOOTING.md
│   ├── FINAL_AI_VALIDATION_SPEC.md
│   ├── FLEXIBLE_INSTANCE_CONFIGURATION.md
│   ├── HTTP_DEPLOYMENT.md
│   ├── img
│   │   ├── cc_command.png
│   │   ├── cc_connected.png
│   │   ├── codex_connected.png
│   │   ├── cursor_tut.png
│   │   ├── Railway_api.png
│   │   ├── Railway_server_address.png
│   │   ├── vsc_ghcp_chat_agent_mode.png
│   │   ├── vsc_ghcp_chat_instruction_files.png
│   │   ├── vsc_ghcp_chat_thinking_tool.png
│   │   └── windsurf_tut.png
│   ├── INSTALLATION.md
│   ├── LIBRARY_USAGE.md
│   ├── local
│   │   ├── DEEP_DIVE_ANALYSIS_2025-10-02.md
│   │   ├── DEEP_DIVE_ANALYSIS_README.md
│   │   ├── Deep_dive_p1_p2.md
│   │   ├── integration-testing-plan.md
│   │   ├── integration-tests-phase1-summary.md
│   │   ├── N8N_AI_WORKFLOW_BUILDER_ANALYSIS.md
│   │   ├── P0_IMPLEMENTATION_PLAN.md
│   │   └── TEMPLATE_MINING_ANALYSIS.md
│   ├── MCP_ESSENTIALS_README.md
│   ├── MCP_QUICK_START_GUIDE.md
│   ├── N8N_DEPLOYMENT.md
│   ├── RAILWAY_DEPLOYMENT.md
│   ├── README_CLAUDE_SETUP.md
│   ├── README.md
│   ├── tools-documentation-usage.md
│   ├── VS_CODE_PROJECT_SETUP.md
│   ├── WINDSURF_SETUP.md
│   └── workflow-diff-examples.md
├── examples
│   └── enhanced-documentation-demo.js
├── fetch_log.txt
├── LICENSE
├── MEMORY_N8N_UPDATE.md
├── MEMORY_TEMPLATE_UPDATE.md
├── monitor_fetch.sh
├── N8N_HTTP_STREAMABLE_SETUP.md
├── n8n-nodes.db
├── P0-R3-TEST-PLAN.md
├── package-lock.json
├── package.json
├── package.runtime.json
├── PRIVACY.md
├── railway.json
├── README.md
├── renovate.json
├── scripts
│   ├── analyze-optimization.sh
│   ├── audit-schema-coverage.ts
│   ├── build-optimized.sh
│   ├── compare-benchmarks.js
│   ├── demo-optimization.sh
│   ├── deploy-http.sh
│   ├── deploy-to-vm.sh
│   ├── export-webhook-workflows.ts
│   ├── extract-changelog.js
│   ├── extract-from-docker.js
│   ├── extract-nodes-docker.sh
│   ├── extract-nodes-simple.sh
│   ├── format-benchmark-results.js
│   ├── generate-benchmark-stub.js
│   ├── generate-detailed-reports.js
│   ├── generate-test-summary.js
│   ├── http-bridge.js
│   ├── mcp-http-client.js
│   ├── migrate-nodes-fts.ts
│   ├── migrate-tool-docs.ts
│   ├── n8n-docs-mcp.service
│   ├── nginx-n8n-mcp.conf
│   ├── prebuild-fts5.ts
│   ├── prepare-release.js
│   ├── publish-npm-quick.sh
│   ├── publish-npm.sh
│   ├── quick-test.ts
│   ├── run-benchmarks-ci.js
│   ├── sync-runtime-version.js
│   ├── test-ai-validation-debug.ts
│   ├── test-code-node-enhancements.ts
│   ├── test-code-node-fixes.ts
│   ├── test-docker-config.sh
│   ├── test-docker-fingerprint.ts
│   ├── test-docker-optimization.sh
│   ├── test-docker.sh
│   ├── test-empty-connection-validation.ts
│   ├── test-error-message-tracking.ts
│   ├── test-error-output-validation.ts
│   ├── test-error-validation.js
│   ├── test-essentials.ts
│   ├── test-expression-code-validation.ts
│   ├── test-expression-format-validation.js
│   ├── test-fts5-search.ts
│   ├── test-fuzzy-fix.ts
│   ├── test-fuzzy-simple.ts
│   ├── test-helpers-validation.ts
│   ├── test-http-search.ts
│   ├── test-http.sh
│   ├── test-jmespath-validation.ts
│   ├── test-multi-tenant-simple.ts
│   ├── test-multi-tenant.ts
│   ├── test-n8n-integration.sh
│   ├── test-node-info.js
│   ├── test-node-type-validation.ts
│   ├── test-nodes-base-prefix.ts
│   ├── test-operation-validation.ts
│   ├── test-optimized-docker.sh
│   ├── test-release-automation.js
│   ├── test-search-improvements.ts
│   ├── test-security.ts
│   ├── test-single-session.sh
│   ├── test-sqljs-triggers.ts
│   ├── test-telemetry-debug.ts
│   ├── test-telemetry-direct.ts
│   ├── test-telemetry-env.ts
│   ├── test-telemetry-integration.ts
│   ├── test-telemetry-no-select.ts
│   ├── test-telemetry-security.ts
│   ├── test-telemetry-simple.ts
│   ├── test-typeversion-validation.ts
│   ├── test-url-configuration.ts
│   ├── test-user-id-persistence.ts
│   ├── test-webhook-validation.ts
│   ├── test-workflow-insert.ts
│   ├── test-workflow-sanitizer.ts
│   ├── test-workflow-tracking-debug.ts
│   ├── update-and-publish-prep.sh
│   ├── update-n8n-deps.js
│   ├── update-readme-version.js
│   ├── vitest-benchmark-json-reporter.js
│   └── vitest-benchmark-reporter.ts
├── SECURITY.md
├── src
│   ├── config
│   │   └── n8n-api.ts
│   ├── data
│   │   └── canonical-ai-tool-examples.json
│   ├── database
│   │   ├── database-adapter.ts
│   │   ├── migrations
│   │   │   └── add-template-node-configs.sql
│   │   ├── node-repository.ts
│   │   ├── nodes.db
│   │   ├── schema-optimized.sql
│   │   └── schema.sql
│   ├── errors
│   │   └── validation-service-error.ts
│   ├── http-server-single-session.ts
│   ├── http-server.ts
│   ├── index.ts
│   ├── loaders
│   │   └── node-loader.ts
│   ├── mappers
│   │   └── docs-mapper.ts
│   ├── mcp
│   │   ├── handlers-n8n-manager.ts
│   │   ├── handlers-workflow-diff.ts
│   │   ├── index.ts
│   │   ├── server.ts
│   │   ├── stdio-wrapper.ts
│   │   ├── tool-docs
│   │   │   ├── configuration
│   │   │   │   ├── get-node-as-tool-info.ts
│   │   │   │   ├── get-node-documentation.ts
│   │   │   │   ├── get-node-essentials.ts
│   │   │   │   ├── get-node-info.ts
│   │   │   │   ├── get-property-dependencies.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── search-node-properties.ts
│   │   │   ├── discovery
│   │   │   │   ├── get-database-statistics.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── list-ai-tools.ts
│   │   │   │   ├── list-nodes.ts
│   │   │   │   └── search-nodes.ts
│   │   │   ├── guides
│   │   │   │   ├── ai-agents-guide.ts
│   │   │   │   └── index.ts
│   │   │   ├── index.ts
│   │   │   ├── system
│   │   │   │   ├── index.ts
│   │   │   │   ├── n8n-diagnostic.ts
│   │   │   │   ├── n8n-health-check.ts
│   │   │   │   ├── n8n-list-available-tools.ts
│   │   │   │   └── tools-documentation.ts
│   │   │   ├── templates
│   │   │   │   ├── get-template.ts
│   │   │   │   ├── get-templates-for-task.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── list-node-templates.ts
│   │   │   │   ├── list-tasks.ts
│   │   │   │   ├── search-templates-by-metadata.ts
│   │   │   │   └── search-templates.ts
│   │   │   ├── types.ts
│   │   │   ├── validation
│   │   │   │   ├── index.ts
│   │   │   │   ├── validate-node-minimal.ts
│   │   │   │   ├── validate-node-operation.ts
│   │   │   │   ├── validate-workflow-connections.ts
│   │   │   │   ├── validate-workflow-expressions.ts
│   │   │   │   └── validate-workflow.ts
│   │   │   └── workflow_management
│   │   │       ├── index.ts
│   │   │       ├── n8n-autofix-workflow.ts
│   │   │       ├── n8n-create-workflow.ts
│   │   │       ├── n8n-delete-execution.ts
│   │   │       ├── n8n-delete-workflow.ts
│   │   │       ├── n8n-get-execution.ts
│   │   │       ├── n8n-get-workflow-details.ts
│   │   │       ├── n8n-get-workflow-minimal.ts
│   │   │       ├── n8n-get-workflow-structure.ts
│   │   │       ├── n8n-get-workflow.ts
│   │   │       ├── n8n-list-executions.ts
│   │   │       ├── n8n-list-workflows.ts
│   │   │       ├── n8n-trigger-webhook-workflow.ts
│   │   │       ├── n8n-update-full-workflow.ts
│   │   │       ├── n8n-update-partial-workflow.ts
│   │   │       └── n8n-validate-workflow.ts
│   │   ├── tools-documentation.ts
│   │   ├── tools-n8n-friendly.ts
│   │   ├── tools-n8n-manager.ts
│   │   ├── tools.ts
│   │   └── workflow-examples.ts
│   ├── mcp-engine.ts
│   ├── mcp-tools-engine.ts
│   ├── n8n
│   │   ├── MCPApi.credentials.ts
│   │   └── MCPNode.node.ts
│   ├── parsers
│   │   ├── node-parser.ts
│   │   ├── property-extractor.ts
│   │   └── simple-parser.ts
│   ├── scripts
│   │   ├── debug-http-search.ts
│   │   ├── extract-from-docker.ts
│   │   ├── fetch-templates-robust.ts
│   │   ├── fetch-templates.ts
│   │   ├── rebuild-database.ts
│   │   ├── rebuild-optimized.ts
│   │   ├── rebuild.ts
│   │   ├── sanitize-templates.ts
│   │   ├── seed-canonical-ai-examples.ts
│   │   ├── test-autofix-documentation.ts
│   │   ├── test-autofix-workflow.ts
│   │   ├── test-execution-filtering.ts
│   │   ├── test-node-suggestions.ts
│   │   ├── test-protocol-negotiation.ts
│   │   ├── test-summary.ts
│   │   ├── test-webhook-autofix.ts
│   │   ├── validate.ts
│   │   └── validation-summary.ts
│   ├── services
│   │   ├── ai-node-validator.ts
│   │   ├── ai-tool-validators.ts
│   │   ├── confidence-scorer.ts
│   │   ├── config-validator.ts
│   │   ├── enhanced-config-validator.ts
│   │   ├── example-generator.ts
│   │   ├── execution-processor.ts
│   │   ├── expression-format-validator.ts
│   │   ├── expression-validator.ts
│   │   ├── n8n-api-client.ts
│   │   ├── n8n-validation.ts
│   │   ├── node-documentation-service.ts
│   │   ├── node-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

--------------------------------------------------------------------------------
/docs/workflow-diff-examples.md:
--------------------------------------------------------------------------------

```markdown
# Workflow Diff Examples

This guide demonstrates how to use the `n8n_update_partial_workflow` tool for efficient workflow editing.

## Overview

The `n8n_update_partial_workflow` tool allows you to make targeted changes to workflows without sending the entire workflow JSON. This results in:
- 80-90% reduction in token usage
- More precise edits
- Clearer intent
- Reduced risk of accidentally modifying unrelated parts

## Basic Usage

```json
{
  "id": "workflow-id-here",
  "operations": [
    {
      "type": "operation-type",
      "...operation-specific-fields..."
    }
  ]
}
```

## Operation Types

### 1. Node Operations

#### Add Node
```json
{
  "type": "addNode",
  "description": "Add HTTP Request node to fetch data",
  "node": {
    "name": "Fetch User Data",
    "type": "n8n-nodes-base.httpRequest",
    "position": [600, 300],
    "parameters": {
      "url": "https://api.example.com/users",
      "method": "GET",
      "authentication": "none"
    }
  }
}
```

#### Remove Node
```json
{
  "type": "removeNode",
  "nodeName": "Old Node Name",
  "description": "Remove deprecated node"
}
```

#### Update Node
```json
{
  "type": "updateNode",
  "nodeName": "HTTP Request",
  "changes": {
    "parameters.url": "https://new-api.example.com/v2/users",
    "parameters.headers.parameters": [
      {
        "name": "Authorization",
        "value": "Bearer {{$credentials.apiKey}}"
      }
    ]
  },
  "description": "Update API endpoint to v2"
}
```

#### Move Node
```json
{
  "type": "moveNode",
  "nodeName": "Set Variable",
  "position": [800, 400],
  "description": "Reposition for better layout"
}
```

#### Enable/Disable Node
```json
{
  "type": "disableNode",
  "nodeName": "Debug Node",
  "description": "Disable debug output for production"
}
```

### 2. Connection Operations

#### Add Connection
```json
{
  "type": "addConnection",
  "source": "Webhook",
  "target": "Process Data",
  "sourceOutput": "main",
  "targetInput": "main",
  "description": "Connect webhook to processor"
}
```

#### Remove Connection
```json
{
  "type": "removeConnection",
  "source": "Old Source",
  "target": "Old Target",
  "description": "Remove unused connection"
}
```

#### Rewire Connection
```json
{
  "type": "rewireConnection",
  "source": "Webhook",
  "from": "Old Handler",
  "to": "New Handler",
  "description": "Rewire connection to new handler"
}
```

#### Smart Parameters for IF Nodes
```json
{
  "type": "addConnection",
  "source": "IF",
  "target": "Success Handler",
  "branch": "true",  // Semantic parameter instead of sourceIndex
  "description": "Route true branch to success handler"
}
```

```json
{
  "type": "addConnection",
  "source": "IF",
  "target": "Error Handler",
  "branch": "false",  // Routes to false branch (sourceIndex=1)
  "description": "Route false branch to error handler"
}
```

#### Smart Parameters for Switch Nodes
```json
{
  "type": "addConnection",
  "source": "Switch",
  "target": "Handler A",
  "case": 0,  // First output
  "description": "Route case 0 to Handler A"
}
```

### 3. Workflow Metadata Operations

#### Update Workflow Name
```json
{
  "type": "updateName",
  "name": "Production User Sync v2",
  "description": "Update workflow name for versioning"
}
```

#### Update Settings
```json
{
  "type": "updateSettings",
  "settings": {
    "executionTimeout": 300,
    "saveDataErrorExecution": "all",
    "timezone": "America/New_York"
  },
  "description": "Configure production settings"
}
```

#### Manage Tags
```json
{
  "type": "addTag",
  "tag": "production",
  "description": "Mark as production workflow"
}
```

## Complete Examples

### Example 1: Add Slack Notification to Workflow
```json
{
  "id": "workflow-123",
  "operations": [
    {
      "type": "addNode",
      "node": {
        "name": "Send Slack Alert",
        "type": "n8n-nodes-base.slack",
        "position": [1000, 300],
        "parameters": {
          "resource": "message",
          "operation": "post",
          "channel": "#alerts",
          "text": "Workflow completed successfully!"
        }
      }
    },
    {
      "type": "addConnection",
      "source": "Process Data",
      "target": "Send Slack Alert"
    }
  ]
}
```

### Example 2: Update Multiple Webhook Paths
```json
{
  "id": "workflow-456",
  "operations": [
    {
      "type": "updateNode",
      "nodeName": "Webhook 1",
      "changes": {
        "parameters.path": "v2/webhook1"
      }
    },
    {
      "type": "updateNode",
      "nodeName": "Webhook 2",
      "changes": {
        "parameters.path": "v2/webhook2"
      }
    },
    {
      "type": "updateName",
      "name": "API v2 Webhooks"
    }
  ]
}
```

### Example 3: Refactor Workflow Structure
```json
{
  "id": "workflow-789",
  "operations": [
    {
      "type": "removeNode",
      "nodeName": "Legacy Processor"
    },
    {
      "type": "addNode",
      "node": {
        "name": "Modern Processor",
        "type": "n8n-nodes-base.code",
        "position": [600, 300],
        "parameters": {
          "mode": "runOnceForEachItem",
          "jsCode": "// Process items\nreturn item;"
        }
      }
    },
    {
      "type": "addConnection",
      "source": "HTTP Request",
      "target": "Modern Processor"
    },
    {
      "type": "addConnection",
      "source": "Modern Processor",
      "target": "Save to Database"
    }
  ]
}
```

### Example 4: Add Error Handling
```json
{
  "id": "workflow-999",
  "operations": [
    {
      "type": "addNode",
      "node": {
        "name": "Error Handler",
        "type": "n8n-nodes-base.errorTrigger",
        "position": [200, 500]
      }
    },
    {
      "type": "addNode",
      "node": {
        "name": "Send Error Email",
        "type": "n8n-nodes-base.emailSend",
        "position": [400, 500],
        "parameters": {
          "toEmail": "[email protected]",
          "subject": "Workflow Error: {{$node['Error Handler'].json.error.message}}",
          "text": "Error details: {{$json}}"
        }
      }
    },
    {
      "type": "addConnection",
      "source": "Error Handler",
      "target": "Send Error Email"
    },
    {
      "type": "updateSettings",
      "settings": {
        "errorWorkflow": "workflow-999"
      }
    }
  ]
}
```

### Example 5: Large Batch Workflow Refactoring
Demonstrates handling many operations in a single request - no longer limited to 5 operations!

```json
{
  "id": "workflow-batch",
  "operations": [
    // Add 10 processing nodes
    {
      "type": "addNode",
      "node": {
        "name": "Filter Active Users",
        "type": "n8n-nodes-base.filter",
        "position": [400, 200],
        "parameters": { "conditions": { "boolean": [{ "value1": "={{$json.active}}", "value2": true }] } }
      }
    },
    {
      "type": "addNode",
      "node": {
        "name": "Transform User Data",
        "type": "n8n-nodes-base.set",
        "position": [600, 200],
        "parameters": { "values": { "string": [{ "name": "formatted_name", "value": "={{$json.firstName}} {{$json.lastName}}" }] } }
      }
    },
    {
      "type": "addNode",
      "node": {
        "name": "Validate Email",
        "type": "n8n-nodes-base.if",
        "position": [800, 200],
        "parameters": { "conditions": { "string": [{ "value1": "={{$json.email}}", "operation": "contains", "value2": "@" }] } }
      }
    },
    {
      "type": "addNode",
      "node": {
        "name": "Enrich with API",
        "type": "n8n-nodes-base.httpRequest",
        "position": [1000, 150],
        "parameters": { "url": "https://api.example.com/enrich", "method": "POST" }
      }
    },
    {
      "type": "addNode",
      "node": {
        "name": "Log Invalid Emails",
        "type": "n8n-nodes-base.code",
        "position": [1000, 350],
        "parameters": { "jsCode": "console.log('Invalid email:', $json.email);\nreturn $json;" }
      }
    },
    {
      "type": "addNode",
      "node": {
        "name": "Merge Results",
        "type": "n8n-nodes-base.merge",
        "position": [1200, 250]
      }
    },
    {
      "type": "addNode",
      "node": {
        "name": "Deduplicate",
        "type": "n8n-nodes-base.removeDuplicates",
        "position": [1400, 250],
        "parameters": { "propertyName": "id" }
      }
    },
    {
      "type": "addNode",
      "node": {
        "name": "Sort by Date",
        "type": "n8n-nodes-base.sort",
        "position": [1600, 250],
        "parameters": { "sortFieldsUi": { "sortField": [{ "fieldName": "created_at", "order": "descending" }] } }
      }
    },
    {
      "type": "addNode",
      "node": {
        "name": "Batch for DB",
        "type": "n8n-nodes-base.splitInBatches",
        "position": [1800, 250],
        "parameters": { "batchSize": 100 }
      }
    },
    {
      "type": "addNode",
      "node": {
        "name": "Save to Database",
        "type": "n8n-nodes-base.postgres",
        "position": [2000, 250],
        "parameters": { "operation": "insert", "table": "processed_users" }
      }
    },
    // Connect all the nodes
    {
      "type": "addConnection",
      "source": "Get Users",
      "target": "Filter Active Users"
    },
    {
      "type": "addConnection",
      "source": "Filter Active Users",
      "target": "Transform User Data"
    },
    {
      "type": "addConnection",
      "source": "Transform User Data",
      "target": "Validate Email"
    },
    {
      "type": "addConnection",
      "source": "Validate Email",
      "sourceOutput": "true",
      "target": "Enrich with API"
    },
    {
      "type": "addConnection",
      "source": "Validate Email",
      "sourceOutput": "false",
      "target": "Log Invalid Emails"
    },
    {
      "type": "addConnection",
      "source": "Enrich with API",
      "target": "Merge Results"
    },
    {
      "type": "addConnection",
      "source": "Log Invalid Emails",
      "target": "Merge Results",
      "targetInput": "input2"
    },
    {
      "type": "addConnection",
      "source": "Merge Results",
      "target": "Deduplicate"
    },
    {
      "type": "addConnection",
      "source": "Deduplicate",
      "target": "Sort by Date"
    },
    {
      "type": "addConnection",
      "source": "Sort by Date",
      "target": "Batch for DB"
    },
    {
      "type": "addConnection",
      "source": "Batch for DB",
      "target": "Save to Database"
    },
    // Update workflow metadata
    {
      "type": "updateName",
      "name": "User Processing Pipeline v2"
    },
    {
      "type": "updateSettings",
      "settings": {
        "executionOrder": "v1",
        "timezone": "UTC",
        "saveDataSuccessExecution": "all"
      }
    },
    {
      "type": "addTag",
      "tag": "production"
    },
    {
      "type": "addTag",
      "tag": "user-processing"
    },
    {
      "type": "addTag",
      "tag": "v2"
    }
  ]
}
```

This example shows 26 operations in a single request, creating a complete data processing pipeline with proper error handling, validation, and batch processing.

## Best Practices

1. **Use Descriptive Names**: Always provide clear node names and descriptions for operations
2. **Batch Related Changes**: Group related operations in a single request
3. **Validate First**: Use `validateOnly: true` to test your operations before applying
4. **Reference by Name**: Prefer node names over IDs for better readability
5. **Small, Focused Changes**: Make targeted edits rather than large structural changes

## Common Patterns

### Add Processing Step
```json
{
  "operations": [
    {
      "type": "removeConnection",
      "source": "Source Node",
      "target": "Target Node"
    },
    {
      "type": "addNode",
      "node": {
        "name": "Process Step",
        "type": "n8n-nodes-base.set",
        "position": [600, 300],
        "parameters": { /* ... */ }
      }
    },
    {
      "type": "addConnection",
      "source": "Source Node",
      "target": "Process Step"
    },
    {
      "type": "addConnection",
      "source": "Process Step",
      "target": "Target Node"
    }
  ]
}
```

### Replace Node
```json
{
  "operations": [
    {
      "type": "addNode",
      "node": {
        "name": "New Implementation",
        "type": "n8n-nodes-base.httpRequest",
        "position": [600, 300],
        "parameters": { /* ... */ }
      }
    },
    {
      "type": "removeConnection",
      "source": "Previous Node",
      "target": "Old Implementation"
    },
    {
      "type": "removeConnection",
      "source": "Old Implementation",
      "target": "Next Node"
    },
    {
      "type": "addConnection",
      "source": "Previous Node",
      "target": "New Implementation"
    },
    {
      "type": "addConnection",
      "source": "New Implementation",
      "target": "Next Node"
    },
    {
      "type": "removeNode",
      "nodeName": "Old Implementation"
    }
  ]
}
```

## Error Handling

The tool validates all operations before applying any changes. Common errors include:

- **Duplicate node names**: Each node must have a unique name
- **Invalid node types**: Use full package prefixes (e.g., `n8n-nodes-base.webhook`)
- **Missing connections**: Referenced nodes must exist
- **Circular dependencies**: Connections cannot create loops

Always check the response for validation errors and adjust your operations accordingly.

## Transactional Updates

The diff engine now supports transactional updates using a **two-pass processing** approach:

### How It Works

1. **No Operation Limit**: Process unlimited operations in a single request
2. **Two-Pass Processing**:
   - **Pass 1**: All node operations (add, remove, update, move, enable, disable)
   - **Pass 2**: All other operations (connections, settings, metadata)

This allows you to add nodes and connect them in the same request:

```json
{
  "id": "workflow-id",
  "operations": [
    // These will be processed in Pass 2 (but work because nodes are added first)
    {
      "type": "addConnection",
      "source": "Webhook",
      "target": "Process Data"
    },
    {
      "type": "addConnection", 
      "source": "Process Data",
      "target": "Send Email"
    },
    // These will be processed in Pass 1
    {
      "type": "addNode",
      "node": {
        "name": "Process Data",
        "type": "n8n-nodes-base.set",
        "position": [400, 300],
        "parameters": {}
      }
    },
    {
      "type": "addNode",
      "node": {
        "name": "Send Email",
        "type": "n8n-nodes-base.emailSend",
        "position": [600, 300],
        "parameters": {
          "to": "[email protected]"
        }
      }
    }
  ]
}
```

### Benefits

- **Order Independence**: You don't need to worry about operation order
- **Atomic Updates**: All operations succeed or all fail (unless continueOnError is enabled)
- **Intuitive Usage**: Add complex workflow structures in one call
- **No Hard Limits**: Process unlimited operations efficiently

### Example: Complete Workflow Addition

```json
{
  "id": "workflow-id",
  "operations": [
    // Add three nodes
    {
      "type": "addNode",
      "node": {
        "name": "Schedule",
        "type": "n8n-nodes-base.schedule",
        "position": [200, 300],
        "parameters": {
          "rule": {
            "interval": [{ "field": "hours", "intervalValue": 1 }]
          }
        }
      }
    },
    {
      "type": "addNode", 
      "node": {
        "name": "Get Data",
        "type": "n8n-nodes-base.httpRequest",
        "position": [400, 300],
        "parameters": {
          "url": "https://api.example.com/data"
        }
      }
    },
    {
      "type": "addNode",
      "node": {
        "name": "Save to Database",
        "type": "n8n-nodes-base.postgres",
        "position": [600, 300],
        "parameters": {
          "operation": "insert"
        }
      }
    },
    // Connect them all
    {
      "type": "addConnection",
      "source": "Schedule",
      "target": "Get Data"
    },
    {
      "type": "addConnection",
      "source": "Get Data", 
      "target": "Save to Database"
    }
  ]
}
```

All operations will be processed correctly regardless of order!
```

--------------------------------------------------------------------------------
/src/database/database-adapter.ts:
--------------------------------------------------------------------------------

```typescript
import { promises as fs } from 'fs';
import * as fsSync from 'fs';
import path from 'path';
import { logger } from '../utils/logger';

/**
 * Unified database interface that abstracts better-sqlite3 and sql.js
 */
export interface DatabaseAdapter {
  prepare(sql: string): PreparedStatement;
  exec(sql: string): void;
  close(): void;
  pragma(key: string, value?: any): any;
  readonly inTransaction: boolean;
  transaction<T>(fn: () => T): T;
  checkFTS5Support(): boolean;
}

export interface PreparedStatement {
  run(...params: any[]): RunResult;
  get(...params: any[]): any;
  all(...params: any[]): any[];
  iterate(...params: any[]): IterableIterator<any>;
  pluck(toggle?: boolean): this;
  expand(toggle?: boolean): this;
  raw(toggle?: boolean): this;
  columns(): ColumnDefinition[];
  bind(...params: any[]): this;
}

export interface RunResult {
  changes: number;
  lastInsertRowid: number | bigint;
}

export interface ColumnDefinition {
  name: string;
  column: string | null;
  table: string | null;
  database: string | null;
  type: string | null;
}

/**
 * Factory function to create a database adapter
 * Tries better-sqlite3 first, falls back to sql.js if needed
 */
export async function createDatabaseAdapter(dbPath: string): Promise<DatabaseAdapter> {
  // Log Node.js version information
  // Only log in non-stdio mode
  if (process.env.MCP_MODE !== 'stdio') {
    logger.info(`Node.js version: ${process.version}`);
  }
  // Only log in non-stdio mode
  if (process.env.MCP_MODE !== 'stdio') {
    logger.info(`Platform: ${process.platform} ${process.arch}`);
  }
  
  // First, try to use better-sqlite3
  try {
    if (process.env.MCP_MODE !== 'stdio') {
      logger.info('Attempting to use better-sqlite3...');
    }
    const adapter = await createBetterSQLiteAdapter(dbPath);
    if (process.env.MCP_MODE !== 'stdio') {
      logger.info('Successfully initialized better-sqlite3 adapter');
    }
    return adapter;
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    
    // Check if it's a version mismatch error
    if (errorMessage.includes('NODE_MODULE_VERSION') || errorMessage.includes('was compiled against a different Node.js version')) {
      if (process.env.MCP_MODE !== 'stdio') {
        logger.warn(`Node.js version mismatch detected. Better-sqlite3 was compiled for a different Node.js version.`);
      }
      if (process.env.MCP_MODE !== 'stdio') {
        logger.warn(`Current Node.js version: ${process.version}`);
      }
    }
    
    if (process.env.MCP_MODE !== 'stdio') {
      logger.warn('Failed to initialize better-sqlite3, falling back to sql.js', error);
    }
    
    // Fall back to sql.js
    try {
      const adapter = await createSQLJSAdapter(dbPath);
      if (process.env.MCP_MODE !== 'stdio') {
        logger.info('Successfully initialized sql.js adapter (pure JavaScript, no native dependencies)');
      }
      return adapter;
    } catch (sqlJsError) {
      if (process.env.MCP_MODE !== 'stdio') {
        logger.error('Failed to initialize sql.js adapter', sqlJsError);
      }
      throw new Error('Failed to initialize any database adapter');
    }
  }
}

/**
 * Create better-sqlite3 adapter
 */
async function createBetterSQLiteAdapter(dbPath: string): Promise<DatabaseAdapter> {
  try {
    const Database = require('better-sqlite3');
    const db = new Database(dbPath);
    
    return new BetterSQLiteAdapter(db);
  } catch (error) {
    throw new Error(`Failed to create better-sqlite3 adapter: ${error}`);
  }
}

/**
 * Create sql.js adapter with persistence
 */
async function createSQLJSAdapter(dbPath: string): Promise<DatabaseAdapter> {
  let initSqlJs;
  try {
    initSqlJs = require('sql.js');
  } catch (error) {
    logger.error('Failed to load sql.js module:', error);
    throw new Error('sql.js module not found. This might be an issue with npm package installation.');
  }
  
  // Initialize sql.js
  const SQL = await initSqlJs({
    // This will look for the wasm file in node_modules
    locateFile: (file: string) => {
      if (file.endsWith('.wasm')) {
        // Try multiple paths to find the WASM file
        const possiblePaths = [
          // Local development path
          path.join(__dirname, '../../node_modules/sql.js/dist/', file),
          // When installed as npm package
          path.join(__dirname, '../../../sql.js/dist/', file),
          // Alternative npm package path
          path.join(process.cwd(), 'node_modules/sql.js/dist/', file),
          // Try to resolve from require
          path.join(path.dirname(require.resolve('sql.js')), '../dist/', file)
        ];
        
        // Find the first existing path
        for (const tryPath of possiblePaths) {
          if (fsSync.existsSync(tryPath)) {
            if (process.env.MCP_MODE !== 'stdio') {
              logger.debug(`Found WASM file at: ${tryPath}`);
            }
            return tryPath;
          }
        }
        
        // If not found, try the last resort - require.resolve
        try {
          const wasmPath = require.resolve('sql.js/dist/sql-wasm.wasm');
          if (process.env.MCP_MODE !== 'stdio') {
            logger.debug(`Found WASM file via require.resolve: ${wasmPath}`);
          }
          return wasmPath;
        } catch (e) {
          // Fall back to the default path
          logger.warn(`Could not find WASM file, using default path: ${file}`);
          return file;
        }
      }
      return file;
    }
  });
  
  // Try to load existing database
  let db: any;
  try {
    const data = await fs.readFile(dbPath);
    db = new SQL.Database(new Uint8Array(data));
    logger.info(`Loaded existing database from ${dbPath}`);
  } catch (error) {
    // Create new database if file doesn't exist
    db = new SQL.Database();
    logger.info(`Created new database at ${dbPath}`);
  }
  
  return new SQLJSAdapter(db, dbPath);
}

/**
 * Adapter for better-sqlite3
 */
class BetterSQLiteAdapter implements DatabaseAdapter {
  constructor(private db: any) {}
  
  prepare(sql: string): PreparedStatement {
    const stmt = this.db.prepare(sql);
    return new BetterSQLiteStatement(stmt);
  }
  
  exec(sql: string): void {
    this.db.exec(sql);
  }
  
  close(): void {
    this.db.close();
  }
  
  pragma(key: string, value?: any): any {
    return this.db.pragma(key, value);
  }
  
  get inTransaction(): boolean {
    return this.db.inTransaction;
  }
  
  transaction<T>(fn: () => T): T {
    return this.db.transaction(fn)();
  }
  
  checkFTS5Support(): boolean {
    try {
      // Test if FTS5 is available
      this.exec("CREATE VIRTUAL TABLE IF NOT EXISTS test_fts5 USING fts5(content);");
      this.exec("DROP TABLE IF EXISTS test_fts5;");
      return true;
    } catch (error) {
      return false;
    }
  }
}

/**
 * Adapter for sql.js with persistence
 */
class SQLJSAdapter implements DatabaseAdapter {
  private saveTimer: NodeJS.Timeout | null = null;
  private saveIntervalMs: number;
  private closed = false; // Prevent multiple close() calls

  // Default save interval: 5 seconds (balance between data safety and performance)
  // Configurable via SQLJS_SAVE_INTERVAL_MS environment variable
  //
  // DATA LOSS WINDOW: Up to 5 seconds of database changes may be lost if process
  // crashes before scheduleSave() timer fires. This is acceptable because:
  // 1. close() calls saveToFile() immediately on graceful shutdown
  // 2. Docker/Kubernetes SIGTERM provides 30s for cleanup (more than enough)
  // 3. The alternative (100ms interval) caused 2.2GB memory leaks in production
  // 4. MCP server is primarily read-heavy (writes are rare)
  private static readonly DEFAULT_SAVE_INTERVAL_MS = 5000;

  constructor(private db: any, private dbPath: string) {
    // Read save interval from environment or use default
    const envInterval = process.env.SQLJS_SAVE_INTERVAL_MS;
    this.saveIntervalMs = envInterval ? parseInt(envInterval, 10) : SQLJSAdapter.DEFAULT_SAVE_INTERVAL_MS;

    // Validate interval (minimum 100ms, maximum 60000ms = 1 minute)
    if (isNaN(this.saveIntervalMs) || this.saveIntervalMs < 100 || this.saveIntervalMs > 60000) {
      logger.warn(
        `Invalid SQLJS_SAVE_INTERVAL_MS value: ${envInterval} (must be 100-60000ms), ` +
        `using default ${SQLJSAdapter.DEFAULT_SAVE_INTERVAL_MS}ms`
      );
      this.saveIntervalMs = SQLJSAdapter.DEFAULT_SAVE_INTERVAL_MS;
    }

    logger.debug(`SQLJSAdapter initialized with save interval: ${this.saveIntervalMs}ms`);

    // NOTE: No initial save scheduled here (optimization)
    // Database is either:
    // 1. Loaded from existing file (already persisted), or
    // 2. New database (will be saved on first write operation)
  }
  
  prepare(sql: string): PreparedStatement {
    const stmt = this.db.prepare(sql);
    // Don't schedule save on prepare - only on actual writes (via SQLJSStatement.run())
    return new SQLJSStatement(stmt, () => this.scheduleSave());
  }
  
  exec(sql: string): void {
    this.db.exec(sql);
    this.scheduleSave();
  }
  
  close(): void {
    if (this.closed) {
      logger.debug('SQLJSAdapter already closed, skipping');
      return;
    }

    this.saveToFile();
    if (this.saveTimer) {
      clearTimeout(this.saveTimer);
      this.saveTimer = null;
    }
    this.db.close();
    this.closed = true;
  }
  
  pragma(key: string, value?: any): any {
    // sql.js doesn't support pragma in the same way
    // We'll handle specific pragmas as needed
    if (key === 'journal_mode' && value === 'WAL') {
      // WAL mode not supported in sql.js, ignore
      return 'memory';
    }
    return null;
  }
  
  get inTransaction(): boolean {
    // sql.js doesn't expose transaction state
    return false;
  }
  
  transaction<T>(fn: () => T): T {
    // Simple transaction implementation for sql.js
    try {
      this.exec('BEGIN');
      const result = fn();
      this.exec('COMMIT');
      return result;
    } catch (error) {
      this.exec('ROLLBACK');
      throw error;
    }
  }
  
  checkFTS5Support(): boolean {
    try {
      // Test if FTS5 is available
      this.exec("CREATE VIRTUAL TABLE IF NOT EXISTS test_fts5 USING fts5(content);");
      this.exec("DROP TABLE IF EXISTS test_fts5;");
      return true;
    } catch (error) {
      // sql.js doesn't support FTS5
      return false;
    }
  }
  
  private scheduleSave(): void {
    if (this.saveTimer) {
      clearTimeout(this.saveTimer);
    }

    // Save after configured interval of inactivity (default: 5000ms)
    // This debouncing reduces memory churn from frequent buffer allocations
    //
    // NOTE: Under constant write load, saves may be delayed until writes stop.
    // This is acceptable because:
    // 1. MCP server is primarily read-heavy (node lookups, searches)
    // 2. Writes are rare (only during database rebuilds)
    // 3. close() saves immediately on shutdown, flushing any pending changes
    this.saveTimer = setTimeout(() => {
      this.saveToFile();
    }, this.saveIntervalMs);
  }
  
  private saveToFile(): void {
    try {
      // Export database to Uint8Array (2-5MB typical)
      const data = this.db.export();

      // Write directly without Buffer.from() copy (saves 50% memory allocation)
      // writeFileSync accepts Uint8Array directly, no need for Buffer conversion
      fsSync.writeFileSync(this.dbPath, data);
      logger.debug(`Database saved to ${this.dbPath}`);

      // Note: 'data' reference is automatically cleared when function exits
      // V8 GC will reclaim the Uint8Array once it's no longer referenced
    } catch (error) {
      logger.error('Failed to save database', error);
    }
  }
}

/**
 * Statement wrapper for better-sqlite3
 */
class BetterSQLiteStatement implements PreparedStatement {
  constructor(private stmt: any) {}
  
  run(...params: any[]): RunResult {
    return this.stmt.run(...params);
  }
  
  get(...params: any[]): any {
    return this.stmt.get(...params);
  }
  
  all(...params: any[]): any[] {
    return this.stmt.all(...params);
  }
  
  iterate(...params: any[]): IterableIterator<any> {
    return this.stmt.iterate(...params);
  }
  
  pluck(toggle?: boolean): this {
    this.stmt.pluck(toggle);
    return this;
  }
  
  expand(toggle?: boolean): this {
    this.stmt.expand(toggle);
    return this;
  }
  
  raw(toggle?: boolean): this {
    this.stmt.raw(toggle);
    return this;
  }
  
  columns(): ColumnDefinition[] {
    return this.stmt.columns();
  }
  
  bind(...params: any[]): this {
    this.stmt.bind(...params);
    return this;
  }
}

/**
 * Statement wrapper for sql.js
 */
class SQLJSStatement implements PreparedStatement {
  private boundParams: any = null;
  
  constructor(private stmt: any, private onModify: () => void) {}
  
  run(...params: any[]): RunResult {
    try {
      if (params.length > 0) {
        this.bindParams(params);
        if (this.boundParams) {
          this.stmt.bind(this.boundParams);
        }
      }
      
      this.stmt.run();
      this.onModify();
      
      // sql.js doesn't provide changes/lastInsertRowid easily
      return {
        changes: 1, // Assume success means 1 change
        lastInsertRowid: 0
      };
    } catch (error) {
      this.stmt.reset();
      throw error;
    }
  }
  
  get(...params: any[]): any {
    try {
      if (params.length > 0) {
        this.bindParams(params);
        if (this.boundParams) {
          this.stmt.bind(this.boundParams);
        }
      }
      
      if (this.stmt.step()) {
        const result = this.stmt.getAsObject();
        this.stmt.reset();
        return this.convertIntegerColumns(result);
      }
      
      this.stmt.reset();
      return undefined;
    } catch (error) {
      this.stmt.reset();
      throw error;
    }
  }
  
  all(...params: any[]): any[] {
    try {
      if (params.length > 0) {
        this.bindParams(params);
        if (this.boundParams) {
          this.stmt.bind(this.boundParams);
        }
      }
      
      const results: any[] = [];
      while (this.stmt.step()) {
        results.push(this.convertIntegerColumns(this.stmt.getAsObject()));
      }
      
      this.stmt.reset();
      return results;
    } catch (error) {
      this.stmt.reset();
      throw error;
    }
  }
  
  iterate(...params: any[]): IterableIterator<any> {
    // sql.js doesn't support generators well, return array iterator
    return this.all(...params)[Symbol.iterator]();
  }
  
  pluck(toggle?: boolean): this {
    // Not directly supported in sql.js
    return this;
  }
  
  expand(toggle?: boolean): this {
    // Not directly supported in sql.js
    return this;
  }
  
  raw(toggle?: boolean): this {
    // Not directly supported in sql.js
    return this;
  }
  
  columns(): ColumnDefinition[] {
    // sql.js has different column info
    return [];
  }
  
  bind(...params: any[]): this {
    this.bindParams(params);
    return this;
  }
  
  private bindParams(params: any[]): void {
    if (params.length === 0) {
      this.boundParams = null;
      return;
    }
    
    if (params.length === 1 && typeof params[0] === 'object' && !Array.isArray(params[0]) && params[0] !== null) {
      // Named parameters passed as object
      this.boundParams = params[0];
    } else {
      // Positional parameters - sql.js uses array for positional
      // Filter out undefined values that might cause issues
      this.boundParams = params.map(p => p === undefined ? null : p);
    }
  }
  
  /**
   * Convert SQLite integer columns to JavaScript numbers
   * sql.js returns all values as strings, but we need proper types for boolean conversion
   */
  private convertIntegerColumns(row: any): any {
    if (!row) return row;
    
    // Known integer columns in the nodes table
    const integerColumns = ['is_ai_tool', 'is_trigger', 'is_webhook', 'is_versioned'];
    
    const converted = { ...row };
    for (const col of integerColumns) {
      if (col in converted && typeof converted[col] === 'string') {
        converted[col] = parseInt(converted[col], 10);
      }
    }
    
    return converted;
  }
}
```

--------------------------------------------------------------------------------
/tests/unit/database/template-repository-core.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { TemplateRepository, StoredTemplate } from '../../../src/templates/template-repository';
import { DatabaseAdapter, PreparedStatement, RunResult } from '../../../src/database/database-adapter';
import { TemplateWorkflow, TemplateDetail } from '../../../src/templates/template-fetcher';

// Mock logger
vi.mock('../../../src/utils/logger', () => ({
  logger: {
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
    debug: vi.fn()
  }
}));

// Mock template sanitizer
vi.mock('../../../src/utils/template-sanitizer', () => {
  class MockTemplateSanitizer {
    sanitizeWorkflow = vi.fn((workflow) => ({ sanitized: workflow, wasModified: false }));
    detectTokens = vi.fn(() => []);
  }
  
  return {
    TemplateSanitizer: MockTemplateSanitizer
  };
});

// Create mock database adapter
class MockDatabaseAdapter implements DatabaseAdapter {
  private statements = new Map<string, MockPreparedStatement>();
  private mockData = new Map<string, any>();
  private _fts5Support = true;
  
  prepare = vi.fn((sql: string) => {
    if (!this.statements.has(sql)) {
      this.statements.set(sql, new MockPreparedStatement(sql, this.mockData));
    }
    return this.statements.get(sql)!;
  });
  
  exec = vi.fn();
  close = vi.fn();
  pragma = vi.fn();
  transaction = vi.fn((fn: () => any) => fn());
  checkFTS5Support = vi.fn(() => this._fts5Support);
  inTransaction = false;
  
  // Test helpers
  _setFTS5Support(supported: boolean) {
    this._fts5Support = supported;
  }
  
  _setMockData(key: string, value: any) {
    this.mockData.set(key, value);
  }
  
  _getStatement(sql: string) {
    return this.statements.get(sql);
  }
}

class MockPreparedStatement implements PreparedStatement {
  run = vi.fn((...params: any[]): RunResult => ({ changes: 1, lastInsertRowid: 1 }));
  get = vi.fn();
  all = vi.fn(() => []);
  iterate = vi.fn();
  pluck = vi.fn(() => this);
  expand = vi.fn(() => this);
  raw = vi.fn(() => this);
  columns = vi.fn(() => []);
  bind = vi.fn(() => this);
  
  constructor(private sql: string, private mockData: Map<string, any>) {
    // Configure based on SQL patterns
    if (sql.includes('SELECT * FROM templates WHERE id = ?')) {
      this.get = vi.fn((id: number) => this.mockData.get(`template:${id}`));
    }
    
    if (sql.includes('SELECT * FROM templates') && sql.includes('LIMIT')) {
      this.all = vi.fn(() => this.mockData.get('all_templates') || []);
    }
    
    if (sql.includes('templates_fts')) {
      this.all = vi.fn(() => this.mockData.get('fts_results') || []);
    }
    
    if (sql.includes('WHERE name LIKE')) {
      this.all = vi.fn(() => this.mockData.get('like_results') || []);
    }
    
    if (sql.includes('COUNT(*) as count')) {
      this.get = vi.fn(() => ({ count: this.mockData.get('template_count') || 0 }));
    }
    
    if (sql.includes('AVG(views)')) {
      this.get = vi.fn(() => ({ avg: this.mockData.get('avg_views') || 0 }));
    }
    
    if (sql.includes('sqlite_master')) {
      this.get = vi.fn(() => this.mockData.get('fts_table_exists') ? { name: 'templates_fts' } : undefined);
    }
  }
}

describe('TemplateRepository - Core Functionality', () => {
  let repository: TemplateRepository;
  let mockAdapter: MockDatabaseAdapter;
  
  beforeEach(() => {
    vi.clearAllMocks();
    mockAdapter = new MockDatabaseAdapter();
    mockAdapter._setMockData('fts_table_exists', false); // Default to creating FTS
    repository = new TemplateRepository(mockAdapter);
  });
  
  describe('FTS5 initialization', () => {
    it('should initialize FTS5 when supported', () => {
      expect(mockAdapter.checkFTS5Support).toHaveBeenCalled();
      expect(mockAdapter.exec).toHaveBeenCalledWith(expect.stringContaining('CREATE VIRTUAL TABLE'));
    });
    
    it('should skip FTS5 when not supported', () => {
      mockAdapter._setFTS5Support(false);
      mockAdapter.exec.mockClear();
      
      const newRepo = new TemplateRepository(mockAdapter);
      
      expect(mockAdapter.exec).not.toHaveBeenCalledWith(expect.stringContaining('CREATE VIRTUAL TABLE'));
    });
  });
  
  describe('saveTemplate', () => {
    it('should save a template with proper JSON serialization', () => {
      const workflow: TemplateWorkflow = {
        id: 123,
        name: 'Test Workflow',
        description: 'A test workflow',
        user: {
          id: 1,
          name: 'John Doe',
          username: 'johndoe',
          verified: true
        },
        nodes: [
          { id: 1, name: 'n8n-nodes-base.httpRequest', icon: 'fa:globe' },
          { id: 2, name: 'n8n-nodes-base.slack', icon: 'fa:slack' }
        ],
        totalViews: 1000,
        createdAt: '2024-01-01T00:00:00Z'
      };
      
      const detail: TemplateDetail = {
        id: 123,
        name: 'Test Workflow',
        description: 'A test workflow',
        views: 1000,
        createdAt: '2024-01-01T00:00:00Z',
        workflow: {
          nodes: [
            { type: 'n8n-nodes-base.httpRequest', name: 'HTTP Request', id: '1', position: [0, 0], parameters: {}, typeVersion: 1 },
            { type: 'n8n-nodes-base.slack', name: 'Slack', id: '2', position: [100, 0], parameters: {}, typeVersion: 1 }
          ],
          connections: {},
          settings: {}
        }
      };
      
      const categories = ['automation', 'integration'];
      
      repository.saveTemplate(workflow, detail, categories);
      
      const stmt = mockAdapter._getStatement(mockAdapter.prepare.mock.calls.find(
        call => call[0].includes('INSERT OR REPLACE INTO templates')
      )?.[0] || '');
      
      // The implementation now uses gzip compression, so we just verify the call happened
      expect(stmt?.run).toHaveBeenCalledWith(
        123, // id
        123, // workflow_id
        'Test Workflow',
        'A test workflow',
        'John Doe',
        'johndoe',
        1, // verified
        JSON.stringify(['n8n-nodes-base.httpRequest', 'n8n-nodes-base.slack']),
        expect.any(String), // compressed workflow JSON
        JSON.stringify(['automation', 'integration']),
        1000, // views
        '2024-01-01T00:00:00Z',
        '2024-01-01T00:00:00Z',
        'https://n8n.io/workflows/123'
      );
    });
  });
  
  describe('getTemplate', () => {
    it('should retrieve a specific template by ID', () => {
      const mockTemplate: StoredTemplate = {
        id: 123,
        workflow_id: 123,
        name: 'Test Template',
        description: 'Description',
        author_name: 'Author',
        author_username: 'author',
        author_verified: 1,
        nodes_used: '[]',
        workflow_json: '{}',
        categories: '[]',
        views: 500,
        created_at: '2024-01-01',
        updated_at: '2024-01-01',
        url: 'https://n8n.io/workflows/123',
        scraped_at: '2024-01-01'
      };
      
      mockAdapter._setMockData('template:123', mockTemplate);
      
      const result = repository.getTemplate(123);
      
      expect(result).toEqual(mockTemplate);
    });
    
    it('should return null for non-existent template', () => {
      const result = repository.getTemplate(999);
      expect(result).toBeNull();
    });
  });
  
  describe('searchTemplates', () => {
    it('should use FTS5 search when available', () => {
      const ftsResults: StoredTemplate[] = [{
        id: 1,
        workflow_id: 1,
        name: 'Chatbot Workflow',
        description: 'AI chatbot',
        author_name: 'Author',
        author_username: 'author',
        author_verified: 0,
        nodes_used: '[]',
        workflow_json: '{}',
        categories: '[]',
        views: 100,
        created_at: '2024-01-01',
        updated_at: '2024-01-01',
        url: 'https://n8n.io/workflows/1',
        scraped_at: '2024-01-01'
      }];
      
      mockAdapter._setMockData('fts_results', ftsResults);
      
      const results = repository.searchTemplates('chatbot', 10);
      
      expect(results).toEqual(ftsResults);
    });
    
    it('should fall back to LIKE search when FTS5 is not supported', () => {
      mockAdapter._setFTS5Support(false);
      const newRepo = new TemplateRepository(mockAdapter);
      
      const likeResults: StoredTemplate[] = [{
        id: 3,
        workflow_id: 3,
        name: 'LIKE only',
        description: 'No FTS5',
        author_name: 'Author',
        author_username: 'author',
        author_verified: 0,
        nodes_used: '[]',
        workflow_json: '{}',
        categories: '[]',
        views: 25,
        created_at: '2024-01-01',
        updated_at: '2024-01-01',
        url: 'https://n8n.io/workflows/3',
        scraped_at: '2024-01-01'
      }];
      
      mockAdapter._setMockData('like_results', likeResults);
      
      const results = newRepo.searchTemplates('test', 20);
      
      expect(results).toEqual(likeResults);
    });
  });
  
  describe('getTemplatesByNodes', () => {
    it('should find templates using specific node types', () => {
      const mockTemplates: StoredTemplate[] = [{
        id: 1,
        workflow_id: 1,
        name: 'HTTP Workflow',
        description: 'Uses HTTP',
        author_name: 'Author',
        author_username: 'author',
        author_verified: 1,
        nodes_used: '["n8n-nodes-base.httpRequest"]',
        workflow_json: '{}',
        categories: '[]',
        views: 100,
        created_at: '2024-01-01',
        updated_at: '2024-01-01',
        url: 'https://n8n.io/workflows/1',
        scraped_at: '2024-01-01'
      }];
      
      // Set up the mock to return our templates
      const stmt = new MockPreparedStatement('', new Map());
      stmt.all = vi.fn(() => mockTemplates);
      mockAdapter.prepare = vi.fn(() => stmt);
      
      const results = repository.getTemplatesByNodes(['n8n-nodes-base.httpRequest'], 5);
      
      expect(stmt.all).toHaveBeenCalledWith('%"n8n-nodes-base.httpRequest"%', 5, 0);
      expect(results).toEqual(mockTemplates);
    });
  });
  
  describe('getTemplatesForTask', () => {
    it('should return templates for known tasks', () => {
      const aiTemplates: StoredTemplate[] = [{
        id: 1,
        workflow_id: 1,
        name: 'AI Workflow',
        description: 'Uses OpenAI',
        author_name: 'Author',
        author_username: 'author',
        author_verified: 1,
        nodes_used: '["@n8n/n8n-nodes-langchain.openAi"]',
        workflow_json: '{}',
        categories: '["ai"]',
        views: 1000,
        created_at: '2024-01-01',
        updated_at: '2024-01-01',
        url: 'https://n8n.io/workflows/1',
        scraped_at: '2024-01-01'
      }];
      
      const stmt = new MockPreparedStatement('', new Map());
      stmt.all = vi.fn(() => aiTemplates);
      mockAdapter.prepare = vi.fn(() => stmt);
      
      const results = repository.getTemplatesForTask('ai_automation');
      
      expect(results).toEqual(aiTemplates);
    });
    
    it('should return empty array for unknown task', () => {
      const results = repository.getTemplatesForTask('unknown_task');
      expect(results).toEqual([]);
    });
  });
  
  describe('template statistics', () => {
    it('should get template count', () => {
      mockAdapter._setMockData('template_count', 42);
      
      const count = repository.getTemplateCount();
      
      expect(count).toBe(42);
    });
    
    it('should get template statistics', () => {
      mockAdapter._setMockData('template_count', 100);
      mockAdapter._setMockData('avg_views', 250.5);
      
      const topTemplates = [
        { nodes_used: '["n8n-nodes-base.httpRequest", "n8n-nodes-base.slack"]' },
        { nodes_used: '["n8n-nodes-base.httpRequest", "n8n-nodes-base.code"]' },
        { nodes_used: '["n8n-nodes-base.slack"]' }
      ];
      
      const stmt = new MockPreparedStatement('', new Map());
      stmt.all = vi.fn(() => topTemplates);
      mockAdapter.prepare = vi.fn((sql) => {
        if (sql.includes('ORDER BY views DESC')) {
          return stmt;
        }
        return new MockPreparedStatement(sql, mockAdapter['mockData']);
      });
      
      const stats = repository.getTemplateStats();
      
      expect(stats.totalTemplates).toBe(100);
      expect(stats.averageViews).toBe(251);
      expect(stats.topUsedNodes).toContainEqual({ node: 'n8n-nodes-base.httpRequest', count: 2 });
    });
  });

  describe('pagination count methods', () => {
    it('should get node templates count', () => {
      mockAdapter._setMockData('node_templates_count', 15);
      
      const stmt = new MockPreparedStatement('', new Map());
      stmt.get = vi.fn(() => ({ count: 15 }));
      mockAdapter.prepare = vi.fn(() => stmt);
      
      const count = repository.getNodeTemplatesCount(['n8n-nodes-base.webhook']);
      
      expect(count).toBe(15);
      expect(stmt.get).toHaveBeenCalledWith('%"n8n-nodes-base.webhook"%');
    });

    it('should get search count', () => {
      const stmt = new MockPreparedStatement('', new Map());
      stmt.get = vi.fn(() => ({ count: 8 }));
      mockAdapter.prepare = vi.fn(() => stmt);
      
      const count = repository.getSearchCount('webhook');
      
      expect(count).toBe(8);
    });

    it('should get task templates count', () => {
      const stmt = new MockPreparedStatement('', new Map());
      stmt.get = vi.fn(() => ({ count: 12 }));
      mockAdapter.prepare = vi.fn(() => stmt);
      
      const count = repository.getTaskTemplatesCount('ai_automation');
      
      expect(count).toBe(12);
    });

    it('should handle pagination in getAllTemplates', () => {
      const mockTemplates = [
        { id: 1, name: 'Template 1' },
        { id: 2, name: 'Template 2' }
      ];
      
      const stmt = new MockPreparedStatement('', new Map());
      stmt.all = vi.fn(() => mockTemplates);
      mockAdapter.prepare = vi.fn(() => stmt);
      
      const results = repository.getAllTemplates(10, 5, 'name');
      
      expect(results).toEqual(mockTemplates);
      expect(stmt.all).toHaveBeenCalledWith(10, 5);
    });

    it('should handle pagination in getTemplatesByNodes', () => {
      const mockTemplates = [
        { id: 1, nodes_used: '["n8n-nodes-base.webhook"]' }
      ];
      
      const stmt = new MockPreparedStatement('', new Map());
      stmt.all = vi.fn(() => mockTemplates);
      mockAdapter.prepare = vi.fn(() => stmt);
      
      const results = repository.getTemplatesByNodes(['n8n-nodes-base.webhook'], 5, 10);
      
      expect(results).toEqual(mockTemplates);
      expect(stmt.all).toHaveBeenCalledWith('%"n8n-nodes-base.webhook"%', 5, 10);
    });

    it('should handle pagination in searchTemplates', () => {
      const mockTemplates = [
        { id: 1, name: 'Search Result 1' }
      ];
      
      mockAdapter._setMockData('fts_results', mockTemplates);
      
      const stmt = new MockPreparedStatement('', new Map());
      stmt.all = vi.fn(() => mockTemplates);
      mockAdapter.prepare = vi.fn(() => stmt);
      
      const results = repository.searchTemplates('webhook', 20, 40);
      
      expect(results).toEqual(mockTemplates);
    });

    it('should handle pagination in getTemplatesForTask', () => {
      const mockTemplates = [
        { id: 1, categories: '["ai"]' }
      ];
      
      const stmt = new MockPreparedStatement('', new Map());
      stmt.all = vi.fn(() => mockTemplates);
      mockAdapter.prepare = vi.fn(() => stmt);
      
      const results = repository.getTemplatesForTask('ai_automation', 15, 30);
      
      expect(results).toEqual(mockTemplates);
    });
  });
  
  describe('maintenance operations', () => {
    it('should clear all templates', () => {
      repository.clearTemplates();
      
      expect(mockAdapter.exec).toHaveBeenCalledWith('DELETE FROM templates');
    });
    
    it('should rebuild FTS5 index when supported', () => {
      repository.rebuildTemplateFTS();
      
      expect(mockAdapter.exec).toHaveBeenCalledWith('DELETE FROM templates_fts');
      expect(mockAdapter.exec).toHaveBeenCalledWith(
        expect.stringContaining('INSERT INTO templates_fts')
      );
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/unit/flexible-instance-security-advanced.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Advanced security and error handling tests for flexible instance configuration
 *
 * This test file focuses on advanced security scenarios, error handling edge cases,
 * and comprehensive testing of security-related code paths
 */

import { describe, it, expect, beforeEach, afterEach, vi, Mock } from 'vitest';
import { InstanceContext, validateInstanceContext } from '../../src/types/instance-context';
import { getN8nApiClient } from '../../src/mcp/handlers-n8n-manager';
import { getN8nApiConfigFromContext } from '../../src/config/n8n-api';
import { N8nApiClient } from '../../src/services/n8n-api-client';
import { logger } from '../../src/utils/logger';
import { createHash } from 'crypto';

// Mock dependencies
vi.mock('../../src/services/n8n-api-client');
vi.mock('../../src/config/n8n-api');
vi.mock('../../src/utils/logger');

describe('Advanced Security and Error Handling Tests', () => {
  let mockN8nApiClient: Mock;
  let mockGetN8nApiConfigFromContext: Mock;
  let mockLogger: any; // Logger mock has complex type

  beforeEach(() => {
    vi.resetAllMocks();
    vi.resetModules();

    mockN8nApiClient = vi.mocked(N8nApiClient);
    mockGetN8nApiConfigFromContext = vi.mocked(getN8nApiConfigFromContext);
    mockLogger = vi.mocked(logger);
  });

  afterEach(() => {
    vi.clearAllMocks();
  });

  describe('Advanced Input Sanitization', () => {
    it('should handle SQL injection attempts in context fields', () => {
      const maliciousContext = {
        n8nApiUrl: "https://api.n8n.cloud'; DROP TABLE users; --",
        n8nApiKey: "key'; DELETE FROM secrets; --",
        instanceId: "'; SELECT * FROM passwords; --"
      };

      const validation = validateInstanceContext(maliciousContext);

      // URL should be invalid due to special characters
      expect(validation.valid).toBe(false);
      expect(validation.errors?.some(error => error.startsWith('Invalid n8nApiUrl:'))).toBe(true);
    });

    it('should handle XSS attempts in context fields', () => {
      const xssContext = {
        n8nApiUrl: 'https://api.n8n.cloud<script>alert("xss")</script>',
        n8nApiKey: '<img src=x onerror=alert("xss")>',
        instanceId: 'javascript:alert("xss")'
      };

      const validation = validateInstanceContext(xssContext);

      // Should be invalid due to malformed URL
      expect(validation.valid).toBe(false);
    });

    it('should handle extremely long input values', () => {
      const longString = 'a'.repeat(100000);
      const longContext: InstanceContext = {
        n8nApiUrl: `https://api.n8n.cloud/${longString}`,
        n8nApiKey: longString,
        instanceId: longString
      };

      // Should handle without crashing
      expect(() => validateInstanceContext(longContext)).not.toThrow();
      expect(() => getN8nApiClient(longContext)).not.toThrow();
    });

    it('should handle Unicode and special characters safely', () => {
      const unicodeContext: InstanceContext = {
        n8nApiUrl: 'https://api.n8n.cloud/测试',
        n8nApiKey: 'key-ñáéíóú-кириллица-🚀',
        instanceId: '用户-123-αβγ'
      };

      expect(() => validateInstanceContext(unicodeContext)).not.toThrow();
      expect(() => getN8nApiClient(unicodeContext)).not.toThrow();
    });

    it('should handle null bytes and control characters', () => {
      const maliciousContext = {
        n8nApiUrl: 'https://api.n8n.cloud\0\x01\x02',
        n8nApiKey: 'key\r\n\t\0',
        instanceId: 'instance\x00\x1f'
      };

      expect(() => validateInstanceContext(maliciousContext)).not.toThrow();
    });
  });

  describe('Prototype Pollution Protection', () => {
    it('should not be vulnerable to prototype pollution via __proto__', () => {
      const pollutionAttempt = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'test-key',
        __proto__: {
          isAdmin: true,
          polluted: 'value'
        }
      };

      expect(() => validateInstanceContext(pollutionAttempt)).not.toThrow();

      // Verify prototype wasn't polluted
      const cleanObject = {};
      expect((cleanObject as any).isAdmin).toBeUndefined();
      expect((cleanObject as any).polluted).toBeUndefined();
    });

    it('should not be vulnerable to prototype pollution via constructor', () => {
      const pollutionAttempt = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'test-key',
        constructor: {
          prototype: {
            isAdmin: true
          }
        }
      };

      expect(() => validateInstanceContext(pollutionAttempt)).not.toThrow();
    });

    it('should handle Object.create(null) safely', () => {
      const nullProtoObject = Object.create(null);
      nullProtoObject.n8nApiUrl = 'https://api.n8n.cloud';
      nullProtoObject.n8nApiKey = 'test-key';

      expect(() => validateInstanceContext(nullProtoObject)).not.toThrow();
    });
  });

  describe('Memory Exhaustion Protection', () => {
    it('should handle deeply nested objects without stack overflow', () => {
      let deepObject: any = { n8nApiUrl: 'https://api.n8n.cloud', n8nApiKey: 'key' };
      for (let i = 0; i < 1000; i++) {
        deepObject = { nested: deepObject };
      }
      deepObject.metadata = deepObject;

      expect(() => validateInstanceContext(deepObject)).not.toThrow();
    });

    it('should handle circular references in metadata', () => {
      const circularContext: any = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'test-key',
        metadata: {}
      };
      circularContext.metadata.self = circularContext;
      circularContext.metadata.circular = circularContext.metadata;

      expect(() => validateInstanceContext(circularContext)).not.toThrow();
    });

    it('should handle massive arrays in metadata', () => {
      const massiveArray = new Array(100000).fill('data');
      const arrayContext: InstanceContext = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'test-key',
        metadata: {
          massiveArray
        }
      };

      expect(() => validateInstanceContext(arrayContext)).not.toThrow();
    });
  });

  describe('Cache Security and Isolation', () => {
    it('should prevent cache key collisions through hash security', () => {
      mockGetN8nApiConfigFromContext.mockReturnValue({
        baseUrl: 'https://api.n8n.cloud',
        apiKey: 'test-key',
        timeout: 30000,
        maxRetries: 3
      });

      // Create contexts that might produce hash collisions
      const context1: InstanceContext = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'abc',
        instanceId: 'def'
      };

      const context2: InstanceContext = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'ab',
        instanceId: 'cdef'
      };

      const hash1 = createHash('sha256')
        .update(`${context1.n8nApiUrl}:${context1.n8nApiKey}:${context1.instanceId}`)
        .digest('hex');

      const hash2 = createHash('sha256')
        .update(`${context2.n8nApiUrl}:${context2.n8nApiKey}:${context2.instanceId}`)
        .digest('hex');

      expect(hash1).not.toBe(hash2);

      // Verify separate cache entries
      getN8nApiClient(context1);
      getN8nApiClient(context2);

      expect(mockN8nApiClient).toHaveBeenCalledTimes(2);
    });

    it('should not expose sensitive data in cache key logs', () => {
      const loggerInfoSpy = vi.spyOn(logger, 'info');
      const sensitiveContext: InstanceContext = {
        n8nApiUrl: 'https://super-secret-api.example.com/v1/secret',
        n8nApiKey: 'sk_live_SUPER_SECRET_API_KEY_123456789',
        instanceId: 'production-instance-sensitive'
      };

      mockGetN8nApiConfigFromContext.mockReturnValue({
        baseUrl: 'https://super-secret-api.example.com/v1/secret',
        apiKey: 'sk_live_SUPER_SECRET_API_KEY_123456789',
        timeout: 30000,
        maxRetries: 3
      });

      getN8nApiClient(sensitiveContext);

      // Check all log calls
      const allLogData = loggerInfoSpy.mock.calls.flat().join(' ');

      // Should not contain sensitive data
      expect(allLogData).not.toContain('sk_live_SUPER_SECRET_API_KEY_123456789');
      expect(allLogData).not.toContain('super-secret-api-key');
      expect(allLogData).not.toContain('/v1/secret');

      // Logs should not expose the actual API key value
      expect(allLogData).not.toContain('SUPER_SECRET');
    });

    it('should handle hash collisions securely', () => {
      // Mock a scenario where two different inputs could theoretically
      // produce the same hash (extremely unlikely with SHA-256)
      const context1: InstanceContext = {
        n8nApiUrl: 'https://api1.n8n.cloud',
        n8nApiKey: 'key1',
        instanceId: 'instance1'
      };

      const context2: InstanceContext = {
        n8nApiUrl: 'https://api2.n8n.cloud',
        n8nApiKey: 'key2',
        instanceId: 'instance2'
      };

      mockGetN8nApiConfigFromContext.mockReturnValue({
        baseUrl: 'https://api.n8n.cloud',
        apiKey: 'test-key',
        timeout: 30000,
        maxRetries: 3
      });

      // Even if hashes were identical, different configs would be isolated
      getN8nApiClient(context1);
      getN8nApiClient(context2);

      expect(mockN8nApiClient).toHaveBeenCalledTimes(2);
    });
  });

  describe('Error Message Security', () => {
    it('should not expose sensitive data in validation error messages', () => {
      const sensitiveContext: InstanceContext = {
        n8nApiUrl: 'https://secret-api.example.com/private-endpoint',
        n8nApiKey: 'super-secret-key-123',
        n8nApiTimeout: -1
      };

      const validation = validateInstanceContext(sensitiveContext);

      expect(validation.valid).toBe(false);

      // Error messages should not contain sensitive data
      const errorMessage = validation.errors?.join(' ') || '';
      expect(errorMessage).not.toContain('super-secret-key-123');
      expect(errorMessage).not.toContain('secret-api');
      expect(errorMessage).not.toContain('private-endpoint');
    });

    it('should sanitize error details in API responses', () => {
      const sensitiveContext: InstanceContext = {
        n8nApiUrl: 'invalid-url-with-secrets/api/key=secret123',
        n8nApiKey: 'another-secret-key'
      };

      const validation = validateInstanceContext(sensitiveContext);

      expect(validation.valid).toBe(false);
      expect(validation.errors?.some(error => error.startsWith('Invalid n8nApiUrl:'))).toBe(true);

      // Should not contain the actual invalid URL
      const errorData = JSON.stringify(validation);
      expect(errorData).not.toContain('secret123');
      expect(errorData).not.toContain('another-secret-key');
    });
  });

  describe('Resource Exhaustion Protection', () => {
    it('should handle memory pressure gracefully', () => {
      // Create many large contexts to simulate memory pressure
      const largeData = 'x'.repeat(10000);

      for (let i = 0; i < 100; i++) {
        const context: InstanceContext = {
          n8nApiUrl: 'https://api.n8n.cloud',
          n8nApiKey: `key-${i}`,
          instanceId: `instance-${i}`,
          metadata: {
            largeData: largeData,
            moreData: new Array(1000).fill(largeData)
          }
        };

        expect(() => validateInstanceContext(context)).not.toThrow();
      }
    });

    it('should handle high frequency validation requests', () => {
      const context: InstanceContext = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'frequency-test-key'
      };

      // Rapid fire validation
      for (let i = 0; i < 1000; i++) {
        expect(() => validateInstanceContext(context)).not.toThrow();
      }
    });
  });

  describe('Cryptographic Security', () => {
    it('should use cryptographically secure hash function', () => {
      const context: InstanceContext = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'crypto-test-key',
        instanceId: 'crypto-instance'
      };

      // Generate hash multiple times - should be deterministic
      const hash1 = createHash('sha256')
        .update(`${context.n8nApiUrl}:${context.n8nApiKey}:${context.instanceId}`)
        .digest('hex');

      const hash2 = createHash('sha256')
        .update(`${context.n8nApiUrl}:${context.n8nApiKey}:${context.instanceId}`)
        .digest('hex');

      expect(hash1).toBe(hash2);
      expect(hash1).toHaveLength(64); // SHA-256 produces 64-character hex string
      expect(hash1).toMatch(/^[a-f0-9]{64}$/);
    });

    it('should handle edge cases in hash input', () => {
      const edgeCases = [
        { url: '', key: '', id: '' },
        { url: 'https://api.n8n.cloud', key: '', id: '' },
        { url: '', key: 'key', id: '' },
        { url: '', key: '', id: 'id' },
        { url: undefined, key: undefined, id: undefined }
      ];

      edgeCases.forEach((testCase, index) => {
        expect(() => {
          createHash('sha256')
            .update(`${testCase.url || ''}:${testCase.key || ''}:${testCase.id || ''}`)
            .digest('hex');
        }).not.toThrow();
      });
    });
  });

  describe('Injection Attack Prevention', () => {
    it('should prevent command injection through context fields', () => {
      const commandInjectionContext = {
        n8nApiUrl: 'https://api.n8n.cloud; rm -rf /',
        n8nApiKey: '$(whoami)',
        instanceId: '`cat /etc/passwd`'
      };

      expect(() => validateInstanceContext(commandInjectionContext)).not.toThrow();

      // URL should be invalid
      const validation = validateInstanceContext(commandInjectionContext);
      expect(validation.valid).toBe(false);
    });

    it('should prevent path traversal attempts', () => {
      const pathTraversalContext = {
        n8nApiUrl: 'https://api.n8n.cloud/../../../etc/passwd',
        n8nApiKey: '..\\..\\windows\\system32\\config\\sam',
        instanceId: '../secrets.txt'
      };

      expect(() => validateInstanceContext(pathTraversalContext)).not.toThrow();
    });

    it('should prevent LDAP injection attempts', () => {
      const ldapInjectionContext = {
        n8nApiUrl: 'https://api.n8n.cloud)(|(password=*))',
        n8nApiKey: '*)(uid=*',
        instanceId: '*))(|(cn=*'
      };

      expect(() => validateInstanceContext(ldapInjectionContext)).not.toThrow();
    });
  });

  describe('State Management Security', () => {
    it('should maintain isolation between contexts', () => {
      const context1: InstanceContext = {
        n8nApiUrl: 'https://tenant1.n8n.cloud',
        n8nApiKey: 'tenant1-key',
        instanceId: 'tenant1'
      };

      const context2: InstanceContext = {
        n8nApiUrl: 'https://tenant2.n8n.cloud',
        n8nApiKey: 'tenant2-key',
        instanceId: 'tenant2'
      };

      mockGetN8nApiConfigFromContext
        .mockReturnValueOnce({
          baseUrl: 'https://tenant1.n8n.cloud',
          apiKey: 'tenant1-key',
          timeout: 30000,
          maxRetries: 3
        })
        .mockReturnValueOnce({
          baseUrl: 'https://tenant2.n8n.cloud',
          apiKey: 'tenant2-key',
          timeout: 30000,
          maxRetries: 3
        });

      const client1 = getN8nApiClient(context1);
      const client2 = getN8nApiClient(context2);

      // Should create separate clients
      expect(mockN8nApiClient).toHaveBeenCalledTimes(2);
      expect(client1).not.toBe(client2);
    });

    it('should handle concurrent access securely', async () => {
      const contexts = Array(50).fill(null).map((_, i) => ({
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: `concurrent-key-${i}`,
        instanceId: `concurrent-${i}`
      }));

      mockGetN8nApiConfigFromContext.mockReturnValue({
        baseUrl: 'https://api.n8n.cloud',
        apiKey: 'test-key',
        timeout: 30000,
        maxRetries: 3
      });

      // Simulate concurrent access
      const promises = contexts.map(context =>
        Promise.resolve(getN8nApiClient(context))
      );

      const results = await Promise.all(promises);

      // All should succeed
      results.forEach(result => {
        expect(result).toBeDefined();
      });

      expect(mockN8nApiClient).toHaveBeenCalledTimes(50);
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/unit/services/workflow-validator-expression-format.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { WorkflowValidator } from '../../../src/services/workflow-validator';
import { NodeRepository } from '../../../src/database/node-repository';
import { EnhancedConfigValidator } from '../../../src/services/enhanced-config-validator';

// Mock the database
vi.mock('../../../src/database/node-repository');

describe('WorkflowValidator - Expression Format Validation', () => {
  let validator: WorkflowValidator;
  let mockNodeRepository: any;

  beforeEach(() => {
    // Create mock repository
    mockNodeRepository = {
      findNodeByType: vi.fn().mockImplementation((type: string) => {
        // Return mock nodes for common types
        if (type === 'n8n-nodes-base.emailSend') {
          return {
            node_type: 'n8n-nodes-base.emailSend',
            display_name: 'Email Send',
            properties: {},
            version: 2.1
          };
        }
        if (type === 'n8n-nodes-base.github') {
          return {
            node_type: 'n8n-nodes-base.github',
            display_name: 'GitHub',
            properties: {},
            version: 1.1
          };
        }
        if (type === 'n8n-nodes-base.webhook') {
          return {
            node_type: 'n8n-nodes-base.webhook',
            display_name: 'Webhook',
            properties: {},
            version: 1
          };
        }
        if (type === 'n8n-nodes-base.httpRequest') {
          return {
            node_type: 'n8n-nodes-base.httpRequest',
            display_name: 'HTTP Request',
            properties: {},
            version: 4
          };
        }
        return null;
      }),
      searchNodes: vi.fn().mockReturnValue([]),
      getAllNodes: vi.fn().mockReturnValue([]),
      close: vi.fn()
    };

    validator = new WorkflowValidator(mockNodeRepository, EnhancedConfigValidator);
  });

  describe('Expression Format Detection', () => {
    it('should detect missing = prefix in simple expressions', async () => {
      const workflow = {
        nodes: [
          {
            id: '1',
            name: 'Send Email',
            type: 'n8n-nodes-base.emailSend',
            position: [0, 0] as [number, number],
            parameters: {
              fromEmail: '{{ $env.SENDER_EMAIL }}',
              toEmail: '[email protected]',
              subject: 'Test Email'
            },
            typeVersion: 2.1
          }
        ],
        connections: {}
      };

      const result = await validator.validateWorkflow(workflow);

      expect(result.valid).toBe(false);

      // Find expression format errors
      const formatErrors = result.errors.filter(e => e.message.includes('Expression format error'));
      expect(formatErrors).toHaveLength(1);

      const error = formatErrors[0];
      expect(error.message).toContain('Expression format error');
      expect(error.message).toContain('fromEmail');
      expect(error.message).toContain('{{ $env.SENDER_EMAIL }}');
      expect(error.message).toContain('={{ $env.SENDER_EMAIL }}');
    });

    it('should detect missing resource locator format for GitHub fields', async () => {
      const workflow = {
        nodes: [
          {
            id: '1',
            name: 'GitHub',
            type: 'n8n-nodes-base.github',
            position: [0, 0] as [number, number],
            parameters: {
              operation: 'createComment',
              owner: '{{ $vars.GITHUB_OWNER }}',
              repository: '{{ $vars.GITHUB_REPO }}',
              issueNumber: 123,
              body: 'Test comment'
            },
            typeVersion: 1.1
          }
        ],
        connections: {}
      };

      const result = await validator.validateWorkflow(workflow);

      expect(result.valid).toBe(false);
      // Should have errors for both owner and repository
      const ownerError = result.errors.find(e => e.message.includes('owner'));
      const repoError = result.errors.find(e => e.message.includes('repository'));

      expect(ownerError).toBeTruthy();
      expect(repoError).toBeTruthy();
      expect(ownerError?.message).toContain('resource locator format');
      expect(ownerError?.message).toContain('__rl');
    });

    it('should detect mixed content without prefix', async () => {
      const workflow = {
        nodes: [
          {
            id: '1',
            name: 'HTTP Request',
            type: 'n8n-nodes-base.httpRequest',
            position: [0, 0] as [number, number],
            parameters: {
              url: 'https://api.example.com/{{ $json.endpoint }}',
              headers: {
                Authorization: 'Bearer {{ $env.API_TOKEN }}'
              }
            },
            typeVersion: 4
          }
        ],
        connections: {}
      };

      const result = await validator.validateWorkflow(workflow);

      expect(result.valid).toBe(false);
      const errors = result.errors.filter(e => e.message.includes('Expression format'));
      expect(errors.length).toBeGreaterThan(0);

      // Check for URL error
      const urlError = errors.find(e => e.message.includes('url'));
      expect(urlError).toBeTruthy();
      expect(urlError?.message).toContain('=https://api.example.com/{{ $json.endpoint }}');
    });

    it('should accept properly formatted expressions', async () => {
      const workflow = {
        nodes: [
          {
            id: '1',
            name: 'Send Email',
            type: 'n8n-nodes-base.emailSend',
            position: [0, 0] as [number, number],
            parameters: {
              fromEmail: '={{ $env.SENDER_EMAIL }}',
              toEmail: '[email protected]',
              subject: '=Test {{ $json.type }}'
            },
            typeVersion: 2.1
          }
        ],
        connections: {}
      };

      const result = await validator.validateWorkflow(workflow);

      // Should have no expression format errors
      const formatErrors = result.errors.filter(e => e.message.includes('Expression format'));
      expect(formatErrors).toHaveLength(0);
    });

    it('should accept resource locator format', async () => {
      const workflow = {
        nodes: [
          {
            id: '1',
            name: 'GitHub',
            type: 'n8n-nodes-base.github',
            position: [0, 0] as [number, number],
            parameters: {
              operation: 'createComment',
              owner: {
                __rl: true,
                value: '={{ $vars.GITHUB_OWNER }}',
                mode: 'expression'
              },
              repository: {
                __rl: true,
                value: '={{ $vars.GITHUB_REPO }}',
                mode: 'expression'
              },
              issueNumber: 123,
              body: '=Test comment from {{ $json.author }}'
            },
            typeVersion: 1.1
          }
        ],
        connections: {}
      };

      const result = await validator.validateWorkflow(workflow);

      // Should have no expression format errors
      const formatErrors = result.errors.filter(e => e.message.includes('Expression format'));
      expect(formatErrors).toHaveLength(0);
    });

    it('should validate nested expressions in complex parameters', async () => {
      const workflow = {
        nodes: [
          {
            id: '1',
            name: 'HTTP Request',
            type: 'n8n-nodes-base.httpRequest',
            position: [0, 0] as [number, number],
            parameters: {
              method: 'POST',
              url: 'https://api.example.com',
              sendBody: true,
              bodyParameters: {
                parameters: [
                  {
                    name: 'userId',
                    value: '{{ $json.id }}'
                  },
                  {
                    name: 'timestamp',
                    value: '={{ $now }}'
                  }
                ]
              }
            },
            typeVersion: 4
          }
        ],
        connections: {}
      };

      const result = await validator.validateWorkflow(workflow);

      // Should detect the missing prefix in nested parameter
      const errors = result.errors.filter(e => e.message.includes('Expression format'));
      expect(errors.length).toBeGreaterThan(0);

      const nestedError = errors.find(e => e.message.includes('bodyParameters'));
      expect(nestedError).toBeTruthy();
    });

    it('should warn about RL format even with prefix', async () => {
      const workflow = {
        nodes: [
          {
            id: '1',
            name: 'GitHub',
            type: 'n8n-nodes-base.github',
            position: [0, 0] as [number, number],
            parameters: {
              operation: 'createComment',
              owner: '={{ $vars.GITHUB_OWNER }}',
              repository: '={{ $vars.GITHUB_REPO }}',
              issueNumber: 123,
              body: 'Test'
            },
            typeVersion: 1.1
          }
        ],
        connections: {}
      };

      const result = await validator.validateWorkflow(workflow);

      // Should have warnings about using RL format
      const warnings = result.warnings.filter(w => w.message.includes('resource locator format'));
      expect(warnings.length).toBeGreaterThan(0);
    });
  });

  describe('Real-world workflow examples', () => {
    it('should validate Email workflow with expression issues', async () => {
      const workflow = {
        name: 'Error Notification Workflow',
        nodes: [
          {
            id: 'webhook-1',
            name: 'Webhook',
            type: 'n8n-nodes-base.webhook',
            position: [250, 300] as [number, number],
            parameters: {
              path: 'error-handler',
              httpMethod: 'POST'
            },
            typeVersion: 1
          },
          {
            id: 'email-1',
            name: 'Error Handler',
            type: 'n8n-nodes-base.emailSend',
            position: [450, 300] as [number, number],
            parameters: {
              fromEmail: '{{ $env.ADMIN_EMAIL }}',
              toEmail: '[email protected]',
              subject: 'Error in {{ $json.workflow }}',
              message: 'An error occurred: {{ $json.error }}',
              options: {
                replyTo: '={{ $env.SUPPORT_EMAIL }}'
              }
            },
            typeVersion: 2.1
          }
        ],
        connections: {
          'Webhook': {
            main: [[{ node: 'Error Handler', type: 'main', index: 0 }]]
          }
        }
      };

      const result = await validator.validateWorkflow(workflow);

      // Should have multiple expression format errors
      const formatErrors = result.errors.filter(e => e.message.includes('Expression format'));
      expect(formatErrors.length).toBeGreaterThanOrEqual(3); // fromEmail, subject, message

      // Check specific errors
      const fromEmailError = formatErrors.find(e => e.message.includes('fromEmail'));
      expect(fromEmailError).toBeTruthy();
      expect(fromEmailError?.message).toContain('={{ $env.ADMIN_EMAIL }}');
    });

    it('should validate GitHub workflow with resource locator issues', async () => {
      const workflow = {
        name: 'GitHub Issue Handler',
        nodes: [
          {
            id: 'webhook-1',
            name: 'Issue Webhook',
            type: 'n8n-nodes-base.webhook',
            position: [250, 300] as [number, number],
            parameters: {
              path: 'github-issue',
              httpMethod: 'POST'
            },
            typeVersion: 1
          },
          {
            id: 'github-1',
            name: 'Create Comment',
            type: 'n8n-nodes-base.github',
            position: [450, 300] as [number, number],
            parameters: {
              operation: 'createComment',
              owner: '{{ $vars.GITHUB_OWNER }}',
              repository: '{{ $vars.GITHUB_REPO }}',
              issueNumber: '={{ $json.body.issue.number }}',
              body: 'Thanks for the issue @{{ $json.body.issue.user.login }}!'
            },
            typeVersion: 1.1
          }
        ],
        connections: {
          'Issue Webhook': {
            main: [[{ node: 'Create Comment', type: 'main', index: 0 }]]
          }
        }
      };

      const result = await validator.validateWorkflow(workflow);

      // Should have errors for owner, repository, and body
      const formatErrors = result.errors.filter(e => e.message.includes('Expression format'));
      expect(formatErrors.length).toBeGreaterThanOrEqual(3);

      // Check for resource locator suggestions
      const ownerError = formatErrors.find(e => e.message.includes('owner'));
      expect(ownerError?.message).toContain('__rl');
      expect(ownerError?.message).toContain('resource locator format');
    });

    it('should provide clear fix examples in error messages', async () => {
      const workflow = {
        nodes: [
          {
            id: '1',
            name: 'Process Data',
            type: 'n8n-nodes-base.httpRequest',
            position: [0, 0] as [number, number],
            parameters: {
              url: 'https://api.example.com/users/{{ $json.userId }}'
            },
            typeVersion: 4
          }
        ],
        connections: {}
      };

      const result = await validator.validateWorkflow(workflow);

      const error = result.errors.find(e => e.message.includes('Expression format'));
      expect(error).toBeTruthy();

      // Error message should contain both incorrect and correct examples
      expect(error?.message).toContain('Current (incorrect):');
      expect(error?.message).toContain('"url": "https://api.example.com/users/{{ $json.userId }}"');
      expect(error?.message).toContain('Fixed (correct):');
      expect(error?.message).toContain('"url": "=https://api.example.com/users/{{ $json.userId }}"');
    });
  });

  describe('Integration with other validations', () => {
    it('should validate expression format alongside syntax', async () => {
      const workflow = {
        nodes: [
          {
            id: '1',
            name: 'Test Node',
            type: 'n8n-nodes-base.httpRequest',
            position: [0, 0] as [number, number],
            parameters: {
              url: '{{ $json.url',  // Syntax error: unclosed expression
              headers: {
                'X-Token': '{{ $env.TOKEN }}'  // Format error: missing prefix
              }
            },
            typeVersion: 4
          }
        ],
        connections: {}
      };

      const result = await validator.validateWorkflow(workflow);

      // Should have both syntax and format errors
      const syntaxErrors = result.errors.filter(e => e.message.includes('Unmatched expression brackets'));
      const formatErrors = result.errors.filter(e => e.message.includes('Expression format'));

      expect(syntaxErrors.length).toBeGreaterThan(0);
      expect(formatErrors.length).toBeGreaterThan(0);
    });

    it('should not interfere with node validation', async () => {
      // Test that expression format validation works alongside other validations
      const workflow = {
        nodes: [
          {
            id: '1',
            name: 'HTTP Request',
            type: 'n8n-nodes-base.httpRequest',
            position: [0, 0] as [number, number],
            parameters: {
              url: '{{ $json.endpoint }}',  // Expression format error
              headers: {
                Authorization: '={{ $env.TOKEN }}'  // Correct format
              }
            },
            typeVersion: 4
          }
        ],
        connections: {}
      };

      const result = await validator.validateWorkflow(workflow);

      // Should have expression format error for url field
      const formatErrors = result.errors.filter(e => e.message.includes('Expression format'));
      expect(formatErrors).toHaveLength(1);
      expect(formatErrors[0].message).toContain('url');

      // The workflow should still have structure validation (no trigger warning, etc)
      // This proves that expression validation doesn't interfere with other checks
      expect(result.warnings.some(w => w.message.includes('trigger'))).toBe(true);
    });
  });
});
```

--------------------------------------------------------------------------------
/docs/LIBRARY_USAGE.md:
--------------------------------------------------------------------------------

```markdown
# Library Usage Guide - Multi-Tenant / Hosted Deployments

This guide covers using n8n-mcp as a library dependency for building multi-tenant hosted services.

## Overview

n8n-mcp can be used as a Node.js library to build multi-tenant backends that provide MCP services to multiple users or instances. The package exports all necessary components for integration into your existing services.

## Installation

```bash
npm install n8n-mcp
```

## Core Concepts

### Library Mode vs CLI Mode

- **CLI Mode** (default): Single-player usage via `npx n8n-mcp` or Docker
- **Library Mode**: Multi-tenant usage by importing and using the `N8NMCPEngine` class

### Instance Context

The `InstanceContext` type allows you to pass per-request configuration to the MCP engine:

```typescript
interface InstanceContext {
  // Instance-specific n8n API configuration
  n8nApiUrl?: string;
  n8nApiKey?: string;
  n8nApiTimeout?: number;
  n8nApiMaxRetries?: number;

  // Instance identification
  instanceId?: string;
  sessionId?: string;

  // Extensible metadata
  metadata?: Record<string, any>;
}
```

## Basic Example

```typescript
import express from 'express';
import { N8NMCPEngine } from 'n8n-mcp';

const app = express();
const mcpEngine = new N8NMCPEngine({
  sessionTimeout: 3600000, // 1 hour
  logLevel: 'info'
});

// Handle MCP requests with per-user context
app.post('/mcp', async (req, res) => {
  const instanceContext = {
    n8nApiUrl: req.user.n8nUrl,
    n8nApiKey: req.user.n8nApiKey,
    instanceId: req.user.id
  };

  await mcpEngine.processRequest(req, res, instanceContext);
});

app.listen(3000);
```

## Multi-Tenant Backend Example

This example shows a complete multi-tenant implementation with user authentication and instance management:

```typescript
import express from 'express';
import { N8NMCPEngine, InstanceContext, validateInstanceContext } from 'n8n-mcp';

const app = express();
const mcpEngine = new N8NMCPEngine({
  sessionTimeout: 3600000, // 1 hour
  logLevel: 'info'
});

// Start MCP engine
await mcpEngine.start();

// Authentication middleware
const authenticate = async (req, res, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  // Verify token and attach user to request
  req.user = await getUserFromToken(token);
  next();
};

// Get instance configuration from database
const getInstanceConfig = async (instanceId: string, userId: string) => {
  // Your database logic here
  const instance = await db.instances.findOne({
    where: { id: instanceId, userId }
  });

  if (!instance) {
    throw new Error('Instance not found');
  }

  return {
    n8nApiUrl: instance.n8nUrl,
    n8nApiKey: await decryptApiKey(instance.encryptedApiKey),
    instanceId: instance.id
  };
};

// MCP endpoint with per-instance context
app.post('/api/instances/:instanceId/mcp', authenticate, async (req, res) => {
  try {
    // Get instance configuration
    const instance = await getInstanceConfig(req.params.instanceId, req.user.id);

    // Create instance context
    const context: InstanceContext = {
      n8nApiUrl: instance.n8nApiUrl,
      n8nApiKey: instance.n8nApiKey,
      instanceId: instance.instanceId,
      metadata: {
        userId: req.user.id,
        userAgent: req.headers['user-agent'],
        ip: req.ip
      }
    };

    // Validate context before processing
    const validation = validateInstanceContext(context);
    if (!validation.valid) {
      return res.status(400).json({
        error: 'Invalid instance configuration',
        details: validation.errors
      });
    }

    // Process request with instance context
    await mcpEngine.processRequest(req, res, context);

  } catch (error) {
    console.error('MCP request error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Health endpoint
app.get('/health', async (req, res) => {
  const health = await mcpEngine.healthCheck();
  res.status(health.status === 'healthy' ? 200 : 503).json(health);
});

// Graceful shutdown
process.on('SIGTERM', async () => {
  await mcpEngine.shutdown();
  process.exit(0);
});

app.listen(3000);
```

## API Reference

### N8NMCPEngine

#### Constructor

```typescript
new N8NMCPEngine(options?: {
  sessionTimeout?: number;  // Session TTL in ms (default: 1800000 = 30min)
  logLevel?: 'error' | 'warn' | 'info' | 'debug';  // Default: 'info'
})
```

#### Methods

##### `async processRequest(req, res, context?)`

Process a single MCP request with optional instance context.

**Parameters:**
- `req`: Express request object
- `res`: Express response object
- `context` (optional): InstanceContext with per-instance configuration

**Example:**
```typescript
const context: InstanceContext = {
  n8nApiUrl: 'https://instance1.n8n.cloud',
  n8nApiKey: 'instance1-key',
  instanceId: 'tenant-123'
};

await engine.processRequest(req, res, context);
```

##### `async healthCheck()`

Get engine health status for monitoring.

**Returns:** `EngineHealth`
```typescript
{
  status: 'healthy' | 'unhealthy';
  uptime: number;  // seconds
  sessionActive: boolean;
  memoryUsage: {
    used: number;
    total: number;
    unit: string;
  };
  version: string;
}
```

**Example:**
```typescript
app.get('/health', async (req, res) => {
  const health = await engine.healthCheck();
  res.status(health.status === 'healthy' ? 200 : 503).json(health);
});
```

##### `getSessionInfo()`

Get current session information for debugging.

**Returns:**
```typescript
{
  active: boolean;
  sessionId?: string;
  age?: number;  // milliseconds
  sessions?: {
    total: number;
    active: number;
    expired: number;
    max: number;
    sessionIds: string[];
  };
}
```

##### `async start()`

Start the engine (for standalone mode). Not needed when using `processRequest()` directly.

##### `async shutdown()`

Graceful shutdown for service lifecycle management.

**Example:**
```typescript
process.on('SIGTERM', async () => {
  await engine.shutdown();
  process.exit(0);
});
```

### Types

#### InstanceContext

Configuration for a specific user instance:

```typescript
interface InstanceContext {
  n8nApiUrl?: string;
  n8nApiKey?: string;
  n8nApiTimeout?: number;
  n8nApiMaxRetries?: number;
  instanceId?: string;
  sessionId?: string;
  metadata?: Record<string, any>;
}
```

#### Validation Functions

##### `validateInstanceContext(context: InstanceContext)`

Validate and sanitize instance context.

**Returns:**
```typescript
{
  valid: boolean;
  errors?: string[];
}
```

**Example:**
```typescript
import { validateInstanceContext } from 'n8n-mcp';

const validation = validateInstanceContext(context);
if (!validation.valid) {
  console.error('Invalid context:', validation.errors);
}
```

##### `isInstanceContext(obj: any)`

Type guard to check if an object is a valid InstanceContext.

**Example:**
```typescript
import { isInstanceContext } from 'n8n-mcp';

if (isInstanceContext(req.body.context)) {
  // TypeScript knows this is InstanceContext
  await engine.processRequest(req, res, req.body.context);
}
```

## Session Management

### Session Strategies

The MCP engine supports flexible session ID formats:

- **UUIDv4**: Internal n8n-mcp format (default)
- **Instance-prefixed**: `instance-{userId}-{hash}-{uuid}` for multi-tenant isolation
- **Custom formats**: Any non-empty string for mcp-remote and other proxies

Session validation happens via transport lookup, not format validation. This ensures compatibility with all MCP clients.

### Multi-Tenant Configuration

Set these environment variables for multi-tenant mode:

```bash
# Enable multi-tenant mode
ENABLE_MULTI_TENANT=true

# Session strategy: "instance" (default) or "shared"
MULTI_TENANT_SESSION_STRATEGY=instance
```

**Session Strategies:**

- **instance** (recommended): Each tenant gets isolated sessions
  - Session ID: `instance-{instanceId}-{configHash}-{uuid}`
  - Better isolation and security
  - Easier debugging per tenant

- **shared**: Multiple tenants share sessions with context switching
  - More efficient for high tenant count
  - Requires careful context management

## Security Considerations

### API Key Management

Always encrypt API keys server-side:

```typescript
import { createCipheriv, createDecipheriv } from 'crypto';

// Encrypt before storing
const encryptApiKey = (apiKey: string) => {
  const cipher = createCipheriv('aes-256-gcm', encryptionKey, iv);
  return cipher.update(apiKey, 'utf8', 'hex') + cipher.final('hex');
};

// Decrypt before using
const decryptApiKey = (encrypted: string) => {
  const decipher = createDecipheriv('aes-256-gcm', encryptionKey, iv);
  return decipher.update(encrypted, 'hex', 'utf8') + decipher.final('utf8');
};

// Use decrypted key in context
const context: InstanceContext = {
  n8nApiKey: await decryptApiKey(instance.encryptedApiKey),
  // ...
};
```

### Input Validation

Always validate instance context before processing:

```typescript
import { validateInstanceContext } from 'n8n-mcp';

const validation = validateInstanceContext(context);
if (!validation.valid) {
  throw new Error(`Invalid context: ${validation.errors?.join(', ')}`);
}
```

### Rate Limiting

Implement rate limiting per tenant:

```typescript
import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  keyGenerator: (req) => req.user?.id || req.ip
});

app.post('/api/instances/:instanceId/mcp', authenticate, limiter, async (req, res) => {
  // ...
});
```

## Error Handling

Always wrap MCP requests in try-catch blocks:

```typescript
app.post('/api/instances/:instanceId/mcp', authenticate, async (req, res) => {
  try {
    const context = await getInstanceConfig(req.params.instanceId, req.user.id);
    await mcpEngine.processRequest(req, res, context);
  } catch (error) {
    console.error('MCP error:', error);

    // Don't leak internal errors to clients
    if (error.message.includes('not found')) {
      return res.status(404).json({ error: 'Instance not found' });
    }

    res.status(500).json({ error: 'Internal server error' });
  }
});
```

## Monitoring

### Health Checks

Set up periodic health checks:

```typescript
setInterval(async () => {
  const health = await mcpEngine.healthCheck();

  if (health.status === 'unhealthy') {
    console.error('MCP engine unhealthy:', health);
    // Alert your monitoring system
  }

  // Log metrics
  console.log('MCP engine metrics:', {
    uptime: health.uptime,
    memory: health.memoryUsage,
    sessionActive: health.sessionActive
  });
}, 60000); // Every minute
```

### Session Monitoring

Track active sessions:

```typescript
app.get('/admin/sessions', authenticate, async (req, res) => {
  if (!req.user.isAdmin) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  const sessionInfo = mcpEngine.getSessionInfo();
  res.json(sessionInfo);
});
```

## Testing

### Unit Testing

```typescript
import { N8NMCPEngine, InstanceContext } from 'n8n-mcp';

describe('MCP Engine', () => {
  let engine: N8NMCPEngine;

  beforeEach(() => {
    engine = new N8NMCPEngine({ logLevel: 'error' });
  });

  afterEach(async () => {
    await engine.shutdown();
  });

  it('should process request with context', async () => {
    const context: InstanceContext = {
      n8nApiUrl: 'https://test.n8n.io',
      n8nApiKey: 'test-key',
      instanceId: 'test-instance'
    };

    const mockReq = createMockRequest();
    const mockRes = createMockResponse();

    await engine.processRequest(mockReq, mockRes, context);

    expect(mockRes.status).toBe(200);
  });
});
```

### Integration Testing

```typescript
import request from 'supertest';
import { createApp } from './app';

describe('Multi-tenant MCP API', () => {
  let app;
  let authToken;

  beforeAll(async () => {
    app = await createApp();
    authToken = await getTestAuthToken();
  });

  it('should handle MCP request for instance', async () => {
    const response = await request(app)
      .post('/api/instances/test-instance/mcp')
      .set('Authorization', `Bearer ${authToken}`)
      .send({
        jsonrpc: '2.0',
        method: 'initialize',
        params: {
          protocolVersion: '2024-11-05',
          capabilities: {}
        },
        id: 1
      });

    expect(response.status).toBe(200);
    expect(response.body.result).toBeDefined();
  });
});
```

## Deployment Considerations

### Environment Variables

```bash
# Required for multi-tenant mode
ENABLE_MULTI_TENANT=true
MULTI_TENANT_SESSION_STRATEGY=instance

# Optional: Logging
LOG_LEVEL=info
DISABLE_CONSOLE_OUTPUT=false

# Optional: Session configuration
SESSION_TIMEOUT=1800000  # 30 minutes in milliseconds
MAX_SESSIONS=100

# Optional: Performance
NODE_ENV=production
```

### Docker Deployment

```dockerfile
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

ENV NODE_ENV=production
ENV ENABLE_MULTI_TENANT=true
ENV LOG_LEVEL=info

EXPOSE 3000

CMD ["node", "dist/server.js"]
```

### Kubernetes Deployment

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: n8n-mcp-backend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: n8n-mcp-backend
  template:
    metadata:
      labels:
        app: n8n-mcp-backend
    spec:
      containers:
      - name: backend
        image: your-registry/n8n-mcp-backend:latest
        ports:
        - containerPort: 3000
        env:
        - name: ENABLE_MULTI_TENANT
          value: "true"
        - name: LOG_LEVEL
          value: "info"
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 10
          periodSeconds: 30
        readinessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 10
```

## Examples

### Complete Multi-Tenant SaaS Example

For a complete implementation example, see:
- [n8n-mcp-backend](https://github.com/czlonkowski/n8n-mcp-backend) - Full hosted service implementation

### Migration from Single-Player

If you're migrating from single-player (CLI/Docker) to multi-tenant:

1. **Keep backward compatibility** - Use environment fallback:
```typescript
const context: InstanceContext = {
  n8nApiUrl: instanceUrl || process.env.N8N_API_URL,
  n8nApiKey: instanceKey || process.env.N8N_API_KEY,
  instanceId: instanceId || 'default'
};
```

2. **Gradual rollout** - Start with a feature flag:
```typescript
const isMultiTenant = process.env.ENABLE_MULTI_TENANT === 'true';

if (isMultiTenant) {
  const context = await getInstanceConfig(req.params.instanceId);
  await engine.processRequest(req, res, context);
} else {
  // Legacy single-player mode
  await engine.processRequest(req, res);
}
```

## Troubleshooting

### Common Issues

#### Module Resolution Errors

If you see `Cannot find module 'n8n-mcp'`:

```bash
# Clear node_modules and reinstall
rm -rf node_modules package-lock.json
npm install

# Verify package has types field
npm info n8n-mcp

# Check TypeScript can resolve it
npx tsc --noEmit
```

#### Session ID Validation Errors

If you see `Invalid session ID format` errors:

- Ensure you're using n8n-mcp v2.18.9 or later
- Session IDs can be any non-empty string
- No need to generate UUIDs - use your own format

#### Memory Leaks

If memory usage grows over time:

```typescript
// Ensure proper cleanup
process.on('SIGTERM', async () => {
  await engine.shutdown();
  process.exit(0);
});

// Monitor session count
const sessionInfo = engine.getSessionInfo();
console.log('Active sessions:', sessionInfo.sessions?.active);
```

## Further Reading

- [MCP Protocol Specification](https://modelcontextprotocol.io/docs)
- [n8n API Documentation](https://docs.n8n.io/api/)
- [Express.js Guide](https://expressjs.com/en/guide/routing.html)
- [n8n-mcp Main README](../README.md)

## Support

- **Issues**: [GitHub Issues](https://github.com/czlonkowski/n8n-mcp/issues)
- **Discussions**: [GitHub Discussions](https://github.com/czlonkowski/n8n-mcp/discussions)
- **Security**: For security issues, see [SECURITY.md](../SECURITY.md)

```

--------------------------------------------------------------------------------
/src/utils/node-source-extractor.ts:
--------------------------------------------------------------------------------

```typescript
import * as fs from 'fs/promises';
import * as path from 'path';
import { logger } from './logger';

export interface NodeSourceInfo {
  nodeType: string;
  sourceCode: string;
  credentialCode?: string;
  packageInfo?: any;
  location: string;
}

export class NodeSourceExtractor {
  private n8nBasePaths = [
    '/usr/local/lib/node_modules/n8n/node_modules',
    '/app/node_modules',
    '/home/node/.n8n/custom/nodes',
    './node_modules',
    // Docker volume paths
    '/var/lib/docker/volumes/n8n-mcp_n8n_modules/_data',
    '/n8n-modules',
    // Common n8n installation paths
    process.env.N8N_CUSTOM_EXTENSIONS || '',
    // Additional local path for testing
    path.join(process.cwd(), 'node_modules'),
  ].filter(Boolean);

  /**
   * Extract source code for a specific n8n node
   */
  async extractNodeSource(nodeType: string): Promise<NodeSourceInfo> {
    logger.info(`Extracting source code for node: ${nodeType}`);
    
    // Parse node type to get package and node name
    const { packageName, nodeName } = this.parseNodeType(nodeType);
    
    // Search for the node in known locations
    for (const basePath of this.n8nBasePaths) {
      try {
        const nodeInfo = await this.searchNodeInPath(basePath, packageName, nodeName);
        if (nodeInfo) {
          logger.info(`Found node source at: ${nodeInfo.location}`);
          return nodeInfo;
        }
      } catch (error) {
        logger.debug(`Failed to search in ${basePath}: ${error}`);
      }
    }
    
    throw new Error(`Node source code not found for: ${nodeType}`);
  }

  /**
   * Parse node type identifier
   */
  private parseNodeType(nodeType: string): { packageName: string; nodeName: string } {
    // Handle different formats:
    // - @n8n/n8n-nodes-langchain.Agent
    // - n8n-nodes-base.HttpRequest
    // - customNode
    
    if (nodeType.includes('.')) {
      const [pkg, node] = nodeType.split('.');
      return { packageName: pkg, nodeName: node };
    }
    
    // Default to n8n-nodes-base for simple node names
    return { packageName: 'n8n-nodes-base', nodeName: nodeType };
  }

  /**
   * Search for node in a specific path
   */
  private async searchNodeInPath(
    basePath: string,
    packageName: string,
    nodeName: string
  ): Promise<NodeSourceInfo | null> {
    try {
      // Try both the provided case and capitalized first letter
      const nodeNameVariants = [
        nodeName,
        nodeName.charAt(0).toUpperCase() + nodeName.slice(1), // Capitalize first letter
        nodeName.toLowerCase(), // All lowercase
        nodeName.toUpperCase(), // All uppercase
      ];
      
      // First, try standard patterns with all case variants
      for (const nameVariant of nodeNameVariants) {
        const standardPatterns = [
          `${packageName}/dist/nodes/${nameVariant}/${nameVariant}.node.js`,
          `${packageName}/dist/nodes/${nameVariant}.node.js`,
          `${packageName}/nodes/${nameVariant}/${nameVariant}.node.js`,
          `${packageName}/nodes/${nameVariant}.node.js`,
          `${nameVariant}/${nameVariant}.node.js`,
          `${nameVariant}.node.js`,
        ];

        // Additional patterns for nested node structures (e.g., agents/Agent)
        const nestedPatterns = [
          `${packageName}/dist/nodes/*/${nameVariant}/${nameVariant}.node.js`,
          `${packageName}/dist/nodes/**/${nameVariant}/${nameVariant}.node.js`,
          `${packageName}/nodes/*/${nameVariant}/${nameVariant}.node.js`,
          `${packageName}/nodes/**/${nameVariant}/${nameVariant}.node.js`,
        ];

        // Try standard patterns first
        for (const pattern of standardPatterns) {
          const fullPath = path.join(basePath, pattern);
          const result = await this.tryLoadNodeFile(fullPath, packageName, nodeName, basePath);
          if (result) return result;
        }

        // Try nested patterns (with glob-like search)
        for (const pattern of nestedPatterns) {
          const result = await this.searchWithGlobPattern(basePath, pattern, packageName, nodeName);
          if (result) return result;
        }
      }

      // If basePath contains .pnpm, search in pnpm structure
      if (basePath.includes('node_modules')) {
        const pnpmPath = path.join(basePath, '.pnpm');
        try {
          await fs.access(pnpmPath);
          const result = await this.searchInPnpm(pnpmPath, packageName, nodeName);
          if (result) return result;
        } catch {
          // .pnpm directory doesn't exist
        }
      }
    } catch (error) {
      logger.debug(`Error searching in path ${basePath}: ${error}`);
    }

    return null;
  }

  /**
   * Search for nodes in pnpm's special directory structure
   */
  private async searchInPnpm(
    pnpmPath: string,
    packageName: string,
    nodeName: string
  ): Promise<NodeSourceInfo | null> {
    try {
      const entries = await fs.readdir(pnpmPath);
      
      // Filter entries that might contain our package
      const packageEntries = entries.filter(entry => 
        entry.includes(packageName.replace('/', '+')) || 
        entry.includes(packageName)
      );

      for (const entry of packageEntries) {
        const entryPath = path.join(pnpmPath, entry, 'node_modules', packageName);
        
        // Search patterns within the pnpm package directory
        const patterns = [
          `dist/nodes/${nodeName}/${nodeName}.node.js`,
          `dist/nodes/${nodeName}.node.js`,
          `dist/nodes/*/${nodeName}/${nodeName}.node.js`,
          `dist/nodes/**/${nodeName}/${nodeName}.node.js`,
        ];

        for (const pattern of patterns) {
          if (pattern.includes('*')) {
            const result = await this.searchWithGlobPattern(entryPath, pattern, packageName, nodeName);
            if (result) return result;
          } else {
            const fullPath = path.join(entryPath, pattern);
            const result = await this.tryLoadNodeFile(fullPath, packageName, nodeName, entryPath);
            if (result) return result;
          }
        }
      }
    } catch (error) {
      logger.debug(`Error searching in pnpm directory: ${error}`);
    }

    return null;
  }

  /**
   * Search for files matching a glob-like pattern
   */
  private async searchWithGlobPattern(
    basePath: string,
    pattern: string,
    packageName: string,
    nodeName: string
  ): Promise<NodeSourceInfo | null> {
    // Convert glob pattern to regex parts
    const parts = pattern.split('/');
    const targetFile = `${nodeName}.node.js`;
    
    async function searchDir(currentPath: string, remainingParts: string[]): Promise<string | null> {
      if (remainingParts.length === 0) return null;
      
      const part = remainingParts[0];
      const isLastPart = remainingParts.length === 1;
      
      try {
        if (isLastPart && part === targetFile) {
          // Check if file exists
          const fullPath = path.join(currentPath, part);
          await fs.access(fullPath);
          return fullPath;
        }
        
        const entries = await fs.readdir(currentPath, { withFileTypes: true });
        
        for (const entry of entries) {
          if (!entry.isDirectory() && !isLastPart) continue;
          
          if (part === '*' || part === '**') {
            // Match any directory
            if (entry.isDirectory()) {
              const result = await searchDir(
                path.join(currentPath, entry.name),
                part === '**' ? remainingParts : remainingParts.slice(1)
              );
              if (result) return result;
            }
          } else if (entry.name === part || (isLastPart && entry.name === targetFile)) {
            if (isLastPart && entry.isFile()) {
              return path.join(currentPath, entry.name);
            } else if (!isLastPart && entry.isDirectory()) {
              const result = await searchDir(
                path.join(currentPath, entry.name),
                remainingParts.slice(1)
              );
              if (result) return result;
            }
          }
        }
      } catch {
        // Directory doesn't exist or can't be read
      }
      
      return null;
    }
    
    const foundPath = await searchDir(basePath, parts);
    if (foundPath) {
      return this.tryLoadNodeFile(foundPath, packageName, nodeName, basePath);
    }
    
    return null;
  }

  /**
   * Try to load a node file and its associated files
   */
  private async tryLoadNodeFile(
    fullPath: string,
    packageName: string,
    nodeName: string,
    packageBasePath: string
  ): Promise<NodeSourceInfo | null> {
    try {
      const sourceCode = await fs.readFile(fullPath, 'utf-8');
      
      // Try to find credential files
      let credentialCode: string | undefined;
      
      // First, try alongside the node file
      const credentialPath = fullPath.replace('.node.js', '.credentials.js');
      try {
        credentialCode = await fs.readFile(credentialPath, 'utf-8');
      } catch {
        // Try in the credentials directory
        const possibleCredentialPaths = [
          // Standard n8n structure: dist/credentials/NodeNameApi.credentials.js
          path.join(packageBasePath, packageName, 'dist/credentials', `${nodeName}Api.credentials.js`),
          path.join(packageBasePath, packageName, 'dist/credentials', `${nodeName}OAuth2Api.credentials.js`),
          path.join(packageBasePath, packageName, 'credentials', `${nodeName}Api.credentials.js`),
          path.join(packageBasePath, packageName, 'credentials', `${nodeName}OAuth2Api.credentials.js`),
          // Without packageName in path
          path.join(packageBasePath, 'dist/credentials', `${nodeName}Api.credentials.js`),
          path.join(packageBasePath, 'dist/credentials', `${nodeName}OAuth2Api.credentials.js`),
          path.join(packageBasePath, 'credentials', `${nodeName}Api.credentials.js`),
          path.join(packageBasePath, 'credentials', `${nodeName}OAuth2Api.credentials.js`),
          // Try relative to node location
          path.join(path.dirname(path.dirname(fullPath)), 'credentials', `${nodeName}Api.credentials.js`),
          path.join(path.dirname(path.dirname(fullPath)), 'credentials', `${nodeName}OAuth2Api.credentials.js`),
          path.join(path.dirname(path.dirname(path.dirname(fullPath))), 'credentials', `${nodeName}Api.credentials.js`),
          path.join(path.dirname(path.dirname(path.dirname(fullPath))), 'credentials', `${nodeName}OAuth2Api.credentials.js`),
        ];
        
        // Try to find any credential file
        const allCredentials: string[] = [];
        for (const credPath of possibleCredentialPaths) {
          try {
            const content = await fs.readFile(credPath, 'utf-8');
            allCredentials.push(content);
            logger.debug(`Found credential file at: ${credPath}`);
          } catch {
            // Continue searching
          }
        }
        
        // If we found credentials, combine them
        if (allCredentials.length > 0) {
          credentialCode = allCredentials.join('\n\n// --- Next Credential File ---\n\n');
        }
      }

      // Try to get package.json info
      let packageInfo: any;
      const possiblePackageJsonPaths = [
        path.join(packageBasePath, 'package.json'),
        path.join(packageBasePath, packageName, 'package.json'),
        path.join(path.dirname(path.dirname(fullPath)), 'package.json'),
        path.join(path.dirname(path.dirname(path.dirname(fullPath))), 'package.json'),
        // Try to go up from the node location to find package.json
        path.join(fullPath.split('/dist/')[0], 'package.json'),
        path.join(fullPath.split('/nodes/')[0], 'package.json'),
      ];

      for (const packageJsonPath of possiblePackageJsonPaths) {
        try {
          const packageJson = await fs.readFile(packageJsonPath, 'utf-8');
          packageInfo = JSON.parse(packageJson);
          logger.debug(`Found package.json at: ${packageJsonPath}`);
          break;
        } catch {
          // Try next path
        }
      }

      return {
        nodeType: `${packageName}.${nodeName}`,
        sourceCode,
        credentialCode,
        packageInfo,
        location: fullPath,
      };
    } catch {
      return null;
    }
  }

  /**
   * List all available nodes
   */
  async listAvailableNodes(category?: string, search?: string): Promise<any[]> {
    const nodes: any[] = [];
    const seenNodes = new Set<string>(); // Track unique nodes
    
    for (const basePath of this.n8nBasePaths) {
      try {
        // Check for n8n-nodes-base specifically
        const n8nNodesBasePath = path.join(basePath, 'n8n-nodes-base', 'dist', 'nodes');
        try {
          await fs.access(n8nNodesBasePath);
          await this.scanDirectoryForNodes(n8nNodesBasePath, nodes, category, search, seenNodes);
        } catch {
          // Try without dist
          const altPath = path.join(basePath, 'n8n-nodes-base', 'nodes');
          try {
            await fs.access(altPath);
            await this.scanDirectoryForNodes(altPath, nodes, category, search, seenNodes);
          } catch {
            // Try the base path directly
            await this.scanDirectoryForNodes(basePath, nodes, category, search, seenNodes);
          }
        }
      } catch (error) {
        logger.debug(`Failed to scan ${basePath}: ${error}`);
      }
    }

    return nodes;
  }

  /**
   * Scan directory for n8n nodes
   */
  private async scanDirectoryForNodes(
    dirPath: string,
    nodes: any[],
    category?: string,
    search?: string,
    seenNodes?: Set<string>
  ): Promise<void> {
    try {
      const entries = await fs.readdir(dirPath, { withFileTypes: true });
      
      for (const entry of entries) {
        if (entry.isFile() && entry.name.endsWith('.node.js')) {
          try {
            const fullPath = path.join(dirPath, entry.name);
            const content = await fs.readFile(fullPath, 'utf-8');
            
            // Extract basic info from the source
            const nameMatch = content.match(/displayName:\s*['"`]([^'"`]+)['"`]/);
            const descriptionMatch = content.match(/description:\s*['"`]([^'"`]+)['"`]/);
            
            if (nameMatch) {
              const nodeName = entry.name.replace('.node.js', '');
              
              // Skip if we've already seen this node
              if (seenNodes && seenNodes.has(nodeName)) {
                continue;
              }
              
              const nodeInfo = {
                name: nodeName,
                displayName: nameMatch[1],
                description: descriptionMatch ? descriptionMatch[1] : '',
                location: fullPath,
              };

              // Apply filters
              if (category && !nodeInfo.displayName.toLowerCase().includes(category.toLowerCase())) {
                continue;
              }
              if (search && !nodeInfo.displayName.toLowerCase().includes(search.toLowerCase()) &&
                  !nodeInfo.description.toLowerCase().includes(search.toLowerCase())) {
                continue;
              }

              nodes.push(nodeInfo);
              if (seenNodes) {
                seenNodes.add(nodeName);
              }
            }
          } catch {
            // Skip files we can't read
          }
        } else if (entry.isDirectory()) {
          // Special handling for .pnpm directories
          if (entry.name === '.pnpm') {
            await this.scanPnpmDirectory(path.join(dirPath, entry.name), nodes, category, search, seenNodes);
          } else if (entry.name !== 'node_modules') {
            // Recursively scan subdirectories
            await this.scanDirectoryForNodes(path.join(dirPath, entry.name), nodes, category, search, seenNodes);
          }
        }
      }
    } catch (error) {
      logger.debug(`Error scanning directory ${dirPath}: ${error}`);
    }
  }

  /**
   * Scan pnpm directory structure for nodes
   */
  private async scanPnpmDirectory(
    pnpmPath: string,
    nodes: any[],
    category?: string,
    search?: string,
    seenNodes?: Set<string>
  ): Promise<void> {
    try {
      const entries = await fs.readdir(pnpmPath);
      
      for (const entry of entries) {
        const entryPath = path.join(pnpmPath, entry, 'node_modules');
        try {
          await fs.access(entryPath);
          await this.scanDirectoryForNodes(entryPath, nodes, category, search, seenNodes);
        } catch {
          // Skip if node_modules doesn't exist
        }
      }
    } catch (error) {
      logger.debug(`Error scanning pnpm directory ${pnpmPath}: ${error}`);
    }
  }

  /**
   * Extract AI Agent node specifically
   */
  async extractAIAgentNode(): Promise<NodeSourceInfo> {
    // AI Agent is typically in @n8n/n8n-nodes-langchain package
    return this.extractNodeSource('@n8n/n8n-nodes-langchain.Agent');
  }
}
```

--------------------------------------------------------------------------------
/tests/unit/__mocks__/n8n-nodes-base.ts:
--------------------------------------------------------------------------------

```typescript
import { vi } from 'vitest';

// Mock types that match n8n-workflow
interface INodeExecutionData {
  json: any;
  binary?: any;
  pairedItem?: any;
}

interface IExecuteFunctions {
  getInputData(): INodeExecutionData[];
  getNodeParameter(parameterName: string, itemIndex: number, fallbackValue?: any): any;
  getCredentials(type: string): Promise<any>;
  helpers: {
    returnJsonArray(data: any): INodeExecutionData[];
    httpRequest(options: any): Promise<any>;
    webhook(): any;
  };
}

interface IWebhookFunctions {
  getWebhookName(): string;
  getBodyData(): any;
  getHeaderData(): any;
  getQueryData(): any;
  getRequestObject(): any;
  getResponseObject(): any;
  helpers: {
    returnJsonArray(data: any): INodeExecutionData[];
  };
}

interface INodeTypeDescription {
  displayName: string;
  name: string;
  group: string[];
  version: number;
  description: string;
  defaults: { name: string };
  inputs: string[];
  outputs: string[];
  credentials?: any[];
  webhooks?: any[];
  properties: any[];
  icon?: string;
  subtitle?: string;
}

interface INodeType {
  description: INodeTypeDescription;
  execute?(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
  webhook?(this: IWebhookFunctions): Promise<any>;
  trigger?(this: any): Promise<void>;
  poll?(this: any): Promise<INodeExecutionData[][] | null>;
}

// Base mock node implementation
class BaseMockNode implements INodeType {
  description: INodeTypeDescription;
  execute: any;
  webhook: any;
  
  constructor(description: INodeTypeDescription, execute?: any, webhook?: any) {
    this.description = description;
    this.execute = execute ? vi.fn(execute) : undefined;
    this.webhook = webhook ? vi.fn(webhook) : undefined;
  }
}

// Mock implementations for each node type
const mockWebhookNode = new BaseMockNode(
  {
    displayName: 'Webhook',
    name: 'webhook',
    group: ['trigger'],
    version: 1,
    description: 'Starts the workflow when a webhook is called',
    defaults: { name: 'Webhook' },
    inputs: [],
    outputs: ['main'],
    webhooks: [
      {
        name: 'default',
        httpMethod: '={{$parameter["httpMethod"]}}',
        path: '={{$parameter["path"]}}',
        responseMode: '={{$parameter["responseMode"]}}',
      }
    ],
    properties: [
      {
        displayName: 'Path',
        name: 'path',
        type: 'string',
        default: 'webhook',
        required: true,
        description: 'The path to listen on',
      },
      {
        displayName: 'HTTP Method',
        name: 'httpMethod',
        type: 'options',
        default: 'GET',
        options: [
          { name: 'GET', value: 'GET' },
          { name: 'POST', value: 'POST' },
          { name: 'PUT', value: 'PUT' },
          { name: 'DELETE', value: 'DELETE' },
          { name: 'HEAD', value: 'HEAD' },
          { name: 'PATCH', value: 'PATCH' },
        ],
      },
      {
        displayName: 'Response Mode',
        name: 'responseMode',
        type: 'options',
        default: 'onReceived',
        options: [
          { name: 'On Received', value: 'onReceived' },
          { name: 'Last Node', value: 'lastNode' },
        ],
      },
    ],
  },
  undefined,
  async function webhook(this: IWebhookFunctions) {
    const returnData: INodeExecutionData[] = [];
    returnData.push({
      json: {
        headers: this.getHeaderData(),
        params: this.getQueryData(),
        body: this.getBodyData(),
      }
    });
    return {
      workflowData: [returnData],
    };
  }
);

const mockHttpRequestNode = new BaseMockNode(
  {
    displayName: 'HTTP Request',
    name: 'httpRequest',
    group: ['transform'],
    version: 3,
    description: 'Makes an HTTP request and returns the response',
    defaults: { name: 'HTTP Request' },
    inputs: ['main'],
    outputs: ['main'],
    properties: [
      {
        displayName: 'Method',
        name: 'method',
        type: 'options',
        default: 'GET',
        options: [
          { name: 'GET', value: 'GET' },
          { name: 'POST', value: 'POST' },
          { name: 'PUT', value: 'PUT' },
          { name: 'DELETE', value: 'DELETE' },
          { name: 'HEAD', value: 'HEAD' },
          { name: 'PATCH', value: 'PATCH' },
        ],
      },
      {
        displayName: 'URL',
        name: 'url',
        type: 'string',
        default: '',
        required: true,
        placeholder: 'https://example.com',
      },
      {
        displayName: 'Authentication',
        name: 'authentication',
        type: 'options',
        default: 'none',
        options: [
          { name: 'None', value: 'none' },
          { name: 'Basic Auth', value: 'basicAuth' },
          { name: 'Digest Auth', value: 'digestAuth' },
          { name: 'Header Auth', value: 'headerAuth' },
          { name: 'OAuth1', value: 'oAuth1' },
          { name: 'OAuth2', value: 'oAuth2' },
        ],
      },
      {
        displayName: 'Response Format',
        name: 'responseFormat',
        type: 'options',
        default: 'json',
        options: [
          { name: 'JSON', value: 'json' },
          { name: 'String', value: 'string' },
          { name: 'File', value: 'file' },
        ],
      },
      {
        displayName: 'Options',
        name: 'options',
        type: 'collection',
        placeholder: 'Add Option',
        default: {},
        options: [
          {
            displayName: 'Body Content Type',
            name: 'bodyContentType',
            type: 'options',
            default: 'json',
            options: [
              { name: 'JSON', value: 'json' },
              { name: 'Form Data', value: 'formData' },
              { name: 'Form URL Encoded', value: 'form-urlencoded' },
              { name: 'Raw', value: 'raw' },
            ],
          },
          {
            displayName: 'Headers',
            name: 'headers',
            type: 'fixedCollection',
            default: {},
            typeOptions: {
              multipleValues: true,
            },
          },
          {
            displayName: 'Query Parameters',
            name: 'queryParameters',
            type: 'fixedCollection',
            default: {},
            typeOptions: {
              multipleValues: true,
            },
          },
        ],
      },
    ],
  },
  async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
    const items = this.getInputData();
    const returnData: INodeExecutionData[] = [];
    
    for (let i = 0; i < items.length; i++) {
      const method = this.getNodeParameter('method', i) as string;
      const url = this.getNodeParameter('url', i) as string;
      
      // Mock response
      const response = {
        statusCode: 200,
        headers: {},
        body: { success: true, method, url },
      };
      
      returnData.push({
        json: response,
      });
    }
    
    return [returnData];
  }
);

const mockSlackNode = new BaseMockNode(
  {
    displayName: 'Slack',
    name: 'slack',
    group: ['output'],
    version: 2,
    description: 'Send messages to Slack',
    defaults: { name: 'Slack' },
    inputs: ['main'],
    outputs: ['main'],
    credentials: [
      {
        name: 'slackApi',
        required: true,
      },
    ],
    properties: [
      {
        displayName: 'Resource',
        name: 'resource',
        type: 'options',
        default: 'message',
        options: [
          { name: 'Channel', value: 'channel' },
          { name: 'Message', value: 'message' },
          { name: 'User', value: 'user' },
          { name: 'File', value: 'file' },
        ],
      },
      {
        displayName: 'Operation',
        name: 'operation',
        type: 'options',
        displayOptions: {
          show: {
            resource: ['message'],
          },
        },
        default: 'post',
        options: [
          { name: 'Post', value: 'post' },
          { name: 'Update', value: 'update' },
          { name: 'Delete', value: 'delete' },
        ],
      },
      {
        displayName: 'Channel',
        name: 'channel',
        type: 'options',
        typeOptions: {
          loadOptionsMethod: 'getChannels',
        },
        displayOptions: {
          show: {
            resource: ['message'],
            operation: ['post'],
          },
        },
        default: '',
        required: true,
      },
      {
        displayName: 'Text',
        name: 'text',
        type: 'string',
        typeOptions: {
          alwaysOpenEditWindow: true,
        },
        displayOptions: {
          show: {
            resource: ['message'],
            operation: ['post'],
          },
        },
        default: '',
        required: true,
      },
    ],
  },
  async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
    const items = this.getInputData();
    const returnData: INodeExecutionData[] = [];
    
    for (let i = 0; i < items.length; i++) {
      const resource = this.getNodeParameter('resource', i) as string;
      const operation = this.getNodeParameter('operation', i) as string;
      
      // Mock response
      const response = {
        ok: true,
        channel: this.getNodeParameter('channel', i, '') as string,
        ts: Date.now().toString(),
        message: {
          text: this.getNodeParameter('text', i, '') as string,
        },
      };
      
      returnData.push({
        json: response,
      });
    }
    
    return [returnData];
  }
);

const mockFunctionNode = new BaseMockNode(
  {
    displayName: 'Function',
    name: 'function',
    group: ['transform'],
    version: 1,
    description: 'Execute custom JavaScript code',
    defaults: { name: 'Function' },
    inputs: ['main'],
    outputs: ['main'],
    properties: [
      {
        displayName: 'JavaScript Code',
        name: 'functionCode',
        type: 'string',
        typeOptions: {
          alwaysOpenEditWindow: true,
          codeAutocomplete: 'function',
          editor: 'code',
          rows: 10,
        },
        default: 'return items;',
        description: 'JavaScript code to execute',
      },
    ],
  },
  async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
    const items = this.getInputData();
    const functionCode = this.getNodeParameter('functionCode', 0) as string;
    
    // Simple mock - just return items
    return [items];
  }
);

const mockNoOpNode = new BaseMockNode(
  {
    displayName: 'No Operation',
    name: 'noOp',
    group: ['utility'],
    version: 1,
    description: 'Does nothing',
    defaults: { name: 'No Op' },
    inputs: ['main'],
    outputs: ['main'],
    properties: [],
  },
  async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
    return [this.getInputData()];
  }
);

const mockMergeNode = new BaseMockNode(
  {
    displayName: 'Merge',
    name: 'merge',
    group: ['transform'],
    version: 2,
    description: 'Merge multiple data streams',
    defaults: { name: 'Merge' },
    inputs: ['main', 'main'],
    outputs: ['main'],
    properties: [
      {
        displayName: 'Mode',
        name: 'mode',
        type: 'options',
        default: 'append',
        options: [
          { name: 'Append', value: 'append' },
          { name: 'Merge By Index', value: 'mergeByIndex' },
          { name: 'Merge By Key', value: 'mergeByKey' },
          { name: 'Multiplex', value: 'multiplex' },
        ],
      },
    ],
  },
  async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
    const mode = this.getNodeParameter('mode', 0) as string;
    
    // Mock merge - just return first input
    return [this.getInputData()];
  }
);

const mockIfNode = new BaseMockNode(
  {
    displayName: 'IF',
    name: 'if',
    group: ['transform'],
    version: 1,
    description: 'Conditional logic',
    defaults: { name: 'IF' },
    inputs: ['main'],
    outputs: ['main', 'main'],
    // outputNames: ['true', 'false'], // Not a valid property in INodeTypeDescription
    properties: [
      {
        displayName: 'Conditions',
        name: 'conditions',
        type: 'fixedCollection',
        typeOptions: {
          multipleValues: true,
        },
        default: {},
        options: [
          {
            name: 'string',
            displayName: 'String',
            values: [
              {
                displayName: 'Value 1',
                name: 'value1',
                type: 'string',
                default: '',
              },
              {
                displayName: 'Operation',
                name: 'operation',
                type: 'options',
                default: 'equals',
                options: [
                  { name: 'Equals', value: 'equals' },
                  { name: 'Not Equals', value: 'notEquals' },
                  { name: 'Contains', value: 'contains' },
                  { name: 'Not Contains', value: 'notContains' },
                ],
              },
              {
                displayName: 'Value 2',
                name: 'value2',
                type: 'string',
                default: '',
              },
            ],
          },
        ],
      },
    ],
  },
  async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
    const items = this.getInputData();
    const trueItems: INodeExecutionData[] = [];
    const falseItems: INodeExecutionData[] = [];
    
    // Mock condition - split 50/50
    items.forEach((item, index) => {
      if (index % 2 === 0) {
        trueItems.push(item);
      } else {
        falseItems.push(item);
      }
    });
    
    return [trueItems, falseItems];
  }
);

const mockSwitchNode = new BaseMockNode(
  {
    displayName: 'Switch',
    name: 'switch',
    group: ['transform'],
    version: 1,
    description: 'Route items based on conditions',
    defaults: { name: 'Switch' },
    inputs: ['main'],
    outputs: ['main', 'main', 'main', 'main'],
    properties: [
      {
        displayName: 'Mode',
        name: 'mode',
        type: 'options',
        default: 'expression',
        options: [
          { name: 'Expression', value: 'expression' },
          { name: 'Rules', value: 'rules' },
        ],
      },
      {
        displayName: 'Output',
        name: 'output',
        type: 'options',
        displayOptions: {
          show: {
            mode: ['expression'],
          },
        },
        default: 'all',
        options: [
          { name: 'All', value: 'all' },
          { name: 'First Match', value: 'firstMatch' },
        ],
      },
    ],
  },
  async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
    const items = this.getInputData();
    
    // Mock routing - distribute evenly across outputs
    const outputs: INodeExecutionData[][] = [[], [], [], []];
    items.forEach((item, index) => {
      outputs[index % 4].push(item);
    });
    
    return outputs;
  }
);

// Node registry
const nodeRegistry = new Map<string, INodeType>([
  ['webhook', mockWebhookNode],
  ['httpRequest', mockHttpRequestNode],
  ['slack', mockSlackNode],
  ['function', mockFunctionNode],
  ['noOp', mockNoOpNode],
  ['merge', mockMergeNode],
  ['if', mockIfNode],
  ['switch', mockSwitchNode],
]);

// Export mock functions
export const getNodeTypes = vi.fn(() => ({
  getByName: vi.fn((name: string) => nodeRegistry.get(name)),
  getByNameAndVersion: vi.fn((name: string, version: number) => nodeRegistry.get(name)),
}));

// Export individual node classes for direct import
export const Webhook = mockWebhookNode;
export const HttpRequest = mockHttpRequestNode;
export const Slack = mockSlackNode;
export const Function = mockFunctionNode;
export const NoOp = mockNoOpNode;
export const Merge = mockMergeNode;
export const If = mockIfNode;
export const Switch = mockSwitchNode;

// Test utility to override node behavior
export const mockNodeBehavior = (nodeName: string, overrides: Partial<INodeType>) => {
  const existingNode = nodeRegistry.get(nodeName);
  if (!existingNode) {
    throw new Error(`Node ${nodeName} not found in registry`);
  }
  
  const updatedNode = new BaseMockNode(
    { ...existingNode.description, ...overrides.description },
    overrides.execute || existingNode.execute,
    overrides.webhook || existingNode.webhook
  );
  
  nodeRegistry.set(nodeName, updatedNode);
  return updatedNode;
};

// Test utility to reset all mocks
export const resetAllMocks = () => {
  getNodeTypes.mockClear();
  nodeRegistry.forEach((node) => {
    if (node.execute && vi.isMockFunction(node.execute)) {
      node.execute.mockClear();
    }
    if (node.webhook && vi.isMockFunction(node.webhook)) {
      node.webhook.mockClear();
    }
  });
};

// Test utility to add custom nodes
export const registerMockNode = (name: string, node: INodeType) => {
  nodeRegistry.set(name, node);
};

// Export default for require() compatibility
export default {
  getNodeTypes,
  Webhook,
  HttpRequest,
  Slack,
  Function,
  NoOp,
  Merge,
  If,
  Switch,
  mockNodeBehavior,
  resetAllMocks,
  registerMockNode,
};
```

--------------------------------------------------------------------------------
/tests/integration/mcp-protocol/workflow-error-validation.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { TestableN8NMCPServer } from './test-helpers';

describe('MCP Workflow Error Output Validation Integration', () => {
  let mcpServer: TestableN8NMCPServer;
  let client: Client;

  beforeEach(async () => {
    mcpServer = new TestableN8NMCPServer();
    await mcpServer.initialize();

    const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
    await mcpServer.connectToTransport(serverTransport);

    client = new Client({
      name: 'test-client',
      version: '1.0.0'
    }, {
      capabilities: {}
    });

    await client.connect(clientTransport);
  });

  afterEach(async () => {
    await client.close();
    await mcpServer.close();
  });

  describe('validate_workflow tool - Error Output Configuration', () => {
    it('should detect incorrect error output configuration via MCP', async () => {
      const workflow = {
        nodes: [
          {
            id: '1',
            name: 'Validate Input',
            type: 'n8n-nodes-base.set',
            typeVersion: 3.4,
            position: [-400, 64],
            parameters: {}
          },
          {
            id: '2',
            name: 'Filter URLs',
            type: 'n8n-nodes-base.filter',
            typeVersion: 2.2,
            position: [-176, 64],
            parameters: {}
          },
          {
            id: '3',
            name: 'Error Response1',
            type: 'n8n-nodes-base.respondToWebhook',
            typeVersion: 1.5,
            position: [-160, 240],
            parameters: {}
          }
        ],
        connections: {
          'Validate Input': {
            main: [
              [
                { node: 'Filter URLs', type: 'main', index: 0 },
                { node: 'Error Response1', type: 'main', index: 0 }  // WRONG! Both in main[0]
              ]
            ]
          }
        }
      };

      const response = await client.callTool({
        name: 'validate_workflow',
        arguments: { workflow }
      });

      expect((response as any).content).toHaveLength(1);
      expect((response as any).content[0].type).toBe('text');

      const result = JSON.parse(((response as any).content[0]).text);

      expect(result.valid).toBe(false);
      expect(Array.isArray(result.errors)).toBe(true);

      // Check for the specific error message about incorrect configuration
      const hasIncorrectConfigError = result.errors.some((e: any) =>
        e.message.includes('Incorrect error output configuration') &&
        e.message.includes('Error Response1') &&
        e.message.includes('appear to be error handlers but are in main[0]')
      );
      expect(hasIncorrectConfigError).toBe(true);

      // Verify the error message includes the JSON examples
      const errorMsg = result.errors.find((e: any) =>
        e.message.includes('Incorrect error output configuration')
      );
      expect(errorMsg?.message).toContain('INCORRECT (current)');
      expect(errorMsg?.message).toContain('CORRECT (should be)');
      expect(errorMsg?.message).toContain('main[1] = error output');
    });

    it('should validate correct error output configuration via MCP', async () => {
      const workflow = {
        nodes: [
          {
            id: '1',
            name: 'Validate Input',
            type: 'n8n-nodes-base.set',
            typeVersion: 3.4,
            position: [-400, 64],
            parameters: {},
            onError: 'continueErrorOutput'
          },
          {
            id: '2',
            name: 'Filter URLs',
            type: 'n8n-nodes-base.filter',
            typeVersion: 2.2,
            position: [-176, 64],
            parameters: {}
          },
          {
            id: '3',
            name: 'Error Response1',
            type: 'n8n-nodes-base.respondToWebhook',
            typeVersion: 1.5,
            position: [-160, 240],
            parameters: {}
          }
        ],
        connections: {
          'Validate Input': {
            main: [
              [
                { node: 'Filter URLs', type: 'main', index: 0 }
              ],
              [
                { node: 'Error Response1', type: 'main', index: 0 }  // Correctly in main[1]
              ]
            ]
          }
        }
      };

      const response = await client.callTool({
        name: 'validate_workflow',
        arguments: { workflow }
      });

      expect((response as any).content).toHaveLength(1);
      expect((response as any).content[0].type).toBe('text');

      const result = JSON.parse(((response as any).content[0]).text);

      // Should not have the specific error about incorrect configuration
      const hasIncorrectConfigError = result.errors?.some((e: any) =>
        e.message.includes('Incorrect error output configuration')
      ) ?? false;
      expect(hasIncorrectConfigError).toBe(false);
    });

    it('should detect onError and connection mismatches via MCP', async () => {
      // Test case 1: onError set but no error connections
      const workflow1 = {
        nodes: [
          {
            id: '1',
            name: 'HTTP Request',
            type: 'n8n-nodes-base.httpRequest',
            typeVersion: 4,
            position: [100, 100],
            parameters: {},
            onError: 'continueErrorOutput'
          },
          {
            id: '2',
            name: 'Process Data',
            type: 'n8n-nodes-base.set',
            position: [300, 100],
            parameters: {}
          }
        ],
        connections: {
          'HTTP Request': {
            main: [
              [
                { node: 'Process Data', type: 'main', index: 0 }
              ]
            ]
          }
        }
      };

      // Test case 2: error connections but no onError
      const workflow2 = {
        nodes: [
          {
            id: '1',
            name: 'HTTP Request',
            type: 'n8n-nodes-base.httpRequest',
            typeVersion: 4,
            position: [100, 100],
            parameters: {}
            // No onError property
          },
          {
            id: '2',
            name: 'Process Data',
            type: 'n8n-nodes-base.set',
            position: [300, 100],
            parameters: {}
          },
          {
            id: '3',
            name: 'Error Handler',
            type: 'n8n-nodes-base.set',
            position: [300, 200],
            parameters: {}
          }
        ],
        connections: {
          'HTTP Request': {
            main: [
              [
                { node: 'Process Data', type: 'main', index: 0 }
              ],
              [
                { node: 'Error Handler', type: 'main', index: 0 }
              ]
            ]
          }
        }
      };

      // Test both scenarios
      const workflows = [workflow1, workflow2];

      for (const workflow of workflows) {
        const response = await client.callTool({
          name: 'validate_workflow',
          arguments: { workflow }
        });

        const result = JSON.parse(((response as any).content[0]).text);

        // Should detect some kind of validation issue
        expect(result).toHaveProperty('valid');
        expect(Array.isArray(result.errors || [])).toBe(true);
        expect(Array.isArray(result.warnings || [])).toBe(true);
      }
    });

    it('should handle large workflows with complex error patterns via MCP', async () => {
      // Create a large workflow with multiple error handling scenarios
      const nodes = [];
      const connections: any = {};

      // Create 50 nodes with various error handling patterns
      for (let i = 1; i <= 50; i++) {
        nodes.push({
          id: i.toString(),
          name: `Node${i}`,
          type: i % 5 === 0 ? 'n8n-nodes-base.httpRequest' : 'n8n-nodes-base.set',
          typeVersion: 1,
          position: [i * 100, 100],
          parameters: {},
          ...(i % 3 === 0 ? { onError: 'continueErrorOutput' } : {})
        });
      }

      // Create connections with mixed correct and incorrect error handling
      for (let i = 1; i < 50; i++) {
        const hasErrorHandling = i % 3 === 0;
        const nextNode = `Node${i + 1}`;

        if (hasErrorHandling && i % 6 === 0) {
          // Incorrect: error handler in main[0] with success node
          connections[`Node${i}`] = {
            main: [
              [
                { node: nextNode, type: 'main', index: 0 },
                { node: 'Error Handler', type: 'main', index: 0 }  // Wrong placement
              ]
            ]
          };
        } else if (hasErrorHandling) {
          // Correct: separate success and error outputs
          connections[`Node${i}`] = {
            main: [
              [
                { node: nextNode, type: 'main', index: 0 }
              ],
              [
                { node: 'Error Handler', type: 'main', index: 0 }
              ]
            ]
          };
        } else {
          // Normal connection
          connections[`Node${i}`] = {
            main: [
              [
                { node: nextNode, type: 'main', index: 0 }
              ]
            ]
          };
        }
      }

      // Add error handler node
      nodes.push({
        id: '51',
        name: 'Error Handler',
        type: 'n8n-nodes-base.set',
        typeVersion: 1,
        position: [2600, 200],
        parameters: {}
      });

      const workflow = { nodes, connections };

      const startTime = Date.now();
      const response = await client.callTool({
        name: 'validate_workflow',
        arguments: { workflow }
      });
      const endTime = Date.now();

      // Validation should complete quickly even for large workflows
      expect(endTime - startTime).toBeLessThan(5000); // Less than 5 seconds

      const result = JSON.parse(((response as any).content[0]).text);

      // Should detect the incorrect error configurations
      const hasErrors = result.errors && result.errors.length > 0;
      expect(hasErrors).toBe(true);

      // Specifically check for incorrect error output configuration errors
      const incorrectConfigErrors = result.errors.filter((e: any) =>
        e.message.includes('Incorrect error output configuration')
      );
      expect(incorrectConfigErrors.length).toBeGreaterThan(0);
    });

    it('should handle edge cases gracefully via MCP', async () => {
      const edgeCaseWorkflows = [
        // Empty workflow
        { nodes: [], connections: {} },

        // Single isolated node
        {
          nodes: [{
            id: '1',
            name: 'Isolated',
            type: 'n8n-nodes-base.set',
            position: [100, 100],
            parameters: {}
          }],
          connections: {}
        },

        // Node with null/undefined connections
        {
          nodes: [{
            id: '1',
            name: 'Source',
            type: 'n8n-nodes-base.httpRequest',
            position: [100, 100],
            parameters: {}
          }],
          connections: {
            'Source': {
              main: [null, undefined]
            }
          }
        }
      ];

      for (const workflow of edgeCaseWorkflows) {
        const response = await client.callTool({
          name: 'validate_workflow',
          arguments: { workflow }
        });

        expect((response as any).content).toHaveLength(1);
        const result = JSON.parse(((response as any).content[0]).text);

        // Should not crash and should return a valid validation result
        expect(result).toHaveProperty('valid');
        expect(typeof result.valid).toBe('boolean');
        expect(Array.isArray(result.errors || [])).toBe(true);
        expect(Array.isArray(result.warnings || [])).toBe(true);
      }
    });

    it('should validate with different validation profiles via MCP', async () => {
      const workflow = {
        nodes: [
          {
            id: '1',
            name: 'API Call',
            type: 'n8n-nodes-base.httpRequest',
            position: [100, 100],
            parameters: {}
          },
          {
            id: '2',
            name: 'Success Handler',
            type: 'n8n-nodes-base.set',
            position: [300, 100],
            parameters: {}
          },
          {
            id: '3',
            name: 'Error Response',
            type: 'n8n-nodes-base.respondToWebhook',
            position: [300, 200],
            parameters: {}
          }
        ],
        connections: {
          'API Call': {
            main: [
              [
                { node: 'Success Handler', type: 'main', index: 0 },
                { node: 'Error Response', type: 'main', index: 0 }  // Incorrect placement
              ]
            ]
          }
        }
      };

      const profiles = ['minimal', 'runtime', 'ai-friendly', 'strict'];

      for (const profile of profiles) {
        const response = await client.callTool({
          name: 'validate_workflow',
          arguments: {
            workflow,
            options: { profile }
          }
        });

        const result = JSON.parse(((response as any).content[0]).text);

        // All profiles should detect this error output configuration issue
        const hasIncorrectConfigError = result.errors?.some((e: any) =>
          e.message.includes('Incorrect error output configuration')
        );
        expect(hasIncorrectConfigError).toBe(true);
      }
    });
  });

  describe('Error Message Format Consistency', () => {
    it('should format error messages consistently across different scenarios', async () => {
      const scenarios = [
        {
          name: 'Single error handler in wrong place',
          workflow: {
            nodes: [
              { id: '1', name: 'Source', type: 'n8n-nodes-base.httpRequest', position: [0, 0], parameters: {} },
              { id: '2', name: 'Success', type: 'n8n-nodes-base.set', position: [200, 0], parameters: {} },
              { id: '3', name: 'Error Handler', type: 'n8n-nodes-base.set', position: [200, 100], parameters: {} }
            ],
            connections: {
              'Source': {
                main: [[
                  { node: 'Success', type: 'main', index: 0 },
                  { node: 'Error Handler', type: 'main', index: 0 }
                ]]
              }
            }
          }
        },
        {
          name: 'Multiple error handlers in wrong place',
          workflow: {
            nodes: [
              { id: '1', name: 'Source', type: 'n8n-nodes-base.httpRequest', position: [0, 0], parameters: {} },
              { id: '2', name: 'Success', type: 'n8n-nodes-base.set', position: [200, 0], parameters: {} },
              { id: '3', name: 'Error Handler 1', type: 'n8n-nodes-base.set', position: [200, 100], parameters: {} },
              { id: '4', name: 'Error Handler 2', type: 'n8n-nodes-base.emailSend', position: [200, 200], parameters: {} }
            ],
            connections: {
              'Source': {
                main: [[
                  { node: 'Success', type: 'main', index: 0 },
                  { node: 'Error Handler 1', type: 'main', index: 0 },
                  { node: 'Error Handler 2', type: 'main', index: 0 }
                ]]
              }
            }
          }
        }
      ];

      for (const scenario of scenarios) {
        const response = await client.callTool({
          name: 'validate_workflow',
          arguments: { workflow: scenario.workflow }
        });

        const result = JSON.parse(((response as any).content[0]).text);

        const errorConfigError = result.errors.find((e: any) =>
          e.message.includes('Incorrect error output configuration')
        );

        expect(errorConfigError).toBeDefined();

        // Check that error message follows consistent format
        expect(errorConfigError.message).toContain('INCORRECT (current):');
        expect(errorConfigError.message).toContain('CORRECT (should be):');
        expect(errorConfigError.message).toContain('main[0] = success output');
        expect(errorConfigError.message).toContain('main[1] = error output');
        expect(errorConfigError.message).toContain('Also add: "onError": "continueErrorOutput"');

        // Check JSON format is valid
        const incorrectSection = errorConfigError.message.match(/INCORRECT \(current\):\n([\s\S]*?)\n\nCORRECT/);
        const correctSection = errorConfigError.message.match(/CORRECT \(should be\):\n([\s\S]*?)\n\nAlso add/);

        expect(incorrectSection).toBeDefined();
        expect(correctSection).toBeDefined();

        // Verify JSON structure is present (but don't parse due to comments)
        expect(incorrectSection).toBeDefined();
        expect(correctSection).toBeDefined();
        expect(incorrectSection![1]).toContain('main');
        expect(correctSection![1]).toContain('main');
      }
    });
  });
});
```

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

```typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { WorkflowValidator } from '@/services/workflow-validator';
import { NodeRepository } from '@/database/node-repository';
import { EnhancedConfigValidator } from '@/services/enhanced-config-validator';

vi.mock('@/utils/logger');

describe('WorkflowValidator - Performance Tests', () => {
  let validator: WorkflowValidator;
  let mockNodeRepository: any;

  beforeEach(() => {
    vi.clearAllMocks();

    // Create mock repository with performance optimizations
    mockNodeRepository = {
      getNode: vi.fn((type: string) => {
        // Return mock node info for any node type to avoid database calls
        return {
          node_type: type,
          display_name: 'Mock Node',
          isVersioned: true,
          version: 1
        };
      })
    };

    validator = new WorkflowValidator(mockNodeRepository, EnhancedConfigValidator);
  });

  describe('Large Workflow Performance', () => {
    it('should validate large workflows with many error paths efficiently', async () => {
      // Generate a large workflow with 500 nodes
      const nodeCount = 500;
      const nodes = [];
      const connections: any = {};

      // Create nodes with various error handling patterns
      for (let i = 1; i <= nodeCount; i++) {
        nodes.push({
          id: i.toString(),
          name: `Node${i}`,
          type: i % 5 === 0 ? 'n8n-nodes-base.httpRequest' : 'n8n-nodes-base.set',
          typeVersion: 1,
          position: [i * 10, (i % 10) * 100],
          parameters: {},
          ...(i % 3 === 0 ? { onError: 'continueErrorOutput' } : {})
        });
      }

      // Create connections with multiple error handling scenarios
      for (let i = 1; i < nodeCount; i++) {
        const hasErrorHandling = i % 3 === 0;
        const hasMultipleConnections = i % 7 === 0;

        if (hasErrorHandling && hasMultipleConnections) {
          // Mix correct and incorrect error handling patterns
          const isIncorrect = i % 14 === 0;

          if (isIncorrect) {
            // Incorrect: error handlers mixed with success nodes in main[0]
            connections[`Node${i}`] = {
              main: [
                [
                  { node: `Node${i + 1}`, type: 'main', index: 0 },
                  { node: `Error Handler ${i}`, type: 'main', index: 0 } // Wrong!
                ]
              ]
            };
          } else {
            // Correct: separate success and error outputs
            connections[`Node${i}`] = {
              main: [
                [
                  { node: `Node${i + 1}`, type: 'main', index: 0 }
                ],
                [
                  { node: `Error Handler ${i}`, type: 'main', index: 0 }
                ]
              ]
            };
          }

          // Add error handler node
          nodes.push({
            id: `error-${i}`,
            name: `Error Handler ${i}`,
            type: 'n8n-nodes-base.respondToWebhook',
            typeVersion: 1,
            position: [(i + nodeCount) * 10, 500],
            parameters: {}
          });
        } else {
          // Simple connection
          connections[`Node${i}`] = {
            main: [
              [
                { node: `Node${i + 1}`, type: 'main', index: 0 }
              ]
            ]
          };
        }
      }

      const workflow = { nodes, connections };

      const startTime = performance.now();
      const result = await validator.validateWorkflow(workflow as any);
      const endTime = performance.now();

      const executionTime = endTime - startTime;

      // Validation should complete within reasonable time
      expect(executionTime).toBeLessThan(10000); // Less than 10 seconds

      // Should still catch validation errors
      expect(Array.isArray(result.errors)).toBe(true);
      expect(Array.isArray(result.warnings)).toBe(true);

      // Should detect incorrect error configurations
      const incorrectConfigErrors = result.errors.filter(e =>
        e.message.includes('Incorrect error output configuration')
      );
      expect(incorrectConfigErrors.length).toBeGreaterThan(0);

      console.log(`Validated ${nodes.length} nodes in ${executionTime.toFixed(2)}ms`);
      console.log(`Found ${result.errors.length} errors and ${result.warnings.length} warnings`);
    });

    it('should handle deeply nested error handling chains efficiently', async () => {
      // Create a chain of error handlers, each with their own error handling
      const chainLength = 100;
      const nodes = [];
      const connections: any = {};

      for (let i = 1; i <= chainLength; i++) {
        // Main processing node
        nodes.push({
          id: `main-${i}`,
          name: `Main ${i}`,
          type: 'n8n-nodes-base.httpRequest',
          typeVersion: 1,
          position: [i * 150, 100],
          parameters: {},
          onError: 'continueErrorOutput'
        });

        // Error handler node
        nodes.push({
          id: `error-${i}`,
          name: `Error Handler ${i}`,
          type: 'n8n-nodes-base.httpRequest',
          typeVersion: 1,
          position: [i * 150, 300],
          parameters: {},
          onError: 'continueErrorOutput'
        });

        // Fallback error node
        nodes.push({
          id: `fallback-${i}`,
          name: `Fallback ${i}`,
          type: 'n8n-nodes-base.set',
          typeVersion: 1,
          position: [i * 150, 500],
          parameters: {}
        });

        // Connections
        connections[`Main ${i}`] = {
          main: [
            // Success path
            i < chainLength ? [{ node: `Main ${i + 1}`, type: 'main', index: 0 }] : [],
            // Error path
            [{ node: `Error Handler ${i}`, type: 'main', index: 0 }]
          ]
        };

        connections[`Error Handler ${i}`] = {
          main: [
            // Success path (continue to next error handler or end)
            [],
            // Error path (go to fallback)
            [{ node: `Fallback ${i}`, type: 'main', index: 0 }]
          ]
        };
      }

      const workflow = { nodes, connections };

      const startTime = performance.now();
      const result = await validator.validateWorkflow(workflow as any);
      const endTime = performance.now();

      const executionTime = endTime - startTime;

      // Should complete quickly even with complex nested error handling
      expect(executionTime).toBeLessThan(5000); // Less than 5 seconds

      // Should not have errors about incorrect configuration (this is correct)
      const incorrectConfigErrors = result.errors.filter(e =>
        e.message.includes('Incorrect error output configuration')
      );
      expect(incorrectConfigErrors.length).toBe(0);

      console.log(`Validated ${nodes.length} nodes with nested error handling in ${executionTime.toFixed(2)}ms`);
    });

    it('should efficiently validate workflows with many parallel error paths', async () => {
      // Create a workflow with one source node that fans out to many parallel paths,
      // each with their own error handling
      const parallelPathCount = 200;
      const nodes = [
        {
          id: 'source',
          name: 'Source',
          type: 'n8n-nodes-base.webhook',
          typeVersion: 1,
          position: [0, 0],
          parameters: {}
        }
      ];
      const connections: any = {
        'Source': {
          main: [[]]
        }
      };

      // Create parallel paths
      for (let i = 1; i <= parallelPathCount; i++) {
        // Processing node
        nodes.push({
          id: `process-${i}`,
          name: `Process ${i}`,
          type: 'n8n-nodes-base.httpRequest',
          typeVersion: 1,
          position: [200, i * 20],
          parameters: {},
          onError: 'continueErrorOutput'
        } as any);

        // Success handler
        nodes.push({
          id: `success-${i}`,
          name: `Success ${i}`,
          type: 'n8n-nodes-base.set',
          typeVersion: 1,
          position: [400, i * 20],
          parameters: {}
        });

        // Error handler
        nodes.push({
          id: `error-${i}`,
          name: `Error Handler ${i}`,
          type: 'n8n-nodes-base.respondToWebhook',
          typeVersion: 1,
          position: [400, i * 20 + 10],
          parameters: {}
        });

        // Connect source to processing node
        connections['Source'].main[0].push({
          node: `Process ${i}`,
          type: 'main',
          index: 0
        });

        // Connect processing node to success and error handlers
        connections[`Process ${i}`] = {
          main: [
            [{ node: `Success ${i}`, type: 'main', index: 0 }],
            [{ node: `Error Handler ${i}`, type: 'main', index: 0 }]
          ]
        };
      }

      const workflow = { nodes, connections };

      const startTime = performance.now();
      const result = await validator.validateWorkflow(workflow as any);
      const endTime = performance.now();

      const executionTime = endTime - startTime;

      // Should validate efficiently despite many parallel paths
      expect(executionTime).toBeLessThan(8000); // Less than 8 seconds

      // Should not have errors about incorrect configuration
      const incorrectConfigErrors = result.errors.filter(e =>
        e.message.includes('Incorrect error output configuration')
      );
      expect(incorrectConfigErrors.length).toBe(0);

      console.log(`Validated ${nodes.length} nodes with ${parallelPathCount} parallel error paths in ${executionTime.toFixed(2)}ms`);
    });

    it('should handle worst-case scenario with many incorrect configurations efficiently', async () => {
      // Create a workflow where many nodes have the incorrect error configuration
      // This tests the performance of the error detection algorithm
      const nodeCount = 300;
      const nodes = [];
      const connections: any = {};

      for (let i = 1; i <= nodeCount; i++) {
        // Main node
        nodes.push({
          id: `main-${i}`,
          name: `Main ${i}`,
          type: 'n8n-nodes-base.httpRequest',
          typeVersion: 1,
          position: [i * 20, 100],
          parameters: {}
        });

        // Success handler
        nodes.push({
          id: `success-${i}`,
          name: `Success ${i}`,
          type: 'n8n-nodes-base.set',
          typeVersion: 1,
          position: [i * 20, 200],
          parameters: {}
        });

        // Error handler (with error-indicating name)
        nodes.push({
          id: `error-${i}`,
          name: `Error Handler ${i}`,
          type: 'n8n-nodes-base.respondToWebhook',
          typeVersion: 1,
          position: [i * 20, 300],
          parameters: {}
        });

        // INCORRECT configuration: both success and error handlers in main[0]
        connections[`Main ${i}`] = {
          main: [
            [
              { node: `Success ${i}`, type: 'main', index: 0 },
              { node: `Error Handler ${i}`, type: 'main', index: 0 } // Wrong!
            ]
          ]
        };
      }

      const workflow = { nodes, connections };

      const startTime = performance.now();
      const result = await validator.validateWorkflow(workflow as any);
      const endTime = performance.now();

      const executionTime = endTime - startTime;

      // Should complete within reasonable time even when generating many errors
      expect(executionTime).toBeLessThan(15000); // Less than 15 seconds

      // Should detect ALL incorrect configurations
      const incorrectConfigErrors = result.errors.filter(e =>
        e.message.includes('Incorrect error output configuration')
      );
      expect(incorrectConfigErrors.length).toBe(nodeCount); // One error per node

      console.log(`Detected ${incorrectConfigErrors.length} incorrect configurations in ${nodes.length} nodes in ${executionTime.toFixed(2)}ms`);
    });
  });

  describe('Memory Usage and Optimization', () => {
    it('should not leak memory during large workflow validation', async () => {
      // Get initial memory usage
      const initialMemory = process.memoryUsage().heapUsed;

      // Validate multiple large workflows
      for (let run = 0; run < 5; run++) {
        const nodeCount = 200;
        const nodes = [];
        const connections: any = {};

        for (let i = 1; i <= nodeCount; i++) {
          nodes.push({
            id: i.toString(),
            name: `Node${i}`,
            type: 'n8n-nodes-base.httpRequest',
            typeVersion: 1,
            position: [i * 10, 100],
            parameters: {},
            onError: 'continueErrorOutput'
          });

          if (i > 1) {
            connections[`Node${i - 1}`] = {
              main: [
                [{ node: `Node${i}`, type: 'main', index: 0 }],
                [{ node: `Error${i}`, type: 'main', index: 0 }]
              ]
            };

            nodes.push({
              id: `error-${i}`,
              name: `Error${i}`,
              type: 'n8n-nodes-base.set',
              typeVersion: 1,
              position: [i * 10, 200],
              parameters: {}
            });
          }
        }

        const workflow = { nodes, connections };
        await validator.validateWorkflow(workflow as any);

        // Force garbage collection if available
        if (global.gc) {
          global.gc();
        }
      }

      const finalMemory = process.memoryUsage().heapUsed;
      const memoryIncrease = finalMemory - initialMemory;
      const memoryIncreaseMB = memoryIncrease / (1024 * 1024);

      // Memory increase should be reasonable (less than 50MB)
      expect(memoryIncreaseMB).toBeLessThan(50);

      console.log(`Memory increase after 5 large workflow validations: ${memoryIncreaseMB.toFixed(2)}MB`);
    });

    it('should handle concurrent validation requests efficiently', async () => {
      // Create multiple validation requests that run concurrently
      const concurrentRequests = 10;
      const workflows = [];

      // Prepare workflows
      for (let r = 0; r < concurrentRequests; r++) {
        const nodeCount = 50;
        const nodes = [];
        const connections: any = {};

        for (let i = 1; i <= nodeCount; i++) {
          nodes.push({
            id: `${r}-${i}`,
            name: `R${r}Node${i}`,
            type: i % 2 === 0 ? 'n8n-nodes-base.httpRequest' : 'n8n-nodes-base.set',
            typeVersion: 1,
            position: [i * 20, r * 100],
            parameters: {},
            ...(i % 3 === 0 ? { onError: 'continueErrorOutput' } : {})
          });

          if (i > 1) {
            const hasError = i % 3 === 0;
            const isIncorrect = i % 6 === 0;

            if (hasError && isIncorrect) {
              // Incorrect configuration
              connections[`R${r}Node${i - 1}`] = {
                main: [
                  [
                    { node: `R${r}Node${i}`, type: 'main', index: 0 },
                    { node: `R${r}Error${i}`, type: 'main', index: 0 } // Wrong!
                  ]
                ]
              };

              nodes.push({
                id: `${r}-error-${i}`,
                name: `R${r}Error${i}`,
                type: 'n8n-nodes-base.respondToWebhook',
                typeVersion: 1,
                position: [i * 20, r * 100 + 50],
                parameters: {}
              });
            } else if (hasError) {
              // Correct configuration
              connections[`R${r}Node${i - 1}`] = {
                main: [
                  [{ node: `R${r}Node${i}`, type: 'main', index: 0 }],
                  [{ node: `R${r}Error${i}`, type: 'main', index: 0 }]
                ]
              };

              nodes.push({
                id: `${r}-error-${i}`,
                name: `R${r}Error${i}`,
                type: 'n8n-nodes-base.set',
                typeVersion: 1,
                position: [i * 20, r * 100 + 50],
                parameters: {}
              });
            } else {
              // Normal connection
              connections[`R${r}Node${i - 1}`] = {
                main: [
                  [{ node: `R${r}Node${i}`, type: 'main', index: 0 }]
                ]
              };
            }
          }
        }

        workflows.push({ nodes, connections });
      }

      // Run concurrent validations
      const startTime = performance.now();
      const results = await Promise.all(
        workflows.map(workflow => validator.validateWorkflow(workflow as any))
      );
      const endTime = performance.now();

      const totalTime = endTime - startTime;

      // All validations should complete
      expect(results).toHaveLength(concurrentRequests);

      // Each result should be valid
      results.forEach(result => {
        expect(Array.isArray(result.errors)).toBe(true);
        expect(Array.isArray(result.warnings)).toBe(true);
      });

      // Concurrent execution should be efficient
      expect(totalTime).toBeLessThan(20000); // Less than 20 seconds total

      console.log(`Completed ${concurrentRequests} concurrent validations in ${totalTime.toFixed(2)}ms`);
    });
  });
});
```
Page 18/46FirstPrevNextLast