#
tokens: 49425/50000 8/620 files (page 24/60)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 24 of 60. Use http://codebase.md/czlonkowski/n8n-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── _config.yml
├── .claude
│   └── agents
│       ├── code-reviewer.md
│       ├── context-manager.md
│       ├── debugger.md
│       ├── deployment-engineer.md
│       ├── mcp-backend-engineer.md
│       ├── n8n-mcp-tester.md
│       ├── technical-researcher.md
│       └── test-automator.md
├── .dockerignore
├── .env.docker
├── .env.example
├── .env.n8n.example
├── .env.test
├── .env.test.example
├── .github
│   ├── ABOUT.md
│   ├── BENCHMARK_THRESHOLDS.md
│   ├── FUNDING.yml
│   ├── gh-pages.yml
│   ├── secret_scanning.yml
│   └── workflows
│       ├── benchmark-pr.yml
│       ├── benchmark.yml
│       ├── docker-build-fast.yml
│       ├── docker-build-n8n.yml
│       ├── docker-build.yml
│       ├── release.yml
│       ├── test.yml
│       └── update-n8n-deps.yml
├── .gitignore
├── .npmignore
├── ATTRIBUTION.md
├── CHANGELOG.md
├── CLAUDE.md
├── codecov.yml
├── coverage.json
├── data
│   ├── .gitkeep
│   ├── nodes.db
│   ├── nodes.db-shm
│   ├── nodes.db-wal
│   └── templates.db
├── deploy
│   └── quick-deploy-n8n.sh
├── docker
│   ├── docker-entrypoint.sh
│   ├── n8n-mcp
│   ├── parse-config.js
│   └── README.md
├── docker-compose.buildkit.yml
├── docker-compose.extract.yml
├── docker-compose.n8n.yml
├── docker-compose.override.yml.example
├── docker-compose.test-n8n.yml
├── docker-compose.yml
├── Dockerfile
├── Dockerfile.railway
├── Dockerfile.test
├── docs
│   ├── AUTOMATED_RELEASES.md
│   ├── BENCHMARKS.md
│   ├── CHANGELOG.md
│   ├── CI_TEST_INFRASTRUCTURE.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
│   │   ├── skills.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
│       ├── expression-utils.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
│   │   │   ├── expression-utils.test.ts
│   │   │   ├── fixed-collection-validator.test.ts
│   │   │   ├── n8n-errors.test.ts
│   │   │   ├── node-type-normalizer.test.ts
│   │   │   ├── node-type-utils.test.ts
│   │   │   ├── node-utils.test.ts
│   │   │   ├── simple-cache-memory-leak-fix.test.ts
│   │   │   ├── ssrf-protection.test.ts
│   │   │   └── template-node-resolver.test.ts
│   │   └── validation-fixes.test.ts
│   └── utils
│       ├── assertions.ts
│       ├── builders
│       │   └── workflow.builder.ts
│       ├── data-generators.ts
│       ├── database-utils.ts
│       ├── README.md
│       └── test-helpers.ts
├── thumbnail.png
├── tsconfig.build.json
├── tsconfig.json
├── types
│   ├── mcp.d.ts
│   └── test-env.d.ts
├── verify-telemetry-fix.js
├── versioned-nodes.md
├── vitest.config.benchmark.ts
├── vitest.config.integration.ts
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/tests/unit/multi-tenant-integration.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Integration tests for multi-tenant support across the entire codebase
  3 |  *
  4 |  * This test file provides comprehensive coverage for the multi-tenant implementation
  5 |  * by testing the actual behavior and integration points rather than implementation details.
  6 |  */
  7 | 
  8 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
  9 | import { InstanceContext, isInstanceContext, validateInstanceContext } from '../../src/types/instance-context';
 10 | 
 11 | // Mock logger properly
 12 | vi.mock('../../src/utils/logger', () => ({
 13 |   Logger: vi.fn().mockImplementation(() => ({
 14 |     debug: vi.fn(),
 15 |     info: vi.fn(),
 16 |     warn: vi.fn(),
 17 |     error: vi.fn()
 18 |   })),
 19 |   logger: {
 20 |     debug: vi.fn(),
 21 |     info: vi.fn(),
 22 |     warn: vi.fn(),
 23 |     error: vi.fn()
 24 |   }
 25 | }));
 26 | 
 27 | describe('Multi-Tenant Support Integration', () => {
 28 |   let originalEnv: NodeJS.ProcessEnv;
 29 | 
 30 |   beforeEach(() => {
 31 |     originalEnv = { ...process.env };
 32 |     vi.clearAllMocks();
 33 |   });
 34 | 
 35 |   afterEach(() => {
 36 |     process.env = originalEnv;
 37 |   });
 38 | 
 39 |   describe('InstanceContext Validation', () => {
 40 |     describe('Real-world URL patterns', () => {
 41 |       const validUrls = [
 42 |         'https://app.n8n.cloud',
 43 |         'https://tenant1.n8n.cloud',
 44 |         'https://my-company.n8n.cloud',
 45 |         'https://n8n.example.com',
 46 |         'https://automation.company.com',
 47 |         'http://localhost:5678',
 48 |         'https://localhost:8443',
 49 |         'http://127.0.0.1:5678',
 50 |         'https://192.168.1.100:8080',
 51 |         'https://10.0.0.1:3000',
 52 |         'http://n8n.internal.company.com',
 53 |         'https://workflow.enterprise.local'
 54 |       ];
 55 | 
 56 |       validUrls.forEach(url => {
 57 |         it(`should accept realistic n8n URL: ${url}`, () => {
 58 |           const context: InstanceContext = {
 59 |             n8nApiUrl: url,
 60 |             n8nApiKey: 'valid-api-key-123'
 61 |           };
 62 | 
 63 |           expect(isInstanceContext(context)).toBe(true);
 64 | 
 65 |           const validation = validateInstanceContext(context);
 66 |           expect(validation.valid).toBe(true);
 67 |           expect(validation.errors).toBeUndefined();
 68 |         });
 69 |       });
 70 |     });
 71 | 
 72 |     describe('Security validation', () => {
 73 |       const maliciousUrls = [
 74 |         'javascript:alert("xss")',
 75 |         'vbscript:msgbox("xss")',
 76 |         'data:text/html,<script>alert("xss")</script>',
 77 |         'file:///etc/passwd',
 78 |         'ldap://attacker.com/cn=admin',
 79 |         'ftp://malicious.com'
 80 |       ];
 81 | 
 82 |       maliciousUrls.forEach(url => {
 83 |         it(`should reject potentially malicious URL: ${url}`, () => {
 84 |           const context: InstanceContext = {
 85 |             n8nApiUrl: url,
 86 |             n8nApiKey: 'valid-key'
 87 |           };
 88 | 
 89 |           expect(isInstanceContext(context)).toBe(false);
 90 | 
 91 |           const validation = validateInstanceContext(context);
 92 |           expect(validation.valid).toBe(false);
 93 |           expect(validation.errors).toBeDefined();
 94 |         });
 95 |       });
 96 |     });
 97 | 
 98 |     describe('API key validation', () => {
 99 |       const invalidApiKeys = [
100 |         '',
101 |         'placeholder',
102 |         'YOUR_API_KEY',
103 |         'example',
104 |         'your_api_key_here'
105 |       ];
106 | 
107 |       invalidApiKeys.forEach(key => {
108 |         it(`should reject invalid API key: "${key}"`, () => {
109 |           const context: InstanceContext = {
110 |             n8nApiUrl: 'https://valid.n8n.cloud',
111 |             n8nApiKey: key
112 |           };
113 | 
114 |           if (key === '') {
115 |             // Empty string validation
116 |             const validation = validateInstanceContext(context);
117 |             expect(validation.valid).toBe(false);
118 |             expect(validation.errors?.[0]).toContain('empty string');
119 |           } else {
120 |             // Placeholder validation
121 |             expect(isInstanceContext(context)).toBe(false);
122 |           }
123 |         });
124 |       });
125 | 
126 |       it('should accept valid API keys', () => {
127 |         const validKeys = [
128 |           'sk_live_AbCdEf123456789',
129 |           'api-key-12345-abcdef',
130 |           'n8n_api_key_production_v1_xyz',
131 |           'Bearer-token-abc123',
132 |           'jwt.eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9'
133 |         ];
134 | 
135 |         validKeys.forEach(key => {
136 |           const context: InstanceContext = {
137 |             n8nApiUrl: 'https://valid.n8n.cloud',
138 |             n8nApiKey: key
139 |           };
140 | 
141 |           expect(isInstanceContext(context)).toBe(true);
142 |           const validation = validateInstanceContext(context);
143 |           expect(validation.valid).toBe(true);
144 |         });
145 |       });
146 |     });
147 | 
148 |     describe('Edge cases and error handling', () => {
149 |       it('should handle partial instance context', () => {
150 |         const partialContext: InstanceContext = {
151 |           n8nApiUrl: 'https://tenant1.n8n.cloud'
152 |           // n8nApiKey intentionally missing
153 |         };
154 | 
155 |         expect(isInstanceContext(partialContext)).toBe(true);
156 |         const validation = validateInstanceContext(partialContext);
157 |         expect(validation.valid).toBe(true);
158 |       });
159 | 
160 |       it('should handle completely empty context', () => {
161 |         const emptyContext: InstanceContext = {};
162 | 
163 |         expect(isInstanceContext(emptyContext)).toBe(true);
164 |         const validation = validateInstanceContext(emptyContext);
165 |         expect(validation.valid).toBe(true);
166 |       });
167 | 
168 |       it('should handle numerical values gracefully', () => {
169 |         const contextWithNumbers: InstanceContext = {
170 |           n8nApiUrl: 'https://tenant1.n8n.cloud',
171 |           n8nApiKey: 'valid-key',
172 |           n8nApiTimeout: 30000,
173 |           n8nApiMaxRetries: 3
174 |         };
175 | 
176 |         expect(isInstanceContext(contextWithNumbers)).toBe(true);
177 |         const validation = validateInstanceContext(contextWithNumbers);
178 |         expect(validation.valid).toBe(true);
179 |       });
180 | 
181 |       it('should reject invalid numerical values', () => {
182 |         const invalidTimeout: InstanceContext = {
183 |           n8nApiUrl: 'https://tenant1.n8n.cloud',
184 |           n8nApiKey: 'valid-key',
185 |           n8nApiTimeout: -1
186 |         };
187 | 
188 |         expect(isInstanceContext(invalidTimeout)).toBe(false);
189 |         const validation = validateInstanceContext(invalidTimeout);
190 |         expect(validation.valid).toBe(false);
191 |         expect(validation.errors?.[0]).toContain('Must be positive');
192 |       });
193 | 
194 |       it('should reject invalid retry values', () => {
195 |         const invalidRetries: InstanceContext = {
196 |           n8nApiUrl: 'https://tenant1.n8n.cloud',
197 |           n8nApiKey: 'valid-key',
198 |           n8nApiMaxRetries: -5
199 |         };
200 | 
201 |         expect(isInstanceContext(invalidRetries)).toBe(false);
202 |         const validation = validateInstanceContext(invalidRetries);
203 |         expect(validation.valid).toBe(false);
204 |         expect(validation.errors?.[0]).toContain('Must be non-negative');
205 |       });
206 |     });
207 |   });
208 | 
209 |   describe('Environment Variable Handling', () => {
210 |     it('should handle ENABLE_MULTI_TENANT flag correctly', () => {
211 |       // Test various flag values
212 |       const flagValues = [
213 |         { value: 'true', expected: true },
214 |         { value: 'false', expected: false },
215 |         { value: 'TRUE', expected: false },  // Case sensitive
216 |         { value: 'yes', expected: false },
217 |         { value: '1', expected: false },
218 |         { value: '', expected: false },
219 |         { value: undefined, expected: false }
220 |       ];
221 | 
222 |       flagValues.forEach(({ value, expected }) => {
223 |         if (value === undefined) {
224 |           delete process.env.ENABLE_MULTI_TENANT;
225 |         } else {
226 |           process.env.ENABLE_MULTI_TENANT = value;
227 |         }
228 | 
229 |         const isEnabled = process.env.ENABLE_MULTI_TENANT === 'true';
230 |         expect(isEnabled).toBe(expected);
231 |       });
232 |     });
233 | 
234 |     it('should handle N8N_API_URL and N8N_API_KEY environment variables', () => {
235 |       // Test backward compatibility
236 |       process.env.N8N_API_URL = 'https://env.n8n.cloud';
237 |       process.env.N8N_API_KEY = 'env-api-key';
238 | 
239 |       const hasEnvConfig = !!(process.env.N8N_API_URL || process.env.N8N_API_KEY);
240 |       expect(hasEnvConfig).toBe(true);
241 | 
242 |       // Test when not set
243 |       delete process.env.N8N_API_URL;
244 |       delete process.env.N8N_API_KEY;
245 | 
246 |       const hasNoEnvConfig = !!(process.env.N8N_API_URL || process.env.N8N_API_KEY);
247 |       expect(hasNoEnvConfig).toBe(false);
248 |     });
249 |   });
250 | 
251 |   describe('Header Processing Simulation', () => {
252 |     it('should process multi-tenant headers correctly', () => {
253 |       // Simulate Express request headers
254 |       const mockHeaders = {
255 |         'x-n8n-url': 'https://tenant1.n8n.cloud',
256 |         'x-n8n-key': 'tenant1-api-key',
257 |         'x-instance-id': 'tenant1-instance',
258 |         'x-session-id': 'tenant1-session-123'
259 |       };
260 | 
261 |       // Simulate header extraction
262 |       const extractedContext: InstanceContext = {
263 |         n8nApiUrl: mockHeaders['x-n8n-url'],
264 |         n8nApiKey: mockHeaders['x-n8n-key'],
265 |         instanceId: mockHeaders['x-instance-id'],
266 |         sessionId: mockHeaders['x-session-id']
267 |       };
268 | 
269 |       expect(isInstanceContext(extractedContext)).toBe(true);
270 |       const validation = validateInstanceContext(extractedContext);
271 |       expect(validation.valid).toBe(true);
272 |     });
273 | 
274 |     it('should handle missing headers gracefully', () => {
275 |       const mockHeaders: any = {
276 |         'authorization': 'Bearer token',
277 |         'content-type': 'application/json'
278 |         // No x-n8n-* headers
279 |       };
280 | 
281 |       const extractedContext = {
282 |         n8nApiUrl: mockHeaders['x-n8n-url'], // undefined
283 |         n8nApiKey: mockHeaders['x-n8n-key']  // undefined
284 |       };
285 | 
286 |       // When no relevant headers exist, context should be undefined
287 |       const shouldCreateContext = !!(extractedContext.n8nApiUrl || extractedContext.n8nApiKey);
288 |       expect(shouldCreateContext).toBe(false);
289 |     });
290 | 
291 |     it('should handle malformed headers', () => {
292 |       const mockHeaders = {
293 |         'x-n8n-url': 'not-a-url',
294 |         'x-n8n-key': 'placeholder'
295 |       };
296 | 
297 |       const extractedContext: InstanceContext = {
298 |         n8nApiUrl: mockHeaders['x-n8n-url'],
299 |         n8nApiKey: mockHeaders['x-n8n-key']
300 |       };
301 | 
302 |       expect(isInstanceContext(extractedContext)).toBe(false);
303 |       const validation = validateInstanceContext(extractedContext);
304 |       expect(validation.valid).toBe(false);
305 |     });
306 |   });
307 | 
308 |   describe('Configuration Priority Logic', () => {
309 |     it('should implement correct priority logic for tool inclusion', () => {
310 |       // Test the shouldIncludeManagementTools logic
311 |       const scenarios = [
312 |         {
313 |           name: 'env config only',
314 |           envUrl: 'https://env.example.com',
315 |           envKey: 'env-key',
316 |           instanceContext: undefined,
317 |           multiTenant: false,
318 |           expected: true
319 |         },
320 |         {
321 |           name: 'instance config only',
322 |           envUrl: undefined,
323 |           envKey: undefined,
324 |           instanceContext: { n8nApiUrl: 'https://tenant.example.com', n8nApiKey: 'tenant-key' },
325 |           multiTenant: false,
326 |           expected: true
327 |         },
328 |         {
329 |           name: 'multi-tenant flag only',
330 |           envUrl: undefined,
331 |           envKey: undefined,
332 |           instanceContext: undefined,
333 |           multiTenant: true,
334 |           expected: true
335 |         },
336 |         {
337 |           name: 'no configuration',
338 |           envUrl: undefined,
339 |           envKey: undefined,
340 |           instanceContext: undefined,
341 |           multiTenant: false,
342 |           expected: false
343 |         }
344 |       ];
345 | 
346 |       scenarios.forEach(({ name, envUrl, envKey, instanceContext, multiTenant, expected }) => {
347 |         // Setup environment
348 |         if (envUrl) process.env.N8N_API_URL = envUrl;
349 |         else delete process.env.N8N_API_URL;
350 | 
351 |         if (envKey) process.env.N8N_API_KEY = envKey;
352 |         else delete process.env.N8N_API_KEY;
353 | 
354 |         if (multiTenant) process.env.ENABLE_MULTI_TENANT = 'true';
355 |         else delete process.env.ENABLE_MULTI_TENANT;
356 | 
357 |         // Test logic
358 |         const hasEnvConfig = !!(process.env.N8N_API_URL || process.env.N8N_API_KEY);
359 |         const hasInstanceConfig = !!(instanceContext?.n8nApiUrl || instanceContext?.n8nApiKey);
360 |         const isMultiTenantEnabled = process.env.ENABLE_MULTI_TENANT === 'true';
361 | 
362 |         const shouldIncludeManagementTools = hasEnvConfig || hasInstanceConfig || isMultiTenantEnabled;
363 | 
364 |         expect(shouldIncludeManagementTools).toBe(expected);
365 |       });
366 |     });
367 |   });
368 | 
369 |   describe('Session Management Concepts', () => {
370 |     it('should generate consistent identifiers for same configuration', () => {
371 |       const config1 = {
372 |         n8nApiUrl: 'https://tenant1.n8n.cloud',
373 |         n8nApiKey: 'api-key-123'
374 |       };
375 | 
376 |       const config2 = {
377 |         n8nApiUrl: 'https://tenant1.n8n.cloud',
378 |         n8nApiKey: 'api-key-123'
379 |       };
380 | 
381 |       // Same configuration should produce same hash
382 |       const hash1 = JSON.stringify(config1);
383 |       const hash2 = JSON.stringify(config2);
384 |       expect(hash1).toBe(hash2);
385 |     });
386 | 
387 |     it('should generate different identifiers for different configurations', () => {
388 |       const config1 = {
389 |         n8nApiUrl: 'https://tenant1.n8n.cloud',
390 |         n8nApiKey: 'api-key-123'
391 |       };
392 | 
393 |       const config2 = {
394 |         n8nApiUrl: 'https://tenant2.n8n.cloud',
395 |         n8nApiKey: 'different-api-key'
396 |       };
397 | 
398 |       // Different configuration should produce different hash
399 |       const hash1 = JSON.stringify(config1);
400 |       const hash2 = JSON.stringify(config2);
401 |       expect(hash1).not.toBe(hash2);
402 |     });
403 | 
404 |     it('should handle session isolation concepts', () => {
405 |       const sessions = new Map();
406 | 
407 |       // Simulate creating sessions for different tenants
408 |       const tenant1Context = {
409 |         n8nApiUrl: 'https://tenant1.n8n.cloud',
410 |         n8nApiKey: 'tenant1-key',
411 |         instanceId: 'tenant1'
412 |       };
413 | 
414 |       const tenant2Context = {
415 |         n8nApiUrl: 'https://tenant2.n8n.cloud',
416 |         n8nApiKey: 'tenant2-key',
417 |         instanceId: 'tenant2'
418 |       };
419 | 
420 |       sessions.set('session-1', { context: tenant1Context, lastAccess: new Date() });
421 |       sessions.set('session-2', { context: tenant2Context, lastAccess: new Date() });
422 | 
423 |       // Verify isolation
424 |       expect(sessions.get('session-1').context.instanceId).toBe('tenant1');
425 |       expect(sessions.get('session-2').context.instanceId).toBe('tenant2');
426 |       expect(sessions.size).toBe(2);
427 |     });
428 |   });
429 | 
430 |   describe('Error Scenarios and Recovery', () => {
431 |     it('should handle validation errors gracefully', () => {
432 |       const invalidContext: InstanceContext = {
433 |         n8nApiUrl: '',  // Empty URL
434 |         n8nApiKey: '',  // Empty key
435 |         n8nApiTimeout: -1,  // Invalid timeout
436 |         n8nApiMaxRetries: -1  // Invalid retries
437 |       };
438 | 
439 |       // Should not throw
440 |       expect(() => isInstanceContext(invalidContext)).not.toThrow();
441 |       expect(() => validateInstanceContext(invalidContext)).not.toThrow();
442 | 
443 |       const validation = validateInstanceContext(invalidContext);
444 |       expect(validation.valid).toBe(false);
445 |       expect(validation.errors?.length).toBeGreaterThan(0);
446 | 
447 |       // Each error should be descriptive
448 |       validation.errors?.forEach(error => {
449 |         expect(error).toContain('Invalid');
450 |         expect(typeof error).toBe('string');
451 |         expect(error.length).toBeGreaterThan(10);
452 |       });
453 |     });
454 | 
455 |     it('should provide specific error messages', () => {
456 |       const testCases = [
457 |         {
458 |           context: { n8nApiUrl: '', n8nApiKey: 'valid' },
459 |           expectedError: 'empty string'
460 |         },
461 |         {
462 |           context: { n8nApiUrl: 'https://example.com', n8nApiKey: 'placeholder' },
463 |           expectedError: 'placeholder'
464 |         },
465 |         {
466 |           context: { n8nApiUrl: 'https://example.com', n8nApiKey: 'valid', n8nApiTimeout: -1 },
467 |           expectedError: 'Must be positive'
468 |         },
469 |         {
470 |           context: { n8nApiUrl: 'https://example.com', n8nApiKey: 'valid', n8nApiMaxRetries: -1 },
471 |           expectedError: 'Must be non-negative'
472 |         }
473 |       ];
474 | 
475 |       testCases.forEach(({ context, expectedError }) => {
476 |         const validation = validateInstanceContext(context);
477 |         expect(validation.valid).toBe(false);
478 |         expect(validation.errors?.some(err => err.includes(expectedError))).toBe(true);
479 |       });
480 |     });
481 |   });
482 | });
```

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

```markdown
  1 | # Workflow Diff Examples
  2 | 
  3 | This guide demonstrates how to use the `n8n_update_partial_workflow` tool for efficient workflow editing.
  4 | 
  5 | ## Overview
  6 | 
  7 | The `n8n_update_partial_workflow` tool allows you to make targeted changes to workflows without sending the entire workflow JSON. This results in:
  8 | - 80-90% reduction in token usage
  9 | - More precise edits
 10 | - Clearer intent
 11 | - Reduced risk of accidentally modifying unrelated parts
 12 | 
 13 | ## Basic Usage
 14 | 
 15 | ```json
 16 | {
 17 |   "id": "workflow-id-here",
 18 |   "operations": [
 19 |     {
 20 |       "type": "operation-type",
 21 |       "...operation-specific-fields..."
 22 |     }
 23 |   ]
 24 | }
 25 | ```
 26 | 
 27 | ## Operation Types
 28 | 
 29 | ### 1. Node Operations
 30 | 
 31 | #### Add Node
 32 | ```json
 33 | {
 34 |   "type": "addNode",
 35 |   "description": "Add HTTP Request node to fetch data",
 36 |   "node": {
 37 |     "name": "Fetch User Data",
 38 |     "type": "n8n-nodes-base.httpRequest",
 39 |     "position": [600, 300],
 40 |     "parameters": {
 41 |       "url": "https://api.example.com/users",
 42 |       "method": "GET",
 43 |       "authentication": "none"
 44 |     }
 45 |   }
 46 | }
 47 | ```
 48 | 
 49 | #### Remove Node
 50 | ```json
 51 | {
 52 |   "type": "removeNode",
 53 |   "nodeName": "Old Node Name",
 54 |   "description": "Remove deprecated node"
 55 | }
 56 | ```
 57 | 
 58 | #### Update Node
 59 | ```json
 60 | {
 61 |   "type": "updateNode",
 62 |   "nodeName": "HTTP Request",
 63 |   "changes": {
 64 |     "parameters.url": "https://new-api.example.com/v2/users",
 65 |     "parameters.headers.parameters": [
 66 |       {
 67 |         "name": "Authorization",
 68 |         "value": "Bearer {{$credentials.apiKey}}"
 69 |       }
 70 |     ]
 71 |   },
 72 |   "description": "Update API endpoint to v2"
 73 | }
 74 | ```
 75 | 
 76 | #### Move Node
 77 | ```json
 78 | {
 79 |   "type": "moveNode",
 80 |   "nodeName": "Set Variable",
 81 |   "position": [800, 400],
 82 |   "description": "Reposition for better layout"
 83 | }
 84 | ```
 85 | 
 86 | #### Enable/Disable Node
 87 | ```json
 88 | {
 89 |   "type": "disableNode",
 90 |   "nodeName": "Debug Node",
 91 |   "description": "Disable debug output for production"
 92 | }
 93 | ```
 94 | 
 95 | ### 2. Connection Operations
 96 | 
 97 | #### Add Connection
 98 | ```json
 99 | {
100 |   "type": "addConnection",
101 |   "source": "Webhook",
102 |   "target": "Process Data",
103 |   "sourceOutput": "main",
104 |   "targetInput": "main",
105 |   "description": "Connect webhook to processor"
106 | }
107 | ```
108 | 
109 | #### Remove Connection
110 | ```json
111 | {
112 |   "type": "removeConnection",
113 |   "source": "Old Source",
114 |   "target": "Old Target",
115 |   "description": "Remove unused connection"
116 | }
117 | ```
118 | 
119 | #### Rewire Connection
120 | ```json
121 | {
122 |   "type": "rewireConnection",
123 |   "source": "Webhook",
124 |   "from": "Old Handler",
125 |   "to": "New Handler",
126 |   "description": "Rewire connection to new handler"
127 | }
128 | ```
129 | 
130 | #### Smart Parameters for IF Nodes
131 | ```json
132 | {
133 |   "type": "addConnection",
134 |   "source": "IF",
135 |   "target": "Success Handler",
136 |   "branch": "true",  // Semantic parameter instead of sourceIndex
137 |   "description": "Route true branch to success handler"
138 | }
139 | ```
140 | 
141 | ```json
142 | {
143 |   "type": "addConnection",
144 |   "source": "IF",
145 |   "target": "Error Handler",
146 |   "branch": "false",  // Routes to false branch (sourceIndex=1)
147 |   "description": "Route false branch to error handler"
148 | }
149 | ```
150 | 
151 | #### Smart Parameters for Switch Nodes
152 | ```json
153 | {
154 |   "type": "addConnection",
155 |   "source": "Switch",
156 |   "target": "Handler A",
157 |   "case": 0,  // First output
158 |   "description": "Route case 0 to Handler A"
159 | }
160 | ```
161 | 
162 | ### 3. Workflow Metadata Operations
163 | 
164 | #### Update Workflow Name
165 | ```json
166 | {
167 |   "type": "updateName",
168 |   "name": "Production User Sync v2",
169 |   "description": "Update workflow name for versioning"
170 | }
171 | ```
172 | 
173 | #### Update Settings
174 | ```json
175 | {
176 |   "type": "updateSettings",
177 |   "settings": {
178 |     "executionTimeout": 300,
179 |     "saveDataErrorExecution": "all",
180 |     "timezone": "America/New_York"
181 |   },
182 |   "description": "Configure production settings"
183 | }
184 | ```
185 | 
186 | #### Manage Tags
187 | ```json
188 | {
189 |   "type": "addTag",
190 |   "tag": "production",
191 |   "description": "Mark as production workflow"
192 | }
193 | ```
194 | 
195 | ## Complete Examples
196 | 
197 | ### Example 1: Add Slack Notification to Workflow
198 | ```json
199 | {
200 |   "id": "workflow-123",
201 |   "operations": [
202 |     {
203 |       "type": "addNode",
204 |       "node": {
205 |         "name": "Send Slack Alert",
206 |         "type": "n8n-nodes-base.slack",
207 |         "position": [1000, 300],
208 |         "parameters": {
209 |           "resource": "message",
210 |           "operation": "post",
211 |           "channel": "#alerts",
212 |           "text": "Workflow completed successfully!"
213 |         }
214 |       }
215 |     },
216 |     {
217 |       "type": "addConnection",
218 |       "source": "Process Data",
219 |       "target": "Send Slack Alert"
220 |     }
221 |   ]
222 | }
223 | ```
224 | 
225 | ### Example 2: Update Multiple Webhook Paths
226 | ```json
227 | {
228 |   "id": "workflow-456",
229 |   "operations": [
230 |     {
231 |       "type": "updateNode",
232 |       "nodeName": "Webhook 1",
233 |       "changes": {
234 |         "parameters.path": "v2/webhook1"
235 |       }
236 |     },
237 |     {
238 |       "type": "updateNode",
239 |       "nodeName": "Webhook 2",
240 |       "changes": {
241 |         "parameters.path": "v2/webhook2"
242 |       }
243 |     },
244 |     {
245 |       "type": "updateName",
246 |       "name": "API v2 Webhooks"
247 |     }
248 |   ]
249 | }
250 | ```
251 | 
252 | ### Example 3: Refactor Workflow Structure
253 | ```json
254 | {
255 |   "id": "workflow-789",
256 |   "operations": [
257 |     {
258 |       "type": "removeNode",
259 |       "nodeName": "Legacy Processor"
260 |     },
261 |     {
262 |       "type": "addNode",
263 |       "node": {
264 |         "name": "Modern Processor",
265 |         "type": "n8n-nodes-base.code",
266 |         "position": [600, 300],
267 |         "parameters": {
268 |           "mode": "runOnceForEachItem",
269 |           "jsCode": "// Process items\nreturn item;"
270 |         }
271 |       }
272 |     },
273 |     {
274 |       "type": "addConnection",
275 |       "source": "HTTP Request",
276 |       "target": "Modern Processor"
277 |     },
278 |     {
279 |       "type": "addConnection",
280 |       "source": "Modern Processor",
281 |       "target": "Save to Database"
282 |     }
283 |   ]
284 | }
285 | ```
286 | 
287 | ### Example 4: Add Error Handling
288 | ```json
289 | {
290 |   "id": "workflow-999",
291 |   "operations": [
292 |     {
293 |       "type": "addNode",
294 |       "node": {
295 |         "name": "Error Handler",
296 |         "type": "n8n-nodes-base.errorTrigger",
297 |         "position": [200, 500]
298 |       }
299 |     },
300 |     {
301 |       "type": "addNode",
302 |       "node": {
303 |         "name": "Send Error Email",
304 |         "type": "n8n-nodes-base.emailSend",
305 |         "position": [400, 500],
306 |         "parameters": {
307 |           "toEmail": "[email protected]",
308 |           "subject": "Workflow Error: {{$node['Error Handler'].json.error.message}}",
309 |           "text": "Error details: {{$json}}"
310 |         }
311 |       }
312 |     },
313 |     {
314 |       "type": "addConnection",
315 |       "source": "Error Handler",
316 |       "target": "Send Error Email"
317 |     },
318 |     {
319 |       "type": "updateSettings",
320 |       "settings": {
321 |         "errorWorkflow": "workflow-999"
322 |       }
323 |     }
324 |   ]
325 | }
326 | ```
327 | 
328 | ### Example 5: Large Batch Workflow Refactoring
329 | Demonstrates handling many operations in a single request - no longer limited to 5 operations!
330 | 
331 | ```json
332 | {
333 |   "id": "workflow-batch",
334 |   "operations": [
335 |     // Add 10 processing nodes
336 |     {
337 |       "type": "addNode",
338 |       "node": {
339 |         "name": "Filter Active Users",
340 |         "type": "n8n-nodes-base.filter",
341 |         "position": [400, 200],
342 |         "parameters": { "conditions": { "boolean": [{ "value1": "={{$json.active}}", "value2": true }] } }
343 |       }
344 |     },
345 |     {
346 |       "type": "addNode",
347 |       "node": {
348 |         "name": "Transform User Data",
349 |         "type": "n8n-nodes-base.set",
350 |         "position": [600, 200],
351 |         "parameters": { "values": { "string": [{ "name": "formatted_name", "value": "={{$json.firstName}} {{$json.lastName}}" }] } }
352 |       }
353 |     },
354 |     {
355 |       "type": "addNode",
356 |       "node": {
357 |         "name": "Validate Email",
358 |         "type": "n8n-nodes-base.if",
359 |         "position": [800, 200],
360 |         "parameters": { "conditions": { "string": [{ "value1": "={{$json.email}}", "operation": "contains", "value2": "@" }] } }
361 |       }
362 |     },
363 |     {
364 |       "type": "addNode",
365 |       "node": {
366 |         "name": "Enrich with API",
367 |         "type": "n8n-nodes-base.httpRequest",
368 |         "position": [1000, 150],
369 |         "parameters": { "url": "https://api.example.com/enrich", "method": "POST" }
370 |       }
371 |     },
372 |     {
373 |       "type": "addNode",
374 |       "node": {
375 |         "name": "Log Invalid Emails",
376 |         "type": "n8n-nodes-base.code",
377 |         "position": [1000, 350],
378 |         "parameters": { "jsCode": "console.log('Invalid email:', $json.email);\nreturn $json;" }
379 |       }
380 |     },
381 |     {
382 |       "type": "addNode",
383 |       "node": {
384 |         "name": "Merge Results",
385 |         "type": "n8n-nodes-base.merge",
386 |         "position": [1200, 250]
387 |       }
388 |     },
389 |     {
390 |       "type": "addNode",
391 |       "node": {
392 |         "name": "Deduplicate",
393 |         "type": "n8n-nodes-base.removeDuplicates",
394 |         "position": [1400, 250],
395 |         "parameters": { "propertyName": "id" }
396 |       }
397 |     },
398 |     {
399 |       "type": "addNode",
400 |       "node": {
401 |         "name": "Sort by Date",
402 |         "type": "n8n-nodes-base.sort",
403 |         "position": [1600, 250],
404 |         "parameters": { "sortFieldsUi": { "sortField": [{ "fieldName": "created_at", "order": "descending" }] } }
405 |       }
406 |     },
407 |     {
408 |       "type": "addNode",
409 |       "node": {
410 |         "name": "Batch for DB",
411 |         "type": "n8n-nodes-base.splitInBatches",
412 |         "position": [1800, 250],
413 |         "parameters": { "batchSize": 100 }
414 |       }
415 |     },
416 |     {
417 |       "type": "addNode",
418 |       "node": {
419 |         "name": "Save to Database",
420 |         "type": "n8n-nodes-base.postgres",
421 |         "position": [2000, 250],
422 |         "parameters": { "operation": "insert", "table": "processed_users" }
423 |       }
424 |     },
425 |     // Connect all the nodes
426 |     {
427 |       "type": "addConnection",
428 |       "source": "Get Users",
429 |       "target": "Filter Active Users"
430 |     },
431 |     {
432 |       "type": "addConnection",
433 |       "source": "Filter Active Users",
434 |       "target": "Transform User Data"
435 |     },
436 |     {
437 |       "type": "addConnection",
438 |       "source": "Transform User Data",
439 |       "target": "Validate Email"
440 |     },
441 |     {
442 |       "type": "addConnection",
443 |       "source": "Validate Email",
444 |       "sourceOutput": "true",
445 |       "target": "Enrich with API"
446 |     },
447 |     {
448 |       "type": "addConnection",
449 |       "source": "Validate Email",
450 |       "sourceOutput": "false",
451 |       "target": "Log Invalid Emails"
452 |     },
453 |     {
454 |       "type": "addConnection",
455 |       "source": "Enrich with API",
456 |       "target": "Merge Results"
457 |     },
458 |     {
459 |       "type": "addConnection",
460 |       "source": "Log Invalid Emails",
461 |       "target": "Merge Results",
462 |       "targetInput": "input2"
463 |     },
464 |     {
465 |       "type": "addConnection",
466 |       "source": "Merge Results",
467 |       "target": "Deduplicate"
468 |     },
469 |     {
470 |       "type": "addConnection",
471 |       "source": "Deduplicate",
472 |       "target": "Sort by Date"
473 |     },
474 |     {
475 |       "type": "addConnection",
476 |       "source": "Sort by Date",
477 |       "target": "Batch for DB"
478 |     },
479 |     {
480 |       "type": "addConnection",
481 |       "source": "Batch for DB",
482 |       "target": "Save to Database"
483 |     },
484 |     // Update workflow metadata
485 |     {
486 |       "type": "updateName",
487 |       "name": "User Processing Pipeline v2"
488 |     },
489 |     {
490 |       "type": "updateSettings",
491 |       "settings": {
492 |         "executionOrder": "v1",
493 |         "timezone": "UTC",
494 |         "saveDataSuccessExecution": "all"
495 |       }
496 |     },
497 |     {
498 |       "type": "addTag",
499 |       "tag": "production"
500 |     },
501 |     {
502 |       "type": "addTag",
503 |       "tag": "user-processing"
504 |     },
505 |     {
506 |       "type": "addTag",
507 |       "tag": "v2"
508 |     }
509 |   ]
510 | }
511 | ```
512 | 
513 | This example shows 26 operations in a single request, creating a complete data processing pipeline with proper error handling, validation, and batch processing.
514 | 
515 | ## Best Practices
516 | 
517 | 1. **Use Descriptive Names**: Always provide clear node names and descriptions for operations
518 | 2. **Batch Related Changes**: Group related operations in a single request
519 | 3. **Validate First**: Use `validateOnly: true` to test your operations before applying
520 | 4. **Reference by Name**: Prefer node names over IDs for better readability
521 | 5. **Small, Focused Changes**: Make targeted edits rather than large structural changes
522 | 
523 | ## Common Patterns
524 | 
525 | ### Add Processing Step
526 | ```json
527 | {
528 |   "operations": [
529 |     {
530 |       "type": "removeConnection",
531 |       "source": "Source Node",
532 |       "target": "Target Node"
533 |     },
534 |     {
535 |       "type": "addNode",
536 |       "node": {
537 |         "name": "Process Step",
538 |         "type": "n8n-nodes-base.set",
539 |         "position": [600, 300],
540 |         "parameters": { /* ... */ }
541 |       }
542 |     },
543 |     {
544 |       "type": "addConnection",
545 |       "source": "Source Node",
546 |       "target": "Process Step"
547 |     },
548 |     {
549 |       "type": "addConnection",
550 |       "source": "Process Step",
551 |       "target": "Target Node"
552 |     }
553 |   ]
554 | }
555 | ```
556 | 
557 | ### Replace Node
558 | ```json
559 | {
560 |   "operations": [
561 |     {
562 |       "type": "addNode",
563 |       "node": {
564 |         "name": "New Implementation",
565 |         "type": "n8n-nodes-base.httpRequest",
566 |         "position": [600, 300],
567 |         "parameters": { /* ... */ }
568 |       }
569 |     },
570 |     {
571 |       "type": "removeConnection",
572 |       "source": "Previous Node",
573 |       "target": "Old Implementation"
574 |     },
575 |     {
576 |       "type": "removeConnection",
577 |       "source": "Old Implementation",
578 |       "target": "Next Node"
579 |     },
580 |     {
581 |       "type": "addConnection",
582 |       "source": "Previous Node",
583 |       "target": "New Implementation"
584 |     },
585 |     {
586 |       "type": "addConnection",
587 |       "source": "New Implementation",
588 |       "target": "Next Node"
589 |     },
590 |     {
591 |       "type": "removeNode",
592 |       "nodeName": "Old Implementation"
593 |     }
594 |   ]
595 | }
596 | ```
597 | 
598 | ## Error Handling
599 | 
600 | The tool validates all operations before applying any changes. Common errors include:
601 | 
602 | - **Duplicate node names**: Each node must have a unique name
603 | - **Invalid node types**: Use full package prefixes (e.g., `n8n-nodes-base.webhook`)
604 | - **Missing connections**: Referenced nodes must exist
605 | - **Circular dependencies**: Connections cannot create loops
606 | 
607 | Always check the response for validation errors and adjust your operations accordingly.
608 | 
609 | ## Transactional Updates
610 | 
611 | The diff engine now supports transactional updates using a **two-pass processing** approach:
612 | 
613 | ### How It Works
614 | 
615 | 1. **No Operation Limit**: Process unlimited operations in a single request
616 | 2. **Two-Pass Processing**:
617 |    - **Pass 1**: All node operations (add, remove, update, move, enable, disable)
618 |    - **Pass 2**: All other operations (connections, settings, metadata)
619 | 
620 | This allows you to add nodes and connect them in the same request:
621 | 
622 | ```json
623 | {
624 |   "id": "workflow-id",
625 |   "operations": [
626 |     // These will be processed in Pass 2 (but work because nodes are added first)
627 |     {
628 |       "type": "addConnection",
629 |       "source": "Webhook",
630 |       "target": "Process Data"
631 |     },
632 |     {
633 |       "type": "addConnection", 
634 |       "source": "Process Data",
635 |       "target": "Send Email"
636 |     },
637 |     // These will be processed in Pass 1
638 |     {
639 |       "type": "addNode",
640 |       "node": {
641 |         "name": "Process Data",
642 |         "type": "n8n-nodes-base.set",
643 |         "position": [400, 300],
644 |         "parameters": {}
645 |       }
646 |     },
647 |     {
648 |       "type": "addNode",
649 |       "node": {
650 |         "name": "Send Email",
651 |         "type": "n8n-nodes-base.emailSend",
652 |         "position": [600, 300],
653 |         "parameters": {
654 |           "to": "[email protected]"
655 |         }
656 |       }
657 |     }
658 |   ]
659 | }
660 | ```
661 | 
662 | ### Benefits
663 | 
664 | - **Order Independence**: You don't need to worry about operation order
665 | - **Atomic Updates**: All operations succeed or all fail (unless continueOnError is enabled)
666 | - **Intuitive Usage**: Add complex workflow structures in one call
667 | - **No Hard Limits**: Process unlimited operations efficiently
668 | 
669 | ### Example: Complete Workflow Addition
670 | 
671 | ```json
672 | {
673 |   "id": "workflow-id",
674 |   "operations": [
675 |     // Add three nodes
676 |     {
677 |       "type": "addNode",
678 |       "node": {
679 |         "name": "Schedule",
680 |         "type": "n8n-nodes-base.schedule",
681 |         "position": [200, 300],
682 |         "parameters": {
683 |           "rule": {
684 |             "interval": [{ "field": "hours", "intervalValue": 1 }]
685 |           }
686 |         }
687 |       }
688 |     },
689 |     {
690 |       "type": "addNode", 
691 |       "node": {
692 |         "name": "Get Data",
693 |         "type": "n8n-nodes-base.httpRequest",
694 |         "position": [400, 300],
695 |         "parameters": {
696 |           "url": "https://api.example.com/data"
697 |         }
698 |       }
699 |     },
700 |     {
701 |       "type": "addNode",
702 |       "node": {
703 |         "name": "Save to Database",
704 |         "type": "n8n-nodes-base.postgres",
705 |         "position": [600, 300],
706 |         "parameters": {
707 |           "operation": "insert"
708 |         }
709 |       }
710 |     },
711 |     // Connect them all
712 |     {
713 |       "type": "addConnection",
714 |       "source": "Schedule",
715 |       "target": "Get Data"
716 |     },
717 |     {
718 |       "type": "addConnection",
719 |       "source": "Get Data", 
720 |       "target": "Save to Database"
721 |     }
722 |   ]
723 | }
724 | ```
725 | 
726 | All operations will be processed correctly regardless of order!
```

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

```typescript
  1 | import { promises as fs } from 'fs';
  2 | import * as fsSync from 'fs';
  3 | import path from 'path';
  4 | import { logger } from '../utils/logger';
  5 | 
  6 | /**
  7 |  * Unified database interface that abstracts better-sqlite3 and sql.js
  8 |  */
  9 | export interface DatabaseAdapter {
 10 |   prepare(sql: string): PreparedStatement;
 11 |   exec(sql: string): void;
 12 |   close(): void;
 13 |   pragma(key: string, value?: any): any;
 14 |   readonly inTransaction: boolean;
 15 |   transaction<T>(fn: () => T): T;
 16 |   checkFTS5Support(): boolean;
 17 | }
 18 | 
 19 | export interface PreparedStatement {
 20 |   run(...params: any[]): RunResult;
 21 |   get(...params: any[]): any;
 22 |   all(...params: any[]): any[];
 23 |   iterate(...params: any[]): IterableIterator<any>;
 24 |   pluck(toggle?: boolean): this;
 25 |   expand(toggle?: boolean): this;
 26 |   raw(toggle?: boolean): this;
 27 |   columns(): ColumnDefinition[];
 28 |   bind(...params: any[]): this;
 29 | }
 30 | 
 31 | export interface RunResult {
 32 |   changes: number;
 33 |   lastInsertRowid: number | bigint;
 34 | }
 35 | 
 36 | export interface ColumnDefinition {
 37 |   name: string;
 38 |   column: string | null;
 39 |   table: string | null;
 40 |   database: string | null;
 41 |   type: string | null;
 42 | }
 43 | 
 44 | /**
 45 |  * Factory function to create a database adapter
 46 |  * Tries better-sqlite3 first, falls back to sql.js if needed
 47 |  */
 48 | export async function createDatabaseAdapter(dbPath: string): Promise<DatabaseAdapter> {
 49 |   // Log Node.js version information
 50 |   // Only log in non-stdio mode
 51 |   if (process.env.MCP_MODE !== 'stdio') {
 52 |     logger.info(`Node.js version: ${process.version}`);
 53 |   }
 54 |   // Only log in non-stdio mode
 55 |   if (process.env.MCP_MODE !== 'stdio') {
 56 |     logger.info(`Platform: ${process.platform} ${process.arch}`);
 57 |   }
 58 |   
 59 |   // First, try to use better-sqlite3
 60 |   try {
 61 |     if (process.env.MCP_MODE !== 'stdio') {
 62 |       logger.info('Attempting to use better-sqlite3...');
 63 |     }
 64 |     const adapter = await createBetterSQLiteAdapter(dbPath);
 65 |     if (process.env.MCP_MODE !== 'stdio') {
 66 |       logger.info('Successfully initialized better-sqlite3 adapter');
 67 |     }
 68 |     return adapter;
 69 |   } catch (error) {
 70 |     const errorMessage = error instanceof Error ? error.message : String(error);
 71 |     
 72 |     // Check if it's a version mismatch error
 73 |     if (errorMessage.includes('NODE_MODULE_VERSION') || errorMessage.includes('was compiled against a different Node.js version')) {
 74 |       if (process.env.MCP_MODE !== 'stdio') {
 75 |         logger.warn(`Node.js version mismatch detected. Better-sqlite3 was compiled for a different Node.js version.`);
 76 |       }
 77 |       if (process.env.MCP_MODE !== 'stdio') {
 78 |         logger.warn(`Current Node.js version: ${process.version}`);
 79 |       }
 80 |     }
 81 |     
 82 |     if (process.env.MCP_MODE !== 'stdio') {
 83 |       logger.warn('Failed to initialize better-sqlite3, falling back to sql.js', error);
 84 |     }
 85 |     
 86 |     // Fall back to sql.js
 87 |     try {
 88 |       const adapter = await createSQLJSAdapter(dbPath);
 89 |       if (process.env.MCP_MODE !== 'stdio') {
 90 |         logger.info('Successfully initialized sql.js adapter (pure JavaScript, no native dependencies)');
 91 |       }
 92 |       return adapter;
 93 |     } catch (sqlJsError) {
 94 |       if (process.env.MCP_MODE !== 'stdio') {
 95 |         logger.error('Failed to initialize sql.js adapter', sqlJsError);
 96 |       }
 97 |       throw new Error('Failed to initialize any database adapter');
 98 |     }
 99 |   }
100 | }
101 | 
102 | /**
103 |  * Create better-sqlite3 adapter
104 |  */
105 | async function createBetterSQLiteAdapter(dbPath: string): Promise<DatabaseAdapter> {
106 |   try {
107 |     const Database = require('better-sqlite3');
108 |     const db = new Database(dbPath);
109 |     
110 |     return new BetterSQLiteAdapter(db);
111 |   } catch (error) {
112 |     throw new Error(`Failed to create better-sqlite3 adapter: ${error}`);
113 |   }
114 | }
115 | 
116 | /**
117 |  * Create sql.js adapter with persistence
118 |  */
119 | async function createSQLJSAdapter(dbPath: string): Promise<DatabaseAdapter> {
120 |   let initSqlJs;
121 |   try {
122 |     initSqlJs = require('sql.js');
123 |   } catch (error) {
124 |     logger.error('Failed to load sql.js module:', error);
125 |     throw new Error('sql.js module not found. This might be an issue with npm package installation.');
126 |   }
127 |   
128 |   // Initialize sql.js
129 |   const SQL = await initSqlJs({
130 |     // This will look for the wasm file in node_modules
131 |     locateFile: (file: string) => {
132 |       if (file.endsWith('.wasm')) {
133 |         // Try multiple paths to find the WASM file
134 |         const possiblePaths = [
135 |           // Local development path
136 |           path.join(__dirname, '../../node_modules/sql.js/dist/', file),
137 |           // When installed as npm package
138 |           path.join(__dirname, '../../../sql.js/dist/', file),
139 |           // Alternative npm package path
140 |           path.join(process.cwd(), 'node_modules/sql.js/dist/', file),
141 |           // Try to resolve from require
142 |           path.join(path.dirname(require.resolve('sql.js')), '../dist/', file)
143 |         ];
144 |         
145 |         // Find the first existing path
146 |         for (const tryPath of possiblePaths) {
147 |           if (fsSync.existsSync(tryPath)) {
148 |             if (process.env.MCP_MODE !== 'stdio') {
149 |               logger.debug(`Found WASM file at: ${tryPath}`);
150 |             }
151 |             return tryPath;
152 |           }
153 |         }
154 |         
155 |         // If not found, try the last resort - require.resolve
156 |         try {
157 |           const wasmPath = require.resolve('sql.js/dist/sql-wasm.wasm');
158 |           if (process.env.MCP_MODE !== 'stdio') {
159 |             logger.debug(`Found WASM file via require.resolve: ${wasmPath}`);
160 |           }
161 |           return wasmPath;
162 |         } catch (e) {
163 |           // Fall back to the default path
164 |           logger.warn(`Could not find WASM file, using default path: ${file}`);
165 |           return file;
166 |         }
167 |       }
168 |       return file;
169 |     }
170 |   });
171 |   
172 |   // Try to load existing database
173 |   let db: any;
174 |   try {
175 |     const data = await fs.readFile(dbPath);
176 |     db = new SQL.Database(new Uint8Array(data));
177 |     logger.info(`Loaded existing database from ${dbPath}`);
178 |   } catch (error) {
179 |     // Create new database if file doesn't exist
180 |     db = new SQL.Database();
181 |     logger.info(`Created new database at ${dbPath}`);
182 |   }
183 |   
184 |   return new SQLJSAdapter(db, dbPath);
185 | }
186 | 
187 | /**
188 |  * Adapter for better-sqlite3
189 |  */
190 | class BetterSQLiteAdapter implements DatabaseAdapter {
191 |   constructor(private db: any) {}
192 |   
193 |   prepare(sql: string): PreparedStatement {
194 |     const stmt = this.db.prepare(sql);
195 |     return new BetterSQLiteStatement(stmt);
196 |   }
197 |   
198 |   exec(sql: string): void {
199 |     this.db.exec(sql);
200 |   }
201 |   
202 |   close(): void {
203 |     this.db.close();
204 |   }
205 |   
206 |   pragma(key: string, value?: any): any {
207 |     return this.db.pragma(key, value);
208 |   }
209 |   
210 |   get inTransaction(): boolean {
211 |     return this.db.inTransaction;
212 |   }
213 |   
214 |   transaction<T>(fn: () => T): T {
215 |     return this.db.transaction(fn)();
216 |   }
217 |   
218 |   checkFTS5Support(): boolean {
219 |     try {
220 |       // Test if FTS5 is available
221 |       this.exec("CREATE VIRTUAL TABLE IF NOT EXISTS test_fts5 USING fts5(content);");
222 |       this.exec("DROP TABLE IF EXISTS test_fts5;");
223 |       return true;
224 |     } catch (error) {
225 |       return false;
226 |     }
227 |   }
228 | }
229 | 
230 | /**
231 |  * Adapter for sql.js with persistence
232 |  */
233 | class SQLJSAdapter implements DatabaseAdapter {
234 |   private saveTimer: NodeJS.Timeout | null = null;
235 |   private saveIntervalMs: number;
236 |   private closed = false; // Prevent multiple close() calls
237 | 
238 |   // Default save interval: 5 seconds (balance between data safety and performance)
239 |   // Configurable via SQLJS_SAVE_INTERVAL_MS environment variable
240 |   //
241 |   // DATA LOSS WINDOW: Up to 5 seconds of database changes may be lost if process
242 |   // crashes before scheduleSave() timer fires. This is acceptable because:
243 |   // 1. close() calls saveToFile() immediately on graceful shutdown
244 |   // 2. Docker/Kubernetes SIGTERM provides 30s for cleanup (more than enough)
245 |   // 3. The alternative (100ms interval) caused 2.2GB memory leaks in production
246 |   // 4. MCP server is primarily read-heavy (writes are rare)
247 |   private static readonly DEFAULT_SAVE_INTERVAL_MS = 5000;
248 | 
249 |   constructor(private db: any, private dbPath: string) {
250 |     // Read save interval from environment or use default
251 |     const envInterval = process.env.SQLJS_SAVE_INTERVAL_MS;
252 |     this.saveIntervalMs = envInterval ? parseInt(envInterval, 10) : SQLJSAdapter.DEFAULT_SAVE_INTERVAL_MS;
253 | 
254 |     // Validate interval (minimum 100ms, maximum 60000ms = 1 minute)
255 |     if (isNaN(this.saveIntervalMs) || this.saveIntervalMs < 100 || this.saveIntervalMs > 60000) {
256 |       logger.warn(
257 |         `Invalid SQLJS_SAVE_INTERVAL_MS value: ${envInterval} (must be 100-60000ms), ` +
258 |         `using default ${SQLJSAdapter.DEFAULT_SAVE_INTERVAL_MS}ms`
259 |       );
260 |       this.saveIntervalMs = SQLJSAdapter.DEFAULT_SAVE_INTERVAL_MS;
261 |     }
262 | 
263 |     logger.debug(`SQLJSAdapter initialized with save interval: ${this.saveIntervalMs}ms`);
264 | 
265 |     // NOTE: No initial save scheduled here (optimization)
266 |     // Database is either:
267 |     // 1. Loaded from existing file (already persisted), or
268 |     // 2. New database (will be saved on first write operation)
269 |   }
270 |   
271 |   prepare(sql: string): PreparedStatement {
272 |     const stmt = this.db.prepare(sql);
273 |     // Don't schedule save on prepare - only on actual writes (via SQLJSStatement.run())
274 |     return new SQLJSStatement(stmt, () => this.scheduleSave());
275 |   }
276 |   
277 |   exec(sql: string): void {
278 |     this.db.exec(sql);
279 |     this.scheduleSave();
280 |   }
281 |   
282 |   close(): void {
283 |     if (this.closed) {
284 |       logger.debug('SQLJSAdapter already closed, skipping');
285 |       return;
286 |     }
287 | 
288 |     this.saveToFile();
289 |     if (this.saveTimer) {
290 |       clearTimeout(this.saveTimer);
291 |       this.saveTimer = null;
292 |     }
293 |     this.db.close();
294 |     this.closed = true;
295 |   }
296 |   
297 |   pragma(key: string, value?: any): any {
298 |     // sql.js doesn't support pragma in the same way
299 |     // We'll handle specific pragmas as needed
300 |     if (key === 'journal_mode' && value === 'WAL') {
301 |       // WAL mode not supported in sql.js, ignore
302 |       return 'memory';
303 |     }
304 |     return null;
305 |   }
306 |   
307 |   get inTransaction(): boolean {
308 |     // sql.js doesn't expose transaction state
309 |     return false;
310 |   }
311 |   
312 |   transaction<T>(fn: () => T): T {
313 |     // Simple transaction implementation for sql.js
314 |     try {
315 |       this.exec('BEGIN');
316 |       const result = fn();
317 |       this.exec('COMMIT');
318 |       return result;
319 |     } catch (error) {
320 |       this.exec('ROLLBACK');
321 |       throw error;
322 |     }
323 |   }
324 |   
325 |   checkFTS5Support(): boolean {
326 |     try {
327 |       // Test if FTS5 is available
328 |       this.exec("CREATE VIRTUAL TABLE IF NOT EXISTS test_fts5 USING fts5(content);");
329 |       this.exec("DROP TABLE IF EXISTS test_fts5;");
330 |       return true;
331 |     } catch (error) {
332 |       // sql.js doesn't support FTS5
333 |       return false;
334 |     }
335 |   }
336 |   
337 |   private scheduleSave(): void {
338 |     if (this.saveTimer) {
339 |       clearTimeout(this.saveTimer);
340 |     }
341 | 
342 |     // Save after configured interval of inactivity (default: 5000ms)
343 |     // This debouncing reduces memory churn from frequent buffer allocations
344 |     //
345 |     // NOTE: Under constant write load, saves may be delayed until writes stop.
346 |     // This is acceptable because:
347 |     // 1. MCP server is primarily read-heavy (node lookups, searches)
348 |     // 2. Writes are rare (only during database rebuilds)
349 |     // 3. close() saves immediately on shutdown, flushing any pending changes
350 |     this.saveTimer = setTimeout(() => {
351 |       this.saveToFile();
352 |     }, this.saveIntervalMs);
353 |   }
354 |   
355 |   private saveToFile(): void {
356 |     try {
357 |       // Export database to Uint8Array (2-5MB typical)
358 |       const data = this.db.export();
359 | 
360 |       // Write directly without Buffer.from() copy (saves 50% memory allocation)
361 |       // writeFileSync accepts Uint8Array directly, no need for Buffer conversion
362 |       fsSync.writeFileSync(this.dbPath, data);
363 |       logger.debug(`Database saved to ${this.dbPath}`);
364 | 
365 |       // Note: 'data' reference is automatically cleared when function exits
366 |       // V8 GC will reclaim the Uint8Array once it's no longer referenced
367 |     } catch (error) {
368 |       logger.error('Failed to save database', error);
369 |     }
370 |   }
371 | }
372 | 
373 | /**
374 |  * Statement wrapper for better-sqlite3
375 |  */
376 | class BetterSQLiteStatement implements PreparedStatement {
377 |   constructor(private stmt: any) {}
378 |   
379 |   run(...params: any[]): RunResult {
380 |     return this.stmt.run(...params);
381 |   }
382 |   
383 |   get(...params: any[]): any {
384 |     return this.stmt.get(...params);
385 |   }
386 |   
387 |   all(...params: any[]): any[] {
388 |     return this.stmt.all(...params);
389 |   }
390 |   
391 |   iterate(...params: any[]): IterableIterator<any> {
392 |     return this.stmt.iterate(...params);
393 |   }
394 |   
395 |   pluck(toggle?: boolean): this {
396 |     this.stmt.pluck(toggle);
397 |     return this;
398 |   }
399 |   
400 |   expand(toggle?: boolean): this {
401 |     this.stmt.expand(toggle);
402 |     return this;
403 |   }
404 |   
405 |   raw(toggle?: boolean): this {
406 |     this.stmt.raw(toggle);
407 |     return this;
408 |   }
409 |   
410 |   columns(): ColumnDefinition[] {
411 |     return this.stmt.columns();
412 |   }
413 |   
414 |   bind(...params: any[]): this {
415 |     this.stmt.bind(...params);
416 |     return this;
417 |   }
418 | }
419 | 
420 | /**
421 |  * Statement wrapper for sql.js
422 |  */
423 | class SQLJSStatement implements PreparedStatement {
424 |   private boundParams: any = null;
425 |   
426 |   constructor(private stmt: any, private onModify: () => void) {}
427 |   
428 |   run(...params: any[]): RunResult {
429 |     try {
430 |       if (params.length > 0) {
431 |         this.bindParams(params);
432 |         if (this.boundParams) {
433 |           this.stmt.bind(this.boundParams);
434 |         }
435 |       }
436 |       
437 |       this.stmt.run();
438 |       this.onModify();
439 |       
440 |       // sql.js doesn't provide changes/lastInsertRowid easily
441 |       return {
442 |         changes: 1, // Assume success means 1 change
443 |         lastInsertRowid: 0
444 |       };
445 |     } catch (error) {
446 |       this.stmt.reset();
447 |       throw error;
448 |     }
449 |   }
450 |   
451 |   get(...params: any[]): any {
452 |     try {
453 |       if (params.length > 0) {
454 |         this.bindParams(params);
455 |         if (this.boundParams) {
456 |           this.stmt.bind(this.boundParams);
457 |         }
458 |       }
459 |       
460 |       if (this.stmt.step()) {
461 |         const result = this.stmt.getAsObject();
462 |         this.stmt.reset();
463 |         return this.convertIntegerColumns(result);
464 |       }
465 |       
466 |       this.stmt.reset();
467 |       return undefined;
468 |     } catch (error) {
469 |       this.stmt.reset();
470 |       throw error;
471 |     }
472 |   }
473 |   
474 |   all(...params: any[]): any[] {
475 |     try {
476 |       if (params.length > 0) {
477 |         this.bindParams(params);
478 |         if (this.boundParams) {
479 |           this.stmt.bind(this.boundParams);
480 |         }
481 |       }
482 |       
483 |       const results: any[] = [];
484 |       while (this.stmt.step()) {
485 |         results.push(this.convertIntegerColumns(this.stmt.getAsObject()));
486 |       }
487 |       
488 |       this.stmt.reset();
489 |       return results;
490 |     } catch (error) {
491 |       this.stmt.reset();
492 |       throw error;
493 |     }
494 |   }
495 |   
496 |   iterate(...params: any[]): IterableIterator<any> {
497 |     // sql.js doesn't support generators well, return array iterator
498 |     return this.all(...params)[Symbol.iterator]();
499 |   }
500 |   
501 |   pluck(toggle?: boolean): this {
502 |     // Not directly supported in sql.js
503 |     return this;
504 |   }
505 |   
506 |   expand(toggle?: boolean): this {
507 |     // Not directly supported in sql.js
508 |     return this;
509 |   }
510 |   
511 |   raw(toggle?: boolean): this {
512 |     // Not directly supported in sql.js
513 |     return this;
514 |   }
515 |   
516 |   columns(): ColumnDefinition[] {
517 |     // sql.js has different column info
518 |     return [];
519 |   }
520 |   
521 |   bind(...params: any[]): this {
522 |     this.bindParams(params);
523 |     return this;
524 |   }
525 |   
526 |   private bindParams(params: any[]): void {
527 |     if (params.length === 0) {
528 |       this.boundParams = null;
529 |       return;
530 |     }
531 |     
532 |     if (params.length === 1 && typeof params[0] === 'object' && !Array.isArray(params[0]) && params[0] !== null) {
533 |       // Named parameters passed as object
534 |       this.boundParams = params[0];
535 |     } else {
536 |       // Positional parameters - sql.js uses array for positional
537 |       // Filter out undefined values that might cause issues
538 |       this.boundParams = params.map(p => p === undefined ? null : p);
539 |     }
540 |   }
541 |   
542 |   /**
543 |    * Convert SQLite integer columns to JavaScript numbers
544 |    * sql.js returns all values as strings, but we need proper types for boolean conversion
545 |    */
546 |   private convertIntegerColumns(row: any): any {
547 |     if (!row) return row;
548 |     
549 |     // Known integer columns in the nodes table
550 |     const integerColumns = ['is_ai_tool', 'is_trigger', 'is_webhook', 'is_versioned'];
551 |     
552 |     const converted = { ...row };
553 |     for (const col of integerColumns) {
554 |       if (col in converted && typeof converted[col] === 'string') {
555 |         converted[col] = parseInt(converted[col], 10);
556 |       }
557 |     }
558 |     
559 |     return converted;
560 |   }
561 | }
```

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

```typescript
  1 | import { describe, it, expect, beforeEach, vi } from 'vitest';
  2 | import { TemplateRepository, StoredTemplate } from '../../../src/templates/template-repository';
  3 | import { DatabaseAdapter, PreparedStatement, RunResult } from '../../../src/database/database-adapter';
  4 | import { TemplateWorkflow, TemplateDetail } from '../../../src/templates/template-fetcher';
  5 | 
  6 | // Mock logger
  7 | vi.mock('../../../src/utils/logger', () => ({
  8 |   logger: {
  9 |     info: vi.fn(),
 10 |     warn: vi.fn(),
 11 |     error: vi.fn(),
 12 |     debug: vi.fn()
 13 |   }
 14 | }));
 15 | 
 16 | // Mock template sanitizer
 17 | vi.mock('../../../src/utils/template-sanitizer', () => {
 18 |   class MockTemplateSanitizer {
 19 |     sanitizeWorkflow = vi.fn((workflow) => ({ sanitized: workflow, wasModified: false }));
 20 |     detectTokens = vi.fn(() => []);
 21 |   }
 22 |   
 23 |   return {
 24 |     TemplateSanitizer: MockTemplateSanitizer
 25 |   };
 26 | });
 27 | 
 28 | // Create mock database adapter
 29 | class MockDatabaseAdapter implements DatabaseAdapter {
 30 |   private statements = new Map<string, MockPreparedStatement>();
 31 |   private mockData = new Map<string, any>();
 32 |   private _fts5Support = true;
 33 |   
 34 |   prepare = vi.fn((sql: string) => {
 35 |     if (!this.statements.has(sql)) {
 36 |       this.statements.set(sql, new MockPreparedStatement(sql, this.mockData));
 37 |     }
 38 |     return this.statements.get(sql)!;
 39 |   });
 40 |   
 41 |   exec = vi.fn();
 42 |   close = vi.fn();
 43 |   pragma = vi.fn();
 44 |   transaction = vi.fn((fn: () => any) => fn());
 45 |   checkFTS5Support = vi.fn(() => this._fts5Support);
 46 |   inTransaction = false;
 47 |   
 48 |   // Test helpers
 49 |   _setFTS5Support(supported: boolean) {
 50 |     this._fts5Support = supported;
 51 |   }
 52 |   
 53 |   _setMockData(key: string, value: any) {
 54 |     this.mockData.set(key, value);
 55 |   }
 56 |   
 57 |   _getStatement(sql: string) {
 58 |     return this.statements.get(sql);
 59 |   }
 60 | }
 61 | 
 62 | class MockPreparedStatement implements PreparedStatement {
 63 |   run = vi.fn((...params: any[]): RunResult => ({ changes: 1, lastInsertRowid: 1 }));
 64 |   get = vi.fn();
 65 |   all = vi.fn(() => []);
 66 |   iterate = vi.fn();
 67 |   pluck = vi.fn(() => this);
 68 |   expand = vi.fn(() => this);
 69 |   raw = vi.fn(() => this);
 70 |   columns = vi.fn(() => []);
 71 |   bind = vi.fn(() => this);
 72 |   
 73 |   constructor(private sql: string, private mockData: Map<string, any>) {
 74 |     // Configure based on SQL patterns
 75 |     if (sql.includes('SELECT * FROM templates WHERE id = ?')) {
 76 |       this.get = vi.fn((id: number) => this.mockData.get(`template:${id}`));
 77 |     }
 78 |     
 79 |     if (sql.includes('SELECT * FROM templates') && sql.includes('LIMIT')) {
 80 |       this.all = vi.fn(() => this.mockData.get('all_templates') || []);
 81 |     }
 82 |     
 83 |     if (sql.includes('templates_fts')) {
 84 |       this.all = vi.fn(() => this.mockData.get('fts_results') || []);
 85 |     }
 86 |     
 87 |     if (sql.includes('WHERE name LIKE')) {
 88 |       this.all = vi.fn(() => this.mockData.get('like_results') || []);
 89 |     }
 90 |     
 91 |     if (sql.includes('COUNT(*) as count')) {
 92 |       this.get = vi.fn(() => ({ count: this.mockData.get('template_count') || 0 }));
 93 |     }
 94 |     
 95 |     if (sql.includes('AVG(views)')) {
 96 |       this.get = vi.fn(() => ({ avg: this.mockData.get('avg_views') || 0 }));
 97 |     }
 98 |     
 99 |     if (sql.includes('sqlite_master')) {
100 |       this.get = vi.fn(() => this.mockData.get('fts_table_exists') ? { name: 'templates_fts' } : undefined);
101 |     }
102 |   }
103 | }
104 | 
105 | describe('TemplateRepository - Core Functionality', () => {
106 |   let repository: TemplateRepository;
107 |   let mockAdapter: MockDatabaseAdapter;
108 |   
109 |   beforeEach(() => {
110 |     vi.clearAllMocks();
111 |     mockAdapter = new MockDatabaseAdapter();
112 |     mockAdapter._setMockData('fts_table_exists', false); // Default to creating FTS
113 |     repository = new TemplateRepository(mockAdapter);
114 |   });
115 |   
116 |   describe('FTS5 initialization', () => {
117 |     it('should initialize FTS5 when supported', () => {
118 |       expect(mockAdapter.checkFTS5Support).toHaveBeenCalled();
119 |       expect(mockAdapter.exec).toHaveBeenCalledWith(expect.stringContaining('CREATE VIRTUAL TABLE'));
120 |     });
121 |     
122 |     it('should skip FTS5 when not supported', () => {
123 |       mockAdapter._setFTS5Support(false);
124 |       mockAdapter.exec.mockClear();
125 |       
126 |       const newRepo = new TemplateRepository(mockAdapter);
127 |       
128 |       expect(mockAdapter.exec).not.toHaveBeenCalledWith(expect.stringContaining('CREATE VIRTUAL TABLE'));
129 |     });
130 |   });
131 |   
132 |   describe('saveTemplate', () => {
133 |     it('should save a template with proper JSON serialization', () => {
134 |       const workflow: TemplateWorkflow = {
135 |         id: 123,
136 |         name: 'Test Workflow',
137 |         description: 'A test workflow',
138 |         user: {
139 |           id: 1,
140 |           name: 'John Doe',
141 |           username: 'johndoe',
142 |           verified: true
143 |         },
144 |         nodes: [
145 |           { id: 1, name: 'n8n-nodes-base.httpRequest', icon: 'fa:globe' },
146 |           { id: 2, name: 'n8n-nodes-base.slack', icon: 'fa:slack' }
147 |         ],
148 |         totalViews: 1000,
149 |         createdAt: '2024-01-01T00:00:00Z'
150 |       };
151 |       
152 |       const detail: TemplateDetail = {
153 |         id: 123,
154 |         name: 'Test Workflow',
155 |         description: 'A test workflow',
156 |         views: 1000,
157 |         createdAt: '2024-01-01T00:00:00Z',
158 |         workflow: {
159 |           nodes: [
160 |             { type: 'n8n-nodes-base.httpRequest', name: 'HTTP Request', id: '1', position: [0, 0], parameters: {}, typeVersion: 1 },
161 |             { type: 'n8n-nodes-base.slack', name: 'Slack', id: '2', position: [100, 0], parameters: {}, typeVersion: 1 }
162 |           ],
163 |           connections: {},
164 |           settings: {}
165 |         }
166 |       };
167 |       
168 |       const categories = ['automation', 'integration'];
169 |       
170 |       repository.saveTemplate(workflow, detail, categories);
171 |       
172 |       const stmt = mockAdapter._getStatement(mockAdapter.prepare.mock.calls.find(
173 |         call => call[0].includes('INSERT OR REPLACE INTO templates')
174 |       )?.[0] || '');
175 |       
176 |       // The implementation now uses gzip compression, so we just verify the call happened
177 |       expect(stmt?.run).toHaveBeenCalledWith(
178 |         123, // id
179 |         123, // workflow_id
180 |         'Test Workflow',
181 |         'A test workflow',
182 |         'John Doe',
183 |         'johndoe',
184 |         1, // verified
185 |         JSON.stringify(['n8n-nodes-base.httpRequest', 'n8n-nodes-base.slack']),
186 |         expect.any(String), // compressed workflow JSON
187 |         JSON.stringify(['automation', 'integration']),
188 |         1000, // views
189 |         '2024-01-01T00:00:00Z',
190 |         '2024-01-01T00:00:00Z',
191 |         'https://n8n.io/workflows/123'
192 |       );
193 |     });
194 |   });
195 |   
196 |   describe('getTemplate', () => {
197 |     it('should retrieve a specific template by ID', () => {
198 |       const mockTemplate: StoredTemplate = {
199 |         id: 123,
200 |         workflow_id: 123,
201 |         name: 'Test Template',
202 |         description: 'Description',
203 |         author_name: 'Author',
204 |         author_username: 'author',
205 |         author_verified: 1,
206 |         nodes_used: '[]',
207 |         workflow_json: '{}',
208 |         categories: '[]',
209 |         views: 500,
210 |         created_at: '2024-01-01',
211 |         updated_at: '2024-01-01',
212 |         url: 'https://n8n.io/workflows/123',
213 |         scraped_at: '2024-01-01'
214 |       };
215 |       
216 |       mockAdapter._setMockData('template:123', mockTemplate);
217 |       
218 |       const result = repository.getTemplate(123);
219 |       
220 |       expect(result).toEqual(mockTemplate);
221 |     });
222 |     
223 |     it('should return null for non-existent template', () => {
224 |       const result = repository.getTemplate(999);
225 |       expect(result).toBeNull();
226 |     });
227 |   });
228 |   
229 |   describe('searchTemplates', () => {
230 |     it('should use FTS5 search when available', () => {
231 |       const ftsResults: StoredTemplate[] = [{
232 |         id: 1,
233 |         workflow_id: 1,
234 |         name: 'Chatbot Workflow',
235 |         description: 'AI chatbot',
236 |         author_name: 'Author',
237 |         author_username: 'author',
238 |         author_verified: 0,
239 |         nodes_used: '[]',
240 |         workflow_json: '{}',
241 |         categories: '[]',
242 |         views: 100,
243 |         created_at: '2024-01-01',
244 |         updated_at: '2024-01-01',
245 |         url: 'https://n8n.io/workflows/1',
246 |         scraped_at: '2024-01-01'
247 |       }];
248 |       
249 |       mockAdapter._setMockData('fts_results', ftsResults);
250 |       
251 |       const results = repository.searchTemplates('chatbot', 10);
252 |       
253 |       expect(results).toEqual(ftsResults);
254 |     });
255 |     
256 |     it('should fall back to LIKE search when FTS5 is not supported', () => {
257 |       mockAdapter._setFTS5Support(false);
258 |       const newRepo = new TemplateRepository(mockAdapter);
259 |       
260 |       const likeResults: StoredTemplate[] = [{
261 |         id: 3,
262 |         workflow_id: 3,
263 |         name: 'LIKE only',
264 |         description: 'No FTS5',
265 |         author_name: 'Author',
266 |         author_username: 'author',
267 |         author_verified: 0,
268 |         nodes_used: '[]',
269 |         workflow_json: '{}',
270 |         categories: '[]',
271 |         views: 25,
272 |         created_at: '2024-01-01',
273 |         updated_at: '2024-01-01',
274 |         url: 'https://n8n.io/workflows/3',
275 |         scraped_at: '2024-01-01'
276 |       }];
277 |       
278 |       mockAdapter._setMockData('like_results', likeResults);
279 |       
280 |       const results = newRepo.searchTemplates('test', 20);
281 |       
282 |       expect(results).toEqual(likeResults);
283 |     });
284 |   });
285 |   
286 |   describe('getTemplatesByNodes', () => {
287 |     it('should find templates using specific node types', () => {
288 |       const mockTemplates: StoredTemplate[] = [{
289 |         id: 1,
290 |         workflow_id: 1,
291 |         name: 'HTTP Workflow',
292 |         description: 'Uses HTTP',
293 |         author_name: 'Author',
294 |         author_username: 'author',
295 |         author_verified: 1,
296 |         nodes_used: '["n8n-nodes-base.httpRequest"]',
297 |         workflow_json: '{}',
298 |         categories: '[]',
299 |         views: 100,
300 |         created_at: '2024-01-01',
301 |         updated_at: '2024-01-01',
302 |         url: 'https://n8n.io/workflows/1',
303 |         scraped_at: '2024-01-01'
304 |       }];
305 |       
306 |       // Set up the mock to return our templates
307 |       const stmt = new MockPreparedStatement('', new Map());
308 |       stmt.all = vi.fn(() => mockTemplates);
309 |       mockAdapter.prepare = vi.fn(() => stmt);
310 |       
311 |       const results = repository.getTemplatesByNodes(['n8n-nodes-base.httpRequest'], 5);
312 |       
313 |       expect(stmt.all).toHaveBeenCalledWith('%"n8n-nodes-base.httpRequest"%', 5, 0);
314 |       expect(results).toEqual(mockTemplates);
315 |     });
316 |   });
317 |   
318 |   describe('getTemplatesForTask', () => {
319 |     it('should return templates for known tasks', () => {
320 |       const aiTemplates: StoredTemplate[] = [{
321 |         id: 1,
322 |         workflow_id: 1,
323 |         name: 'AI Workflow',
324 |         description: 'Uses OpenAI',
325 |         author_name: 'Author',
326 |         author_username: 'author',
327 |         author_verified: 1,
328 |         nodes_used: '["@n8n/n8n-nodes-langchain.openAi"]',
329 |         workflow_json: '{}',
330 |         categories: '["ai"]',
331 |         views: 1000,
332 |         created_at: '2024-01-01',
333 |         updated_at: '2024-01-01',
334 |         url: 'https://n8n.io/workflows/1',
335 |         scraped_at: '2024-01-01'
336 |       }];
337 |       
338 |       const stmt = new MockPreparedStatement('', new Map());
339 |       stmt.all = vi.fn(() => aiTemplates);
340 |       mockAdapter.prepare = vi.fn(() => stmt);
341 |       
342 |       const results = repository.getTemplatesForTask('ai_automation');
343 |       
344 |       expect(results).toEqual(aiTemplates);
345 |     });
346 |     
347 |     it('should return empty array for unknown task', () => {
348 |       const results = repository.getTemplatesForTask('unknown_task');
349 |       expect(results).toEqual([]);
350 |     });
351 |   });
352 |   
353 |   describe('template statistics', () => {
354 |     it('should get template count', () => {
355 |       mockAdapter._setMockData('template_count', 42);
356 |       
357 |       const count = repository.getTemplateCount();
358 |       
359 |       expect(count).toBe(42);
360 |     });
361 |     
362 |     it('should get template statistics', () => {
363 |       mockAdapter._setMockData('template_count', 100);
364 |       mockAdapter._setMockData('avg_views', 250.5);
365 |       
366 |       const topTemplates = [
367 |         { nodes_used: '["n8n-nodes-base.httpRequest", "n8n-nodes-base.slack"]' },
368 |         { nodes_used: '["n8n-nodes-base.httpRequest", "n8n-nodes-base.code"]' },
369 |         { nodes_used: '["n8n-nodes-base.slack"]' }
370 |       ];
371 |       
372 |       const stmt = new MockPreparedStatement('', new Map());
373 |       stmt.all = vi.fn(() => topTemplates);
374 |       mockAdapter.prepare = vi.fn((sql) => {
375 |         if (sql.includes('ORDER BY views DESC')) {
376 |           return stmt;
377 |         }
378 |         return new MockPreparedStatement(sql, mockAdapter['mockData']);
379 |       });
380 |       
381 |       const stats = repository.getTemplateStats();
382 |       
383 |       expect(stats.totalTemplates).toBe(100);
384 |       expect(stats.averageViews).toBe(251);
385 |       expect(stats.topUsedNodes).toContainEqual({ node: 'n8n-nodes-base.httpRequest', count: 2 });
386 |     });
387 |   });
388 | 
389 |   describe('pagination count methods', () => {
390 |     it('should get node templates count', () => {
391 |       mockAdapter._setMockData('node_templates_count', 15);
392 |       
393 |       const stmt = new MockPreparedStatement('', new Map());
394 |       stmt.get = vi.fn(() => ({ count: 15 }));
395 |       mockAdapter.prepare = vi.fn(() => stmt);
396 |       
397 |       const count = repository.getNodeTemplatesCount(['n8n-nodes-base.webhook']);
398 |       
399 |       expect(count).toBe(15);
400 |       expect(stmt.get).toHaveBeenCalledWith('%"n8n-nodes-base.webhook"%');
401 |     });
402 | 
403 |     it('should get search count', () => {
404 |       const stmt = new MockPreparedStatement('', new Map());
405 |       stmt.get = vi.fn(() => ({ count: 8 }));
406 |       mockAdapter.prepare = vi.fn(() => stmt);
407 |       
408 |       const count = repository.getSearchCount('webhook');
409 |       
410 |       expect(count).toBe(8);
411 |     });
412 | 
413 |     it('should get task templates count', () => {
414 |       const stmt = new MockPreparedStatement('', new Map());
415 |       stmt.get = vi.fn(() => ({ count: 12 }));
416 |       mockAdapter.prepare = vi.fn(() => stmt);
417 |       
418 |       const count = repository.getTaskTemplatesCount('ai_automation');
419 |       
420 |       expect(count).toBe(12);
421 |     });
422 | 
423 |     it('should handle pagination in getAllTemplates', () => {
424 |       const mockTemplates = [
425 |         { id: 1, name: 'Template 1' },
426 |         { id: 2, name: 'Template 2' }
427 |       ];
428 |       
429 |       const stmt = new MockPreparedStatement('', new Map());
430 |       stmt.all = vi.fn(() => mockTemplates);
431 |       mockAdapter.prepare = vi.fn(() => stmt);
432 |       
433 |       const results = repository.getAllTemplates(10, 5, 'name');
434 |       
435 |       expect(results).toEqual(mockTemplates);
436 |       expect(stmt.all).toHaveBeenCalledWith(10, 5);
437 |     });
438 | 
439 |     it('should handle pagination in getTemplatesByNodes', () => {
440 |       const mockTemplates = [
441 |         { id: 1, nodes_used: '["n8n-nodes-base.webhook"]' }
442 |       ];
443 |       
444 |       const stmt = new MockPreparedStatement('', new Map());
445 |       stmt.all = vi.fn(() => mockTemplates);
446 |       mockAdapter.prepare = vi.fn(() => stmt);
447 |       
448 |       const results = repository.getTemplatesByNodes(['n8n-nodes-base.webhook'], 5, 10);
449 |       
450 |       expect(results).toEqual(mockTemplates);
451 |       expect(stmt.all).toHaveBeenCalledWith('%"n8n-nodes-base.webhook"%', 5, 10);
452 |     });
453 | 
454 |     it('should handle pagination in searchTemplates', () => {
455 |       const mockTemplates = [
456 |         { id: 1, name: 'Search Result 1' }
457 |       ];
458 |       
459 |       mockAdapter._setMockData('fts_results', mockTemplates);
460 |       
461 |       const stmt = new MockPreparedStatement('', new Map());
462 |       stmt.all = vi.fn(() => mockTemplates);
463 |       mockAdapter.prepare = vi.fn(() => stmt);
464 |       
465 |       const results = repository.searchTemplates('webhook', 20, 40);
466 |       
467 |       expect(results).toEqual(mockTemplates);
468 |     });
469 | 
470 |     it('should handle pagination in getTemplatesForTask', () => {
471 |       const mockTemplates = [
472 |         { id: 1, categories: '["ai"]' }
473 |       ];
474 |       
475 |       const stmt = new MockPreparedStatement('', new Map());
476 |       stmt.all = vi.fn(() => mockTemplates);
477 |       mockAdapter.prepare = vi.fn(() => stmt);
478 |       
479 |       const results = repository.getTemplatesForTask('ai_automation', 15, 30);
480 |       
481 |       expect(results).toEqual(mockTemplates);
482 |     });
483 |   });
484 |   
485 |   describe('maintenance operations', () => {
486 |     it('should clear all templates', () => {
487 |       repository.clearTemplates();
488 |       
489 |       expect(mockAdapter.exec).toHaveBeenCalledWith('DELETE FROM templates');
490 |     });
491 |     
492 |     it('should rebuild FTS5 index when supported', () => {
493 |       repository.rebuildTemplateFTS();
494 |       
495 |       expect(mockAdapter.exec).toHaveBeenCalledWith('DELETE FROM templates_fts');
496 |       expect(mockAdapter.exec).toHaveBeenCalledWith(
497 |         expect.stringContaining('INSERT INTO templates_fts')
498 |       );
499 |     });
500 |   });
501 | });
```

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

```typescript
  1 | /**
  2 |  * Advanced security and error handling tests for flexible instance configuration
  3 |  *
  4 |  * This test file focuses on advanced security scenarios, error handling edge cases,
  5 |  * and comprehensive testing of security-related code paths
  6 |  */
  7 | 
  8 | import { describe, it, expect, beforeEach, afterEach, vi, Mock } from 'vitest';
  9 | import { InstanceContext, validateInstanceContext } from '../../src/types/instance-context';
 10 | import { getN8nApiClient } from '../../src/mcp/handlers-n8n-manager';
 11 | import { getN8nApiConfigFromContext } from '../../src/config/n8n-api';
 12 | import { N8nApiClient } from '../../src/services/n8n-api-client';
 13 | import { logger } from '../../src/utils/logger';
 14 | import { createHash } from 'crypto';
 15 | 
 16 | // Mock dependencies
 17 | vi.mock('../../src/services/n8n-api-client');
 18 | vi.mock('../../src/config/n8n-api');
 19 | vi.mock('../../src/utils/logger');
 20 | 
 21 | describe('Advanced Security and Error Handling Tests', () => {
 22 |   let mockN8nApiClient: Mock;
 23 |   let mockGetN8nApiConfigFromContext: Mock;
 24 |   let mockLogger: any; // Logger mock has complex type
 25 | 
 26 |   beforeEach(() => {
 27 |     vi.resetAllMocks();
 28 |     vi.resetModules();
 29 | 
 30 |     mockN8nApiClient = vi.mocked(N8nApiClient);
 31 |     mockGetN8nApiConfigFromContext = vi.mocked(getN8nApiConfigFromContext);
 32 |     mockLogger = vi.mocked(logger);
 33 |   });
 34 | 
 35 |   afterEach(() => {
 36 |     vi.clearAllMocks();
 37 |   });
 38 | 
 39 |   describe('Advanced Input Sanitization', () => {
 40 |     it('should handle SQL injection attempts in context fields', () => {
 41 |       const maliciousContext = {
 42 |         n8nApiUrl: "https://api.n8n.cloud'; DROP TABLE users; --",
 43 |         n8nApiKey: "key'; DELETE FROM secrets; --",
 44 |         instanceId: "'; SELECT * FROM passwords; --"
 45 |       };
 46 | 
 47 |       const validation = validateInstanceContext(maliciousContext);
 48 | 
 49 |       // URL should be invalid due to special characters
 50 |       expect(validation.valid).toBe(false);
 51 |       expect(validation.errors?.some(error => error.startsWith('Invalid n8nApiUrl:'))).toBe(true);
 52 |     });
 53 | 
 54 |     it('should handle XSS attempts in context fields', () => {
 55 |       const xssContext = {
 56 |         n8nApiUrl: 'https://api.n8n.cloud<script>alert("xss")</script>',
 57 |         n8nApiKey: '<img src=x onerror=alert("xss")>',
 58 |         instanceId: 'javascript:alert("xss")'
 59 |       };
 60 | 
 61 |       const validation = validateInstanceContext(xssContext);
 62 | 
 63 |       // Should be invalid due to malformed URL
 64 |       expect(validation.valid).toBe(false);
 65 |     });
 66 | 
 67 |     it('should handle extremely long input values', () => {
 68 |       const longString = 'a'.repeat(100000);
 69 |       const longContext: InstanceContext = {
 70 |         n8nApiUrl: `https://api.n8n.cloud/${longString}`,
 71 |         n8nApiKey: longString,
 72 |         instanceId: longString
 73 |       };
 74 | 
 75 |       // Should handle without crashing
 76 |       expect(() => validateInstanceContext(longContext)).not.toThrow();
 77 |       expect(() => getN8nApiClient(longContext)).not.toThrow();
 78 |     });
 79 | 
 80 |     it('should handle Unicode and special characters safely', () => {
 81 |       const unicodeContext: InstanceContext = {
 82 |         n8nApiUrl: 'https://api.n8n.cloud/测试',
 83 |         n8nApiKey: 'key-ñáéíóú-кириллица-🚀',
 84 |         instanceId: '用户-123-αβγ'
 85 |       };
 86 | 
 87 |       expect(() => validateInstanceContext(unicodeContext)).not.toThrow();
 88 |       expect(() => getN8nApiClient(unicodeContext)).not.toThrow();
 89 |     });
 90 | 
 91 |     it('should handle null bytes and control characters', () => {
 92 |       const maliciousContext = {
 93 |         n8nApiUrl: 'https://api.n8n.cloud\0\x01\x02',
 94 |         n8nApiKey: 'key\r\n\t\0',
 95 |         instanceId: 'instance\x00\x1f'
 96 |       };
 97 | 
 98 |       expect(() => validateInstanceContext(maliciousContext)).not.toThrow();
 99 |     });
100 |   });
101 | 
102 |   describe('Prototype Pollution Protection', () => {
103 |     it('should not be vulnerable to prototype pollution via __proto__', () => {
104 |       const pollutionAttempt = {
105 |         n8nApiUrl: 'https://api.n8n.cloud',
106 |         n8nApiKey: 'test-key',
107 |         __proto__: {
108 |           isAdmin: true,
109 |           polluted: 'value'
110 |         }
111 |       };
112 | 
113 |       expect(() => validateInstanceContext(pollutionAttempt)).not.toThrow();
114 | 
115 |       // Verify prototype wasn't polluted
116 |       const cleanObject = {};
117 |       expect((cleanObject as any).isAdmin).toBeUndefined();
118 |       expect((cleanObject as any).polluted).toBeUndefined();
119 |     });
120 | 
121 |     it('should not be vulnerable to prototype pollution via constructor', () => {
122 |       const pollutionAttempt = {
123 |         n8nApiUrl: 'https://api.n8n.cloud',
124 |         n8nApiKey: 'test-key',
125 |         constructor: {
126 |           prototype: {
127 |             isAdmin: true
128 |           }
129 |         }
130 |       };
131 | 
132 |       expect(() => validateInstanceContext(pollutionAttempt)).not.toThrow();
133 |     });
134 | 
135 |     it('should handle Object.create(null) safely', () => {
136 |       const nullProtoObject = Object.create(null);
137 |       nullProtoObject.n8nApiUrl = 'https://api.n8n.cloud';
138 |       nullProtoObject.n8nApiKey = 'test-key';
139 | 
140 |       expect(() => validateInstanceContext(nullProtoObject)).not.toThrow();
141 |     });
142 |   });
143 | 
144 |   describe('Memory Exhaustion Protection', () => {
145 |     it('should handle deeply nested objects without stack overflow', () => {
146 |       let deepObject: any = { n8nApiUrl: 'https://api.n8n.cloud', n8nApiKey: 'key' };
147 |       for (let i = 0; i < 1000; i++) {
148 |         deepObject = { nested: deepObject };
149 |       }
150 |       deepObject.metadata = deepObject;
151 | 
152 |       expect(() => validateInstanceContext(deepObject)).not.toThrow();
153 |     });
154 | 
155 |     it('should handle circular references in metadata', () => {
156 |       const circularContext: any = {
157 |         n8nApiUrl: 'https://api.n8n.cloud',
158 |         n8nApiKey: 'test-key',
159 |         metadata: {}
160 |       };
161 |       circularContext.metadata.self = circularContext;
162 |       circularContext.metadata.circular = circularContext.metadata;
163 | 
164 |       expect(() => validateInstanceContext(circularContext)).not.toThrow();
165 |     });
166 | 
167 |     it('should handle massive arrays in metadata', () => {
168 |       const massiveArray = new Array(100000).fill('data');
169 |       const arrayContext: InstanceContext = {
170 |         n8nApiUrl: 'https://api.n8n.cloud',
171 |         n8nApiKey: 'test-key',
172 |         metadata: {
173 |           massiveArray
174 |         }
175 |       };
176 | 
177 |       expect(() => validateInstanceContext(arrayContext)).not.toThrow();
178 |     });
179 |   });
180 | 
181 |   describe('Cache Security and Isolation', () => {
182 |     it('should prevent cache key collisions through hash security', () => {
183 |       mockGetN8nApiConfigFromContext.mockReturnValue({
184 |         baseUrl: 'https://api.n8n.cloud',
185 |         apiKey: 'test-key',
186 |         timeout: 30000,
187 |         maxRetries: 3
188 |       });
189 | 
190 |       // Create contexts that might produce hash collisions
191 |       const context1: InstanceContext = {
192 |         n8nApiUrl: 'https://api.n8n.cloud',
193 |         n8nApiKey: 'abc',
194 |         instanceId: 'def'
195 |       };
196 | 
197 |       const context2: InstanceContext = {
198 |         n8nApiUrl: 'https://api.n8n.cloud',
199 |         n8nApiKey: 'ab',
200 |         instanceId: 'cdef'
201 |       };
202 | 
203 |       const hash1 = createHash('sha256')
204 |         .update(`${context1.n8nApiUrl}:${context1.n8nApiKey}:${context1.instanceId}`)
205 |         .digest('hex');
206 | 
207 |       const hash2 = createHash('sha256')
208 |         .update(`${context2.n8nApiUrl}:${context2.n8nApiKey}:${context2.instanceId}`)
209 |         .digest('hex');
210 | 
211 |       expect(hash1).not.toBe(hash2);
212 | 
213 |       // Verify separate cache entries
214 |       getN8nApiClient(context1);
215 |       getN8nApiClient(context2);
216 | 
217 |       expect(mockN8nApiClient).toHaveBeenCalledTimes(2);
218 |     });
219 | 
220 |     it('should not expose sensitive data in cache key logs', () => {
221 |       const loggerInfoSpy = vi.spyOn(logger, 'info');
222 |       const sensitiveContext: InstanceContext = {
223 |         n8nApiUrl: 'https://super-secret-api.example.com/v1/secret',
224 |         n8nApiKey: 'sk_live_SUPER_SECRET_API_KEY_123456789',
225 |         instanceId: 'production-instance-sensitive'
226 |       };
227 | 
228 |       mockGetN8nApiConfigFromContext.mockReturnValue({
229 |         baseUrl: 'https://super-secret-api.example.com/v1/secret',
230 |         apiKey: 'sk_live_SUPER_SECRET_API_KEY_123456789',
231 |         timeout: 30000,
232 |         maxRetries: 3
233 |       });
234 | 
235 |       getN8nApiClient(sensitiveContext);
236 | 
237 |       // Check all log calls
238 |       const allLogData = loggerInfoSpy.mock.calls.flat().join(' ');
239 | 
240 |       // Should not contain sensitive data
241 |       expect(allLogData).not.toContain('sk_live_SUPER_SECRET_API_KEY_123456789');
242 |       expect(allLogData).not.toContain('super-secret-api-key');
243 |       expect(allLogData).not.toContain('/v1/secret');
244 | 
245 |       // Logs should not expose the actual API key value
246 |       expect(allLogData).not.toContain('SUPER_SECRET');
247 |     });
248 | 
249 |     it('should handle hash collisions securely', () => {
250 |       // Mock a scenario where two different inputs could theoretically
251 |       // produce the same hash (extremely unlikely with SHA-256)
252 |       const context1: InstanceContext = {
253 |         n8nApiUrl: 'https://api1.n8n.cloud',
254 |         n8nApiKey: 'key1',
255 |         instanceId: 'instance1'
256 |       };
257 | 
258 |       const context2: InstanceContext = {
259 |         n8nApiUrl: 'https://api2.n8n.cloud',
260 |         n8nApiKey: 'key2',
261 |         instanceId: 'instance2'
262 |       };
263 | 
264 |       mockGetN8nApiConfigFromContext.mockReturnValue({
265 |         baseUrl: 'https://api.n8n.cloud',
266 |         apiKey: 'test-key',
267 |         timeout: 30000,
268 |         maxRetries: 3
269 |       });
270 | 
271 |       // Even if hashes were identical, different configs would be isolated
272 |       getN8nApiClient(context1);
273 |       getN8nApiClient(context2);
274 | 
275 |       expect(mockN8nApiClient).toHaveBeenCalledTimes(2);
276 |     });
277 |   });
278 | 
279 |   describe('Error Message Security', () => {
280 |     it('should not expose sensitive data in validation error messages', () => {
281 |       const sensitiveContext: InstanceContext = {
282 |         n8nApiUrl: 'https://secret-api.example.com/private-endpoint',
283 |         n8nApiKey: 'super-secret-key-123',
284 |         n8nApiTimeout: -1
285 |       };
286 | 
287 |       const validation = validateInstanceContext(sensitiveContext);
288 | 
289 |       expect(validation.valid).toBe(false);
290 | 
291 |       // Error messages should not contain sensitive data
292 |       const errorMessage = validation.errors?.join(' ') || '';
293 |       expect(errorMessage).not.toContain('super-secret-key-123');
294 |       expect(errorMessage).not.toContain('secret-api');
295 |       expect(errorMessage).not.toContain('private-endpoint');
296 |     });
297 | 
298 |     it('should sanitize error details in API responses', () => {
299 |       const sensitiveContext: InstanceContext = {
300 |         n8nApiUrl: 'invalid-url-with-secrets/api/key=secret123',
301 |         n8nApiKey: 'another-secret-key'
302 |       };
303 | 
304 |       const validation = validateInstanceContext(sensitiveContext);
305 | 
306 |       expect(validation.valid).toBe(false);
307 |       expect(validation.errors?.some(error => error.startsWith('Invalid n8nApiUrl:'))).toBe(true);
308 | 
309 |       // Should not contain the actual invalid URL
310 |       const errorData = JSON.stringify(validation);
311 |       expect(errorData).not.toContain('secret123');
312 |       expect(errorData).not.toContain('another-secret-key');
313 |     });
314 |   });
315 | 
316 |   describe('Resource Exhaustion Protection', () => {
317 |     it('should handle memory pressure gracefully', () => {
318 |       // Create many large contexts to simulate memory pressure
319 |       const largeData = 'x'.repeat(10000);
320 | 
321 |       for (let i = 0; i < 100; i++) {
322 |         const context: InstanceContext = {
323 |           n8nApiUrl: 'https://api.n8n.cloud',
324 |           n8nApiKey: `key-${i}`,
325 |           instanceId: `instance-${i}`,
326 |           metadata: {
327 |             largeData: largeData,
328 |             moreData: new Array(1000).fill(largeData)
329 |           }
330 |         };
331 | 
332 |         expect(() => validateInstanceContext(context)).not.toThrow();
333 |       }
334 |     });
335 | 
336 |     it('should handle high frequency validation requests', () => {
337 |       const context: InstanceContext = {
338 |         n8nApiUrl: 'https://api.n8n.cloud',
339 |         n8nApiKey: 'frequency-test-key'
340 |       };
341 | 
342 |       // Rapid fire validation
343 |       for (let i = 0; i < 1000; i++) {
344 |         expect(() => validateInstanceContext(context)).not.toThrow();
345 |       }
346 |     });
347 |   });
348 | 
349 |   describe('Cryptographic Security', () => {
350 |     it('should use cryptographically secure hash function', () => {
351 |       const context: InstanceContext = {
352 |         n8nApiUrl: 'https://api.n8n.cloud',
353 |         n8nApiKey: 'crypto-test-key',
354 |         instanceId: 'crypto-instance'
355 |       };
356 | 
357 |       // Generate hash multiple times - should be deterministic
358 |       const hash1 = createHash('sha256')
359 |         .update(`${context.n8nApiUrl}:${context.n8nApiKey}:${context.instanceId}`)
360 |         .digest('hex');
361 | 
362 |       const hash2 = createHash('sha256')
363 |         .update(`${context.n8nApiUrl}:${context.n8nApiKey}:${context.instanceId}`)
364 |         .digest('hex');
365 | 
366 |       expect(hash1).toBe(hash2);
367 |       expect(hash1).toHaveLength(64); // SHA-256 produces 64-character hex string
368 |       expect(hash1).toMatch(/^[a-f0-9]{64}$/);
369 |     });
370 | 
371 |     it('should handle edge cases in hash input', () => {
372 |       const edgeCases = [
373 |         { url: '', key: '', id: '' },
374 |         { url: 'https://api.n8n.cloud', key: '', id: '' },
375 |         { url: '', key: 'key', id: '' },
376 |         { url: '', key: '', id: 'id' },
377 |         { url: undefined, key: undefined, id: undefined }
378 |       ];
379 | 
380 |       edgeCases.forEach((testCase, index) => {
381 |         expect(() => {
382 |           createHash('sha256')
383 |             .update(`${testCase.url || ''}:${testCase.key || ''}:${testCase.id || ''}`)
384 |             .digest('hex');
385 |         }).not.toThrow();
386 |       });
387 |     });
388 |   });
389 | 
390 |   describe('Injection Attack Prevention', () => {
391 |     it('should prevent command injection through context fields', () => {
392 |       const commandInjectionContext = {
393 |         n8nApiUrl: 'https://api.n8n.cloud; rm -rf /',
394 |         n8nApiKey: '$(whoami)',
395 |         instanceId: '`cat /etc/passwd`'
396 |       };
397 | 
398 |       expect(() => validateInstanceContext(commandInjectionContext)).not.toThrow();
399 | 
400 |       // URL should be invalid
401 |       const validation = validateInstanceContext(commandInjectionContext);
402 |       expect(validation.valid).toBe(false);
403 |     });
404 | 
405 |     it('should prevent path traversal attempts', () => {
406 |       const pathTraversalContext = {
407 |         n8nApiUrl: 'https://api.n8n.cloud/../../../etc/passwd',
408 |         n8nApiKey: '..\\..\\windows\\system32\\config\\sam',
409 |         instanceId: '../secrets.txt'
410 |       };
411 | 
412 |       expect(() => validateInstanceContext(pathTraversalContext)).not.toThrow();
413 |     });
414 | 
415 |     it('should prevent LDAP injection attempts', () => {
416 |       const ldapInjectionContext = {
417 |         n8nApiUrl: 'https://api.n8n.cloud)(|(password=*))',
418 |         n8nApiKey: '*)(uid=*',
419 |         instanceId: '*))(|(cn=*'
420 |       };
421 | 
422 |       expect(() => validateInstanceContext(ldapInjectionContext)).not.toThrow();
423 |     });
424 |   });
425 | 
426 |   describe('State Management Security', () => {
427 |     it('should maintain isolation between contexts', () => {
428 |       const context1: InstanceContext = {
429 |         n8nApiUrl: 'https://tenant1.n8n.cloud',
430 |         n8nApiKey: 'tenant1-key',
431 |         instanceId: 'tenant1'
432 |       };
433 | 
434 |       const context2: InstanceContext = {
435 |         n8nApiUrl: 'https://tenant2.n8n.cloud',
436 |         n8nApiKey: 'tenant2-key',
437 |         instanceId: 'tenant2'
438 |       };
439 | 
440 |       mockGetN8nApiConfigFromContext
441 |         .mockReturnValueOnce({
442 |           baseUrl: 'https://tenant1.n8n.cloud',
443 |           apiKey: 'tenant1-key',
444 |           timeout: 30000,
445 |           maxRetries: 3
446 |         })
447 |         .mockReturnValueOnce({
448 |           baseUrl: 'https://tenant2.n8n.cloud',
449 |           apiKey: 'tenant2-key',
450 |           timeout: 30000,
451 |           maxRetries: 3
452 |         });
453 | 
454 |       const client1 = getN8nApiClient(context1);
455 |       const client2 = getN8nApiClient(context2);
456 | 
457 |       // Should create separate clients
458 |       expect(mockN8nApiClient).toHaveBeenCalledTimes(2);
459 |       expect(client1).not.toBe(client2);
460 |     });
461 | 
462 |     it('should handle concurrent access securely', async () => {
463 |       const contexts = Array(50).fill(null).map((_, i) => ({
464 |         n8nApiUrl: 'https://api.n8n.cloud',
465 |         n8nApiKey: `concurrent-key-${i}`,
466 |         instanceId: `concurrent-${i}`
467 |       }));
468 | 
469 |       mockGetN8nApiConfigFromContext.mockReturnValue({
470 |         baseUrl: 'https://api.n8n.cloud',
471 |         apiKey: 'test-key',
472 |         timeout: 30000,
473 |         maxRetries: 3
474 |       });
475 | 
476 |       // Simulate concurrent access
477 |       const promises = contexts.map(context =>
478 |         Promise.resolve(getN8nApiClient(context))
479 |       );
480 | 
481 |       const results = await Promise.all(promises);
482 | 
483 |       // All should succeed
484 |       results.forEach(result => {
485 |         expect(result).toBeDefined();
486 |       });
487 | 
488 |       expect(mockN8nApiClient).toHaveBeenCalledTimes(50);
489 |     });
490 |   });
491 | });
```

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

```typescript
  1 | import { describe, it, expect, beforeEach, vi } from 'vitest';
  2 | import { WorkflowValidator } from '../../../src/services/workflow-validator';
  3 | import { NodeRepository } from '../../../src/database/node-repository';
  4 | import { EnhancedConfigValidator } from '../../../src/services/enhanced-config-validator';
  5 | 
  6 | // Mock the database
  7 | vi.mock('../../../src/database/node-repository');
  8 | 
  9 | describe('WorkflowValidator - Expression Format Validation', () => {
 10 |   let validator: WorkflowValidator;
 11 |   let mockNodeRepository: any;
 12 | 
 13 |   beforeEach(() => {
 14 |     // Create mock repository
 15 |     mockNodeRepository = {
 16 |       findNodeByType: vi.fn().mockImplementation((type: string) => {
 17 |         // Return mock nodes for common types
 18 |         if (type === 'n8n-nodes-base.emailSend') {
 19 |           return {
 20 |             node_type: 'n8n-nodes-base.emailSend',
 21 |             display_name: 'Email Send',
 22 |             properties: {},
 23 |             version: 2.1
 24 |           };
 25 |         }
 26 |         if (type === 'n8n-nodes-base.github') {
 27 |           return {
 28 |             node_type: 'n8n-nodes-base.github',
 29 |             display_name: 'GitHub',
 30 |             properties: {},
 31 |             version: 1.1
 32 |           };
 33 |         }
 34 |         if (type === 'n8n-nodes-base.webhook') {
 35 |           return {
 36 |             node_type: 'n8n-nodes-base.webhook',
 37 |             display_name: 'Webhook',
 38 |             properties: {},
 39 |             version: 1
 40 |           };
 41 |         }
 42 |         if (type === 'n8n-nodes-base.httpRequest') {
 43 |           return {
 44 |             node_type: 'n8n-nodes-base.httpRequest',
 45 |             display_name: 'HTTP Request',
 46 |             properties: {},
 47 |             version: 4
 48 |           };
 49 |         }
 50 |         return null;
 51 |       }),
 52 |       searchNodes: vi.fn().mockReturnValue([]),
 53 |       getAllNodes: vi.fn().mockReturnValue([]),
 54 |       close: vi.fn()
 55 |     };
 56 | 
 57 |     validator = new WorkflowValidator(mockNodeRepository, EnhancedConfigValidator);
 58 |   });
 59 | 
 60 |   describe('Expression Format Detection', () => {
 61 |     it('should detect missing = prefix in simple expressions', async () => {
 62 |       const workflow = {
 63 |         nodes: [
 64 |           {
 65 |             id: '1',
 66 |             name: 'Send Email',
 67 |             type: 'n8n-nodes-base.emailSend',
 68 |             position: [0, 0] as [number, number],
 69 |             parameters: {
 70 |               fromEmail: '{{ $env.SENDER_EMAIL }}',
 71 |               toEmail: '[email protected]',
 72 |               subject: 'Test Email'
 73 |             },
 74 |             typeVersion: 2.1
 75 |           }
 76 |         ],
 77 |         connections: {}
 78 |       };
 79 | 
 80 |       const result = await validator.validateWorkflow(workflow);
 81 | 
 82 |       expect(result.valid).toBe(false);
 83 | 
 84 |       // Find expression format errors
 85 |       const formatErrors = result.errors.filter(e => e.message.includes('Expression format error'));
 86 |       expect(formatErrors).toHaveLength(1);
 87 | 
 88 |       const error = formatErrors[0];
 89 |       expect(error.message).toContain('Expression format error');
 90 |       expect(error.message).toContain('fromEmail');
 91 |       expect(error.message).toContain('{{ $env.SENDER_EMAIL }}');
 92 |       expect(error.message).toContain('={{ $env.SENDER_EMAIL }}');
 93 |     });
 94 | 
 95 |     it('should detect missing resource locator format for GitHub fields', async () => {
 96 |       const workflow = {
 97 |         nodes: [
 98 |           {
 99 |             id: '1',
100 |             name: 'GitHub',
101 |             type: 'n8n-nodes-base.github',
102 |             position: [0, 0] as [number, number],
103 |             parameters: {
104 |               operation: 'createComment',
105 |               owner: '{{ $vars.GITHUB_OWNER }}',
106 |               repository: '{{ $vars.GITHUB_REPO }}',
107 |               issueNumber: 123,
108 |               body: 'Test comment'
109 |             },
110 |             typeVersion: 1.1
111 |           }
112 |         ],
113 |         connections: {}
114 |       };
115 | 
116 |       const result = await validator.validateWorkflow(workflow);
117 | 
118 |       expect(result.valid).toBe(false);
119 |       // Should have errors for both owner and repository
120 |       const ownerError = result.errors.find(e => e.message.includes('owner'));
121 |       const repoError = result.errors.find(e => e.message.includes('repository'));
122 | 
123 |       expect(ownerError).toBeTruthy();
124 |       expect(repoError).toBeTruthy();
125 |       expect(ownerError?.message).toContain('resource locator format');
126 |       expect(ownerError?.message).toContain('__rl');
127 |     });
128 | 
129 |     it('should detect mixed content without prefix', async () => {
130 |       const workflow = {
131 |         nodes: [
132 |           {
133 |             id: '1',
134 |             name: 'HTTP Request',
135 |             type: 'n8n-nodes-base.httpRequest',
136 |             position: [0, 0] as [number, number],
137 |             parameters: {
138 |               url: 'https://api.example.com/{{ $json.endpoint }}',
139 |               headers: {
140 |                 Authorization: 'Bearer {{ $env.API_TOKEN }}'
141 |               }
142 |             },
143 |             typeVersion: 4
144 |           }
145 |         ],
146 |         connections: {}
147 |       };
148 | 
149 |       const result = await validator.validateWorkflow(workflow);
150 | 
151 |       expect(result.valid).toBe(false);
152 |       const errors = result.errors.filter(e => e.message.includes('Expression format'));
153 |       expect(errors.length).toBeGreaterThan(0);
154 | 
155 |       // Check for URL error
156 |       const urlError = errors.find(e => e.message.includes('url'));
157 |       expect(urlError).toBeTruthy();
158 |       expect(urlError?.message).toContain('=https://api.example.com/{{ $json.endpoint }}');
159 |     });
160 | 
161 |     it('should accept properly formatted expressions', async () => {
162 |       const workflow = {
163 |         nodes: [
164 |           {
165 |             id: '1',
166 |             name: 'Send Email',
167 |             type: 'n8n-nodes-base.emailSend',
168 |             position: [0, 0] as [number, number],
169 |             parameters: {
170 |               fromEmail: '={{ $env.SENDER_EMAIL }}',
171 |               toEmail: '[email protected]',
172 |               subject: '=Test {{ $json.type }}'
173 |             },
174 |             typeVersion: 2.1
175 |           }
176 |         ],
177 |         connections: {}
178 |       };
179 | 
180 |       const result = await validator.validateWorkflow(workflow);
181 | 
182 |       // Should have no expression format errors
183 |       const formatErrors = result.errors.filter(e => e.message.includes('Expression format'));
184 |       expect(formatErrors).toHaveLength(0);
185 |     });
186 | 
187 |     it('should accept resource locator format', async () => {
188 |       const workflow = {
189 |         nodes: [
190 |           {
191 |             id: '1',
192 |             name: 'GitHub',
193 |             type: 'n8n-nodes-base.github',
194 |             position: [0, 0] as [number, number],
195 |             parameters: {
196 |               operation: 'createComment',
197 |               owner: {
198 |                 __rl: true,
199 |                 value: '={{ $vars.GITHUB_OWNER }}',
200 |                 mode: 'expression'
201 |               },
202 |               repository: {
203 |                 __rl: true,
204 |                 value: '={{ $vars.GITHUB_REPO }}',
205 |                 mode: 'expression'
206 |               },
207 |               issueNumber: 123,
208 |               body: '=Test comment from {{ $json.author }}'
209 |             },
210 |             typeVersion: 1.1
211 |           }
212 |         ],
213 |         connections: {}
214 |       };
215 | 
216 |       const result = await validator.validateWorkflow(workflow);
217 | 
218 |       // Should have no expression format errors
219 |       const formatErrors = result.errors.filter(e => e.message.includes('Expression format'));
220 |       expect(formatErrors).toHaveLength(0);
221 |     });
222 | 
223 |     it('should validate nested expressions in complex parameters', async () => {
224 |       const workflow = {
225 |         nodes: [
226 |           {
227 |             id: '1',
228 |             name: 'HTTP Request',
229 |             type: 'n8n-nodes-base.httpRequest',
230 |             position: [0, 0] as [number, number],
231 |             parameters: {
232 |               method: 'POST',
233 |               url: 'https://api.example.com',
234 |               sendBody: true,
235 |               bodyParameters: {
236 |                 parameters: [
237 |                   {
238 |                     name: 'userId',
239 |                     value: '{{ $json.id }}'
240 |                   },
241 |                   {
242 |                     name: 'timestamp',
243 |                     value: '={{ $now }}'
244 |                   }
245 |                 ]
246 |               }
247 |             },
248 |             typeVersion: 4
249 |           }
250 |         ],
251 |         connections: {}
252 |       };
253 | 
254 |       const result = await validator.validateWorkflow(workflow);
255 | 
256 |       // Should detect the missing prefix in nested parameter
257 |       const errors = result.errors.filter(e => e.message.includes('Expression format'));
258 |       expect(errors.length).toBeGreaterThan(0);
259 | 
260 |       const nestedError = errors.find(e => e.message.includes('bodyParameters'));
261 |       expect(nestedError).toBeTruthy();
262 |     });
263 | 
264 |     it('should warn about RL format even with prefix', async () => {
265 |       const workflow = {
266 |         nodes: [
267 |           {
268 |             id: '1',
269 |             name: 'GitHub',
270 |             type: 'n8n-nodes-base.github',
271 |             position: [0, 0] as [number, number],
272 |             parameters: {
273 |               operation: 'createComment',
274 |               owner: '={{ $vars.GITHUB_OWNER }}',
275 |               repository: '={{ $vars.GITHUB_REPO }}',
276 |               issueNumber: 123,
277 |               body: 'Test'
278 |             },
279 |             typeVersion: 1.1
280 |           }
281 |         ],
282 |         connections: {}
283 |       };
284 | 
285 |       const result = await validator.validateWorkflow(workflow);
286 | 
287 |       // Should have warnings about using RL format
288 |       const warnings = result.warnings.filter(w => w.message.includes('resource locator format'));
289 |       expect(warnings.length).toBeGreaterThan(0);
290 |     });
291 |   });
292 | 
293 |   describe('Real-world workflow examples', () => {
294 |     it('should validate Email workflow with expression issues', async () => {
295 |       const workflow = {
296 |         name: 'Error Notification Workflow',
297 |         nodes: [
298 |           {
299 |             id: 'webhook-1',
300 |             name: 'Webhook',
301 |             type: 'n8n-nodes-base.webhook',
302 |             position: [250, 300] as [number, number],
303 |             parameters: {
304 |               path: 'error-handler',
305 |               httpMethod: 'POST'
306 |             },
307 |             typeVersion: 1
308 |           },
309 |           {
310 |             id: 'email-1',
311 |             name: 'Error Handler',
312 |             type: 'n8n-nodes-base.emailSend',
313 |             position: [450, 300] as [number, number],
314 |             parameters: {
315 |               fromEmail: '{{ $env.ADMIN_EMAIL }}',
316 |               toEmail: '[email protected]',
317 |               subject: 'Error in {{ $json.workflow }}',
318 |               message: 'An error occurred: {{ $json.error }}',
319 |               options: {
320 |                 replyTo: '={{ $env.SUPPORT_EMAIL }}'
321 |               }
322 |             },
323 |             typeVersion: 2.1
324 |           }
325 |         ],
326 |         connections: {
327 |           'Webhook': {
328 |             main: [[{ node: 'Error Handler', type: 'main', index: 0 }]]
329 |           }
330 |         }
331 |       };
332 | 
333 |       const result = await validator.validateWorkflow(workflow);
334 | 
335 |       // Should have multiple expression format errors
336 |       const formatErrors = result.errors.filter(e => e.message.includes('Expression format'));
337 |       expect(formatErrors.length).toBeGreaterThanOrEqual(3); // fromEmail, subject, message
338 | 
339 |       // Check specific errors
340 |       const fromEmailError = formatErrors.find(e => e.message.includes('fromEmail'));
341 |       expect(fromEmailError).toBeTruthy();
342 |       expect(fromEmailError?.message).toContain('={{ $env.ADMIN_EMAIL }}');
343 |     });
344 | 
345 |     it('should validate GitHub workflow with resource locator issues', async () => {
346 |       const workflow = {
347 |         name: 'GitHub Issue Handler',
348 |         nodes: [
349 |           {
350 |             id: 'webhook-1',
351 |             name: 'Issue Webhook',
352 |             type: 'n8n-nodes-base.webhook',
353 |             position: [250, 300] as [number, number],
354 |             parameters: {
355 |               path: 'github-issue',
356 |               httpMethod: 'POST'
357 |             },
358 |             typeVersion: 1
359 |           },
360 |           {
361 |             id: 'github-1',
362 |             name: 'Create Comment',
363 |             type: 'n8n-nodes-base.github',
364 |             position: [450, 300] as [number, number],
365 |             parameters: {
366 |               operation: 'createComment',
367 |               owner: '{{ $vars.GITHUB_OWNER }}',
368 |               repository: '{{ $vars.GITHUB_REPO }}',
369 |               issueNumber: '={{ $json.body.issue.number }}',
370 |               body: 'Thanks for the issue @{{ $json.body.issue.user.login }}!'
371 |             },
372 |             typeVersion: 1.1
373 |           }
374 |         ],
375 |         connections: {
376 |           'Issue Webhook': {
377 |             main: [[{ node: 'Create Comment', type: 'main', index: 0 }]]
378 |           }
379 |         }
380 |       };
381 | 
382 |       const result = await validator.validateWorkflow(workflow);
383 | 
384 |       // Should have errors for owner, repository, and body
385 |       const formatErrors = result.errors.filter(e => e.message.includes('Expression format'));
386 |       expect(formatErrors.length).toBeGreaterThanOrEqual(3);
387 | 
388 |       // Check for resource locator suggestions
389 |       const ownerError = formatErrors.find(e => e.message.includes('owner'));
390 |       expect(ownerError?.message).toContain('__rl');
391 |       expect(ownerError?.message).toContain('resource locator format');
392 |     });
393 | 
394 |     it('should provide clear fix examples in error messages', async () => {
395 |       const workflow = {
396 |         nodes: [
397 |           {
398 |             id: '1',
399 |             name: 'Process Data',
400 |             type: 'n8n-nodes-base.httpRequest',
401 |             position: [0, 0] as [number, number],
402 |             parameters: {
403 |               url: 'https://api.example.com/users/{{ $json.userId }}'
404 |             },
405 |             typeVersion: 4
406 |           }
407 |         ],
408 |         connections: {}
409 |       };
410 | 
411 |       const result = await validator.validateWorkflow(workflow);
412 | 
413 |       const error = result.errors.find(e => e.message.includes('Expression format'));
414 |       expect(error).toBeTruthy();
415 | 
416 |       // Error message should contain both incorrect and correct examples
417 |       expect(error?.message).toContain('Current (incorrect):');
418 |       expect(error?.message).toContain('"url": "https://api.example.com/users/{{ $json.userId }}"');
419 |       expect(error?.message).toContain('Fixed (correct):');
420 |       expect(error?.message).toContain('"url": "=https://api.example.com/users/{{ $json.userId }}"');
421 |     });
422 |   });
423 | 
424 |   describe('Integration with other validations', () => {
425 |     it('should validate expression format alongside syntax', async () => {
426 |       const workflow = {
427 |         nodes: [
428 |           {
429 |             id: '1',
430 |             name: 'Test Node',
431 |             type: 'n8n-nodes-base.httpRequest',
432 |             position: [0, 0] as [number, number],
433 |             parameters: {
434 |               url: '{{ $json.url',  // Syntax error: unclosed expression
435 |               headers: {
436 |                 'X-Token': '{{ $env.TOKEN }}'  // Format error: missing prefix
437 |               }
438 |             },
439 |             typeVersion: 4
440 |           }
441 |         ],
442 |         connections: {}
443 |       };
444 | 
445 |       const result = await validator.validateWorkflow(workflow);
446 | 
447 |       // Should have both syntax and format errors
448 |       const syntaxErrors = result.errors.filter(e => e.message.includes('Unmatched expression brackets'));
449 |       const formatErrors = result.errors.filter(e => e.message.includes('Expression format'));
450 | 
451 |       expect(syntaxErrors.length).toBeGreaterThan(0);
452 |       expect(formatErrors.length).toBeGreaterThan(0);
453 |     });
454 | 
455 |     it('should not interfere with node validation', async () => {
456 |       // Test that expression format validation works alongside other validations
457 |       const workflow = {
458 |         nodes: [
459 |           {
460 |             id: '1',
461 |             name: 'HTTP Request',
462 |             type: 'n8n-nodes-base.httpRequest',
463 |             position: [0, 0] as [number, number],
464 |             parameters: {
465 |               url: '{{ $json.endpoint }}',  // Expression format error
466 |               headers: {
467 |                 Authorization: '={{ $env.TOKEN }}'  // Correct format
468 |               }
469 |             },
470 |             typeVersion: 4
471 |           }
472 |         ],
473 |         connections: {}
474 |       };
475 | 
476 |       const result = await validator.validateWorkflow(workflow);
477 | 
478 |       // Should have expression format error for url field
479 |       const formatErrors = result.errors.filter(e => e.message.includes('Expression format'));
480 |       expect(formatErrors).toHaveLength(1);
481 |       expect(formatErrors[0].message).toContain('url');
482 | 
483 |       // The workflow should still have structure validation (no trigger warning, etc)
484 |       // This proves that expression validation doesn't interfere with other checks
485 |       expect(result.warnings.some(w => w.message.includes('trigger'))).toBe(true);
486 |     });
487 |   });
488 | });
```

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

```markdown
  1 | # Library Usage Guide - Multi-Tenant / Hosted Deployments
  2 | 
  3 | This guide covers using n8n-mcp as a library dependency for building multi-tenant hosted services.
  4 | 
  5 | ## Overview
  6 | 
  7 | 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.
  8 | 
  9 | ## Installation
 10 | 
 11 | ```bash
 12 | npm install n8n-mcp
 13 | ```
 14 | 
 15 | ## Core Concepts
 16 | 
 17 | ### Library Mode vs CLI Mode
 18 | 
 19 | - **CLI Mode** (default): Single-player usage via `npx n8n-mcp` or Docker
 20 | - **Library Mode**: Multi-tenant usage by importing and using the `N8NMCPEngine` class
 21 | 
 22 | ### Instance Context
 23 | 
 24 | The `InstanceContext` type allows you to pass per-request configuration to the MCP engine:
 25 | 
 26 | ```typescript
 27 | interface InstanceContext {
 28 |   // Instance-specific n8n API configuration
 29 |   n8nApiUrl?: string;
 30 |   n8nApiKey?: string;
 31 |   n8nApiTimeout?: number;
 32 |   n8nApiMaxRetries?: number;
 33 | 
 34 |   // Instance identification
 35 |   instanceId?: string;
 36 |   sessionId?: string;
 37 | 
 38 |   // Extensible metadata
 39 |   metadata?: Record<string, any>;
 40 | }
 41 | ```
 42 | 
 43 | ## Basic Example
 44 | 
 45 | ```typescript
 46 | import express from 'express';
 47 | import { N8NMCPEngine } from 'n8n-mcp';
 48 | 
 49 | const app = express();
 50 | const mcpEngine = new N8NMCPEngine({
 51 |   sessionTimeout: 3600000, // 1 hour
 52 |   logLevel: 'info'
 53 | });
 54 | 
 55 | // Handle MCP requests with per-user context
 56 | app.post('/mcp', async (req, res) => {
 57 |   const instanceContext = {
 58 |     n8nApiUrl: req.user.n8nUrl,
 59 |     n8nApiKey: req.user.n8nApiKey,
 60 |     instanceId: req.user.id
 61 |   };
 62 | 
 63 |   await mcpEngine.processRequest(req, res, instanceContext);
 64 | });
 65 | 
 66 | app.listen(3000);
 67 | ```
 68 | 
 69 | ## Multi-Tenant Backend Example
 70 | 
 71 | This example shows a complete multi-tenant implementation with user authentication and instance management:
 72 | 
 73 | ```typescript
 74 | import express from 'express';
 75 | import { N8NMCPEngine, InstanceContext, validateInstanceContext } from 'n8n-mcp';
 76 | 
 77 | const app = express();
 78 | const mcpEngine = new N8NMCPEngine({
 79 |   sessionTimeout: 3600000, // 1 hour
 80 |   logLevel: 'info'
 81 | });
 82 | 
 83 | // Start MCP engine
 84 | await mcpEngine.start();
 85 | 
 86 | // Authentication middleware
 87 | const authenticate = async (req, res, next) => {
 88 |   const token = req.headers.authorization?.replace('Bearer ', '');
 89 |   if (!token) {
 90 |     return res.status(401).json({ error: 'Unauthorized' });
 91 |   }
 92 | 
 93 |   // Verify token and attach user to request
 94 |   req.user = await getUserFromToken(token);
 95 |   next();
 96 | };
 97 | 
 98 | // Get instance configuration from database
 99 | const getInstanceConfig = async (instanceId: string, userId: string) => {
100 |   // Your database logic here
101 |   const instance = await db.instances.findOne({
102 |     where: { id: instanceId, userId }
103 |   });
104 | 
105 |   if (!instance) {
106 |     throw new Error('Instance not found');
107 |   }
108 | 
109 |   return {
110 |     n8nApiUrl: instance.n8nUrl,
111 |     n8nApiKey: await decryptApiKey(instance.encryptedApiKey),
112 |     instanceId: instance.id
113 |   };
114 | };
115 | 
116 | // MCP endpoint with per-instance context
117 | app.post('/api/instances/:instanceId/mcp', authenticate, async (req, res) => {
118 |   try {
119 |     // Get instance configuration
120 |     const instance = await getInstanceConfig(req.params.instanceId, req.user.id);
121 | 
122 |     // Create instance context
123 |     const context: InstanceContext = {
124 |       n8nApiUrl: instance.n8nApiUrl,
125 |       n8nApiKey: instance.n8nApiKey,
126 |       instanceId: instance.instanceId,
127 |       metadata: {
128 |         userId: req.user.id,
129 |         userAgent: req.headers['user-agent'],
130 |         ip: req.ip
131 |       }
132 |     };
133 | 
134 |     // Validate context before processing
135 |     const validation = validateInstanceContext(context);
136 |     if (!validation.valid) {
137 |       return res.status(400).json({
138 |         error: 'Invalid instance configuration',
139 |         details: validation.errors
140 |       });
141 |     }
142 | 
143 |     // Process request with instance context
144 |     await mcpEngine.processRequest(req, res, context);
145 | 
146 |   } catch (error) {
147 |     console.error('MCP request error:', error);
148 |     res.status(500).json({ error: 'Internal server error' });
149 |   }
150 | });
151 | 
152 | // Health endpoint
153 | app.get('/health', async (req, res) => {
154 |   const health = await mcpEngine.healthCheck();
155 |   res.status(health.status === 'healthy' ? 200 : 503).json(health);
156 | });
157 | 
158 | // Graceful shutdown
159 | process.on('SIGTERM', async () => {
160 |   await mcpEngine.shutdown();
161 |   process.exit(0);
162 | });
163 | 
164 | app.listen(3000);
165 | ```
166 | 
167 | ## API Reference
168 | 
169 | ### N8NMCPEngine
170 | 
171 | #### Constructor
172 | 
173 | ```typescript
174 | new N8NMCPEngine(options?: {
175 |   sessionTimeout?: number;  // Session TTL in ms (default: 1800000 = 30min)
176 |   logLevel?: 'error' | 'warn' | 'info' | 'debug';  // Default: 'info'
177 | })
178 | ```
179 | 
180 | #### Methods
181 | 
182 | ##### `async processRequest(req, res, context?)`
183 | 
184 | Process a single MCP request with optional instance context.
185 | 
186 | **Parameters:**
187 | - `req`: Express request object
188 | - `res`: Express response object
189 | - `context` (optional): InstanceContext with per-instance configuration
190 | 
191 | **Example:**
192 | ```typescript
193 | const context: InstanceContext = {
194 |   n8nApiUrl: 'https://instance1.n8n.cloud',
195 |   n8nApiKey: 'instance1-key',
196 |   instanceId: 'tenant-123'
197 | };
198 | 
199 | await engine.processRequest(req, res, context);
200 | ```
201 | 
202 | ##### `async healthCheck()`
203 | 
204 | Get engine health status for monitoring.
205 | 
206 | **Returns:** `EngineHealth`
207 | ```typescript
208 | {
209 |   status: 'healthy' | 'unhealthy';
210 |   uptime: number;  // seconds
211 |   sessionActive: boolean;
212 |   memoryUsage: {
213 |     used: number;
214 |     total: number;
215 |     unit: string;
216 |   };
217 |   version: string;
218 | }
219 | ```
220 | 
221 | **Example:**
222 | ```typescript
223 | app.get('/health', async (req, res) => {
224 |   const health = await engine.healthCheck();
225 |   res.status(health.status === 'healthy' ? 200 : 503).json(health);
226 | });
227 | ```
228 | 
229 | ##### `getSessionInfo()`
230 | 
231 | Get current session information for debugging.
232 | 
233 | **Returns:**
234 | ```typescript
235 | {
236 |   active: boolean;
237 |   sessionId?: string;
238 |   age?: number;  // milliseconds
239 |   sessions?: {
240 |     total: number;
241 |     active: number;
242 |     expired: number;
243 |     max: number;
244 |     sessionIds: string[];
245 |   };
246 | }
247 | ```
248 | 
249 | ##### `async start()`
250 | 
251 | Start the engine (for standalone mode). Not needed when using `processRequest()` directly.
252 | 
253 | ##### `async shutdown()`
254 | 
255 | Graceful shutdown for service lifecycle management.
256 | 
257 | **Example:**
258 | ```typescript
259 | process.on('SIGTERM', async () => {
260 |   await engine.shutdown();
261 |   process.exit(0);
262 | });
263 | ```
264 | 
265 | ### Types
266 | 
267 | #### InstanceContext
268 | 
269 | Configuration for a specific user instance:
270 | 
271 | ```typescript
272 | interface InstanceContext {
273 |   n8nApiUrl?: string;
274 |   n8nApiKey?: string;
275 |   n8nApiTimeout?: number;
276 |   n8nApiMaxRetries?: number;
277 |   instanceId?: string;
278 |   sessionId?: string;
279 |   metadata?: Record<string, any>;
280 | }
281 | ```
282 | 
283 | #### Validation Functions
284 | 
285 | ##### `validateInstanceContext(context: InstanceContext)`
286 | 
287 | Validate and sanitize instance context.
288 | 
289 | **Returns:**
290 | ```typescript
291 | {
292 |   valid: boolean;
293 |   errors?: string[];
294 | }
295 | ```
296 | 
297 | **Example:**
298 | ```typescript
299 | import { validateInstanceContext } from 'n8n-mcp';
300 | 
301 | const validation = validateInstanceContext(context);
302 | if (!validation.valid) {
303 |   console.error('Invalid context:', validation.errors);
304 | }
305 | ```
306 | 
307 | ##### `isInstanceContext(obj: any)`
308 | 
309 | Type guard to check if an object is a valid InstanceContext.
310 | 
311 | **Example:**
312 | ```typescript
313 | import { isInstanceContext } from 'n8n-mcp';
314 | 
315 | if (isInstanceContext(req.body.context)) {
316 |   // TypeScript knows this is InstanceContext
317 |   await engine.processRequest(req, res, req.body.context);
318 | }
319 | ```
320 | 
321 | ## Session Management
322 | 
323 | ### Session Strategies
324 | 
325 | The MCP engine supports flexible session ID formats:
326 | 
327 | - **UUIDv4**: Internal n8n-mcp format (default)
328 | - **Instance-prefixed**: `instance-{userId}-{hash}-{uuid}` for multi-tenant isolation
329 | - **Custom formats**: Any non-empty string for mcp-remote and other proxies
330 | 
331 | Session validation happens via transport lookup, not format validation. This ensures compatibility with all MCP clients.
332 | 
333 | ### Multi-Tenant Configuration
334 | 
335 | Set these environment variables for multi-tenant mode:
336 | 
337 | ```bash
338 | # Enable multi-tenant mode
339 | ENABLE_MULTI_TENANT=true
340 | 
341 | # Session strategy: "instance" (default) or "shared"
342 | MULTI_TENANT_SESSION_STRATEGY=instance
343 | ```
344 | 
345 | **Session Strategies:**
346 | 
347 | - **instance** (recommended): Each tenant gets isolated sessions
348 |   - Session ID: `instance-{instanceId}-{configHash}-{uuid}`
349 |   - Better isolation and security
350 |   - Easier debugging per tenant
351 | 
352 | - **shared**: Multiple tenants share sessions with context switching
353 |   - More efficient for high tenant count
354 |   - Requires careful context management
355 | 
356 | ## Security Considerations
357 | 
358 | ### API Key Management
359 | 
360 | Always encrypt API keys server-side:
361 | 
362 | ```typescript
363 | import { createCipheriv, createDecipheriv } from 'crypto';
364 | 
365 | // Encrypt before storing
366 | const encryptApiKey = (apiKey: string) => {
367 |   const cipher = createCipheriv('aes-256-gcm', encryptionKey, iv);
368 |   return cipher.update(apiKey, 'utf8', 'hex') + cipher.final('hex');
369 | };
370 | 
371 | // Decrypt before using
372 | const decryptApiKey = (encrypted: string) => {
373 |   const decipher = createDecipheriv('aes-256-gcm', encryptionKey, iv);
374 |   return decipher.update(encrypted, 'hex', 'utf8') + decipher.final('utf8');
375 | };
376 | 
377 | // Use decrypted key in context
378 | const context: InstanceContext = {
379 |   n8nApiKey: await decryptApiKey(instance.encryptedApiKey),
380 |   // ...
381 | };
382 | ```
383 | 
384 | ### Input Validation
385 | 
386 | Always validate instance context before processing:
387 | 
388 | ```typescript
389 | import { validateInstanceContext } from 'n8n-mcp';
390 | 
391 | const validation = validateInstanceContext(context);
392 | if (!validation.valid) {
393 |   throw new Error(`Invalid context: ${validation.errors?.join(', ')}`);
394 | }
395 | ```
396 | 
397 | ### Rate Limiting
398 | 
399 | Implement rate limiting per tenant:
400 | 
401 | ```typescript
402 | import rateLimit from 'express-rate-limit';
403 | 
404 | const limiter = rateLimit({
405 |   windowMs: 15 * 60 * 1000, // 15 minutes
406 |   max: 100, // limit each IP to 100 requests per windowMs
407 |   keyGenerator: (req) => req.user?.id || req.ip
408 | });
409 | 
410 | app.post('/api/instances/:instanceId/mcp', authenticate, limiter, async (req, res) => {
411 |   // ...
412 | });
413 | ```
414 | 
415 | ## Error Handling
416 | 
417 | Always wrap MCP requests in try-catch blocks:
418 | 
419 | ```typescript
420 | app.post('/api/instances/:instanceId/mcp', authenticate, async (req, res) => {
421 |   try {
422 |     const context = await getInstanceConfig(req.params.instanceId, req.user.id);
423 |     await mcpEngine.processRequest(req, res, context);
424 |   } catch (error) {
425 |     console.error('MCP error:', error);
426 | 
427 |     // Don't leak internal errors to clients
428 |     if (error.message.includes('not found')) {
429 |       return res.status(404).json({ error: 'Instance not found' });
430 |     }
431 | 
432 |     res.status(500).json({ error: 'Internal server error' });
433 |   }
434 | });
435 | ```
436 | 
437 | ## Monitoring
438 | 
439 | ### Health Checks
440 | 
441 | Set up periodic health checks:
442 | 
443 | ```typescript
444 | setInterval(async () => {
445 |   const health = await mcpEngine.healthCheck();
446 | 
447 |   if (health.status === 'unhealthy') {
448 |     console.error('MCP engine unhealthy:', health);
449 |     // Alert your monitoring system
450 |   }
451 | 
452 |   // Log metrics
453 |   console.log('MCP engine metrics:', {
454 |     uptime: health.uptime,
455 |     memory: health.memoryUsage,
456 |     sessionActive: health.sessionActive
457 |   });
458 | }, 60000); // Every minute
459 | ```
460 | 
461 | ### Session Monitoring
462 | 
463 | Track active sessions:
464 | 
465 | ```typescript
466 | app.get('/admin/sessions', authenticate, async (req, res) => {
467 |   if (!req.user.isAdmin) {
468 |     return res.status(403).json({ error: 'Forbidden' });
469 |   }
470 | 
471 |   const sessionInfo = mcpEngine.getSessionInfo();
472 |   res.json(sessionInfo);
473 | });
474 | ```
475 | 
476 | ## Testing
477 | 
478 | ### Unit Testing
479 | 
480 | ```typescript
481 | import { N8NMCPEngine, InstanceContext } from 'n8n-mcp';
482 | 
483 | describe('MCP Engine', () => {
484 |   let engine: N8NMCPEngine;
485 | 
486 |   beforeEach(() => {
487 |     engine = new N8NMCPEngine({ logLevel: 'error' });
488 |   });
489 | 
490 |   afterEach(async () => {
491 |     await engine.shutdown();
492 |   });
493 | 
494 |   it('should process request with context', async () => {
495 |     const context: InstanceContext = {
496 |       n8nApiUrl: 'https://test.n8n.io',
497 |       n8nApiKey: 'test-key',
498 |       instanceId: 'test-instance'
499 |     };
500 | 
501 |     const mockReq = createMockRequest();
502 |     const mockRes = createMockResponse();
503 | 
504 |     await engine.processRequest(mockReq, mockRes, context);
505 | 
506 |     expect(mockRes.status).toBe(200);
507 |   });
508 | });
509 | ```
510 | 
511 | ### Integration Testing
512 | 
513 | ```typescript
514 | import request from 'supertest';
515 | import { createApp } from './app';
516 | 
517 | describe('Multi-tenant MCP API', () => {
518 |   let app;
519 |   let authToken;
520 | 
521 |   beforeAll(async () => {
522 |     app = await createApp();
523 |     authToken = await getTestAuthToken();
524 |   });
525 | 
526 |   it('should handle MCP request for instance', async () => {
527 |     const response = await request(app)
528 |       .post('/api/instances/test-instance/mcp')
529 |       .set('Authorization', `Bearer ${authToken}`)
530 |       .send({
531 |         jsonrpc: '2.0',
532 |         method: 'initialize',
533 |         params: {
534 |           protocolVersion: '2024-11-05',
535 |           capabilities: {}
536 |         },
537 |         id: 1
538 |       });
539 | 
540 |     expect(response.status).toBe(200);
541 |     expect(response.body.result).toBeDefined();
542 |   });
543 | });
544 | ```
545 | 
546 | ## Deployment Considerations
547 | 
548 | ### Environment Variables
549 | 
550 | ```bash
551 | # Required for multi-tenant mode
552 | ENABLE_MULTI_TENANT=true
553 | MULTI_TENANT_SESSION_STRATEGY=instance
554 | 
555 | # Optional: Logging
556 | LOG_LEVEL=info
557 | DISABLE_CONSOLE_OUTPUT=false
558 | 
559 | # Optional: Session configuration
560 | SESSION_TIMEOUT=1800000  # 30 minutes in milliseconds
561 | MAX_SESSIONS=100
562 | 
563 | # Optional: Performance
564 | NODE_ENV=production
565 | ```
566 | 
567 | ### Docker Deployment
568 | 
569 | ```dockerfile
570 | FROM node:20-alpine
571 | 
572 | WORKDIR /app
573 | 
574 | COPY package*.json ./
575 | RUN npm ci --only=production
576 | 
577 | COPY . .
578 | 
579 | ENV NODE_ENV=production
580 | ENV ENABLE_MULTI_TENANT=true
581 | ENV LOG_LEVEL=info
582 | 
583 | EXPOSE 3000
584 | 
585 | CMD ["node", "dist/server.js"]
586 | ```
587 | 
588 | ### Kubernetes Deployment
589 | 
590 | ```yaml
591 | apiVersion: apps/v1
592 | kind: Deployment
593 | metadata:
594 |   name: n8n-mcp-backend
595 | spec:
596 |   replicas: 3
597 |   selector:
598 |     matchLabels:
599 |       app: n8n-mcp-backend
600 |   template:
601 |     metadata:
602 |       labels:
603 |         app: n8n-mcp-backend
604 |     spec:
605 |       containers:
606 |       - name: backend
607 |         image: your-registry/n8n-mcp-backend:latest
608 |         ports:
609 |         - containerPort: 3000
610 |         env:
611 |         - name: ENABLE_MULTI_TENANT
612 |           value: "true"
613 |         - name: LOG_LEVEL
614 |           value: "info"
615 |         resources:
616 |           requests:
617 |             memory: "256Mi"
618 |             cpu: "250m"
619 |           limits:
620 |             memory: "512Mi"
621 |             cpu: "500m"
622 |         livenessProbe:
623 |           httpGet:
624 |             path: /health
625 |             port: 3000
626 |           initialDelaySeconds: 10
627 |           periodSeconds: 30
628 |         readinessProbe:
629 |           httpGet:
630 |             path: /health
631 |             port: 3000
632 |           initialDelaySeconds: 5
633 |           periodSeconds: 10
634 | ```
635 | 
636 | ## Examples
637 | 
638 | ### Complete Multi-Tenant SaaS Example
639 | 
640 | For a complete implementation example, see:
641 | - [n8n-mcp-backend](https://github.com/czlonkowski/n8n-mcp-backend) - Full hosted service implementation
642 | 
643 | ### Migration from Single-Player
644 | 
645 | If you're migrating from single-player (CLI/Docker) to multi-tenant:
646 | 
647 | 1. **Keep backward compatibility** - Use environment fallback:
648 | ```typescript
649 | const context: InstanceContext = {
650 |   n8nApiUrl: instanceUrl || process.env.N8N_API_URL,
651 |   n8nApiKey: instanceKey || process.env.N8N_API_KEY,
652 |   instanceId: instanceId || 'default'
653 | };
654 | ```
655 | 
656 | 2. **Gradual rollout** - Start with a feature flag:
657 | ```typescript
658 | const isMultiTenant = process.env.ENABLE_MULTI_TENANT === 'true';
659 | 
660 | if (isMultiTenant) {
661 |   const context = await getInstanceConfig(req.params.instanceId);
662 |   await engine.processRequest(req, res, context);
663 | } else {
664 |   // Legacy single-player mode
665 |   await engine.processRequest(req, res);
666 | }
667 | ```
668 | 
669 | ## Troubleshooting
670 | 
671 | ### Common Issues
672 | 
673 | #### Module Resolution Errors
674 | 
675 | If you see `Cannot find module 'n8n-mcp'`:
676 | 
677 | ```bash
678 | # Clear node_modules and reinstall
679 | rm -rf node_modules package-lock.json
680 | npm install
681 | 
682 | # Verify package has types field
683 | npm info n8n-mcp
684 | 
685 | # Check TypeScript can resolve it
686 | npx tsc --noEmit
687 | ```
688 | 
689 | #### Session ID Validation Errors
690 | 
691 | If you see `Invalid session ID format` errors:
692 | 
693 | - Ensure you're using n8n-mcp v2.18.9 or later
694 | - Session IDs can be any non-empty string
695 | - No need to generate UUIDs - use your own format
696 | 
697 | #### Memory Leaks
698 | 
699 | If memory usage grows over time:
700 | 
701 | ```typescript
702 | // Ensure proper cleanup
703 | process.on('SIGTERM', async () => {
704 |   await engine.shutdown();
705 |   process.exit(0);
706 | });
707 | 
708 | // Monitor session count
709 | const sessionInfo = engine.getSessionInfo();
710 | console.log('Active sessions:', sessionInfo.sessions?.active);
711 | ```
712 | 
713 | ## Further Reading
714 | 
715 | - [MCP Protocol Specification](https://modelcontextprotocol.io/docs)
716 | - [n8n API Documentation](https://docs.n8n.io/api/)
717 | - [Express.js Guide](https://expressjs.com/en/guide/routing.html)
718 | - [n8n-mcp Main README](../README.md)
719 | 
720 | ## Support
721 | 
722 | - **Issues**: [GitHub Issues](https://github.com/czlonkowski/n8n-mcp/issues)
723 | - **Discussions**: [GitHub Discussions](https://github.com/czlonkowski/n8n-mcp/discussions)
724 | - **Security**: For security issues, see [SECURITY.md](../SECURITY.md)
725 | 
```

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

```typescript
  1 | import * as fs from 'fs/promises';
  2 | import * as path from 'path';
  3 | import { logger } from './logger';
  4 | 
  5 | export interface NodeSourceInfo {
  6 |   nodeType: string;
  7 |   sourceCode: string;
  8 |   credentialCode?: string;
  9 |   packageInfo?: any;
 10 |   location: string;
 11 | }
 12 | 
 13 | export class NodeSourceExtractor {
 14 |   private n8nBasePaths = [
 15 |     '/usr/local/lib/node_modules/n8n/node_modules',
 16 |     '/app/node_modules',
 17 |     '/home/node/.n8n/custom/nodes',
 18 |     './node_modules',
 19 |     // Docker volume paths
 20 |     '/var/lib/docker/volumes/n8n-mcp_n8n_modules/_data',
 21 |     '/n8n-modules',
 22 |     // Common n8n installation paths
 23 |     process.env.N8N_CUSTOM_EXTENSIONS || '',
 24 |     // Additional local path for testing
 25 |     path.join(process.cwd(), 'node_modules'),
 26 |   ].filter(Boolean);
 27 | 
 28 |   /**
 29 |    * Extract source code for a specific n8n node
 30 |    */
 31 |   async extractNodeSource(nodeType: string): Promise<NodeSourceInfo> {
 32 |     logger.info(`Extracting source code for node: ${nodeType}`);
 33 |     
 34 |     // Parse node type to get package and node name
 35 |     const { packageName, nodeName } = this.parseNodeType(nodeType);
 36 |     
 37 |     // Search for the node in known locations
 38 |     for (const basePath of this.n8nBasePaths) {
 39 |       try {
 40 |         const nodeInfo = await this.searchNodeInPath(basePath, packageName, nodeName);
 41 |         if (nodeInfo) {
 42 |           logger.info(`Found node source at: ${nodeInfo.location}`);
 43 |           return nodeInfo;
 44 |         }
 45 |       } catch (error) {
 46 |         logger.debug(`Failed to search in ${basePath}: ${error}`);
 47 |       }
 48 |     }
 49 |     
 50 |     throw new Error(`Node source code not found for: ${nodeType}`);
 51 |   }
 52 | 
 53 |   /**
 54 |    * Parse node type identifier
 55 |    */
 56 |   private parseNodeType(nodeType: string): { packageName: string; nodeName: string } {
 57 |     // Handle different formats:
 58 |     // - @n8n/n8n-nodes-langchain.Agent
 59 |     // - n8n-nodes-base.HttpRequest
 60 |     // - customNode
 61 |     
 62 |     if (nodeType.includes('.')) {
 63 |       const [pkg, node] = nodeType.split('.');
 64 |       return { packageName: pkg, nodeName: node };
 65 |     }
 66 |     
 67 |     // Default to n8n-nodes-base for simple node names
 68 |     return { packageName: 'n8n-nodes-base', nodeName: nodeType };
 69 |   }
 70 | 
 71 |   /**
 72 |    * Search for node in a specific path
 73 |    */
 74 |   private async searchNodeInPath(
 75 |     basePath: string,
 76 |     packageName: string,
 77 |     nodeName: string
 78 |   ): Promise<NodeSourceInfo | null> {
 79 |     try {
 80 |       // Try both the provided case and capitalized first letter
 81 |       const nodeNameVariants = [
 82 |         nodeName,
 83 |         nodeName.charAt(0).toUpperCase() + nodeName.slice(1), // Capitalize first letter
 84 |         nodeName.toLowerCase(), // All lowercase
 85 |         nodeName.toUpperCase(), // All uppercase
 86 |       ];
 87 |       
 88 |       // First, try standard patterns with all case variants
 89 |       for (const nameVariant of nodeNameVariants) {
 90 |         const standardPatterns = [
 91 |           `${packageName}/dist/nodes/${nameVariant}/${nameVariant}.node.js`,
 92 |           `${packageName}/dist/nodes/${nameVariant}.node.js`,
 93 |           `${packageName}/nodes/${nameVariant}/${nameVariant}.node.js`,
 94 |           `${packageName}/nodes/${nameVariant}.node.js`,
 95 |           `${nameVariant}/${nameVariant}.node.js`,
 96 |           `${nameVariant}.node.js`,
 97 |         ];
 98 | 
 99 |         // Additional patterns for nested node structures (e.g., agents/Agent)
100 |         const nestedPatterns = [
101 |           `${packageName}/dist/nodes/*/${nameVariant}/${nameVariant}.node.js`,
102 |           `${packageName}/dist/nodes/**/${nameVariant}/${nameVariant}.node.js`,
103 |           `${packageName}/nodes/*/${nameVariant}/${nameVariant}.node.js`,
104 |           `${packageName}/nodes/**/${nameVariant}/${nameVariant}.node.js`,
105 |         ];
106 | 
107 |         // Try standard patterns first
108 |         for (const pattern of standardPatterns) {
109 |           const fullPath = path.join(basePath, pattern);
110 |           const result = await this.tryLoadNodeFile(fullPath, packageName, nodeName, basePath);
111 |           if (result) return result;
112 |         }
113 | 
114 |         // Try nested patterns (with glob-like search)
115 |         for (const pattern of nestedPatterns) {
116 |           const result = await this.searchWithGlobPattern(basePath, pattern, packageName, nodeName);
117 |           if (result) return result;
118 |         }
119 |       }
120 | 
121 |       // If basePath contains .pnpm, search in pnpm structure
122 |       if (basePath.includes('node_modules')) {
123 |         const pnpmPath = path.join(basePath, '.pnpm');
124 |         try {
125 |           await fs.access(pnpmPath);
126 |           const result = await this.searchInPnpm(pnpmPath, packageName, nodeName);
127 |           if (result) return result;
128 |         } catch {
129 |           // .pnpm directory doesn't exist
130 |         }
131 |       }
132 |     } catch (error) {
133 |       logger.debug(`Error searching in path ${basePath}: ${error}`);
134 |     }
135 | 
136 |     return null;
137 |   }
138 | 
139 |   /**
140 |    * Search for nodes in pnpm's special directory structure
141 |    */
142 |   private async searchInPnpm(
143 |     pnpmPath: string,
144 |     packageName: string,
145 |     nodeName: string
146 |   ): Promise<NodeSourceInfo | null> {
147 |     try {
148 |       const entries = await fs.readdir(pnpmPath);
149 |       
150 |       // Filter entries that might contain our package
151 |       const packageEntries = entries.filter(entry => 
152 |         entry.includes(packageName.replace('/', '+')) || 
153 |         entry.includes(packageName)
154 |       );
155 | 
156 |       for (const entry of packageEntries) {
157 |         const entryPath = path.join(pnpmPath, entry, 'node_modules', packageName);
158 |         
159 |         // Search patterns within the pnpm package directory
160 |         const patterns = [
161 |           `dist/nodes/${nodeName}/${nodeName}.node.js`,
162 |           `dist/nodes/${nodeName}.node.js`,
163 |           `dist/nodes/*/${nodeName}/${nodeName}.node.js`,
164 |           `dist/nodes/**/${nodeName}/${nodeName}.node.js`,
165 |         ];
166 | 
167 |         for (const pattern of patterns) {
168 |           if (pattern.includes('*')) {
169 |             const result = await this.searchWithGlobPattern(entryPath, pattern, packageName, nodeName);
170 |             if (result) return result;
171 |           } else {
172 |             const fullPath = path.join(entryPath, pattern);
173 |             const result = await this.tryLoadNodeFile(fullPath, packageName, nodeName, entryPath);
174 |             if (result) return result;
175 |           }
176 |         }
177 |       }
178 |     } catch (error) {
179 |       logger.debug(`Error searching in pnpm directory: ${error}`);
180 |     }
181 | 
182 |     return null;
183 |   }
184 | 
185 |   /**
186 |    * Search for files matching a glob-like pattern
187 |    */
188 |   private async searchWithGlobPattern(
189 |     basePath: string,
190 |     pattern: string,
191 |     packageName: string,
192 |     nodeName: string
193 |   ): Promise<NodeSourceInfo | null> {
194 |     // Convert glob pattern to regex parts
195 |     const parts = pattern.split('/');
196 |     const targetFile = `${nodeName}.node.js`;
197 |     
198 |     async function searchDir(currentPath: string, remainingParts: string[]): Promise<string | null> {
199 |       if (remainingParts.length === 0) return null;
200 |       
201 |       const part = remainingParts[0];
202 |       const isLastPart = remainingParts.length === 1;
203 |       
204 |       try {
205 |         if (isLastPart && part === targetFile) {
206 |           // Check if file exists
207 |           const fullPath = path.join(currentPath, part);
208 |           await fs.access(fullPath);
209 |           return fullPath;
210 |         }
211 |         
212 |         const entries = await fs.readdir(currentPath, { withFileTypes: true });
213 |         
214 |         for (const entry of entries) {
215 |           if (!entry.isDirectory() && !isLastPart) continue;
216 |           
217 |           if (part === '*' || part === '**') {
218 |             // Match any directory
219 |             if (entry.isDirectory()) {
220 |               const result = await searchDir(
221 |                 path.join(currentPath, entry.name),
222 |                 part === '**' ? remainingParts : remainingParts.slice(1)
223 |               );
224 |               if (result) return result;
225 |             }
226 |           } else if (entry.name === part || (isLastPart && entry.name === targetFile)) {
227 |             if (isLastPart && entry.isFile()) {
228 |               return path.join(currentPath, entry.name);
229 |             } else if (!isLastPart && entry.isDirectory()) {
230 |               const result = await searchDir(
231 |                 path.join(currentPath, entry.name),
232 |                 remainingParts.slice(1)
233 |               );
234 |               if (result) return result;
235 |             }
236 |           }
237 |         }
238 |       } catch {
239 |         // Directory doesn't exist or can't be read
240 |       }
241 |       
242 |       return null;
243 |     }
244 |     
245 |     const foundPath = await searchDir(basePath, parts);
246 |     if (foundPath) {
247 |       return this.tryLoadNodeFile(foundPath, packageName, nodeName, basePath);
248 |     }
249 |     
250 |     return null;
251 |   }
252 | 
253 |   /**
254 |    * Try to load a node file and its associated files
255 |    */
256 |   private async tryLoadNodeFile(
257 |     fullPath: string,
258 |     packageName: string,
259 |     nodeName: string,
260 |     packageBasePath: string
261 |   ): Promise<NodeSourceInfo | null> {
262 |     try {
263 |       const sourceCode = await fs.readFile(fullPath, 'utf-8');
264 |       
265 |       // Try to find credential files
266 |       let credentialCode: string | undefined;
267 |       
268 |       // First, try alongside the node file
269 |       const credentialPath = fullPath.replace('.node.js', '.credentials.js');
270 |       try {
271 |         credentialCode = await fs.readFile(credentialPath, 'utf-8');
272 |       } catch {
273 |         // Try in the credentials directory
274 |         const possibleCredentialPaths = [
275 |           // Standard n8n structure: dist/credentials/NodeNameApi.credentials.js
276 |           path.join(packageBasePath, packageName, 'dist/credentials', `${nodeName}Api.credentials.js`),
277 |           path.join(packageBasePath, packageName, 'dist/credentials', `${nodeName}OAuth2Api.credentials.js`),
278 |           path.join(packageBasePath, packageName, 'credentials', `${nodeName}Api.credentials.js`),
279 |           path.join(packageBasePath, packageName, 'credentials', `${nodeName}OAuth2Api.credentials.js`),
280 |           // Without packageName in path
281 |           path.join(packageBasePath, 'dist/credentials', `${nodeName}Api.credentials.js`),
282 |           path.join(packageBasePath, 'dist/credentials', `${nodeName}OAuth2Api.credentials.js`),
283 |           path.join(packageBasePath, 'credentials', `${nodeName}Api.credentials.js`),
284 |           path.join(packageBasePath, 'credentials', `${nodeName}OAuth2Api.credentials.js`),
285 |           // Try relative to node location
286 |           path.join(path.dirname(path.dirname(fullPath)), 'credentials', `${nodeName}Api.credentials.js`),
287 |           path.join(path.dirname(path.dirname(fullPath)), 'credentials', `${nodeName}OAuth2Api.credentials.js`),
288 |           path.join(path.dirname(path.dirname(path.dirname(fullPath))), 'credentials', `${nodeName}Api.credentials.js`),
289 |           path.join(path.dirname(path.dirname(path.dirname(fullPath))), 'credentials', `${nodeName}OAuth2Api.credentials.js`),
290 |         ];
291 |         
292 |         // Try to find any credential file
293 |         const allCredentials: string[] = [];
294 |         for (const credPath of possibleCredentialPaths) {
295 |           try {
296 |             const content = await fs.readFile(credPath, 'utf-8');
297 |             allCredentials.push(content);
298 |             logger.debug(`Found credential file at: ${credPath}`);
299 |           } catch {
300 |             // Continue searching
301 |           }
302 |         }
303 |         
304 |         // If we found credentials, combine them
305 |         if (allCredentials.length > 0) {
306 |           credentialCode = allCredentials.join('\n\n// --- Next Credential File ---\n\n');
307 |         }
308 |       }
309 | 
310 |       // Try to get package.json info
311 |       let packageInfo: any;
312 |       const possiblePackageJsonPaths = [
313 |         path.join(packageBasePath, 'package.json'),
314 |         path.join(packageBasePath, packageName, 'package.json'),
315 |         path.join(path.dirname(path.dirname(fullPath)), 'package.json'),
316 |         path.join(path.dirname(path.dirname(path.dirname(fullPath))), 'package.json'),
317 |         // Try to go up from the node location to find package.json
318 |         path.join(fullPath.split('/dist/')[0], 'package.json'),
319 |         path.join(fullPath.split('/nodes/')[0], 'package.json'),
320 |       ];
321 | 
322 |       for (const packageJsonPath of possiblePackageJsonPaths) {
323 |         try {
324 |           const packageJson = await fs.readFile(packageJsonPath, 'utf-8');
325 |           packageInfo = JSON.parse(packageJson);
326 |           logger.debug(`Found package.json at: ${packageJsonPath}`);
327 |           break;
328 |         } catch {
329 |           // Try next path
330 |         }
331 |       }
332 | 
333 |       return {
334 |         nodeType: `${packageName}.${nodeName}`,
335 |         sourceCode,
336 |         credentialCode,
337 |         packageInfo,
338 |         location: fullPath,
339 |       };
340 |     } catch {
341 |       return null;
342 |     }
343 |   }
344 | 
345 |   /**
346 |    * List all available nodes
347 |    */
348 |   async listAvailableNodes(category?: string, search?: string): Promise<any[]> {
349 |     const nodes: any[] = [];
350 |     const seenNodes = new Set<string>(); // Track unique nodes
351 |     
352 |     for (const basePath of this.n8nBasePaths) {
353 |       try {
354 |         // Check for n8n-nodes-base specifically
355 |         const n8nNodesBasePath = path.join(basePath, 'n8n-nodes-base', 'dist', 'nodes');
356 |         try {
357 |           await fs.access(n8nNodesBasePath);
358 |           await this.scanDirectoryForNodes(n8nNodesBasePath, nodes, category, search, seenNodes);
359 |         } catch {
360 |           // Try without dist
361 |           const altPath = path.join(basePath, 'n8n-nodes-base', 'nodes');
362 |           try {
363 |             await fs.access(altPath);
364 |             await this.scanDirectoryForNodes(altPath, nodes, category, search, seenNodes);
365 |           } catch {
366 |             // Try the base path directly
367 |             await this.scanDirectoryForNodes(basePath, nodes, category, search, seenNodes);
368 |           }
369 |         }
370 |       } catch (error) {
371 |         logger.debug(`Failed to scan ${basePath}: ${error}`);
372 |       }
373 |     }
374 | 
375 |     return nodes;
376 |   }
377 | 
378 |   /**
379 |    * Scan directory for n8n nodes
380 |    */
381 |   private async scanDirectoryForNodes(
382 |     dirPath: string,
383 |     nodes: any[],
384 |     category?: string,
385 |     search?: string,
386 |     seenNodes?: Set<string>
387 |   ): Promise<void> {
388 |     try {
389 |       const entries = await fs.readdir(dirPath, { withFileTypes: true });
390 |       
391 |       for (const entry of entries) {
392 |         if (entry.isFile() && entry.name.endsWith('.node.js')) {
393 |           try {
394 |             const fullPath = path.join(dirPath, entry.name);
395 |             const content = await fs.readFile(fullPath, 'utf-8');
396 |             
397 |             // Extract basic info from the source
398 |             const nameMatch = content.match(/displayName:\s*['"`]([^'"`]+)['"`]/);
399 |             const descriptionMatch = content.match(/description:\s*['"`]([^'"`]+)['"`]/);
400 |             
401 |             if (nameMatch) {
402 |               const nodeName = entry.name.replace('.node.js', '');
403 |               
404 |               // Skip if we've already seen this node
405 |               if (seenNodes && seenNodes.has(nodeName)) {
406 |                 continue;
407 |               }
408 |               
409 |               const nodeInfo = {
410 |                 name: nodeName,
411 |                 displayName: nameMatch[1],
412 |                 description: descriptionMatch ? descriptionMatch[1] : '',
413 |                 location: fullPath,
414 |               };
415 | 
416 |               // Apply filters
417 |               if (category && !nodeInfo.displayName.toLowerCase().includes(category.toLowerCase())) {
418 |                 continue;
419 |               }
420 |               if (search && !nodeInfo.displayName.toLowerCase().includes(search.toLowerCase()) &&
421 |                   !nodeInfo.description.toLowerCase().includes(search.toLowerCase())) {
422 |                 continue;
423 |               }
424 | 
425 |               nodes.push(nodeInfo);
426 |               if (seenNodes) {
427 |                 seenNodes.add(nodeName);
428 |               }
429 |             }
430 |           } catch {
431 |             // Skip files we can't read
432 |           }
433 |         } else if (entry.isDirectory()) {
434 |           // Special handling for .pnpm directories
435 |           if (entry.name === '.pnpm') {
436 |             await this.scanPnpmDirectory(path.join(dirPath, entry.name), nodes, category, search, seenNodes);
437 |           } else if (entry.name !== 'node_modules') {
438 |             // Recursively scan subdirectories
439 |             await this.scanDirectoryForNodes(path.join(dirPath, entry.name), nodes, category, search, seenNodes);
440 |           }
441 |         }
442 |       }
443 |     } catch (error) {
444 |       logger.debug(`Error scanning directory ${dirPath}: ${error}`);
445 |     }
446 |   }
447 | 
448 |   /**
449 |    * Scan pnpm directory structure for nodes
450 |    */
451 |   private async scanPnpmDirectory(
452 |     pnpmPath: string,
453 |     nodes: any[],
454 |     category?: string,
455 |     search?: string,
456 |     seenNodes?: Set<string>
457 |   ): Promise<void> {
458 |     try {
459 |       const entries = await fs.readdir(pnpmPath);
460 |       
461 |       for (const entry of entries) {
462 |         const entryPath = path.join(pnpmPath, entry, 'node_modules');
463 |         try {
464 |           await fs.access(entryPath);
465 |           await this.scanDirectoryForNodes(entryPath, nodes, category, search, seenNodes);
466 |         } catch {
467 |           // Skip if node_modules doesn't exist
468 |         }
469 |       }
470 |     } catch (error) {
471 |       logger.debug(`Error scanning pnpm directory ${pnpmPath}: ${error}`);
472 |     }
473 |   }
474 | 
475 |   /**
476 |    * Extract AI Agent node specifically
477 |    */
478 |   async extractAIAgentNode(): Promise<NodeSourceInfo> {
479 |     // AI Agent is typically in @n8n/n8n-nodes-langchain package
480 |     return this.extractNodeSource('@n8n/n8n-nodes-langchain.Agent');
481 |   }
482 | }
```
Page 24/60FirstPrevNextLast