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

# Directory Structure

```
├── _config.yml
├── .claude
│   └── agents
│       ├── code-reviewer.md
│       ├── context-manager.md
│       ├── debugger.md
│       ├── deployment-engineer.md
│       ├── mcp-backend-engineer.md
│       ├── n8n-mcp-tester.md
│       ├── technical-researcher.md
│       └── test-automator.md
├── .dockerignore
├── .env.docker
├── .env.example
├── .env.n8n.example
├── .env.test
├── .env.test.example
├── .github
│   ├── ABOUT.md
│   ├── BENCHMARK_THRESHOLDS.md
│   ├── FUNDING.yml
│   ├── gh-pages.yml
│   ├── secret_scanning.yml
│   └── workflows
│       ├── benchmark-pr.yml
│       ├── benchmark.yml
│       ├── docker-build-fast.yml
│       ├── docker-build-n8n.yml
│       ├── docker-build.yml
│       ├── release.yml
│       ├── test.yml
│       └── update-n8n-deps.yml
├── .gitignore
├── .npmignore
├── ATTRIBUTION.md
├── CHANGELOG.md
├── CLAUDE.md
├── codecov.yml
├── coverage.json
├── data
│   ├── .gitkeep
│   ├── nodes.db
│   ├── nodes.db-shm
│   ├── nodes.db-wal
│   └── templates.db
├── deploy
│   └── quick-deploy-n8n.sh
├── docker
│   ├── docker-entrypoint.sh
│   ├── n8n-mcp
│   ├── parse-config.js
│   └── README.md
├── docker-compose.buildkit.yml
├── docker-compose.extract.yml
├── docker-compose.n8n.yml
├── docker-compose.override.yml.example
├── docker-compose.test-n8n.yml
├── docker-compose.yml
├── Dockerfile
├── Dockerfile.railway
├── Dockerfile.test
├── docs
│   ├── AUTOMATED_RELEASES.md
│   ├── BENCHMARKS.md
│   ├── CHANGELOG.md
│   ├── CLAUDE_CODE_SETUP.md
│   ├── CLAUDE_INTERVIEW.md
│   ├── CODECOV_SETUP.md
│   ├── CODEX_SETUP.md
│   ├── CURSOR_SETUP.md
│   ├── DEPENDENCY_UPDATES.md
│   ├── DOCKER_README.md
│   ├── DOCKER_TROUBLESHOOTING.md
│   ├── FINAL_AI_VALIDATION_SPEC.md
│   ├── FLEXIBLE_INSTANCE_CONFIGURATION.md
│   ├── HTTP_DEPLOYMENT.md
│   ├── img
│   │   ├── cc_command.png
│   │   ├── cc_connected.png
│   │   ├── codex_connected.png
│   │   ├── cursor_tut.png
│   │   ├── Railway_api.png
│   │   ├── Railway_server_address.png
│   │   ├── vsc_ghcp_chat_agent_mode.png
│   │   ├── vsc_ghcp_chat_instruction_files.png
│   │   ├── vsc_ghcp_chat_thinking_tool.png
│   │   └── windsurf_tut.png
│   ├── INSTALLATION.md
│   ├── LIBRARY_USAGE.md
│   ├── local
│   │   ├── DEEP_DIVE_ANALYSIS_2025-10-02.md
│   │   ├── DEEP_DIVE_ANALYSIS_README.md
│   │   ├── Deep_dive_p1_p2.md
│   │   ├── integration-testing-plan.md
│   │   ├── integration-tests-phase1-summary.md
│   │   ├── N8N_AI_WORKFLOW_BUILDER_ANALYSIS.md
│   │   ├── P0_IMPLEMENTATION_PLAN.md
│   │   └── TEMPLATE_MINING_ANALYSIS.md
│   ├── MCP_ESSENTIALS_README.md
│   ├── MCP_QUICK_START_GUIDE.md
│   ├── N8N_DEPLOYMENT.md
│   ├── RAILWAY_DEPLOYMENT.md
│   ├── README_CLAUDE_SETUP.md
│   ├── README.md
│   ├── tools-documentation-usage.md
│   ├── VS_CODE_PROJECT_SETUP.md
│   ├── WINDSURF_SETUP.md
│   └── workflow-diff-examples.md
├── examples
│   └── enhanced-documentation-demo.js
├── fetch_log.txt
├── LICENSE
├── MEMORY_N8N_UPDATE.md
├── MEMORY_TEMPLATE_UPDATE.md
├── monitor_fetch.sh
├── N8N_HTTP_STREAMABLE_SETUP.md
├── n8n-nodes.db
├── P0-R3-TEST-PLAN.md
├── package-lock.json
├── package.json
├── package.runtime.json
├── PRIVACY.md
├── railway.json
├── README.md
├── renovate.json
├── scripts
│   ├── analyze-optimization.sh
│   ├── audit-schema-coverage.ts
│   ├── build-optimized.sh
│   ├── compare-benchmarks.js
│   ├── demo-optimization.sh
│   ├── deploy-http.sh
│   ├── deploy-to-vm.sh
│   ├── export-webhook-workflows.ts
│   ├── extract-changelog.js
│   ├── extract-from-docker.js
│   ├── extract-nodes-docker.sh
│   ├── extract-nodes-simple.sh
│   ├── format-benchmark-results.js
│   ├── generate-benchmark-stub.js
│   ├── generate-detailed-reports.js
│   ├── generate-test-summary.js
│   ├── http-bridge.js
│   ├── mcp-http-client.js
│   ├── migrate-nodes-fts.ts
│   ├── migrate-tool-docs.ts
│   ├── n8n-docs-mcp.service
│   ├── nginx-n8n-mcp.conf
│   ├── prebuild-fts5.ts
│   ├── prepare-release.js
│   ├── publish-npm-quick.sh
│   ├── publish-npm.sh
│   ├── quick-test.ts
│   ├── run-benchmarks-ci.js
│   ├── sync-runtime-version.js
│   ├── test-ai-validation-debug.ts
│   ├── test-code-node-enhancements.ts
│   ├── test-code-node-fixes.ts
│   ├── test-docker-config.sh
│   ├── test-docker-fingerprint.ts
│   ├── test-docker-optimization.sh
│   ├── test-docker.sh
│   ├── test-empty-connection-validation.ts
│   ├── test-error-message-tracking.ts
│   ├── test-error-output-validation.ts
│   ├── test-error-validation.js
│   ├── test-essentials.ts
│   ├── test-expression-code-validation.ts
│   ├── test-expression-format-validation.js
│   ├── test-fts5-search.ts
│   ├── test-fuzzy-fix.ts
│   ├── test-fuzzy-simple.ts
│   ├── test-helpers-validation.ts
│   ├── test-http-search.ts
│   ├── test-http.sh
│   ├── test-jmespath-validation.ts
│   ├── test-multi-tenant-simple.ts
│   ├── test-multi-tenant.ts
│   ├── test-n8n-integration.sh
│   ├── test-node-info.js
│   ├── test-node-type-validation.ts
│   ├── test-nodes-base-prefix.ts
│   ├── test-operation-validation.ts
│   ├── test-optimized-docker.sh
│   ├── test-release-automation.js
│   ├── test-search-improvements.ts
│   ├── test-security.ts
│   ├── test-single-session.sh
│   ├── test-sqljs-triggers.ts
│   ├── test-telemetry-debug.ts
│   ├── test-telemetry-direct.ts
│   ├── test-telemetry-env.ts
│   ├── test-telemetry-integration.ts
│   ├── test-telemetry-no-select.ts
│   ├── test-telemetry-security.ts
│   ├── test-telemetry-simple.ts
│   ├── test-typeversion-validation.ts
│   ├── test-url-configuration.ts
│   ├── test-user-id-persistence.ts
│   ├── test-webhook-validation.ts
│   ├── test-workflow-insert.ts
│   ├── test-workflow-sanitizer.ts
│   ├── test-workflow-tracking-debug.ts
│   ├── update-and-publish-prep.sh
│   ├── update-n8n-deps.js
│   ├── update-readme-version.js
│   ├── vitest-benchmark-json-reporter.js
│   └── vitest-benchmark-reporter.ts
├── SECURITY.md
├── src
│   ├── config
│   │   └── n8n-api.ts
│   ├── data
│   │   └── canonical-ai-tool-examples.json
│   ├── database
│   │   ├── database-adapter.ts
│   │   ├── migrations
│   │   │   └── add-template-node-configs.sql
│   │   ├── node-repository.ts
│   │   ├── nodes.db
│   │   ├── schema-optimized.sql
│   │   └── schema.sql
│   ├── errors
│   │   └── validation-service-error.ts
│   ├── http-server-single-session.ts
│   ├── http-server.ts
│   ├── index.ts
│   ├── loaders
│   │   └── node-loader.ts
│   ├── mappers
│   │   └── docs-mapper.ts
│   ├── mcp
│   │   ├── handlers-n8n-manager.ts
│   │   ├── handlers-workflow-diff.ts
│   │   ├── index.ts
│   │   ├── server.ts
│   │   ├── stdio-wrapper.ts
│   │   ├── tool-docs
│   │   │   ├── configuration
│   │   │   │   ├── get-node-as-tool-info.ts
│   │   │   │   ├── get-node-documentation.ts
│   │   │   │   ├── get-node-essentials.ts
│   │   │   │   ├── get-node-info.ts
│   │   │   │   ├── get-property-dependencies.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── search-node-properties.ts
│   │   │   ├── discovery
│   │   │   │   ├── get-database-statistics.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── list-ai-tools.ts
│   │   │   │   ├── list-nodes.ts
│   │   │   │   └── search-nodes.ts
│   │   │   ├── guides
│   │   │   │   ├── ai-agents-guide.ts
│   │   │   │   └── index.ts
│   │   │   ├── index.ts
│   │   │   ├── system
│   │   │   │   ├── index.ts
│   │   │   │   ├── n8n-diagnostic.ts
│   │   │   │   ├── n8n-health-check.ts
│   │   │   │   ├── n8n-list-available-tools.ts
│   │   │   │   └── tools-documentation.ts
│   │   │   ├── templates
│   │   │   │   ├── get-template.ts
│   │   │   │   ├── get-templates-for-task.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── list-node-templates.ts
│   │   │   │   ├── list-tasks.ts
│   │   │   │   ├── search-templates-by-metadata.ts
│   │   │   │   └── search-templates.ts
│   │   │   ├── types.ts
│   │   │   ├── validation
│   │   │   │   ├── index.ts
│   │   │   │   ├── validate-node-minimal.ts
│   │   │   │   ├── validate-node-operation.ts
│   │   │   │   ├── validate-workflow-connections.ts
│   │   │   │   ├── validate-workflow-expressions.ts
│   │   │   │   └── validate-workflow.ts
│   │   │   └── workflow_management
│   │   │       ├── index.ts
│   │   │       ├── n8n-autofix-workflow.ts
│   │   │       ├── n8n-create-workflow.ts
│   │   │       ├── n8n-delete-execution.ts
│   │   │       ├── n8n-delete-workflow.ts
│   │   │       ├── n8n-get-execution.ts
│   │   │       ├── n8n-get-workflow-details.ts
│   │   │       ├── n8n-get-workflow-minimal.ts
│   │   │       ├── n8n-get-workflow-structure.ts
│   │   │       ├── n8n-get-workflow.ts
│   │   │       ├── n8n-list-executions.ts
│   │   │       ├── n8n-list-workflows.ts
│   │   │       ├── n8n-trigger-webhook-workflow.ts
│   │   │       ├── n8n-update-full-workflow.ts
│   │   │       ├── n8n-update-partial-workflow.ts
│   │   │       └── n8n-validate-workflow.ts
│   │   ├── tools-documentation.ts
│   │   ├── tools-n8n-friendly.ts
│   │   ├── tools-n8n-manager.ts
│   │   ├── tools.ts
│   │   └── workflow-examples.ts
│   ├── mcp-engine.ts
│   ├── mcp-tools-engine.ts
│   ├── n8n
│   │   ├── MCPApi.credentials.ts
│   │   └── MCPNode.node.ts
│   ├── parsers
│   │   ├── node-parser.ts
│   │   ├── property-extractor.ts
│   │   └── simple-parser.ts
│   ├── scripts
│   │   ├── debug-http-search.ts
│   │   ├── extract-from-docker.ts
│   │   ├── fetch-templates-robust.ts
│   │   ├── fetch-templates.ts
│   │   ├── rebuild-database.ts
│   │   ├── rebuild-optimized.ts
│   │   ├── rebuild.ts
│   │   ├── sanitize-templates.ts
│   │   ├── seed-canonical-ai-examples.ts
│   │   ├── test-autofix-documentation.ts
│   │   ├── test-autofix-workflow.ts
│   │   ├── test-execution-filtering.ts
│   │   ├── test-node-suggestions.ts
│   │   ├── test-protocol-negotiation.ts
│   │   ├── test-summary.ts
│   │   ├── test-webhook-autofix.ts
│   │   ├── validate.ts
│   │   └── validation-summary.ts
│   ├── services
│   │   ├── ai-node-validator.ts
│   │   ├── ai-tool-validators.ts
│   │   ├── confidence-scorer.ts
│   │   ├── config-validator.ts
│   │   ├── enhanced-config-validator.ts
│   │   ├── example-generator.ts
│   │   ├── execution-processor.ts
│   │   ├── expression-format-validator.ts
│   │   ├── expression-validator.ts
│   │   ├── n8n-api-client.ts
│   │   ├── n8n-validation.ts
│   │   ├── node-documentation-service.ts
│   │   ├── node-similarity-service.ts
│   │   ├── node-specific-validators.ts
│   │   ├── operation-similarity-service.ts
│   │   ├── property-dependencies.ts
│   │   ├── property-filter.ts
│   │   ├── resource-similarity-service.ts
│   │   ├── sqlite-storage-service.ts
│   │   ├── task-templates.ts
│   │   ├── universal-expression-validator.ts
│   │   ├── workflow-auto-fixer.ts
│   │   ├── workflow-diff-engine.ts
│   │   └── workflow-validator.ts
│   ├── telemetry
│   │   ├── batch-processor.ts
│   │   ├── config-manager.ts
│   │   ├── early-error-logger.ts
│   │   ├── error-sanitization-utils.ts
│   │   ├── error-sanitizer.ts
│   │   ├── event-tracker.ts
│   │   ├── event-validator.ts
│   │   ├── index.ts
│   │   ├── performance-monitor.ts
│   │   ├── rate-limiter.ts
│   │   ├── startup-checkpoints.ts
│   │   ├── telemetry-error.ts
│   │   ├── telemetry-manager.ts
│   │   ├── telemetry-types.ts
│   │   └── workflow-sanitizer.ts
│   ├── templates
│   │   ├── batch-processor.ts
│   │   ├── metadata-generator.ts
│   │   ├── README.md
│   │   ├── template-fetcher.ts
│   │   ├── template-repository.ts
│   │   └── template-service.ts
│   ├── types
│   │   ├── index.ts
│   │   ├── instance-context.ts
│   │   ├── n8n-api.ts
│   │   ├── node-types.ts
│   │   └── workflow-diff.ts
│   └── utils
│       ├── auth.ts
│       ├── bridge.ts
│       ├── cache-utils.ts
│       ├── console-manager.ts
│       ├── documentation-fetcher.ts
│       ├── enhanced-documentation-fetcher.ts
│       ├── error-handler.ts
│       ├── example-generator.ts
│       ├── fixed-collection-validator.ts
│       ├── logger.ts
│       ├── mcp-client.ts
│       ├── n8n-errors.ts
│       ├── node-source-extractor.ts
│       ├── node-type-normalizer.ts
│       ├── node-type-utils.ts
│       ├── node-utils.ts
│       ├── npm-version-checker.ts
│       ├── protocol-version.ts
│       ├── simple-cache.ts
│       ├── ssrf-protection.ts
│       ├── template-node-resolver.ts
│       ├── template-sanitizer.ts
│       ├── url-detector.ts
│       ├── validation-schemas.ts
│       └── version.ts
├── test-output.txt
├── test-reinit-fix.sh
├── tests
│   ├── __snapshots__
│   │   └── .gitkeep
│   ├── auth.test.ts
│   ├── benchmarks
│   │   ├── database-queries.bench.ts
│   │   ├── index.ts
│   │   ├── mcp-tools.bench.ts
│   │   ├── mcp-tools.bench.ts.disabled
│   │   ├── mcp-tools.bench.ts.skip
│   │   ├── node-loading.bench.ts.disabled
│   │   ├── README.md
│   │   ├── search-operations.bench.ts.disabled
│   │   └── validation-performance.bench.ts.disabled
│   ├── bridge.test.ts
│   ├── comprehensive-extraction-test.js
│   ├── data
│   │   └── .gitkeep
│   ├── debug-slack-doc.js
│   ├── demo-enhanced-documentation.js
│   ├── docker-tests-README.md
│   ├── error-handler.test.ts
│   ├── examples
│   │   └── using-database-utils.test.ts
│   ├── extracted-nodes-db
│   │   ├── database-import.json
│   │   ├── extraction-report.json
│   │   ├── insert-nodes.sql
│   │   ├── n8n-nodes-base__Airtable.json
│   │   ├── n8n-nodes-base__Discord.json
│   │   ├── n8n-nodes-base__Function.json
│   │   ├── n8n-nodes-base__HttpRequest.json
│   │   ├── n8n-nodes-base__If.json
│   │   ├── n8n-nodes-base__Slack.json
│   │   ├── n8n-nodes-base__SplitInBatches.json
│   │   └── n8n-nodes-base__Webhook.json
│   ├── factories
│   │   ├── node-factory.ts
│   │   └── property-definition-factory.ts
│   ├── fixtures
│   │   ├── .gitkeep
│   │   ├── database
│   │   │   └── test-nodes.json
│   │   ├── factories
│   │   │   ├── node.factory.ts
│   │   │   └── parser-node.factory.ts
│   │   └── template-configs.ts
│   ├── helpers
│   │   └── env-helpers.ts
│   ├── http-server-auth.test.ts
│   ├── integration
│   │   ├── ai-validation
│   │   │   ├── ai-agent-validation.test.ts
│   │   │   ├── ai-tool-validation.test.ts
│   │   │   ├── chat-trigger-validation.test.ts
│   │   │   ├── e2e-validation.test.ts
│   │   │   ├── helpers.ts
│   │   │   ├── llm-chain-validation.test.ts
│   │   │   ├── README.md
│   │   │   └── TEST_REPORT.md
│   │   ├── ci
│   │   │   └── database-population.test.ts
│   │   ├── database
│   │   │   ├── connection-management.test.ts
│   │   │   ├── empty-database.test.ts
│   │   │   ├── fts5-search.test.ts
│   │   │   ├── node-fts5-search.test.ts
│   │   │   ├── node-repository.test.ts
│   │   │   ├── performance.test.ts
│   │   │   ├── 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-similarity-service.test.ts
│   │   │   ├── node-specific-validators.test.ts
│   │   │   ├── operation-similarity-service-comprehensive.test.ts
│   │   │   ├── operation-similarity-service.test.ts
│   │   │   ├── property-dependencies.test.ts
│   │   │   ├── property-filter-edge-cases.test.ts
│   │   │   ├── property-filter.test.ts
│   │   │   ├── resource-similarity-service-comprehensive.test.ts
│   │   │   ├── resource-similarity-service.test.ts
│   │   │   ├── task-templates.test.ts
│   │   │   ├── template-service.test.ts
│   │   │   ├── universal-expression-validator.test.ts
│   │   │   ├── validation-fixes.test.ts
│   │   │   ├── workflow-auto-fixer.test.ts
│   │   │   ├── workflow-diff-engine.test.ts
│   │   │   ├── workflow-fixed-collection-validation.test.ts
│   │   │   ├── workflow-validator-comprehensive.test.ts
│   │   │   ├── workflow-validator-edge-cases.test.ts
│   │   │   ├── workflow-validator-error-outputs.test.ts
│   │   │   ├── workflow-validator-expression-format.test.ts
│   │   │   ├── workflow-validator-loops-simple.test.ts
│   │   │   ├── workflow-validator-loops.test.ts
│   │   │   ├── workflow-validator-mocks.test.ts
│   │   │   ├── workflow-validator-performance.test.ts
│   │   │   ├── workflow-validator-with-mocks.test.ts
│   │   │   └── workflow-validator.test.ts
│   │   ├── telemetry
│   │   │   ├── batch-processor.test.ts
│   │   │   ├── config-manager.test.ts
│   │   │   ├── event-tracker.test.ts
│   │   │   ├── event-validator.test.ts
│   │   │   ├── rate-limiter.test.ts
│   │   │   ├── telemetry-error.test.ts
│   │   │   ├── telemetry-manager.test.ts
│   │   │   ├── v2.18.3-fixes-verification.test.ts
│   │   │   └── workflow-sanitizer.test.ts
│   │   ├── templates
│   │   │   ├── batch-processor.test.ts
│   │   │   ├── metadata-generator.test.ts
│   │   │   ├── template-repository-metadata.test.ts
│   │   │   └── template-repository-security.test.ts
│   │   ├── test-env-example.test.ts
│   │   ├── test-infrastructure.test.ts
│   │   ├── types
│   │   │   ├── instance-context-coverage.test.ts
│   │   │   └── instance-context-multi-tenant.test.ts
│   │   ├── utils
│   │   │   ├── auth-timing-safe.test.ts
│   │   │   ├── cache-utils.test.ts
│   │   │   ├── console-manager.test.ts
│   │   │   ├── database-utils.test.ts
│   │   │   ├── fixed-collection-validator.test.ts
│   │   │   ├── n8n-errors.test.ts
│   │   │   ├── node-type-normalizer.test.ts
│   │   │   ├── node-type-utils.test.ts
│   │   │   ├── node-utils.test.ts
│   │   │   ├── simple-cache-memory-leak-fix.test.ts
│   │   │   ├── ssrf-protection.test.ts
│   │   │   └── template-node-resolver.test.ts
│   │   └── validation-fixes.test.ts
│   └── utils
│       ├── assertions.ts
│       ├── builders
│       │   └── workflow.builder.ts
│       ├── data-generators.ts
│       ├── database-utils.ts
│       ├── README.md
│       └── test-helpers.ts
├── thumbnail.png
├── tsconfig.build.json
├── tsconfig.json
├── types
│   ├── mcp.d.ts
│   └── test-env.d.ts
├── verify-telemetry-fix.js
├── versioned-nodes.md
├── vitest.config.benchmark.ts
├── vitest.config.integration.ts
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/docs/local/TEMPLATE_MINING_ANALYSIS.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Template Mining Analysis - Alternative to P0-R3
  2 | 
  3 | **Date**: 2025-10-02
  4 | **Context**: Analyzing whether to fix `get_node_for_task` (28% failure rate) or replace it with template-based configuration extraction
  5 | 
  6 | ## Executive Summary
  7 | 
  8 | **RECOMMENDATION**: Replace `get_node_for_task` with template-based configuration extraction. The template database contains 2,646 real-world workflows with rich node configurations that far exceed the 31 hardcoded task templates.
  9 | 
 10 | ## Key Findings
 11 | 
 12 | ### 1. Template Database Coverage
 13 | 
 14 | - **Total Templates**: 2,646 production workflows from n8n.io
 15 | - **Unique Node Types**: 543 (covers 103% of our 525 core nodes)
 16 | - **Metadata Coverage**: 100% (AI-generated structured metadata)
 17 | 
 18 | ### 2. Node Type Coverage in Templates
 19 | 
 20 | Top node types by template usage:
 21 | ```
 22 | 3,820 templates: n8n-nodes-base.httpRequest      (144% of total templates!)
 23 | 3,678 templates: n8n-nodes-base.set
 24 | 2,445 templates: n8n-nodes-base.code
 25 | 1,700 templates: n8n-nodes-base.googleSheets
 26 | 1,471 templates: @n8n/n8n-nodes-langchain.agent
 27 | 1,269 templates: @n8n/n8n-nodes-langchain.lmChatOpenAi
 28 |   792 templates: n8n-nodes-base.telegram
 29 |   702 templates: n8n-nodes-base.httpRequestTool
 30 |   596 templates: n8n-nodes-base.gmail
 31 |   466 templates: n8n-nodes-base.webhook
 32 | ```
 33 | 
 34 | **Comparison**:
 35 | - Hardcoded task templates: 31 tasks covering 5.9% of nodes
 36 | - Real templates: 2,646 templates with 2-3k examples for common nodes
 37 | 
 38 | ### 3. Database Structure
 39 | 
 40 | ```sql
 41 | CREATE TABLE templates (
 42 |   id INTEGER PRIMARY KEY,
 43 |   workflow_id INTEGER UNIQUE NOT NULL,
 44 |   name TEXT NOT NULL,
 45 |   description TEXT,
 46 |   -- Node information
 47 |   nodes_used TEXT,              -- JSON array: ["n8n-nodes-base.httpRequest", ...]
 48 |   workflow_json_compressed TEXT, -- Base64 encoded gzip of full workflow
 49 |   -- Metadata (100% coverage)
 50 |   metadata_json TEXT,           -- AI-generated structured metadata
 51 |   -- Stats
 52 |   views INTEGER DEFAULT 0,
 53 |   created_at DATETIME,
 54 |   -- ...
 55 | );
 56 | ```
 57 | 
 58 | ### 4. Real Configuration Examples
 59 | 
 60 | #### HTTP Request Node Configurations
 61 | 
 62 | **Simple URL fetch**:
 63 | ```json
 64 | {
 65 |   "url": "https://api.example.com/data",
 66 |   "options": {}
 67 | }
 68 | ```
 69 | 
 70 | **With authentication**:
 71 | ```json
 72 | {
 73 |   "url": "=https://api.wavespeed.ai/api/v3/predictions/{{ $json.data.id }}/result",
 74 |   "options": {},
 75 |   "authentication": "genericCredentialType",
 76 |   "genericAuthType": "httpHeaderAuth"
 77 | }
 78 | ```
 79 | 
 80 | **Complex expressions**:
 81 | ```json
 82 | {
 83 |   "url": "=https://image.pollinations.ai/prompt/{{$('Social Media Content Factory').item.json.output.description.replaceAll(' ','-').replaceAll(',','').replaceAll('.','') }}",
 84 |   "options": {}
 85 | }
 86 | ```
 87 | 
 88 | #### Webhook Node Configurations
 89 | 
 90 | **Basic webhook**:
 91 | ```json
 92 | {
 93 |   "path": "ytube",
 94 |   "options": {},
 95 |   "httpMethod": "POST",
 96 |   "responseMode": "responseNode"
 97 | }
 98 | ```
 99 | 
100 | **With binary data**:
101 | ```json
102 | {
103 |   "path": "your-endpoint",
104 |   "options": {
105 |     "binaryPropertyName": "data"
106 |   },
107 |   "httpMethod": "POST"
108 | }
109 | ```
110 | 
111 | ### 5. AI-Generated Metadata
112 | 
113 | Each template has structured metadata including:
114 | 
115 | ```json
116 | {
117 |   "categories": ["automation", "integration", "data processing"],
118 |   "complexity": "medium",
119 |   "use_cases": [
120 |     "Extract transaction data from Gmail",
121 |     "Automate bookkeeping",
122 |     "Expense tracking"
123 |   ],
124 |   "estimated_setup_minutes": 30,
125 |   "required_services": ["Gmail", "Google Sheets", "Google Gemini"],
126 |   "key_features": [
127 |     "Fetch emails by label",
128 |     "Extract transaction data",
129 |     "Use LLM for structured output"
130 |   ],
131 |   "target_audience": ["Accountants", "Small business owners"]
132 | }
133 | ```
134 | 
135 | ## Comparison: Task Templates vs Real Templates
136 | 
137 | ### Current Approach (get_node_for_task)
138 | 
139 | **Pros**:
140 | - Curated configurations with best practices
141 | - Predictable, stable responses
142 | - Fast lookup (no decompression needed)
143 | 
144 | **Cons**:
145 | - Only 31 tasks (5.9% node coverage)
146 | - 28% failure rate (users can't find what they need)
147 | - Requires manual maintenance
148 | - Static configurations without real-world context
149 | - Usage ratio 22.5:1 (search_nodes is preferred)
150 | 
151 | ### Template-Based Approach
152 | 
153 | **Pros**:
154 | - 2,646 real workflows with 2-3k examples for common nodes
155 | - 100% metadata coverage for semantic matching
156 | - Real-world patterns and best practices
157 | - Covers 543 node types (103% coverage)
158 | - Self-updating (templates fetched from n8n.io)
159 | - Rich context (use cases, complexity, setup time)
160 | 
161 | **Cons**:
162 | - Requires decompression for full workflow access
163 | - May contain template-specific context (but can be filtered)
164 | - Need ranking/filtering logic for best matches
165 | 
166 | ## Proposed Implementation Strategy
167 | 
168 | ### Phase 1: Extract Node Configurations from Templates
169 | 
170 | Create a new service: `TemplateConfigExtractor`
171 | 
172 | ```typescript
173 | interface ExtractedNodeConfig {
174 |   nodeType: string;
175 |   configuration: Record<string, any>;
176 |   source: {
177 |     templateId: number;
178 |     templateName: string;
179 |     templateViews: number;
180 |     useCases: string[];
181 |     complexity: 'simple' | 'medium' | 'complex';
182 |   };
183 |   patterns: {
184 |     hasAuthentication: boolean;
185 |     hasExpressions: boolean;
186 |     hasOptionalFields: boolean;
187 |   };
188 | }
189 | 
190 | class TemplateConfigExtractor {
191 |   async extractConfigsForNode(
192 |     nodeType: string,
193 |     options?: {
194 |       complexity?: 'simple' | 'medium' | 'complex';
195 |       requiresAuth?: boolean;
196 |       limit?: number;
197 |     }
198 |   ): Promise<ExtractedNodeConfig[]> {
199 |     // 1. Query templates containing nodeType
200 |     // 2. Decompress workflow_json_compressed
201 |     // 3. Extract node configurations
202 |     // 4. Rank by popularity + complexity match
203 |     // 5. Return top N configurations
204 |   }
205 | }
206 | ```
207 | 
208 | ### Phase 2: Integrate with Existing Tools
209 | 
210 | **Option A**: Enhance `get_node_essentials`
211 | - Add `includeExamples: boolean` parameter
212 | - Return 2-3 real configurations from templates
213 | - Preserve existing compact format
214 | 
215 | **Option B**: Enhance `get_node_info`
216 | - Add `examples` section with template-sourced configs
217 | - Include source attribution (template name, views)
218 | 
219 | **Option C**: New tool `get_node_examples`
220 | - Dedicated tool for retrieving configuration examples
221 | - Query by node type, complexity, use case
222 | - Returns ranked list of real configurations
223 | 
224 | ### Phase 3: Deprecate get_node_for_task
225 | 
226 | - Mark as deprecated in tool documentation
227 | - Redirect to enhanced tools
228 | - Remove after 2-3 version cycles
229 | 
230 | ## Performance Considerations
231 | 
232 | ### Decompression Cost
233 | 
234 | - Average compressed size: 6-12 KB
235 | - Decompression time: ~5-10ms per template
236 | - Caching strategy needed for frequently accessed templates
237 | 
238 | ### Query Strategy
239 | 
240 | ```sql
241 | -- Fast: Get templates for a node type (no decompression)
242 | SELECT id, name, views, metadata_json
243 | FROM templates
244 | WHERE nodes_used LIKE '%n8n-nodes-base.httpRequest%'
245 | ORDER BY views DESC
246 | LIMIT 10;
247 | 
248 | -- Then decompress only top matches
249 | ```
250 | 
251 | ### Caching
252 | 
253 | - Cache decompressed workflows for popular templates (top 100)
254 | - TTL: 1 hour
255 | - Estimated memory: 100 * 50KB = 5MB
256 | 
257 | ## Impact on P0-R3
258 | 
259 | **Original P0-R3 Plan**: Expand task library from 31 to 100+ tasks using fuzzy matching
260 | 
261 | **New Approach**: Mine 2,646 templates for real configurations
262 | 
263 | **Impact Assessment**:
264 | 
265 | | Metric | Original Plan | Template Mining |
266 | |--------|--------------|-----------------|
267 | | Configuration examples | 100 (estimated) | 2,646+ actual |
268 | | Node coverage | ~20% | 103% |
269 | | Maintenance | High (manual) | Low (auto-fetch) |
270 | | Accuracy | Curated | Production-tested |
271 | | Context richness | Limited | Rich metadata |
272 | | Development time | 2-3 weeks | 1 week |
273 | 
274 | **Recommendation**: PIVOT to template mining approach for P0-R3
275 | 
276 | ## Implementation Estimate
277 | 
278 | ### Week 1: Core Infrastructure
279 | - Day 1-2: Create `TemplateConfigExtractor` service
280 | - Day 3: Implement caching layer
281 | - Day 4-5: Testing and optimization
282 | 
283 | ### Week 2: Integration
284 | - Day 1-2: Enhance `get_node_essentials` with examples
285 | - Day 3: Update tool documentation
286 | - Day 4-5: Integration testing
287 | 
288 | **Total**: 2 weeks vs 3 weeks for original plan
289 | 
290 | ## Validation Tests
291 | 
292 | ```typescript
293 | // Test: Extract HTTP Request configs
294 | const configs = await extractor.extractConfigsForNode(
295 |   'n8n-nodes-base.httpRequest',
296 |   { complexity: 'simple', limit: 5 }
297 | );
298 | 
299 | // Expected: 5 configs from top templates
300 | // - Simple URL fetch
301 | // - With authentication
302 | // - With custom headers
303 | // - With expressions
304 | // - With error handling
305 | 
306 | // Test: Extract webhook configs
307 | const webhookConfigs = await extractor.extractConfigsForNode(
308 |   'n8n-nodes-base.webhook',
309 |   { limit: 3 }
310 | );
311 | 
312 | // Expected: 3 configs showing different patterns
313 | // - Basic POST webhook
314 | // - With response node
315 | // - With binary data handling
316 | ```
317 | 
318 | ## Risks and Mitigation
319 | 
320 | ### Risk 1: Template Quality Varies
321 | - **Mitigation**: Filter by views (popularity) and metadata complexity rating
322 | - Only use templates with >1000 views for examples
323 | 
324 | ### Risk 2: Decompression Performance
325 | - **Mitigation**: Cache decompressed popular templates
326 | - Implement lazy loading (decompress on demand)
327 | 
328 | ### Risk 3: Template-Specific Context
329 | - **Mitigation**: Extract only node configuration, strip workflow-specific context
330 | - Provide source attribution for context
331 | 
332 | ### Risk 4: Breaking Changes in Template Structure
333 | - **Mitigation**: Robust error handling in decompression
334 | - Fallback to cached configs if template fetch fails
335 | 
336 | ## Success Metrics
337 | 
338 | **Before** (get_node_for_task):
339 | - 392 calls, 72% success rate
340 | - 28% failure rate
341 | - 31 task templates
342 | - 5.9% node coverage
343 | 
344 | **Target** (template-based):
345 | - 90%+ success rate for configuration discovery
346 | - 100%+ node coverage
347 | - 2,646+ real-world examples
348 | - Self-updating from n8n.io
349 | 
350 | ## Next Steps
351 | 
352 | 1. ✅ Complete template database analysis
353 | 2. ⏳ Create `TemplateConfigExtractor` service
354 | 3. ⏳ Implement caching layer
355 | 4. ⏳ Enhance `get_node_essentials` with examples
356 | 5. ⏳ Update P0 implementation plan
357 | 6. ⏳ Begin implementation
358 | 
359 | ## Conclusion
360 | 
361 | The template database provides a vastly superior alternative to hardcoded task templates:
362 | 
363 | - **2,646 templates** vs 31 tasks (85x more examples)
364 | - **103% node coverage** vs 5.9% coverage (17x improvement)
365 | - **Real-world configurations** vs synthetic examples
366 | - **Self-updating** vs manual maintenance
367 | - **Rich metadata** for semantic matching
368 | 
369 | **Recommendation**: Pivot P0-R3 from "expand task library" to "mine template configurations"
370 | 
```

--------------------------------------------------------------------------------
/scripts/test-essentials.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env ts-node
  2 | /**
  3 |  * Test script for validating the get_node_essentials tool
  4 |  * 
  5 |  * This script:
  6 |  * 1. Compares get_node_essentials vs get_node_info response sizes
  7 |  * 2. Validates that essential properties are correctly extracted
  8 |  * 3. Checks that examples are properly generated
  9 |  * 4. Tests the property search functionality
 10 |  */
 11 | 
 12 | import { N8NDocumentationMCPServer } from '../src/mcp/server';
 13 | import { readFileSync, writeFileSync } from 'fs';
 14 | import { join } from 'path';
 15 | 
 16 | // Color codes for terminal output
 17 | const colors = {
 18 |   reset: '\x1b[0m',
 19 |   bright: '\x1b[1m',
 20 |   green: '\x1b[32m',
 21 |   red: '\x1b[31m',
 22 |   yellow: '\x1b[33m',
 23 |   blue: '\x1b[34m',
 24 |   cyan: '\x1b[36m'
 25 | };
 26 | 
 27 | function log(message: string, color: string = colors.reset) {
 28 |   console.log(`${color}${message}${colors.reset}`);
 29 | }
 30 | 
 31 | function logSection(title: string) {
 32 |   console.log('\n' + '='.repeat(60));
 33 |   log(title, colors.bright + colors.cyan);
 34 |   console.log('='.repeat(60));
 35 | }
 36 | 
 37 | function formatBytes(bytes: number): string {
 38 |   if (bytes < 1024) return bytes + ' B';
 39 |   const kb = bytes / 1024;
 40 |   if (kb < 1024) return kb.toFixed(1) + ' KB';
 41 |   const mb = kb / 1024;
 42 |   return mb.toFixed(2) + ' MB';
 43 | }
 44 | 
 45 | async function testNodeEssentials(server: N8NDocumentationMCPServer, nodeType: string) {
 46 |   logSection(`Testing ${nodeType}`);
 47 |   
 48 |   try {
 49 |     // Get full node info
 50 |     const startFull = Date.now();
 51 |     const fullInfo = await server.executeTool('get_node_info', { nodeType });
 52 |     const fullTime = Date.now() - startFull;
 53 |     const fullSize = JSON.stringify(fullInfo).length;
 54 |     
 55 |     // Get essential info
 56 |     const startEssential = Date.now();
 57 |     const essentialInfo = await server.executeTool('get_node_essentials', { nodeType });
 58 |     const essentialTime = Date.now() - startEssential;
 59 |     const essentialSize = JSON.stringify(essentialInfo).length;
 60 |     
 61 |     // Calculate metrics
 62 |     const sizeReduction = ((fullSize - essentialSize) / fullSize * 100).toFixed(1);
 63 |     const speedImprovement = ((fullTime - essentialTime) / fullTime * 100).toFixed(1);
 64 |     
 65 |     // Display results
 66 |     log(`\n📊 Size Comparison:`, colors.bright);
 67 |     log(`   Full response:      ${formatBytes(fullSize)}`, colors.yellow);
 68 |     log(`   Essential response: ${formatBytes(essentialSize)}`, colors.green);
 69 |     log(`   Size reduction:     ${sizeReduction}% ✨`, colors.bright + colors.green);
 70 |     
 71 |     log(`\n⚡ Performance:`, colors.bright);
 72 |     log(`   Full response time:      ${fullTime}ms`);
 73 |     log(`   Essential response time: ${essentialTime}ms`);
 74 |     log(`   Speed improvement:       ${speedImprovement}%`, colors.green);
 75 |     
 76 |     log(`\n📋 Property Count:`, colors.bright);
 77 |     const fullPropCount = fullInfo.properties?.length || 0;
 78 |     const essentialPropCount = (essentialInfo.requiredProperties?.length || 0) + 
 79 |                                (essentialInfo.commonProperties?.length || 0);
 80 |     log(`   Full properties:      ${fullPropCount}`);
 81 |     log(`   Essential properties: ${essentialPropCount}`);
 82 |     log(`   Properties removed:   ${fullPropCount - essentialPropCount} (${((fullPropCount - essentialPropCount) / fullPropCount * 100).toFixed(1)}%)`, colors.green);
 83 |     
 84 |     log(`\n🔧 Essential Properties:`, colors.bright);
 85 |     log(`   Required: ${essentialInfo.requiredProperties?.map((p: any) => p.name).join(', ') || 'None'}`);
 86 |     log(`   Common:   ${essentialInfo.commonProperties?.map((p: any) => p.name).join(', ') || 'None'}`);
 87 |     
 88 |     log(`\n📚 Examples:`, colors.bright);
 89 |     const examples = Object.keys(essentialInfo.examples || {});
 90 |     log(`   Available examples: ${examples.join(', ') || 'None'}`);
 91 |     
 92 |     if (essentialInfo.examples?.minimal) {
 93 |       log(`   Minimal example properties: ${Object.keys(essentialInfo.examples.minimal).join(', ')}`);
 94 |     }
 95 |     
 96 |     log(`\n📊 Metadata:`, colors.bright);
 97 |     log(`   Total properties available: ${essentialInfo.metadata?.totalProperties || 0}`);
 98 |     log(`   Is AI Tool: ${essentialInfo.metadata?.isAITool ? 'Yes' : 'No'}`);
 99 |     log(`   Is Trigger: ${essentialInfo.metadata?.isTrigger ? 'Yes' : 'No'}`);
100 |     log(`   Has Credentials: ${essentialInfo.metadata?.hasCredentials ? 'Yes' : 'No'}`);
101 |     
102 |     // Test property search
103 |     const searchTerms = ['auth', 'header', 'body', 'json'];
104 |     log(`\n🔍 Property Search Test:`, colors.bright);
105 |     
106 |     for (const term of searchTerms) {
107 |       try {
108 |         const searchResult = await server.executeTool('search_node_properties', {
109 |           nodeType,
110 |           query: term,
111 |           maxResults: 5
112 |         });
113 |         log(`   "${term}": Found ${searchResult.totalMatches} properties`);
114 |       } catch (error) {
115 |         log(`   "${term}": Search failed`, colors.red);
116 |       }
117 |     }
118 |     
119 |     return {
120 |       nodeType,
121 |       fullSize,
122 |       essentialSize,
123 |       sizeReduction: parseFloat(sizeReduction),
124 |       fullPropCount,
125 |       essentialPropCount,
126 |       success: true
127 |     };
128 |     
129 |   } catch (error) {
130 |     log(`❌ Error testing ${nodeType}: ${error}`, colors.red);
131 |     return {
132 |       nodeType,
133 |       fullSize: 0,
134 |       essentialSize: 0,
135 |       sizeReduction: 0,
136 |       fullPropCount: 0,
137 |       essentialPropCount: 0,
138 |       success: false,
139 |       error: error instanceof Error ? error.message : String(error)
140 |     };
141 |   }
142 | }
143 | 
144 | async function main() {
145 |   logSection('n8n MCP Essentials Tool Test Suite');
146 |   
147 |   try {
148 |     // Initialize server
149 |     log('\n🚀 Initializing MCP server...', colors.cyan);
150 |     const server = new N8NDocumentationMCPServer();
151 |     
152 |     // Wait for initialization
153 |     await new Promise(resolve => setTimeout(resolve, 1000));
154 |     
155 |     // Test nodes
156 |     const testNodes = [
157 |       'nodes-base.httpRequest',
158 |       'nodes-base.webhook',
159 |       'nodes-base.code',
160 |       'nodes-base.set',
161 |       'nodes-base.if',
162 |       'nodes-base.postgres',
163 |       'nodes-base.openAi',
164 |       'nodes-base.googleSheets',
165 |       'nodes-base.slack',
166 |       'nodes-base.merge'
167 |     ];
168 |     
169 |     const results = [];
170 |     
171 |     for (const nodeType of testNodes) {
172 |       const result = await testNodeEssentials(server, nodeType);
173 |       results.push(result);
174 |     }
175 |     
176 |     // Summary
177 |     logSection('Test Summary');
178 |     
179 |     const successful = results.filter(r => r.success);
180 |     const totalFullSize = successful.reduce((sum, r) => sum + r.fullSize, 0);
181 |     const totalEssentialSize = successful.reduce((sum, r) => sum + r.essentialSize, 0);
182 |     const avgReduction = successful.reduce((sum, r) => sum + r.sizeReduction, 0) / successful.length;
183 |     
184 |     log(`\n✅ Successful tests: ${successful.length}/${results.length}`, colors.green);
185 |     
186 |     if (successful.length > 0) {
187 |       log(`\n📊 Overall Statistics:`, colors.bright);
188 |       log(`   Total full size:      ${formatBytes(totalFullSize)}`);
189 |       log(`   Total essential size: ${formatBytes(totalEssentialSize)}`);
190 |       log(`   Average reduction:    ${avgReduction.toFixed(1)}%`, colors.bright + colors.green);
191 |       
192 |       log(`\n🏆 Best Performers:`, colors.bright);
193 |       const sorted = successful.sort((a, b) => b.sizeReduction - a.sizeReduction);
194 |       sorted.slice(0, 3).forEach((r, i) => {
195 |         log(`   ${i + 1}. ${r.nodeType}: ${r.sizeReduction}% reduction (${formatBytes(r.fullSize)} → ${formatBytes(r.essentialSize)})`);
196 |       });
197 |     }
198 |     
199 |     const failed = results.filter(r => !r.success);
200 |     if (failed.length > 0) {
201 |       log(`\n❌ Failed tests:`, colors.red);
202 |       failed.forEach(r => {
203 |         log(`   - ${r.nodeType}: ${r.error}`, colors.red);
204 |       });
205 |     }
206 |     
207 |     // Save detailed results
208 |     const reportPath = join(process.cwd(), 'test-results-essentials.json');
209 |     writeFileSync(reportPath, JSON.stringify({
210 |       timestamp: new Date().toISOString(),
211 |       summary: {
212 |         totalTests: results.length,
213 |         successful: successful.length,
214 |         failed: failed.length,
215 |         averageReduction: avgReduction,
216 |         totalFullSize,
217 |         totalEssentialSize
218 |       },
219 |       results
220 |     }, null, 2));
221 |     
222 |     log(`\n📄 Detailed results saved to: ${reportPath}`, colors.cyan);
223 |     
224 |     // Recommendations
225 |     logSection('Recommendations');
226 |     
227 |     if (avgReduction > 90) {
228 |       log('✨ Excellent! The essentials tool is achieving >90% size reduction.', colors.green);
229 |     } else if (avgReduction > 80) {
230 |       log('👍 Good! The essentials tool is achieving 80-90% size reduction.', colors.yellow);
231 |       log('   Consider reviewing nodes with lower reduction rates.');
232 |     } else {
233 |       log('⚠️  The average size reduction is below 80%.', colors.yellow);
234 |       log('   Review the essential property lists for optimization.');
235 |     }
236 |     
237 |     // Test specific functionality
238 |     logSection('Testing Advanced Features');
239 |     
240 |     // Test error handling
241 |     log('\n🧪 Testing error handling...', colors.cyan);
242 |     try {
243 |       await server.executeTool('get_node_essentials', { nodeType: 'non-existent-node' });
244 |       log('   ❌ Error handling failed - should have thrown error', colors.red);
245 |     } catch (error) {
246 |       log('   ✅ Error handling works correctly', colors.green);
247 |     }
248 |     
249 |     // Test alternative node type formats
250 |     log('\n🧪 Testing alternative node type formats...', colors.cyan);
251 |     const alternativeFormats = [
252 |       { input: 'httpRequest', expected: 'nodes-base.httpRequest' },
253 |       { input: 'nodes-base.httpRequest', expected: 'nodes-base.httpRequest' },
254 |       { input: 'HTTPREQUEST', expected: 'nodes-base.httpRequest' }
255 |     ];
256 |     
257 |     for (const format of alternativeFormats) {
258 |       try {
259 |         const result = await server.executeTool('get_node_essentials', { nodeType: format.input });
260 |         if (result.nodeType === format.expected) {
261 |           log(`   ✅ "${format.input}" → "${format.expected}"`, colors.green);
262 |         } else {
263 |           log(`   ❌ "${format.input}" → "${result.nodeType}" (expected "${format.expected}")`, colors.red);
264 |         }
265 |       } catch (error) {
266 |         log(`   ❌ "${format.input}" → Error: ${error}`, colors.red);
267 |       }
268 |     }
269 |     
270 |     log('\n✨ Test suite completed!', colors.bright + colors.green);
271 |     
272 |   } catch (error) {
273 |     log(`\n❌ Fatal error: ${error}`, colors.red);
274 |     process.exit(1);
275 |   }
276 | }
277 | 
278 | // Run the test
279 | main().catch(error => {
280 |   console.error('Unhandled error:', error);
281 |   process.exit(1);
282 | });
```

--------------------------------------------------------------------------------
/src/services/expression-validator.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Expression Validator for n8n expressions
  3 |  * Validates expression syntax, variable references, and context availability
  4 |  */
  5 | 
  6 | interface ExpressionValidationResult {
  7 |   valid: boolean;
  8 |   errors: string[];
  9 |   warnings: string[];
 10 |   usedVariables: Set<string>;
 11 |   usedNodes: Set<string>;
 12 | }
 13 | 
 14 | interface ExpressionContext {
 15 |   availableNodes: string[];
 16 |   currentNodeName?: string;
 17 |   isInLoop?: boolean;
 18 |   hasInputData?: boolean;
 19 | }
 20 | 
 21 | export class ExpressionValidator {
 22 |   // Common n8n expression patterns
 23 |   private static readonly EXPRESSION_PATTERN = /\{\{([\s\S]+?)\}\}/g;
 24 |   private static readonly VARIABLE_PATTERNS = {
 25 |     json: /\$json(\.[a-zA-Z_][\w]*|\["[^"]+"\]|\['[^']+'\]|\[\d+\])*/g,
 26 |     node: /\$node\["([^"]+)"\]\.json/g,
 27 |     input: /\$input\.item(\.[a-zA-Z_][\w]*|\["[^"]+"\]|\['[^']+'\]|\[\d+\])*/g,
 28 |     items: /\$items\("([^"]+)"(?:,\s*(-?\d+))?\)/g,
 29 |     parameter: /\$parameter\["([^"]+)"\]/g,
 30 |     env: /\$env\.([a-zA-Z_][\w]*)/g,
 31 |     workflow: /\$workflow\.(id|name|active)/g,
 32 |     execution: /\$execution\.(id|mode|resumeUrl)/g,
 33 |     prevNode: /\$prevNode\.(name|outputIndex|runIndex)/g,
 34 |     itemIndex: /\$itemIndex/g,
 35 |     runIndex: /\$runIndex/g,
 36 |     now: /\$now/g,
 37 |     today: /\$today/g,
 38 |   };
 39 | 
 40 |   /**
 41 |    * Validate a single expression
 42 |    */
 43 |   static validateExpression(
 44 |     expression: string,
 45 |     context: ExpressionContext
 46 |   ): ExpressionValidationResult {
 47 |     const result: ExpressionValidationResult = {
 48 |       valid: true,
 49 |       errors: [],
 50 |       warnings: [],
 51 |       usedVariables: new Set(),
 52 |       usedNodes: new Set(),
 53 |     };
 54 | 
 55 |     // Handle null/undefined expression
 56 |     if (!expression) {
 57 |       return result;
 58 |     }
 59 | 
 60 |     // Handle null/undefined context
 61 |     if (!context) {
 62 |       result.valid = false;
 63 |       result.errors.push('Validation context is required');
 64 |       return result;
 65 |     }
 66 | 
 67 |     // Check for basic syntax errors
 68 |     const syntaxErrors = this.checkSyntaxErrors(expression);
 69 |     result.errors.push(...syntaxErrors);
 70 | 
 71 |     // Extract all expressions
 72 |     const expressions = this.extractExpressions(expression);
 73 |     
 74 |     for (const expr of expressions) {
 75 |       // Validate each expression
 76 |       this.validateSingleExpression(expr, context, result);
 77 |     }
 78 | 
 79 |     // Check for undefined node references
 80 |     this.checkNodeReferences(result, context);
 81 | 
 82 |     result.valid = result.errors.length === 0;
 83 |     return result;
 84 |   }
 85 | 
 86 |   /**
 87 |    * Check for basic syntax errors
 88 |    */
 89 |   private static checkSyntaxErrors(expression: string): string[] {
 90 |     const errors: string[] = [];
 91 | 
 92 |     // Check for unmatched brackets
 93 |     const openBrackets = (expression.match(/\{\{/g) || []).length;
 94 |     const closeBrackets = (expression.match(/\}\}/g) || []).length;
 95 |     
 96 |     if (openBrackets !== closeBrackets) {
 97 |       errors.push('Unmatched expression brackets {{ }}');
 98 |     }
 99 | 
100 |     // Check for nested expressions (not supported in n8n)
101 |     if (expression.includes('{{') && expression.includes('{{', expression.indexOf('{{') + 2)) {
102 |       const match = expression.match(/\{\{.*\{\{/);
103 |       if (match) {
104 |         errors.push('Nested expressions are not supported');
105 |       }
106 |     }
107 | 
108 |     // Check for empty expressions
109 |     const emptyExpressionPattern = /\{\{\s*\}\}/;
110 |     if (emptyExpressionPattern.test(expression)) {
111 |       errors.push('Empty expression found');
112 |     }
113 | 
114 |     return errors;
115 |   }
116 | 
117 |   /**
118 |    * Extract all expressions from a string
119 |    */
120 |   private static extractExpressions(text: string): string[] {
121 |     const expressions: string[] = [];
122 |     let match;
123 |     
124 |     while ((match = this.EXPRESSION_PATTERN.exec(text)) !== null) {
125 |       expressions.push(match[1].trim());
126 |     }
127 |     
128 |     return expressions;
129 |   }
130 | 
131 |   /**
132 |    * Validate a single expression content
133 |    */
134 |   private static validateSingleExpression(
135 |     expr: string,
136 |     context: ExpressionContext,
137 |     result: ExpressionValidationResult
138 |   ): void {
139 |     // Check for $json usage
140 |     let match;
141 |     const jsonPattern = new RegExp(this.VARIABLE_PATTERNS.json.source, this.VARIABLE_PATTERNS.json.flags);
142 |     while ((match = jsonPattern.exec(expr)) !== null) {
143 |       result.usedVariables.add('$json');
144 | 
145 |       if (!context.hasInputData && !context.isInLoop) {
146 |         result.warnings.push(
147 |           'Using $json but node might not have input data'
148 |         );
149 |       }
150 | 
151 |       // Check for suspicious property names that might be test/invalid data
152 |       const fullMatch = match[0];
153 |       if (fullMatch.includes('.invalid') || fullMatch.includes('.undefined') ||
154 |           fullMatch.includes('.null') || fullMatch.includes('.test')) {
155 |         result.warnings.push(
156 |           `Property access '${fullMatch}' looks suspicious - verify this property exists in your data`
157 |         );
158 |       }
159 |     }
160 | 
161 |     // Check for $node references
162 |     const nodePattern = new RegExp(this.VARIABLE_PATTERNS.node.source, this.VARIABLE_PATTERNS.node.flags);
163 |     while ((match = nodePattern.exec(expr)) !== null) {
164 |       const nodeName = match[1];
165 |       result.usedNodes.add(nodeName);
166 |       result.usedVariables.add('$node');
167 |     }
168 | 
169 |     // Check for $input usage
170 |     const inputPattern = new RegExp(this.VARIABLE_PATTERNS.input.source, this.VARIABLE_PATTERNS.input.flags);
171 |     while ((match = inputPattern.exec(expr)) !== null) {
172 |       result.usedVariables.add('$input');
173 |       
174 |       if (!context.hasInputData) {
175 |         result.warnings.push(
176 |           '$input is only available when the node has input data'
177 |         );
178 |       }
179 |     }
180 | 
181 |     // Check for $items usage
182 |     const itemsPattern = new RegExp(this.VARIABLE_PATTERNS.items.source, this.VARIABLE_PATTERNS.items.flags);
183 |     while ((match = itemsPattern.exec(expr)) !== null) {
184 |       const nodeName = match[1];
185 |       result.usedNodes.add(nodeName);
186 |       result.usedVariables.add('$items');
187 |     }
188 | 
189 |     // Check for other variables
190 |     for (const [varName, pattern] of Object.entries(this.VARIABLE_PATTERNS)) {
191 |       if (['json', 'node', 'input', 'items'].includes(varName)) continue;
192 |       
193 |       const testPattern = new RegExp(pattern.source, pattern.flags);
194 |       if (testPattern.test(expr)) {
195 |         result.usedVariables.add(`$${varName}`);
196 |       }
197 |     }
198 | 
199 |     // Check for common mistakes
200 |     this.checkCommonMistakes(expr, result);
201 |   }
202 | 
203 |   /**
204 |    * Check for common expression mistakes
205 |    */
206 |   private static checkCommonMistakes(
207 |     expr: string,
208 |     result: ExpressionValidationResult
209 |   ): void {
210 |     // Check for missing $ prefix - but exclude cases where $ is already present
211 |     const missingPrefixPattern = /(?<!\$)\b(json|node|input|items|workflow|execution)\b(?!\s*:)/;
212 |     if (expr.match(missingPrefixPattern)) {
213 |       result.warnings.push(
214 |         'Possible missing $ prefix for variable (e.g., use $json instead of json)'
215 |       );
216 |     }
217 | 
218 |     // Check for incorrect array access
219 |     if (expr.includes('$json[') && !expr.match(/\$json\[\d+\]/)) {
220 |       result.warnings.push(
221 |         'Array access should use numeric index: $json[0] or property access: $json.property'
222 |       );
223 |     }
224 | 
225 |     // Check for Python-style property access
226 |     if (expr.match(/\$json\['[^']+'\]/)) {
227 |       result.warnings.push(
228 |         "Consider using dot notation: $json.property instead of $json['property']"
229 |       );
230 |     }
231 | 
232 |     // Check for undefined/null access attempts
233 |     if (expr.match(/\?\./)) {
234 |       result.warnings.push(
235 |         'Optional chaining (?.) is not supported in n8n expressions'
236 |       );
237 |     }
238 | 
239 |     // Check for template literals
240 |     if (expr.includes('${')) {
241 |       result.errors.push(
242 |         'Template literals ${} are not supported. Use string concatenation instead'
243 |       );
244 |     }
245 |   }
246 | 
247 |   /**
248 |    * Check that all referenced nodes exist
249 |    */
250 |   private static checkNodeReferences(
251 |     result: ExpressionValidationResult,
252 |     context: ExpressionContext
253 |   ): void {
254 |     for (const nodeName of result.usedNodes) {
255 |       if (!context.availableNodes.includes(nodeName)) {
256 |         result.errors.push(
257 |           `Referenced node "${nodeName}" not found in workflow`
258 |         );
259 |       }
260 |     }
261 |   }
262 | 
263 |   /**
264 |    * Validate all expressions in a node's parameters
265 |    */
266 |   static validateNodeExpressions(
267 |     parameters: any,
268 |     context: ExpressionContext
269 |   ): ExpressionValidationResult {
270 |     const combinedResult: ExpressionValidationResult = {
271 |       valid: true,
272 |       errors: [],
273 |       warnings: [],
274 |       usedVariables: new Set(),
275 |       usedNodes: new Set(),
276 |     };
277 | 
278 |     const visited = new WeakSet();
279 |     this.validateParametersRecursive(parameters, context, combinedResult, '', visited);
280 |     
281 |     combinedResult.valid = combinedResult.errors.length === 0;
282 |     return combinedResult;
283 |   }
284 | 
285 |   /**
286 |    * Recursively validate expressions in parameters
287 |    */
288 |   private static validateParametersRecursive(
289 |     obj: any,
290 |     context: ExpressionContext,
291 |     result: ExpressionValidationResult,
292 |     path: string = '',
293 |     visited: WeakSet<object> = new WeakSet()
294 |   ): void {
295 |     // Handle circular references
296 |     if (obj && typeof obj === 'object') {
297 |       if (visited.has(obj)) {
298 |         return; // Skip already visited objects
299 |       }
300 |       visited.add(obj);
301 |     }
302 |     
303 |     if (typeof obj === 'string') {
304 |       if (obj.includes('{{')) {
305 |         const validation = this.validateExpression(obj, context);
306 |         
307 |         // Add path context to errors
308 |         validation.errors.forEach(error => {
309 |           result.errors.push(path ? `${path}: ${error}` : error);
310 |         });
311 |         
312 |         validation.warnings.forEach(warning => {
313 |           result.warnings.push(path ? `${path}: ${warning}` : warning);
314 |         });
315 |         
316 |         // Merge used variables and nodes
317 |         validation.usedVariables.forEach(v => result.usedVariables.add(v));
318 |         validation.usedNodes.forEach(n => result.usedNodes.add(n));
319 |       }
320 |     } else if (Array.isArray(obj)) {
321 |       obj.forEach((item, index) => {
322 |         this.validateParametersRecursive(
323 |           item,
324 |           context,
325 |           result,
326 |           `${path}[${index}]`,
327 |           visited
328 |         );
329 |       });
330 |     } else if (obj && typeof obj === 'object') {
331 |       Object.entries(obj).forEach(([key, value]) => {
332 |         const newPath = path ? `${path}.${key}` : key;
333 |         this.validateParametersRecursive(value, context, result, newPath, visited);
334 |       });
335 |     }
336 |   }
337 | }
```

--------------------------------------------------------------------------------
/src/mcp/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { N8NDocumentationMCPServer } from './server';
  4 | import { logger } from '../utils/logger';
  5 | import { TelemetryConfigManager } from '../telemetry/config-manager';
  6 | import { EarlyErrorLogger } from '../telemetry/early-error-logger';
  7 | import { STARTUP_CHECKPOINTS, findFailedCheckpoint, StartupCheckpoint } from '../telemetry/startup-checkpoints';
  8 | import { existsSync } from 'fs';
  9 | 
 10 | // Add error details to stderr for Claude Desktop debugging
 11 | process.on('uncaughtException', (error) => {
 12 |   if (process.env.MCP_MODE !== 'stdio') {
 13 |     console.error('Uncaught Exception:', error);
 14 |   }
 15 |   logger.error('Uncaught Exception:', error);
 16 |   process.exit(1);
 17 | });
 18 | 
 19 | process.on('unhandledRejection', (reason, promise) => {
 20 |   if (process.env.MCP_MODE !== 'stdio') {
 21 |     console.error('Unhandled Rejection at:', promise, 'reason:', reason);
 22 |   }
 23 |   logger.error('Unhandled Rejection:', reason);
 24 |   process.exit(1);
 25 | });
 26 | 
 27 | /**
 28 |  * Detects if running in a container environment (Docker, Podman, Kubernetes, etc.)
 29 |  * Uses multiple detection methods for robustness:
 30 |  * 1. Environment variables (IS_DOCKER, IS_CONTAINER with multiple formats)
 31 |  * 2. Filesystem markers (/.dockerenv, /run/.containerenv)
 32 |  */
 33 | function isContainerEnvironment(): boolean {
 34 |   // Check environment variables with multiple truthy formats
 35 |   const dockerEnv = (process.env.IS_DOCKER || '').toLowerCase();
 36 |   const containerEnv = (process.env.IS_CONTAINER || '').toLowerCase();
 37 | 
 38 |   if (['true', '1', 'yes'].includes(dockerEnv)) {
 39 |     return true;
 40 |   }
 41 |   if (['true', '1', 'yes'].includes(containerEnv)) {
 42 |     return true;
 43 |   }
 44 | 
 45 |   // Fallback: Check filesystem markers
 46 |   // /.dockerenv exists in Docker containers
 47 |   // /run/.containerenv exists in Podman containers
 48 |   try {
 49 |     return existsSync('/.dockerenv') || existsSync('/run/.containerenv');
 50 |   } catch (error) {
 51 |     // If filesystem check fails, assume not in container
 52 |     logger.debug('Container detection filesystem check failed:', error);
 53 |     return false;
 54 |   }
 55 | }
 56 | 
 57 | async function main() {
 58 |   // Initialize early error logger for pre-handshake error capture (v2.18.3)
 59 |   // Now using singleton pattern with defensive initialization
 60 |   const startTime = Date.now();
 61 |   const earlyLogger = EarlyErrorLogger.getInstance();
 62 |   const checkpoints: StartupCheckpoint[] = [];
 63 | 
 64 |   try {
 65 |     // Checkpoint: Process started (fire-and-forget, no await)
 66 |     earlyLogger.logCheckpoint(STARTUP_CHECKPOINTS.PROCESS_STARTED);
 67 |     checkpoints.push(STARTUP_CHECKPOINTS.PROCESS_STARTED);
 68 | 
 69 |     // Handle telemetry CLI commands
 70 |     const args = process.argv.slice(2);
 71 |   if (args.length > 0 && args[0] === 'telemetry') {
 72 |     const telemetryConfig = TelemetryConfigManager.getInstance();
 73 |     const action = args[1];
 74 | 
 75 |     switch (action) {
 76 |       case 'enable':
 77 |         telemetryConfig.enable();
 78 |         process.exit(0);
 79 |         break;
 80 |       case 'disable':
 81 |         telemetryConfig.disable();
 82 |         process.exit(0);
 83 |         break;
 84 |       case 'status':
 85 |         console.log(telemetryConfig.getStatus());
 86 |         process.exit(0);
 87 |         break;
 88 |       default:
 89 |         console.log(`
 90 | Usage: n8n-mcp telemetry [command]
 91 | 
 92 | Commands:
 93 |   enable   Enable anonymous telemetry
 94 |   disable  Disable anonymous telemetry
 95 |   status   Show current telemetry status
 96 | 
 97 | Learn more: https://github.com/czlonkowski/n8n-mcp/blob/main/PRIVACY.md
 98 | `);
 99 |         process.exit(args[1] ? 1 : 0);
100 |     }
101 |   }
102 | 
103 |   const mode = process.env.MCP_MODE || 'stdio';
104 | 
105 |     // Checkpoint: Telemetry initializing (fire-and-forget, no await)
106 |     earlyLogger.logCheckpoint(STARTUP_CHECKPOINTS.TELEMETRY_INITIALIZING);
107 |     checkpoints.push(STARTUP_CHECKPOINTS.TELEMETRY_INITIALIZING);
108 | 
109 |     // Telemetry is already initialized by TelemetryConfigManager in imports
110 |     // Mark as ready (fire-and-forget, no await)
111 |     earlyLogger.logCheckpoint(STARTUP_CHECKPOINTS.TELEMETRY_READY);
112 |     checkpoints.push(STARTUP_CHECKPOINTS.TELEMETRY_READY);
113 | 
114 |   try {
115 |     // Only show debug messages in HTTP mode to avoid corrupting stdio communication
116 |     if (mode === 'http') {
117 |       console.error(`Starting n8n Documentation MCP Server in ${mode} mode...`);
118 |       console.error('Current directory:', process.cwd());
119 |       console.error('Node version:', process.version);
120 |     }
121 | 
122 |     // Checkpoint: MCP handshake starting (fire-and-forget, no await)
123 |     earlyLogger.logCheckpoint(STARTUP_CHECKPOINTS.MCP_HANDSHAKE_STARTING);
124 |     checkpoints.push(STARTUP_CHECKPOINTS.MCP_HANDSHAKE_STARTING);
125 |     
126 |     if (mode === 'http') {
127 |       // Check if we should use the fixed implementation
128 |       if (process.env.USE_FIXED_HTTP === 'true') {
129 |         // Use the fixed HTTP implementation that bypasses StreamableHTTPServerTransport issues
130 |         const { startFixedHTTPServer } = await import('../http-server');
131 |         await startFixedHTTPServer();
132 |       } else {
133 |         // HTTP mode - for remote deployment with single-session architecture
134 |         const { SingleSessionHTTPServer } = await import('../http-server-single-session');
135 |         const server = new SingleSessionHTTPServer();
136 |         
137 |         // Graceful shutdown handlers
138 |         const shutdown = async () => {
139 |           await server.shutdown();
140 |           process.exit(0);
141 |         };
142 |         
143 |         process.on('SIGTERM', shutdown);
144 |         process.on('SIGINT', shutdown);
145 |         
146 |         await server.start();
147 |       }
148 |     } else {
149 |       // Stdio mode - for local Claude Desktop
150 |       const server = new N8NDocumentationMCPServer(undefined, earlyLogger);
151 | 
152 |       // Graceful shutdown handler (fixes Issue #277)
153 |       let isShuttingDown = false;
154 |       const shutdown = async (signal: string = 'UNKNOWN') => {
155 |         if (isShuttingDown) return; // Prevent multiple shutdown calls
156 |         isShuttingDown = true;
157 | 
158 |         try {
159 |           logger.info(`Shutdown initiated by: ${signal}`);
160 | 
161 |           await server.shutdown();
162 | 
163 |           // Close stdin to signal we're done reading
164 |           if (process.stdin && !process.stdin.destroyed) {
165 |             process.stdin.pause();
166 |             process.stdin.destroy();
167 |           }
168 | 
169 |           // Exit with timeout to ensure we don't hang
170 |           // Increased to 1000ms for slower systems
171 |           setTimeout(() => {
172 |             logger.warn('Shutdown timeout exceeded, forcing exit');
173 |             process.exit(0);
174 |           }, 1000).unref();
175 | 
176 |           // Let the timeout handle the exit for graceful shutdown
177 |           // (removed immediate exit to allow cleanup to complete)
178 |         } catch (error) {
179 |           logger.error('Error during shutdown:', error);
180 |           process.exit(1);
181 |         }
182 |       };
183 | 
184 |       // Handle termination signals (fixes Issue #277)
185 |       // Signal handling strategy:
186 |       // - Claude Desktop (Windows/macOS/Linux): stdin handlers + signal handlers
187 |       //   Primary: stdin close when Claude quits | Fallback: SIGTERM/SIGINT/SIGHUP
188 |       // - Container environments: signal handlers ONLY
189 |       //   stdin closed in detached mode would trigger immediate shutdown
190 |       //   Container detection via IS_DOCKER/IS_CONTAINER env vars + filesystem markers
191 |       // - Manual execution: Both stdin and signal handlers work
192 |       process.on('SIGTERM', () => shutdown('SIGTERM'));
193 |       process.on('SIGINT', () => shutdown('SIGINT'));
194 |       process.on('SIGHUP', () => shutdown('SIGHUP'));
195 | 
196 |       // Handle stdio disconnect - PRIMARY shutdown mechanism for Claude Desktop
197 |       // Skip in container environments (Docker, Kubernetes, Podman) to prevent
198 |       // premature shutdown when stdin is closed in detached mode.
199 |       // Containers rely on signal handlers (SIGTERM/SIGINT/SIGHUP) for proper shutdown.
200 |       const isContainer = isContainerEnvironment();
201 | 
202 |       if (!isContainer && process.stdin.readable && !process.stdin.destroyed) {
203 |         try {
204 |           process.stdin.on('end', () => shutdown('STDIN_END'));
205 |           process.stdin.on('close', () => shutdown('STDIN_CLOSE'));
206 |         } catch (error) {
207 |           logger.error('Failed to register stdin handlers, using signal handlers only:', error);
208 |           // Continue - signal handlers will still work
209 |         }
210 |       }
211 | 
212 |       await server.run();
213 |     }
214 | 
215 |     // Checkpoint: MCP handshake complete (fire-and-forget, no await)
216 |     earlyLogger.logCheckpoint(STARTUP_CHECKPOINTS.MCP_HANDSHAKE_COMPLETE);
217 |     checkpoints.push(STARTUP_CHECKPOINTS.MCP_HANDSHAKE_COMPLETE);
218 | 
219 |     // Checkpoint: Server ready (fire-and-forget, no await)
220 |     earlyLogger.logCheckpoint(STARTUP_CHECKPOINTS.SERVER_READY);
221 |     checkpoints.push(STARTUP_CHECKPOINTS.SERVER_READY);
222 | 
223 |     // Log successful startup (fire-and-forget, no await)
224 |     const startupDuration = Date.now() - startTime;
225 |     earlyLogger.logStartupSuccess(checkpoints, startupDuration);
226 | 
227 |     logger.info(`Server startup completed in ${startupDuration}ms (${checkpoints.length} checkpoints passed)`);
228 | 
229 |   } catch (error) {
230 |     // Log startup error with checkpoint context (fire-and-forget, no await)
231 |     const failedCheckpoint = findFailedCheckpoint(checkpoints);
232 |     earlyLogger.logStartupError(failedCheckpoint, error);
233 | 
234 |     // In stdio mode, we cannot output to console at all
235 |     if (mode !== 'stdio') {
236 |       console.error('Failed to start MCP server:', error);
237 |       logger.error('Failed to start MCP server', error);
238 | 
239 |       // Provide helpful error messages
240 |       if (error instanceof Error && error.message.includes('nodes.db not found')) {
241 |         console.error('\nTo fix this issue:');
242 |         console.error('1. cd to the n8n-mcp directory');
243 |         console.error('2. Run: npm run build');
244 |         console.error('3. Run: npm run rebuild');
245 |       } else if (error instanceof Error && error.message.includes('NODE_MODULE_VERSION')) {
246 |         console.error('\nTo fix this Node.js version mismatch:');
247 |         console.error('1. cd to the n8n-mcp directory');
248 |         console.error('2. Run: npm rebuild better-sqlite3');
249 |         console.error('3. If that doesn\'t work, try: rm -rf node_modules && npm install');
250 |       }
251 |     }
252 | 
253 |     process.exit(1);
254 |   }
255 |   } catch (outerError) {
256 |     // Outer error catch for early initialization failures
257 |     logger.error('Critical startup error:', outerError);
258 |     process.exit(1);
259 |   }
260 | }
261 | 
262 | // Run if called directly
263 | if (require.main === module) {
264 |   main().catch(console.error);
265 | }
```

--------------------------------------------------------------------------------
/docs/DOCKER_TROUBLESHOOTING.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Docker Troubleshooting Guide
  2 | 
  3 | This guide helps resolve common issues when running n8n-mcp with Docker, especially when connecting to n8n instances.
  4 | 
  5 | ## Table of Contents
  6 | - [Common Issues](#common-issues)
  7 |   - [502 Bad Gateway Errors](#502-bad-gateway-errors)
  8 |   - [Custom Database Path Not Working](#custom-database-path-not-working-v27160)
  9 |   - [Container Name Conflicts](#container-name-conflicts)
 10 |   - [n8n API Connection Issues](#n8n-api-connection-issues)
 11 | - [Docker Networking](#docker-networking)
 12 | - [Quick Solutions](#quick-solutions)
 13 | - [Debugging Steps](#debugging-steps)
 14 | 
 15 | ## Common Issues
 16 | 
 17 | ### Docker Configuration File Not Working (v2.8.2+)
 18 | 
 19 | **Symptoms:**
 20 | - Config file mounted but environment variables not set
 21 | - Container starts but ignores configuration
 22 | - Getting "permission denied" errors
 23 | 
 24 | **Solutions:**
 25 | 
 26 | 1. **Ensure file is mounted correctly:**
 27 | ```bash
 28 | # Correct - mount as read-only
 29 | docker run -v $(pwd)/config.json:/app/config.json:ro ...
 30 | 
 31 | # Check if file is accessible
 32 | docker exec n8n-mcp cat /app/config.json
 33 | ```
 34 | 
 35 | 2. **Verify JSON syntax:**
 36 | ```bash
 37 | # Validate JSON file
 38 | cat config.json | jq .
 39 | ```
 40 | 
 41 | 3. **Check Docker logs for parsing errors:**
 42 | ```bash
 43 | docker logs n8n-mcp | grep -i config
 44 | ```
 45 | 
 46 | 4. **Common issues:**
 47 | - Invalid JSON syntax (use a JSON validator)
 48 | - File permissions (should be readable)
 49 | - Wrong mount path (must be `/app/config.json`)
 50 | - Dangerous variables blocked (PATH, LD_PRELOAD, etc.)
 51 | 
 52 | ### Custom Database Path Not Working (v2.7.16+)
 53 | 
 54 | **Symptoms:**
 55 | - `NODE_DB_PATH` environment variable is set but ignored
 56 | - Database always created at `/app/data/nodes.db`
 57 | - Custom path setting has no effect
 58 | 
 59 | **Root Cause:** Fixed in v2.7.16. Earlier versions had hardcoded paths in docker-entrypoint.sh.
 60 | 
 61 | **Solutions:**
 62 | 
 63 | 1. **Update to v2.7.16 or later:**
 64 | ```bash
 65 | docker pull ghcr.io/czlonkowski/n8n-mcp:latest
 66 | ```
 67 | 
 68 | 2. **Ensure path ends with .db:**
 69 | ```bash
 70 | # Correct
 71 | NODE_DB_PATH=/app/data/custom/my-nodes.db
 72 | 
 73 | # Incorrect (will be rejected)
 74 | NODE_DB_PATH=/app/data/custom/my-nodes
 75 | ```
 76 | 
 77 | 3. **Use path within mounted volume for persistence:**
 78 | ```yaml
 79 | services:
 80 |   n8n-mcp:
 81 |     environment:
 82 |       NODE_DB_PATH: /app/data/custom/nodes.db
 83 |     volumes:
 84 |       - n8n-mcp-data:/app/data  # Ensure parent directory is mounted
 85 | ```
 86 | 
 87 | ### 502 Bad Gateway Errors
 88 | 
 89 | **Symptoms:**
 90 | - `n8n_health_check` returns 502 error
 91 | - All n8n management API calls fail
 92 | - n8n web UI is accessible but API is not
 93 | 
 94 | **Root Cause:** Network connectivity issues between n8n-mcp container and n8n instance.
 95 | 
 96 | **Solutions:**
 97 | 
 98 | #### 1. When n8n runs in Docker on same machine
 99 | 
100 | Use Docker's special hostnames instead of `localhost`:
101 | 
102 | ```json
103 | {
104 |   "mcpServers": {
105 |     "n8n-mcp": {
106 |       "command": "docker",
107 |       "args": [
108 |         "run", "-i", "--rm",
109 |         "-e", "N8N_API_URL=http://host.docker.internal:5678",
110 |         "-e", "N8N_API_KEY=your-api-key",
111 |         "ghcr.io/czlonkowski/n8n-mcp:latest"
112 |       ]
113 |     }
114 |   }
115 | }
116 | ```
117 | 
118 | **Alternative hostnames to try:**
119 | - `host.docker.internal` (Docker Desktop on macOS/Windows)
120 | - `172.17.0.1` (Default Docker bridge IP on Linux)
121 | - Your machine's actual IP address (e.g., `192.168.1.100`)
122 | 
123 | #### 2. When both containers are in same Docker network
124 | 
125 | ```bash
126 | # Create a shared network
127 | docker network create n8n-network
128 | 
129 | # Run n8n in the network
130 | docker run -d --name n8n --network n8n-network -p 5678:5678 n8nio/n8n
131 | 
132 | # Configure n8n-mcp to use container name
133 | ```
134 | 
135 | ```json
136 | {
137 |   "N8N_API_URL": "http://n8n:5678"
138 | }
139 | ```
140 | 
141 | #### 3. For Docker Compose setups
142 | 
143 | ```yaml
144 | # docker-compose.yml
145 | services:
146 |   n8n:
147 |     image: n8nio/n8n
148 |     container_name: n8n
149 |     networks:
150 |       - n8n-net
151 |     ports:
152 |       - "5678:5678"
153 |   
154 |   n8n-mcp:
155 |     image: ghcr.io/czlonkowski/n8n-mcp:latest
156 |     environment:
157 |       N8N_API_URL: http://n8n:5678
158 |       N8N_API_KEY: ${N8N_API_KEY}
159 |     networks:
160 |       - n8n-net
161 | 
162 | networks:
163 |   n8n-net:
164 |     driver: bridge
165 | ```
166 | 
167 | ### Container Cleanup Issues (Fixed in v2.7.20+)
168 | 
169 | **Symptoms:**
170 | - Containers accumulate after Claude Desktop restarts
171 | - Containers show as "unhealthy" but don't clean up
172 | - `--rm` flag doesn't work as expected
173 | 
174 | **Root Cause:** Fixed in v2.7.20 - containers weren't handling termination signals properly.
175 | 
176 | **Solutions:**
177 | 
178 | 1. **Update to v2.7.20+ and use --init flag (Recommended):**
179 | ```json
180 | {
181 |   "command": "docker",
182 |   "args": [
183 |     "run", "-i", "--rm", "--init",
184 |     "ghcr.io/czlonkowski/n8n-mcp:latest"
185 |   ]
186 | }
187 | ```
188 | 
189 | 2. **Manual cleanup of old containers:**
190 | ```bash
191 | # Remove all stopped n8n-mcp containers
192 | docker ps -a | grep n8n-mcp | grep Exited | awk '{print $1}' | xargs -r docker rm
193 | ```
194 | 
195 | 3. **For versions before 2.7.20:**
196 | - Manually clean up containers periodically
197 | - Consider using HTTP mode instead
198 | 
199 | ### Webhooks to Local n8n Fail (v2.16.3+)
200 | 
201 | **Symptoms:**
202 | - `n8n_trigger_webhook_workflow` fails with "SSRF protection" error
203 | - Error message: "SSRF protection: Localhost access is blocked"
204 | - Webhooks work from n8n UI but not from n8n-MCP
205 | 
206 | **Root Cause:** Default strict SSRF protection blocks localhost access to prevent attacks.
207 | 
208 | **Solution:** Use moderate security mode for local development
209 | 
210 | ```bash
211 | # For Docker run
212 | docker run -d \
213 |   --name n8n-mcp \
214 |   -e MCP_MODE=http \
215 |   -e AUTH_TOKEN=your-token \
216 |   -e WEBHOOK_SECURITY_MODE=moderate \
217 |   -p 3000:3000 \
218 |   ghcr.io/czlonkowski/n8n-mcp:latest
219 | 
220 | # For Docker Compose - add to environment:
221 | services:
222 |   n8n-mcp:
223 |     environment:
224 |       WEBHOOK_SECURITY_MODE: moderate
225 | ```
226 | 
227 | **Security Modes Explained:**
228 | - `strict` (default): Blocks localhost + private IPs + cloud metadata (production)
229 | - `moderate`: Allows localhost, blocks private IPs + cloud metadata (local development)
230 | - `permissive`: Allows localhost + private IPs, blocks cloud metadata (testing only)
231 | 
232 | **Important:** Always use `strict` mode in production. Cloud metadata is blocked in all modes.
233 | 
234 | ### n8n API Connection Issues
235 | 
236 | **Symptoms:**
237 | - API calls fail but n8n web UI works
238 | - Authentication errors
239 | - API endpoints return 404
240 | 
241 | **Solutions:**
242 | 
243 | 1. **Verify n8n API is enabled:**
244 |    - Check n8n settings → REST API is enabled
245 |    - Ensure API key is valid and not expired
246 | 
247 | 2. **Test API directly:**
248 | ```bash
249 | # From host machine
250 | curl -H "X-N8N-API-KEY: your-key" http://localhost:5678/api/v1/workflows
251 | 
252 | # From inside Docker container
253 | docker run --rm curlimages/curl \
254 |   -H "X-N8N-API-KEY: your-key" \
255 |   http://host.docker.internal:5678/api/v1/workflows
256 | ```
257 | 
258 | 3. **Check n8n environment variables:**
259 | ```yaml
260 | environment:
261 |   - N8N_BASIC_AUTH_ACTIVE=true
262 |   - N8N_BASIC_AUTH_USER=user
263 |   - N8N_BASIC_AUTH_PASSWORD=password
264 | ```
265 | 
266 | ## Docker Networking
267 | 
268 | ### Understanding Docker Network Modes
269 | 
270 | | Scenario | Use This URL | Why |
271 | |----------|--------------|-----|
272 | | n8n on host, n8n-mcp in Docker | `http://host.docker.internal:5678` | Docker can't reach host's localhost |
273 | | Both in same Docker network | `http://container-name:5678` | Direct container-to-container |
274 | | n8n behind reverse proxy | `http://your-domain.com` | Use public URL |
275 | | Local development | `http://YOUR_LOCAL_IP:5678` | Use machine's IP address |
276 | 
277 | ### Finding Your Configuration
278 | 
279 | ```bash
280 | # Check if n8n is running in Docker
281 | docker ps | grep n8n
282 | 
283 | # Find Docker network
284 | docker network ls
285 | 
286 | # Get container details
287 | docker inspect n8n | grep NetworkMode
288 | 
289 | # Find your local IP
290 | # macOS/Linux
291 | ifconfig | grep "inet " | grep -v 127.0.0.1
292 | 
293 | # Windows
294 | ipconfig | findstr IPv4
295 | ```
296 | 
297 | ## Quick Solutions
298 | 
299 | ### Solution 1: Use Host Network (Linux only)
300 | ```json
301 | {
302 |   "command": "docker",
303 |   "args": [
304 |     "run", "-i", "--rm",
305 |     "--network", "host",
306 |     "-e", "N8N_API_URL=http://localhost:5678",
307 |     "ghcr.io/czlonkowski/n8n-mcp:latest"
308 |   ]
309 | }
310 | ```
311 | 
312 | ### Solution 2: Use Your Machine's IP
313 | ```json
314 | {
315 |   "N8N_API_URL": "http://192.168.1.100:5678"  // Replace with your IP
316 | }
317 | ```
318 | 
319 | ### Solution 3: HTTP Mode Deployment
320 | Deploy n8n-mcp as HTTP server to avoid stdio/Docker issues:
321 | 
322 | ```bash
323 | # Start HTTP server
324 | docker run -d \
325 |   -p 3000:3000 \
326 |   -e MCP_MODE=http \
327 |   -e AUTH_TOKEN=your-token \
328 |   -e N8N_API_URL=http://host.docker.internal:5678 \
329 |   -e N8N_API_KEY=your-n8n-key \
330 |   ghcr.io/czlonkowski/n8n-mcp:latest
331 | 
332 | # Configure Claude with mcp-remote
333 | ```
334 | 
335 | ## Debugging Steps
336 | 
337 | ### 1. Enable Debug Logging
338 | ```json
339 | {
340 |   "env": {
341 |     "LOG_LEVEL": "debug",
342 |     "DEBUG_MCP": "true"
343 |   }
344 | }
345 | ```
346 | 
347 | ### 2. Test Connectivity
348 | ```bash
349 | # Test from n8n-mcp container
350 | docker run --rm ghcr.io/czlonkowski/n8n-mcp:latest \
351 |   sh -c "apk add curl && curl -v http://host.docker.internal:5678/api/v1/workflows"
352 | ```
353 | 
354 | ### 3. Check Docker Logs
355 | ```bash
356 | # View n8n-mcp logs
357 | docker logs $(docker ps -q -f ancestor=ghcr.io/czlonkowski/n8n-mcp:latest)
358 | 
359 | # View n8n logs
360 | docker logs n8n
361 | ```
362 | 
363 | ### 4. Validate Environment
364 | ```bash
365 | # Check what n8n-mcp sees
366 | docker run --rm ghcr.io/czlonkowski/n8n-mcp:latest \
367 |   sh -c "env | grep N8N"
368 | ```
369 | 
370 | ### 5. Network Diagnostics
371 | ```bash
372 | # Check Docker networks
373 | docker network inspect bridge
374 | 
375 | # Test DNS resolution
376 | docker run --rm busybox nslookup host.docker.internal
377 | ```
378 | 
379 | ## Platform-Specific Notes
380 | 
381 | ### Docker Desktop (macOS/Windows)
382 | - `host.docker.internal` works out of the box
383 | - Ensure Docker Desktop is running
384 | - Check Docker Desktop settings → Resources → Network
385 | 
386 | ### Linux
387 | - `host.docker.internal` requires Docker 20.10+
388 | - Alternative: Use `--add-host=host.docker.internal:host-gateway`
389 | - Or use the Docker bridge IP: `172.17.0.1`
390 | 
391 | ### Windows with WSL2
392 | - Use `host.docker.internal` or WSL2 IP
393 | - Check firewall rules for port 5678
394 | - Ensure n8n binds to `0.0.0.0` not `127.0.0.1`
395 | 
396 | ## Still Having Issues?
397 | 
398 | 1. **Check n8n logs** for API-related errors
399 | 2. **Verify firewall/security** isn't blocking connections
400 | 3. **Try simpler setup** - Run n8n-mcp on host instead of Docker
401 | 4. **Report issue** with debug logs at [GitHub Issues](https://github.com/czlonkowski/n8n-mcp/issues)
402 | 
403 | ## Useful Commands
404 | 
405 | ```bash
406 | # Remove all n8n-mcp containers
407 | docker rm -f $(docker ps -aq -f ancestor=ghcr.io/czlonkowski/n8n-mcp:latest)
408 | 
409 | # Test n8n API with curl
410 | curl -H "X-N8N-API-KEY: your-key" http://localhost:5678/api/v1/workflows
411 | 
412 | # Run interactive debug session
413 | docker run -it --rm \
414 |   -e LOG_LEVEL=debug \
415 |   -e N8N_API_URL=http://host.docker.internal:5678 \
416 |   -e N8N_API_KEY=your-key \
417 |   ghcr.io/czlonkowski/n8n-mcp:latest \
418 |   sh
419 | 
420 | # Check container networking
421 | docker run --rm alpine ping -c 4 host.docker.internal
422 | ```
```

--------------------------------------------------------------------------------
/tests/fixtures/factories/parser-node.factory.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Factory } from 'fishery';
  2 | import { faker } from '@faker-js/faker';
  3 | 
  4 | // Declarative node definition
  5 | export interface DeclarativeNodeDefinition {
  6 |   name: string;
  7 |   displayName: string;
  8 |   description: string;
  9 |   version?: number | number[];
 10 |   group?: string[];
 11 |   categories?: string[];
 12 |   routing: {
 13 |     request?: {
 14 |       resource?: {
 15 |         options: Array<{ name: string; value: string }>;
 16 |       };
 17 |       operation?: {
 18 |         options: Record<string, Array<{ name: string; value: string; action?: string }>>;
 19 |       };
 20 |     };
 21 |   };
 22 |   properties?: any[];
 23 |   credentials?: any[];
 24 |   usableAsTool?: boolean;
 25 |   webhooks?: any[];
 26 |   polling?: boolean;
 27 | }
 28 | 
 29 | // Programmatic node definition
 30 | export interface ProgrammaticNodeDefinition {
 31 |   name: string;
 32 |   displayName: string;
 33 |   description: string;
 34 |   version?: number | number[];
 35 |   group?: string[];
 36 |   categories?: string[];
 37 |   properties: any[];
 38 |   credentials?: any[];
 39 |   usableAsTool?: boolean;
 40 |   webhooks?: any[];
 41 |   polling?: boolean;
 42 |   trigger?: boolean;
 43 |   eventTrigger?: boolean;
 44 | }
 45 | 
 46 | // Versioned node class structure
 47 | export interface VersionedNodeClass {
 48 |   baseDescription?: {
 49 |     name: string;
 50 |     displayName: string;
 51 |     description: string;
 52 |     defaultVersion: number;
 53 |   };
 54 |   nodeVersions?: Record<number, { description: any }>;
 55 | }
 56 | 
 57 | // Property definition
 58 | export interface PropertyDefinition {
 59 |   displayName: string;
 60 |   name: string;
 61 |   type: string;
 62 |   default?: any;
 63 |   description?: string;
 64 |   options?: Array<{ name: string; value: string; description?: string; action?: string; displayName?: string }> | any[];
 65 |   required?: boolean;
 66 |   displayOptions?: {
 67 |     show?: Record<string, any[]>;
 68 |     hide?: Record<string, any[]>;
 69 |   };
 70 |   typeOptions?: any;
 71 |   noDataExpression?: boolean;
 72 | }
 73 | 
 74 | // Base property factory
 75 | export const propertyFactory = Factory.define<PropertyDefinition>(() => ({
 76 |   displayName: faker.helpers.arrayElement(['Resource', 'Operation', 'Field', 'Option']),
 77 |   name: faker.helpers.slugify(faker.word.noun()).toLowerCase(),
 78 |   type: faker.helpers.arrayElement(['string', 'number', 'boolean', 'options', 'json', 'collection']),
 79 |   default: '',
 80 |   description: faker.lorem.sentence(),
 81 |   required: faker.datatype.boolean(),
 82 |   noDataExpression: faker.datatype.boolean()
 83 | }));
 84 | 
 85 | // String property factory
 86 | export const stringPropertyFactory = propertyFactory.params({
 87 |   type: 'string',
 88 |   default: faker.lorem.word()
 89 | });
 90 | 
 91 | // Number property factory
 92 | export const numberPropertyFactory = propertyFactory.params({
 93 |   type: 'number',
 94 |   default: faker.number.int({ min: 0, max: 100 })
 95 | });
 96 | 
 97 | // Boolean property factory
 98 | export const booleanPropertyFactory = propertyFactory.params({
 99 |   type: 'boolean',
100 |   default: faker.datatype.boolean()
101 | });
102 | 
103 | // Options property factory
104 | export const optionsPropertyFactory = propertyFactory.params({
105 |   type: 'options',
106 |   options: [
107 |     { name: 'Option A', value: 'a', description: 'First option' },
108 |     { name: 'Option B', value: 'b', description: 'Second option' },
109 |     { name: 'Option C', value: 'c', description: 'Third option' }
110 |   ],
111 |   default: 'a'
112 | });
113 | 
114 | // Resource property for programmatic nodes
115 | export const resourcePropertyFactory = optionsPropertyFactory.params({
116 |   displayName: 'Resource',
117 |   name: 'resource',
118 |   options: [
119 |     { name: 'User', value: 'user' },
120 |     { name: 'Post', value: 'post' },
121 |     { name: 'Comment', value: 'comment' }
122 |   ]
123 | });
124 | 
125 | // Operation property for programmatic nodes
126 | export const operationPropertyFactory = optionsPropertyFactory.params({
127 |   displayName: 'Operation',
128 |   name: 'operation',
129 |   displayOptions: {
130 |     show: {
131 |       resource: ['user']
132 |     }
133 |   },
134 |   options: [
135 |     { name: 'Create', value: 'create', action: 'Create a user' } as any,
136 |     { name: 'Get', value: 'get', action: 'Get a user' } as any,
137 |     { name: 'Update', value: 'update', action: 'Update a user' } as any,
138 |     { name: 'Delete', value: 'delete', action: 'Delete a user' } as any
139 |   ]
140 | });
141 | 
142 | // Collection property factory
143 | export const collectionPropertyFactory = propertyFactory.params({
144 |   type: 'collection',
145 |   default: {},
146 |   options: [
147 |     stringPropertyFactory.build({ name: 'field1', displayName: 'Field 1' }) as any,
148 |     numberPropertyFactory.build({ name: 'field2', displayName: 'Field 2' }) as any
149 |   ]
150 | });
151 | 
152 | // Declarative node factory
153 | export const declarativeNodeFactory = Factory.define<DeclarativeNodeDefinition>(() => ({
154 |   name: faker.helpers.slugify(faker.company.name()).toLowerCase(),
155 |   displayName: faker.company.name(),
156 |   description: faker.lorem.sentence(),
157 |   version: faker.number.int({ min: 1, max: 3 }),
158 |   group: [faker.helpers.arrayElement(['transform', 'output'])],
159 |   routing: {
160 |     request: {
161 |       resource: {
162 |         options: [
163 |           { name: 'User', value: 'user' },
164 |           { name: 'Post', value: 'post' }
165 |         ]
166 |       },
167 |       operation: {
168 |         options: {
169 |           user: [
170 |             { name: 'Create', value: 'create', action: 'Create a user' },
171 |             { name: 'Get', value: 'get', action: 'Get a user' }
172 |           ],
173 |           post: [
174 |             { name: 'Create', value: 'create', action: 'Create a post' },
175 |             { name: 'List', value: 'list', action: 'List posts' }
176 |           ]
177 |         }
178 |       }
179 |     }
180 |   },
181 |   properties: [
182 |     stringPropertyFactory.build({ name: 'apiKey', displayName: 'API Key' })
183 |   ],
184 |   credentials: [
185 |     { name: 'apiCredentials', required: true }
186 |   ]
187 | }));
188 | 
189 | // Programmatic node factory
190 | export const programmaticNodeFactory = Factory.define<ProgrammaticNodeDefinition>(() => ({
191 |   name: faker.helpers.slugify(faker.company.name()).toLowerCase(),
192 |   displayName: faker.company.name(),
193 |   description: faker.lorem.sentence(),
194 |   version: faker.number.int({ min: 1, max: 3 }),
195 |   group: [faker.helpers.arrayElement(['transform', 'output'])],
196 |   properties: [
197 |     resourcePropertyFactory.build(),
198 |     operationPropertyFactory.build(),
199 |     stringPropertyFactory.build({ 
200 |       name: 'field',
201 |       displayName: 'Field',
202 |       displayOptions: {
203 |         show: {
204 |           resource: ['user'],
205 |           operation: ['create', 'update']
206 |         }
207 |       }
208 |     })
209 |   ],
210 |   credentials: []
211 | }));
212 | 
213 | // Trigger node factory
214 | export const triggerNodeFactory = programmaticNodeFactory.params({
215 |   group: ['trigger'],
216 |   trigger: true,
217 |   properties: [
218 |     {
219 |       displayName: 'Event',
220 |       name: 'event',
221 |       type: 'options',
222 |       default: 'created',
223 |       options: [
224 |         { name: 'Created', value: 'created' },
225 |         { name: 'Updated', value: 'updated' },
226 |         { name: 'Deleted', value: 'deleted' }
227 |       ]
228 |     }
229 |   ]
230 | });
231 | 
232 | // Webhook node factory
233 | export const webhookNodeFactory = programmaticNodeFactory.params({
234 |   group: ['trigger'],
235 |   webhooks: [
236 |     {
237 |       name: 'default',
238 |       httpMethod: 'POST',
239 |       responseMode: 'onReceived',
240 |       path: 'webhook'
241 |     }
242 |   ],
243 |   properties: [
244 |     {
245 |       displayName: 'Path',
246 |       name: 'path',
247 |       type: 'string',
248 |       default: 'webhook',
249 |       required: true
250 |     }
251 |   ]
252 | });
253 | 
254 | // AI tool node factory
255 | export const aiToolNodeFactory = declarativeNodeFactory.params({
256 |   usableAsTool: true,
257 |   name: 'openai',
258 |   displayName: 'OpenAI',
259 |   description: 'Use OpenAI models'
260 | });
261 | 
262 | // Versioned node class factory
263 | export const versionedNodeClassFactory = Factory.define<VersionedNodeClass>(() => ({
264 |   baseDescription: {
265 |     name: faker.helpers.slugify(faker.company.name()).toLowerCase(),
266 |     displayName: faker.company.name(),
267 |     description: faker.lorem.sentence(),
268 |     defaultVersion: 2
269 |   },
270 |   nodeVersions: {
271 |     1: {
272 |       description: {
273 |         properties: [
274 |           stringPropertyFactory.build({ name: 'oldField', displayName: 'Old Field' })
275 |         ]
276 |       }
277 |     },
278 |     2: {
279 |       description: {
280 |         properties: [
281 |           stringPropertyFactory.build({ name: 'newField', displayName: 'New Field' }),
282 |           numberPropertyFactory.build({ name: 'version', displayName: 'Version' })
283 |         ]
284 |       }
285 |     }
286 |   }
287 | }));
288 | 
289 | // Malformed node factory (for error testing)
290 | export const malformedNodeFactory = Factory.define<any>(() => ({
291 |   // Missing required 'name' property
292 |   displayName: faker.company.name(),
293 |   description: faker.lorem.sentence()
294 | }));
295 | 
296 | // Complex nested property factory
297 | export const nestedPropertyFactory = Factory.define<PropertyDefinition>(() => ({
298 |   displayName: 'Advanced Options',
299 |   name: 'advancedOptions',
300 |   type: 'collection',
301 |   default: {},
302 |   options: [
303 |     {
304 |       displayName: 'Headers',
305 |       name: 'headers',
306 |       type: 'fixedCollection',
307 |       typeOptions: {
308 |         multipleValues: true
309 |       },
310 |       options: [
311 |         {
312 |           name: 'header',
313 |           displayName: 'Header',
314 |           values: [
315 |             stringPropertyFactory.build({ name: 'name', displayName: 'Name' }),
316 |             stringPropertyFactory.build({ name: 'value', displayName: 'Value' })
317 |           ]
318 |         }
319 |       ]
320 |     } as any,
321 |     {
322 |       displayName: 'Query Parameters',
323 |       name: 'queryParams',
324 |       type: 'collection',
325 |       options: [
326 |         stringPropertyFactory.build({ name: 'key', displayName: 'Key' }),
327 |         stringPropertyFactory.build({ name: 'value', displayName: 'Value' })
328 |       ] as any[]
329 |     } as any
330 |   ]
331 | }));
332 | 
333 | // Node class mock factory
334 | export const nodeClassFactory = Factory.define<any>(({ params }) => {
335 |   const description = params.description || programmaticNodeFactory.build();
336 |   
337 |   return class MockNode {
338 |     description = description;
339 |     
340 |     constructor() {
341 |       // Constructor logic if needed
342 |     }
343 |   };
344 | });
345 | 
346 | // Versioned node type class mock
347 | export const versionedNodeTypeClassFactory = Factory.define<any>(({ params }) => {
348 |   const baseDescription = params.baseDescription || {
349 |     name: 'versionedNode',
350 |     displayName: 'Versioned Node',
351 |     description: 'A versioned node',
352 |     defaultVersion: 2
353 |   };
354 |   
355 |   const nodeVersions = params.nodeVersions || {
356 |     1: {
357 |       description: {
358 |         properties: [propertyFactory.build()]
359 |       }
360 |     },
361 |     2: {
362 |       description: {
363 |         properties: [propertyFactory.build(), propertyFactory.build()]
364 |       }
365 |     }
366 |   };
367 |   
368 |   return class VersionedNodeType {
369 |     baseDescription = baseDescription;
370 |     nodeVersions = nodeVersions;
371 |     currentVersion = baseDescription.defaultVersion;
372 |     
373 |     constructor() {
374 |       Object.defineProperty(this.constructor, 'name', {
375 |         value: 'VersionedNodeType',
376 |         writable: false,
377 |         configurable: true
378 |       });
379 |     }
380 |   };
381 | });
```

--------------------------------------------------------------------------------
/tests/integration/mcp-protocol/protocol-compliance.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  2 | import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
  3 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
  4 | import { TestableN8NMCPServer } from './test-helpers';
  5 | 
  6 | describe('MCP Protocol Compliance', () => {
  7 |   let mcpServer: TestableN8NMCPServer;
  8 |   let transport: InMemoryTransport;
  9 |   let client: Client;
 10 | 
 11 |   beforeEach(async () => {
 12 |     mcpServer = new TestableN8NMCPServer();
 13 |     await mcpServer.initialize();
 14 |     
 15 |     const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
 16 |     transport = serverTransport;
 17 |     
 18 |     // Connect MCP server to transport
 19 |     await mcpServer.connectToTransport(transport);
 20 |     
 21 |     // Create client
 22 |     client = new Client({
 23 |       name: 'test-client',
 24 |       version: '1.0.0'
 25 |     }, {
 26 |       capabilities: {}
 27 |     });
 28 |     
 29 |     await client.connect(clientTransport);
 30 |   });
 31 | 
 32 |   afterEach(async () => {
 33 |     await client.close();
 34 |     await mcpServer.close();
 35 |   });
 36 | 
 37 |   describe('JSON-RPC 2.0 Compliance', () => {
 38 |     it('should return proper JSON-RPC 2.0 response format', async () => {
 39 |       const response = await client.listTools();
 40 | 
 41 |       // Response should have tools array
 42 |       expect(response).toHaveProperty('tools');
 43 |       expect(Array.isArray((response as any).tools)).toBe(true);
 44 |     });
 45 | 
 46 |     it('should handle request with id correctly', async () => {
 47 |       const response = await client.listTools();
 48 | 
 49 |       expect(response).toBeDefined();
 50 |       expect(typeof response).toBe('object');
 51 |     });
 52 | 
 53 |     it('should handle batch requests', async () => {
 54 |       // Send multiple requests concurrently
 55 |       const promises = [
 56 |         client.listTools(),
 57 |         client.listTools(),
 58 |         client.listTools()
 59 |       ];
 60 | 
 61 |       const responses = await Promise.all(promises);
 62 |       
 63 |       expect(responses).toHaveLength(3);
 64 |       responses.forEach(response => {
 65 |         expect(response).toHaveProperty('tools');
 66 |       });
 67 |     });
 68 | 
 69 |     it('should preserve request order in responses', async () => {
 70 |       const requests = [];
 71 |       const expectedOrder = [];
 72 | 
 73 |       // Create requests with different tools to track order
 74 |       for (let i = 0; i < 5; i++) {
 75 |         expectedOrder.push(i);
 76 |         requests.push(
 77 |           client.callTool({ name: 'get_database_statistics', arguments: {} })
 78 |             .then(() => i)
 79 |         );
 80 |       }
 81 | 
 82 |       const results = await Promise.all(requests);
 83 |       expect(results).toEqual(expectedOrder);
 84 |     });
 85 |   });
 86 | 
 87 |   describe('Protocol Version Negotiation', () => {
 88 |     it('should negotiate protocol capabilities', async () => {
 89 |       const serverInfo = await client.getServerVersion();
 90 |       
 91 |       expect(serverInfo).toHaveProperty('name');
 92 |       expect(serverInfo).toHaveProperty('version');
 93 |       expect(serverInfo!.name).toBe('n8n-documentation-mcp');
 94 |     });
 95 | 
 96 |     it('should expose supported capabilities', async () => {
 97 |       const serverCapabilities = client.getServerCapabilities();
 98 |       
 99 |       expect(serverCapabilities).toBeDefined();
100 |       
101 |       // Should support tools
102 |       expect(serverCapabilities).toHaveProperty('tools');
103 |     });
104 |   });
105 | 
106 |   describe('Message Format Validation', () => {
107 |     it('should reject messages without method', async () => {
108 |       // Test by sending raw message through transport
109 |       const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
110 |       const testClient = new Client({ name: 'test', version: '1.0.0' }, {});
111 |       
112 |       await mcpServer.connectToTransport(serverTransport);
113 |       await testClient.connect(clientTransport);
114 | 
115 |       try {
116 |         // This should fail as MCP SDK validates method
117 |         await (testClient as any).request({ method: '', params: {} });
118 |         expect.fail('Should have thrown an error');
119 |       } catch (error) {
120 |         expect(error).toBeDefined();
121 |       } finally {
122 |         await testClient.close();
123 |       }
124 |     });
125 | 
126 |     it('should handle missing params gracefully', async () => {
127 |       // Most tools should work without params
128 |       const response = await client.callTool({ name: 'list_nodes', arguments: {} });
129 |       expect(response).toBeDefined();
130 |     });
131 | 
132 |     it('should validate params schema', async () => {
133 |       try {
134 |         // Invalid nodeType format (missing prefix)
135 |         const response = await client.callTool({ name: 'get_node_info', arguments: {
136 |           nodeType: 'httpRequest' // Should be 'nodes-base.httpRequest'
137 |         } });
138 |         // Check if the response indicates an error
139 |         const text = (response as any).content[0].text;
140 |         expect(text).toContain('not found');
141 |       } catch (error: any) {
142 |         // If it throws, that's also acceptable
143 |         expect(error.message).toContain('not found');
144 |       }
145 |     });
146 |   });
147 | 
148 |   describe('Content Types', () => {
149 |     it('should handle text content in tool responses', async () => {
150 |       const response = await client.callTool({ name: 'get_database_statistics', arguments: {} });
151 |       
152 |       expect((response as any).content).toHaveLength(1);
153 |       expect((response as any).content[0]).toHaveProperty('type', 'text');
154 |       expect((response as any).content[0]).toHaveProperty('text');
155 |       expect(typeof (response as any).content[0].text).toBe('string');
156 |     });
157 | 
158 |     it('should handle large text responses', async () => {
159 |       // Get a large node info response
160 |       const response = await client.callTool({ name: 'get_node_info', arguments: {
161 |         nodeType: 'nodes-base.httpRequest'
162 |       } });
163 | 
164 |       expect((response as any).content).toHaveLength(1);
165 |       expect((response as any).content[0].type).toBe('text');
166 |       expect((response as any).content[0].text.length).toBeGreaterThan(1000);
167 |     });
168 | 
169 |     it('should handle JSON content properly', async () => {
170 |       const response = await client.callTool({ name: 'list_nodes', arguments: {
171 |         limit: 5
172 |       } });
173 | 
174 |       expect((response as any).content).toHaveLength(1);
175 |       const content = JSON.parse((response as any).content[0].text);
176 |       expect(content).toHaveProperty('nodes');
177 |       expect(Array.isArray(content.nodes)).toBe(true);
178 |     });
179 |   });
180 | 
181 |   describe('Request/Response Correlation', () => {
182 |     it('should correlate concurrent requests correctly', async () => {
183 |       const requests = [
184 |         client.callTool({ name: 'get_node_essentials', arguments: { nodeType: 'nodes-base.httpRequest' } }),
185 |         client.callTool({ name: 'get_node_essentials', arguments: { nodeType: 'nodes-base.webhook' } }),
186 |         client.callTool({ name: 'get_node_essentials', arguments: { nodeType: 'nodes-base.slack' } })
187 |       ];
188 | 
189 |       const responses = await Promise.all(requests);
190 | 
191 |       expect((responses[0] as any).content[0].text).toContain('httpRequest');
192 |       expect((responses[1] as any).content[0].text).toContain('webhook');
193 |       expect((responses[2] as any).content[0].text).toContain('slack');
194 |     });
195 | 
196 |     it('should handle interleaved requests', async () => {
197 |       const results: string[] = [];
198 | 
199 |       // Start multiple requests with different delays
200 |       const p1 = client.callTool({ name: 'get_database_statistics', arguments: {} })
201 |         .then(() => { results.push('stats'); return 'stats'; });
202 | 
203 |       const p2 = client.callTool({ name: 'list_nodes', arguments: { limit: 1 } })
204 |         .then(() => { results.push('nodes'); return 'nodes'; });
205 | 
206 |       const p3 = client.callTool({ name: 'search_nodes', arguments: { query: 'http' } })
207 |         .then(() => { results.push('search'); return 'search'; });
208 | 
209 |       const resolved = await Promise.all([p1, p2, p3]);
210 | 
211 |       // All should complete
212 |       expect(resolved).toHaveLength(3);
213 |       expect(results).toHaveLength(3);
214 |     });
215 |   });
216 | 
217 |   describe('Protocol Extensions', () => {
218 |     it('should handle tool-specific extensions', async () => {
219 |       // Test tool with complex params
220 |       const response = await client.callTool({ name: 'validate_node_operation', arguments: {
221 |         nodeType: 'nodes-base.httpRequest',
222 |         config: {
223 |           method: 'GET',
224 |           url: 'https://api.example.com'
225 |         },
226 |         profile: 'runtime'
227 |       } });
228 | 
229 |       expect((response as any).content).toHaveLength(1);
230 |       expect((response as any).content[0].type).toBe('text');
231 |     });
232 | 
233 |     it('should support optional parameters', async () => {
234 |       // Call with minimal params
235 |       const response1 = await client.callTool({ name: 'list_nodes', arguments: {} });
236 |       
237 |       // Call with all params
238 |       const response2 = await client.callTool({ name: 'list_nodes', arguments: {
239 |         limit: 10,
240 |         category: 'trigger',
241 |         package: 'n8n-nodes-base'
242 |       } });
243 | 
244 |       expect(response1).toBeDefined();
245 |       expect(response2).toBeDefined();
246 |     });
247 |   });
248 | 
249 |   describe('Transport Layer', () => {
250 |     it('should handle transport disconnection gracefully', async () => {
251 |       const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
252 |       const testClient = new Client({ name: 'test', version: '1.0.0' }, {});
253 | 
254 |       await mcpServer.connectToTransport(serverTransport);
255 |       await testClient.connect(clientTransport);
256 | 
257 |       // Make a request
258 |       const response = await testClient.callTool({ name: 'get_database_statistics', arguments: {} });
259 |       expect(response).toBeDefined();
260 | 
261 |       // Close client
262 |       await testClient.close();
263 | 
264 |       // Further requests should fail
265 |       try {
266 |         await testClient.callTool({ name: 'get_database_statistics', arguments: {} });
267 |         expect.fail('Should have thrown an error');
268 |       } catch (error) {
269 |         expect(error).toBeDefined();
270 |       }
271 |     });
272 | 
273 |     it('should handle multiple sequential connections', async () => {
274 |       // Close existing connection
275 |       await client.close();
276 |       await mcpServer.close();
277 | 
278 |       // Create new connections
279 |       for (let i = 0; i < 3; i++) {
280 |         const engine = new TestableN8NMCPServer();
281 |         await engine.initialize();
282 | 
283 |         const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
284 |         await engine.connectToTransport(serverTransport);
285 | 
286 |         const testClient = new Client({ name: 'test', version: '1.0.0' }, {});
287 |         await testClient.connect(clientTransport);
288 | 
289 |         const response = await testClient.callTool({ name: 'get_database_statistics', arguments: {} });
290 |         expect(response).toBeDefined();
291 | 
292 |         await testClient.close();
293 |         await engine.close();
294 |       }
295 |     });
296 |   });
297 | });
```

--------------------------------------------------------------------------------
/tests/integration/ai-validation/llm-chain-validation.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Integration Tests: Basic LLM Chain Validation
  3 |  *
  4 |  * Tests Basic LLM Chain validation against real n8n instance.
  5 |  */
  6 | 
  7 | import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest';
  8 | import { createTestContext, TestContext, createTestWorkflowName } from '../n8n-api/utils/test-context';
  9 | import { getTestN8nClient } from '../n8n-api/utils/n8n-client';
 10 | import { N8nApiClient } from '../../../src/services/n8n-api-client';
 11 | import { cleanupOrphanedWorkflows } from '../n8n-api/utils/cleanup-helpers';
 12 | import { createMcpContext } from '../n8n-api/utils/mcp-context';
 13 | import { InstanceContext } from '../../../src/types/instance-context';
 14 | import { handleValidateWorkflow } from '../../../src/mcp/handlers-n8n-manager';
 15 | import { getNodeRepository, closeNodeRepository } from '../n8n-api/utils/node-repository';
 16 | import { NodeRepository } from '../../../src/database/node-repository';
 17 | import { ValidationResponse } from '../n8n-api/types/mcp-responses';
 18 | import {
 19 |   createBasicLLMChainNode,
 20 |   createLanguageModelNode,
 21 |   createMemoryNode,
 22 |   createAIConnection,
 23 |   mergeConnections,
 24 |   createAIWorkflow
 25 | } from './helpers';
 26 | import { WorkflowNode } from '../../../src/types/n8n-api';
 27 | 
 28 | describe('Integration: Basic LLM Chain Validation', () => {
 29 |   let context: TestContext;
 30 |   let client: N8nApiClient;
 31 |   let mcpContext: InstanceContext;
 32 |   let repository: NodeRepository;
 33 | 
 34 |   beforeEach(async () => {
 35 |     context = createTestContext();
 36 |     client = getTestN8nClient();
 37 |     mcpContext = createMcpContext();
 38 |     repository = await getNodeRepository();
 39 |   });
 40 | 
 41 |   afterEach(async () => {
 42 |     await context.cleanup();
 43 |   });
 44 | 
 45 |   afterAll(async () => {
 46 |     await closeNodeRepository();
 47 |     if (!process.env.CI) {
 48 |       await cleanupOrphanedWorkflows();
 49 |     }
 50 |   });
 51 | 
 52 |   // ======================================================================
 53 |   // TEST 1: Missing Language Model
 54 |   // ======================================================================
 55 | 
 56 |   it('should detect missing language model', async () => {
 57 |     const llmChain = createBasicLLMChainNode({
 58 |       name: 'Basic LLM Chain',
 59 |       promptType: 'define',
 60 |       text: 'Test prompt'
 61 |     });
 62 | 
 63 |     const workflow = createAIWorkflow(
 64 |       [llmChain],
 65 |       {}, // No connections
 66 |       {
 67 |         name: createTestWorkflowName('LLM Chain - Missing Model'),
 68 |         tags: ['mcp-integration-test', 'ai-validation']
 69 |       }
 70 |     );
 71 | 
 72 |     const created = await client.createWorkflow(workflow);
 73 |     context.trackWorkflow(created.id!);
 74 | 
 75 |     const response = await handleValidateWorkflow(
 76 |       { id: created.id },
 77 |       repository,
 78 |       mcpContext
 79 |     );
 80 | 
 81 |     expect(response.success).toBe(true);
 82 |     const data = response.data as ValidationResponse;
 83 | 
 84 |     expect(data.valid).toBe(false);
 85 |     expect(data.errors).toBeDefined();
 86 | 
 87 |     const errorCodes = data.errors!.map(e => e.details?.code || e.code);
 88 |     expect(errorCodes).toContain('MISSING_LANGUAGE_MODEL');
 89 |   });
 90 | 
 91 |   // ======================================================================
 92 |   // TEST 2: Missing Prompt Text (promptType=define)
 93 |   // ======================================================================
 94 | 
 95 |   it('should detect missing prompt text', async () => {
 96 |     const languageModel = createLanguageModelNode('openai', {
 97 |       name: 'OpenAI Chat Model'
 98 |     });
 99 | 
100 |     const llmChain = createBasicLLMChainNode({
101 |       name: 'Basic LLM Chain',
102 |       promptType: 'define',
103 |       text: '' // Empty prompt text
104 |     });
105 | 
106 |     const workflow = createAIWorkflow(
107 |       [languageModel, llmChain],
108 |       createAIConnection('OpenAI Chat Model', 'Basic LLM Chain', 'ai_languageModel'),
109 |       {
110 |         name: createTestWorkflowName('LLM Chain - Missing Prompt'),
111 |         tags: ['mcp-integration-test', 'ai-validation']
112 |       }
113 |     );
114 | 
115 |     const created = await client.createWorkflow(workflow);
116 |     context.trackWorkflow(created.id!);
117 | 
118 |     const response = await handleValidateWorkflow(
119 |       { id: created.id },
120 |       repository,
121 |       mcpContext
122 |     );
123 | 
124 |     expect(response.success).toBe(true);
125 |     const data = response.data as ValidationResponse;
126 | 
127 |     expect(data.valid).toBe(false);
128 |     expect(data.errors).toBeDefined();
129 | 
130 |     const errorCodes = data.errors!.map(e => e.details?.code || e.code);
131 |     expect(errorCodes).toContain('MISSING_PROMPT_TEXT');
132 |   });
133 | 
134 |   // ======================================================================
135 |   // TEST 3: Valid Complete LLM Chain
136 |   // ======================================================================
137 | 
138 |   it('should validate complete LLM Chain', async () => {
139 |     const languageModel = createLanguageModelNode('openai', {
140 |       name: 'OpenAI Chat Model'
141 |     });
142 | 
143 |     const llmChain = createBasicLLMChainNode({
144 |       name: 'Basic LLM Chain',
145 |       promptType: 'define',
146 |       text: 'You are a helpful assistant. Answer the following: {{ $json.question }}'
147 |     });
148 | 
149 |     const workflow = createAIWorkflow(
150 |       [languageModel, llmChain],
151 |       createAIConnection('OpenAI Chat Model', 'Basic LLM Chain', 'ai_languageModel'),
152 |       {
153 |         name: createTestWorkflowName('LLM Chain - Valid'),
154 |         tags: ['mcp-integration-test', 'ai-validation']
155 |       }
156 |     );
157 | 
158 |     const created = await client.createWorkflow(workflow);
159 |     context.trackWorkflow(created.id!);
160 | 
161 |     const response = await handleValidateWorkflow(
162 |       { id: created.id },
163 |       repository,
164 |       mcpContext
165 |     );
166 | 
167 |     expect(response.success).toBe(true);
168 |     const data = response.data as ValidationResponse;
169 | 
170 |     expect(data.valid).toBe(true);
171 |     expect(data.errors).toBeUndefined();
172 |     expect(data.summary.errorCount).toBe(0);
173 |   });
174 | 
175 |   // ======================================================================
176 |   // TEST 4: LLM Chain with Memory
177 |   // ======================================================================
178 | 
179 |   it('should validate LLM Chain with memory', async () => {
180 |     const languageModel = createLanguageModelNode('anthropic', {
181 |       name: 'Anthropic Chat Model'
182 |     });
183 | 
184 |     const memory = createMemoryNode({
185 |       name: 'Window Buffer Memory',
186 |       contextWindowLength: 10
187 |     });
188 | 
189 |     const llmChain = createBasicLLMChainNode({
190 |       name: 'Basic LLM Chain',
191 |       promptType: 'auto'
192 |     });
193 | 
194 |     const workflow = createAIWorkflow(
195 |       [languageModel, memory, llmChain],
196 |       mergeConnections(
197 |         createAIConnection('Anthropic Chat Model', 'Basic LLM Chain', 'ai_languageModel'),
198 |         createAIConnection('Window Buffer Memory', 'Basic LLM Chain', 'ai_memory')
199 |       ),
200 |       {
201 |         name: createTestWorkflowName('LLM Chain - With Memory'),
202 |         tags: ['mcp-integration-test', 'ai-validation']
203 |       }
204 |     );
205 | 
206 |     const created = await client.createWorkflow(workflow);
207 |     context.trackWorkflow(created.id!);
208 | 
209 |     const response = await handleValidateWorkflow(
210 |       { id: created.id },
211 |       repository,
212 |       mcpContext
213 |     );
214 | 
215 |     expect(response.success).toBe(true);
216 |     const data = response.data as ValidationResponse;
217 | 
218 |     expect(data.valid).toBe(true);
219 |     expect(data.errors).toBeUndefined();
220 |   });
221 | 
222 |   // ======================================================================
223 |   // TEST 5: LLM Chain with Multiple Language Models (Error)
224 |   // ======================================================================
225 | 
226 |   it('should detect multiple language models', async () => {
227 |     const languageModel1 = createLanguageModelNode('openai', {
228 |       id: 'model-1',
229 |       name: 'OpenAI Chat Model 1'
230 |     });
231 | 
232 |     const languageModel2 = createLanguageModelNode('anthropic', {
233 |       id: 'model-2',
234 |       name: 'Anthropic Chat Model'
235 |     });
236 | 
237 |     const llmChain = createBasicLLMChainNode({
238 |       name: 'Basic LLM Chain',
239 |       promptType: 'define',
240 |       text: 'Test prompt'
241 |     });
242 | 
243 |     const workflow = createAIWorkflow(
244 |       [languageModel1, languageModel2, llmChain],
245 |       mergeConnections(
246 |         createAIConnection('OpenAI Chat Model 1', 'Basic LLM Chain', 'ai_languageModel'),
247 |         createAIConnection('Anthropic Chat Model', 'Basic LLM Chain', 'ai_languageModel') // ERROR: multiple models
248 |       ),
249 |       {
250 |         name: createTestWorkflowName('LLM Chain - Multiple Models'),
251 |         tags: ['mcp-integration-test', 'ai-validation']
252 |       }
253 |     );
254 | 
255 |     const created = await client.createWorkflow(workflow);
256 |     context.trackWorkflow(created.id!);
257 | 
258 |     const response = await handleValidateWorkflow(
259 |       { id: created.id },
260 |       repository,
261 |       mcpContext
262 |     );
263 | 
264 |     expect(response.success).toBe(true);
265 |     const data = response.data as ValidationResponse;
266 | 
267 |     expect(data.valid).toBe(false);
268 |     expect(data.errors).toBeDefined();
269 | 
270 |     const errorCodes = data.errors!.map(e => e.details?.code || e.code);
271 |     expect(errorCodes).toContain('MULTIPLE_LANGUAGE_MODELS');
272 |   });
273 | 
274 |   // ======================================================================
275 |   // TEST 6: LLM Chain with Tools (Error - not supported)
276 |   // ======================================================================
277 | 
278 |   it('should detect tools connection (not supported)', async () => {
279 |     const languageModel = createLanguageModelNode('openai', {
280 |       name: 'OpenAI Chat Model'
281 |     });
282 | 
283 |     // Manually create a tool node
284 |     const toolNode: WorkflowNode = {
285 |       id: 'tool-1',
286 |       name: 'Calculator',
287 |       type: '@n8n/n8n-nodes-langchain.toolCalculator',
288 |       typeVersion: 1,
289 |       position: [250, 400],
290 |       parameters: {}
291 |     };
292 | 
293 |     const llmChain = createBasicLLMChainNode({
294 |       name: 'Basic LLM Chain',
295 |       promptType: 'define',
296 |       text: 'Calculate something'
297 |     });
298 | 
299 |     const workflow = createAIWorkflow(
300 |       [languageModel, toolNode, llmChain],
301 |       mergeConnections(
302 |         createAIConnection('OpenAI Chat Model', 'Basic LLM Chain', 'ai_languageModel'),
303 |         createAIConnection('Calculator', 'Basic LLM Chain', 'ai_tool') // ERROR: tools not supported
304 |       ),
305 |       {
306 |         name: createTestWorkflowName('LLM Chain - With Tools'),
307 |         tags: ['mcp-integration-test', 'ai-validation']
308 |       }
309 |     );
310 | 
311 |     const created = await client.createWorkflow(workflow);
312 |     context.trackWorkflow(created.id!);
313 | 
314 |     const response = await handleValidateWorkflow(
315 |       { id: created.id },
316 |       repository,
317 |       mcpContext
318 |     );
319 | 
320 |     expect(response.success).toBe(true);
321 |     const data = response.data as ValidationResponse;
322 | 
323 |     expect(data.valid).toBe(false);
324 |     expect(data.errors).toBeDefined();
325 | 
326 |     const errorCodes = data.errors!.map(e => e.details?.code || e.code);
327 |     expect(errorCodes).toContain('TOOLS_NOT_SUPPORTED');
328 | 
329 |     const errorMessages = data.errors!.map(e => e.message).join(' ');
330 |     expect(errorMessages).toMatch(/AI Agent/i); // Should suggest using AI Agent
331 |   });
332 | });
333 | 
```

--------------------------------------------------------------------------------
/tests/unit/database/database-adapter-unit.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, vi } from 'vitest';
  2 | 
  3 | // Mock logger
  4 | vi.mock('../../../src/utils/logger', () => ({
  5 |   logger: {
  6 |     info: vi.fn(),
  7 |     warn: vi.fn(),
  8 |     error: vi.fn(),
  9 |     debug: vi.fn()
 10 |   }
 11 | }));
 12 | 
 13 | describe('Database Adapter - Unit Tests', () => {
 14 |   describe('DatabaseAdapter Interface', () => {
 15 |     it('should define interface when adapter is created', () => {
 16 |       // This is a type test - ensuring the interface is correctly defined
 17 |       type DatabaseAdapter = {
 18 |         prepare: (sql: string) => any;
 19 |         exec: (sql: string) => void;
 20 |         close: () => void;
 21 |         pragma: (key: string, value?: any) => any;
 22 |         readonly inTransaction: boolean;
 23 |         transaction: <T>(fn: () => T) => T;
 24 |         checkFTS5Support: () => boolean;
 25 |       };
 26 |       
 27 |       // Type assertion to ensure interface matches
 28 |       const mockAdapter: DatabaseAdapter = {
 29 |         prepare: vi.fn(),
 30 |         exec: vi.fn(),
 31 |         close: vi.fn(),
 32 |         pragma: vi.fn(),
 33 |         inTransaction: false,
 34 |         transaction: vi.fn((fn) => fn()),
 35 |         checkFTS5Support: vi.fn(() => true)
 36 |       };
 37 |       
 38 |       expect(mockAdapter).toBeDefined();
 39 |       expect(mockAdapter.prepare).toBeDefined();
 40 |       expect(mockAdapter.exec).toBeDefined();
 41 |       expect(mockAdapter.close).toBeDefined();
 42 |       expect(mockAdapter.pragma).toBeDefined();
 43 |       expect(mockAdapter.transaction).toBeDefined();
 44 |       expect(mockAdapter.checkFTS5Support).toBeDefined();
 45 |     });
 46 |   });
 47 |   
 48 |   describe('PreparedStatement Interface', () => {
 49 |     it('should define interface when statement is prepared', () => {
 50 |       // Type test for PreparedStatement
 51 |       type PreparedStatement = {
 52 |         run: (...params: any[]) => { changes: number; lastInsertRowid: number | bigint };
 53 |         get: (...params: any[]) => any;
 54 |         all: (...params: any[]) => any[];
 55 |         iterate: (...params: any[]) => IterableIterator<any>;
 56 |         pluck: (toggle?: boolean) => PreparedStatement;
 57 |         expand: (toggle?: boolean) => PreparedStatement;
 58 |         raw: (toggle?: boolean) => PreparedStatement;
 59 |         columns: () => any[];
 60 |         bind: (...params: any[]) => PreparedStatement;
 61 |       };
 62 |       
 63 |       const mockStmt: PreparedStatement = {
 64 |         run: vi.fn(() => ({ changes: 1, lastInsertRowid: 1 })),
 65 |         get: vi.fn(),
 66 |         all: vi.fn(() => []),
 67 |         iterate: vi.fn(function* () {}),
 68 |         pluck: vi.fn(function(this: any) { return this; }),
 69 |         expand: vi.fn(function(this: any) { return this; }),
 70 |         raw: vi.fn(function(this: any) { return this; }),
 71 |         columns: vi.fn(() => []),
 72 |         bind: vi.fn(function(this: any) { return this; })
 73 |       };
 74 |       
 75 |       expect(mockStmt).toBeDefined();
 76 |       expect(mockStmt.run).toBeDefined();
 77 |       expect(mockStmt.get).toBeDefined();
 78 |       expect(mockStmt.all).toBeDefined();
 79 |       expect(mockStmt.iterate).toBeDefined();
 80 |       expect(mockStmt.pluck).toBeDefined();
 81 |       expect(mockStmt.expand).toBeDefined();
 82 |       expect(mockStmt.raw).toBeDefined();
 83 |       expect(mockStmt.columns).toBeDefined();
 84 |       expect(mockStmt.bind).toBeDefined();
 85 |     });
 86 |   });
 87 |   
 88 |   describe('FTS5 Support Detection', () => {
 89 |     it('should detect support when FTS5 module is available', () => {
 90 |       const mockDb = {
 91 |         exec: vi.fn()
 92 |       };
 93 |       
 94 |       // Function to test FTS5 support detection logic
 95 |       const checkFTS5Support = (db: any): boolean => {
 96 |         try {
 97 |           db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS test_fts5 USING fts5(content);");
 98 |           db.exec("DROP TABLE IF EXISTS test_fts5;");
 99 |           return true;
100 |         } catch (error) {
101 |           return false;
102 |         }
103 |       };
104 |       
105 |       // Test when FTS5 is supported
106 |       expect(checkFTS5Support(mockDb)).toBe(true);
107 |       expect(mockDb.exec).toHaveBeenCalledWith(
108 |         "CREATE VIRTUAL TABLE IF NOT EXISTS test_fts5 USING fts5(content);"
109 |       );
110 |       
111 |       // Test when FTS5 is not supported
112 |       mockDb.exec.mockImplementation(() => {
113 |         throw new Error('no such module: fts5');
114 |       });
115 |       
116 |       expect(checkFTS5Support(mockDb)).toBe(false);
117 |     });
118 |   });
119 |   
120 |   describe('Transaction Handling', () => {
121 |     it('should handle commit and rollback when transaction is executed', () => {
122 |       // Test transaction wrapper logic
123 |       const mockDb = {
124 |         exec: vi.fn(),
125 |         inTransaction: false
126 |       };
127 |       
128 |       const transaction = <T>(db: any, fn: () => T): T => {
129 |         try {
130 |           db.exec('BEGIN');
131 |           db.inTransaction = true;
132 |           const result = fn();
133 |           db.exec('COMMIT');
134 |           db.inTransaction = false;
135 |           return result;
136 |         } catch (error) {
137 |           db.exec('ROLLBACK');
138 |           db.inTransaction = false;
139 |           throw error;
140 |         }
141 |       };
142 |       
143 |       // Test successful transaction
144 |       const result = transaction(mockDb, () => 'success');
145 |       expect(result).toBe('success');
146 |       expect(mockDb.exec).toHaveBeenCalledWith('BEGIN');
147 |       expect(mockDb.exec).toHaveBeenCalledWith('COMMIT');
148 |       expect(mockDb.inTransaction).toBe(false);
149 |       
150 |       // Reset mocks
151 |       mockDb.exec.mockClear();
152 |       
153 |       // Test failed transaction
154 |       expect(() => {
155 |         transaction(mockDb, () => {
156 |           throw new Error('transaction error');
157 |         });
158 |       }).toThrow('transaction error');
159 |       
160 |       expect(mockDb.exec).toHaveBeenCalledWith('BEGIN');
161 |       expect(mockDb.exec).toHaveBeenCalledWith('ROLLBACK');
162 |       expect(mockDb.inTransaction).toBe(false);
163 |     });
164 |   });
165 |   
166 |   describe('Pragma Handling', () => {
167 |     it('should return values when pragma commands are executed', () => {
168 |       const mockDb = {
169 |         pragma: vi.fn((key: string, value?: any) => {
170 |           if (key === 'journal_mode' && value === 'WAL') {
171 |             return 'wal';
172 |           }
173 |           return null;
174 |         })
175 |       };
176 | 
177 |       expect(mockDb.pragma('journal_mode', 'WAL')).toBe('wal');
178 |       expect(mockDb.pragma('other_key')).toBe(null);
179 |     });
180 |   });
181 | 
182 |   describe('SQLJSAdapter Save Behavior (Memory Leak Fix - Issue #330)', () => {
183 |     it('should use default 5000ms save interval when env var not set', () => {
184 |       // Verify default interval is 5000ms (not old 100ms)
185 |       const DEFAULT_INTERVAL = 5000;
186 |       expect(DEFAULT_INTERVAL).toBe(5000);
187 |     });
188 | 
189 |     it('should use custom save interval from SQLJS_SAVE_INTERVAL_MS env var', () => {
190 |       // Mock environment variable
191 |       const originalEnv = process.env.SQLJS_SAVE_INTERVAL_MS;
192 |       process.env.SQLJS_SAVE_INTERVAL_MS = '10000';
193 | 
194 |       // Test that interval would be parsed
195 |       const envInterval = process.env.SQLJS_SAVE_INTERVAL_MS;
196 |       const parsedInterval = envInterval ? parseInt(envInterval, 10) : 5000;
197 | 
198 |       expect(parsedInterval).toBe(10000);
199 | 
200 |       // Restore environment
201 |       if (originalEnv !== undefined) {
202 |         process.env.SQLJS_SAVE_INTERVAL_MS = originalEnv;
203 |       } else {
204 |         delete process.env.SQLJS_SAVE_INTERVAL_MS;
205 |       }
206 |     });
207 | 
208 |     it('should fall back to default when invalid env var is provided', () => {
209 |       // Test validation logic
210 |       const testCases = [
211 |         { input: 'invalid', expected: 5000 },
212 |         { input: '50', expected: 5000 },  // Too low (< 100)
213 |         { input: '-100', expected: 5000 }, // Negative
214 |         { input: '0', expected: 5000 },    // Zero
215 |       ];
216 | 
217 |       testCases.forEach(({ input, expected }) => {
218 |         const parsed = parseInt(input, 10);
219 |         const interval = (isNaN(parsed) || parsed < 100) ? 5000 : parsed;
220 |         expect(interval).toBe(expected);
221 |       });
222 |     });
223 | 
224 |     it('should debounce multiple rapid saves using configured interval', () => {
225 |       // Test debounce logic
226 |       let timer: NodeJS.Timeout | null = null;
227 |       const mockSave = vi.fn();
228 | 
229 |       const scheduleSave = (interval: number) => {
230 |         if (timer) {
231 |           clearTimeout(timer);
232 |         }
233 |         timer = setTimeout(() => {
234 |           mockSave();
235 |         }, interval);
236 |       };
237 | 
238 |       // Simulate rapid operations
239 |       scheduleSave(5000);
240 |       scheduleSave(5000);
241 |       scheduleSave(5000);
242 | 
243 |       // Should only schedule once (debounced)
244 |       expect(mockSave).not.toHaveBeenCalled();
245 | 
246 |       // Cleanup
247 |       if (timer) clearTimeout(timer);
248 |     });
249 |   });
250 | 
251 |   describe('SQLJSAdapter Memory Optimization', () => {
252 |     it('should not use Buffer.from() copy in saveToFile()', () => {
253 |       // Test that direct Uint8Array write logic is correct
254 |       const mockData = new Uint8Array([1, 2, 3, 4, 5]);
255 | 
256 |       // Verify Uint8Array can be used directly
257 |       expect(mockData).toBeInstanceOf(Uint8Array);
258 |       expect(mockData.length).toBe(5);
259 | 
260 |       // This test verifies the pattern used in saveToFile()
261 |       // The actual implementation writes mockData directly to fsSync.writeFileSync()
262 |       // without using Buffer.from(mockData) which would double memory usage
263 |     });
264 | 
265 |     it('should cleanup resources with explicit null assignment', () => {
266 |       // Test cleanup pattern used in saveToFile()
267 |       let data: Uint8Array | null = new Uint8Array([1, 2, 3]);
268 | 
269 |       try {
270 |         // Simulate save operation
271 |         expect(data).not.toBeNull();
272 |       } finally {
273 |         // Explicit cleanup helps GC
274 |         data = null;
275 |       }
276 | 
277 |       expect(data).toBeNull();
278 |     });
279 | 
280 |     it('should handle save errors without leaking resources', () => {
281 |       // Test error handling with cleanup
282 |       let data: Uint8Array | null = null;
283 |       let errorThrown = false;
284 | 
285 |       try {
286 |         data = new Uint8Array([1, 2, 3]);
287 |         // Simulate error
288 |         throw new Error('Save failed');
289 |       } catch (error) {
290 |         errorThrown = true;
291 |       } finally {
292 |         // Cleanup happens even on error
293 |         data = null;
294 |       }
295 | 
296 |       expect(errorThrown).toBe(true);
297 |       expect(data).toBeNull();
298 |     });
299 |   });
300 | 
301 |   describe('Read vs Write Operation Handling', () => {
302 |     it('should not trigger save on read-only prepare() calls', () => {
303 |       // Test that prepare() doesn't schedule save
304 |       // Only exec() and SQLJSStatement.run() should trigger saves
305 | 
306 |       const mockScheduleSave = vi.fn();
307 | 
308 |       // Simulate prepare() - should NOT call scheduleSave
309 |       // prepare() just creates statement, doesn't modify DB
310 | 
311 |       // Simulate exec() - SHOULD call scheduleSave
312 |       mockScheduleSave();
313 | 
314 |       expect(mockScheduleSave).toHaveBeenCalledTimes(1);
315 |     });
316 | 
317 |     it('should trigger save on write operations (INSERT/UPDATE/DELETE)', () => {
318 |       const mockScheduleSave = vi.fn();
319 | 
320 |       // Simulate write operations
321 |       mockScheduleSave(); // INSERT
322 |       mockScheduleSave(); // UPDATE
323 |       mockScheduleSave(); // DELETE
324 | 
325 |       expect(mockScheduleSave).toHaveBeenCalledTimes(3);
326 |     });
327 |   });
328 | });
```

--------------------------------------------------------------------------------
/src/utils/cache-utils.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Cache utilities for flexible instance configuration
  3 |  * Provides hash creation, metrics tracking, and cache configuration
  4 |  */
  5 | 
  6 | import { createHash } from 'crypto';
  7 | import { LRUCache } from 'lru-cache';
  8 | import { logger } from './logger';
  9 | 
 10 | /**
 11 |  * Cache metrics for monitoring and optimization
 12 |  */
 13 | export interface CacheMetrics {
 14 |   hits: number;
 15 |   misses: number;
 16 |   evictions: number;
 17 |   sets: number;
 18 |   deletes: number;
 19 |   clears: number;
 20 |   size: number;
 21 |   maxSize: number;
 22 |   avgHitRate: number;
 23 |   createdAt: Date;
 24 |   lastResetAt: Date;
 25 | }
 26 | 
 27 | /**
 28 |  * Cache configuration options
 29 |  */
 30 | export interface CacheConfig {
 31 |   max: number;
 32 |   ttlMinutes: number;
 33 | }
 34 | 
 35 | /**
 36 |  * Simple memoization cache for hash results
 37 |  * Limited size to prevent memory growth
 38 |  */
 39 | const hashMemoCache = new Map<string, string>();
 40 | const MAX_MEMO_SIZE = 1000;
 41 | 
 42 | /**
 43 |  * Metrics tracking for cache operations
 44 |  */
 45 | class CacheMetricsTracker {
 46 |   private metrics!: CacheMetrics;
 47 |   private startTime: Date;
 48 | 
 49 |   constructor() {
 50 |     this.startTime = new Date();
 51 |     this.reset();
 52 |   }
 53 | 
 54 |   /**
 55 |    * Reset all metrics to initial state
 56 |    */
 57 |   reset(): void {
 58 |     this.metrics = {
 59 |       hits: 0,
 60 |       misses: 0,
 61 |       evictions: 0,
 62 |       sets: 0,
 63 |       deletes: 0,
 64 |       clears: 0,
 65 |       size: 0,
 66 |       maxSize: 0,
 67 |       avgHitRate: 0,
 68 |       createdAt: this.startTime,
 69 |       lastResetAt: new Date()
 70 |     };
 71 |   }
 72 | 
 73 |   /**
 74 |    * Record a cache hit
 75 |    */
 76 |   recordHit(): void {
 77 |     this.metrics.hits++;
 78 |     this.updateHitRate();
 79 |   }
 80 | 
 81 |   /**
 82 |    * Record a cache miss
 83 |    */
 84 |   recordMiss(): void {
 85 |     this.metrics.misses++;
 86 |     this.updateHitRate();
 87 |   }
 88 | 
 89 |   /**
 90 |    * Record a cache eviction
 91 |    */
 92 |   recordEviction(): void {
 93 |     this.metrics.evictions++;
 94 |   }
 95 | 
 96 |   /**
 97 |    * Record a cache set operation
 98 |    */
 99 |   recordSet(): void {
100 |     this.metrics.sets++;
101 |   }
102 | 
103 |   /**
104 |    * Record a cache delete operation
105 |    */
106 |   recordDelete(): void {
107 |     this.metrics.deletes++;
108 |   }
109 | 
110 |   /**
111 |    * Record a cache clear operation
112 |    */
113 |   recordClear(): void {
114 |     this.metrics.clears++;
115 |   }
116 | 
117 |   /**
118 |    * Update cache size metrics
119 |    */
120 |   updateSize(current: number, max: number): void {
121 |     this.metrics.size = current;
122 |     this.metrics.maxSize = max;
123 |   }
124 | 
125 |   /**
126 |    * Update average hit rate
127 |    */
128 |   private updateHitRate(): void {
129 |     const total = this.metrics.hits + this.metrics.misses;
130 |     if (total > 0) {
131 |       this.metrics.avgHitRate = this.metrics.hits / total;
132 |     }
133 |   }
134 | 
135 |   /**
136 |    * Get current metrics snapshot
137 |    */
138 |   getMetrics(): CacheMetrics {
139 |     return { ...this.metrics };
140 |   }
141 | 
142 |   /**
143 |    * Get formatted metrics for logging
144 |    */
145 |   getFormattedMetrics(): string {
146 |     const { hits, misses, evictions, avgHitRate, size, maxSize } = this.metrics;
147 |     return `Cache Metrics: Hits=${hits}, Misses=${misses}, HitRate=${(avgHitRate * 100).toFixed(2)}%, Size=${size}/${maxSize}, Evictions=${evictions}`;
148 |   }
149 | }
150 | 
151 | // Global metrics tracker instance
152 | export const cacheMetrics = new CacheMetricsTracker();
153 | 
154 | /**
155 |  * Get cache configuration from environment variables or defaults
156 |  * @returns Cache configuration with max size and TTL
157 |  */
158 | export function getCacheConfig(): CacheConfig {
159 |   const max = parseInt(process.env.INSTANCE_CACHE_MAX || '100', 10);
160 |   const ttlMinutes = parseInt(process.env.INSTANCE_CACHE_TTL_MINUTES || '30', 10);
161 | 
162 |   // Validate configuration bounds
163 |   const validatedMax = Math.max(1, Math.min(10000, max)) || 100;
164 |   const validatedTtl = Math.max(1, Math.min(1440, ttlMinutes)) || 30; // Max 24 hours
165 | 
166 |   if (validatedMax !== max || validatedTtl !== ttlMinutes) {
167 |     logger.warn('Cache configuration adjusted to valid bounds', {
168 |       requestedMax: max,
169 |       requestedTtl: ttlMinutes,
170 |       actualMax: validatedMax,
171 |       actualTtl: validatedTtl
172 |     });
173 |   }
174 | 
175 |   return {
176 |     max: validatedMax,
177 |     ttlMinutes: validatedTtl
178 |   };
179 | }
180 | 
181 | /**
182 |  * Create a secure hash for cache key with memoization
183 |  * @param input - The input string to hash
184 |  * @returns SHA-256 hash as hex string
185 |  */
186 | export function createCacheKey(input: string): string {
187 |   // Check memoization cache first
188 |   if (hashMemoCache.has(input)) {
189 |     return hashMemoCache.get(input)!;
190 |   }
191 | 
192 |   // Create hash
193 |   const hash = createHash('sha256').update(input).digest('hex');
194 | 
195 |   // Add to memoization cache with size limit
196 |   if (hashMemoCache.size >= MAX_MEMO_SIZE) {
197 |     // Remove oldest entries (simple FIFO)
198 |     const firstKey = hashMemoCache.keys().next().value;
199 |     if (firstKey) {
200 |       hashMemoCache.delete(firstKey);
201 |     }
202 |   }
203 |   hashMemoCache.set(input, hash);
204 | 
205 |   return hash;
206 | }
207 | 
208 | /**
209 |  * Create LRU cache with metrics tracking
210 |  * @param onDispose - Optional callback for when items are evicted
211 |  * @returns Configured LRU cache instance
212 |  */
213 | export function createInstanceCache<T extends {}>(
214 |   onDispose?: (value: T, key: string) => void
215 | ): LRUCache<string, T> {
216 |   const config = getCacheConfig();
217 | 
218 |   return new LRUCache<string, T>({
219 |     max: config.max,
220 |     ttl: config.ttlMinutes * 60 * 1000, // Convert to milliseconds
221 |     updateAgeOnGet: true,
222 |     dispose: (value, key) => {
223 |       cacheMetrics.recordEviction();
224 |       if (onDispose) {
225 |         onDispose(value, key);
226 |       }
227 |       logger.debug('Cache eviction', {
228 |         cacheKey: key.substring(0, 8) + '...',
229 |         metrics: cacheMetrics.getFormattedMetrics()
230 |       });
231 |     }
232 |   });
233 | }
234 | 
235 | /**
236 |  * Mutex implementation for cache operations
237 |  * Prevents race conditions during concurrent access
238 |  */
239 | export class CacheMutex {
240 |   private locks: Map<string, Promise<void>> = new Map();
241 |   private lockTimeouts: Map<string, NodeJS.Timeout> = new Map();
242 |   private readonly timeout: number = 5000; // 5 second timeout
243 | 
244 |   /**
245 |    * Acquire a lock for the given key
246 |    * @param key - The cache key to lock
247 |    * @returns Promise that resolves when lock is acquired
248 |    */
249 |   async acquire(key: string): Promise<() => void> {
250 |     while (this.locks.has(key)) {
251 |       try {
252 |         await this.locks.get(key);
253 |       } catch {
254 |         // Previous lock failed, we can proceed
255 |       }
256 |     }
257 | 
258 |     let releaseLock: () => void;
259 |     const lockPromise = new Promise<void>((resolve) => {
260 |       releaseLock = () => {
261 |         resolve();
262 |         this.locks.delete(key);
263 |         const timeout = this.lockTimeouts.get(key);
264 |         if (timeout) {
265 |           clearTimeout(timeout);
266 |           this.lockTimeouts.delete(key);
267 |         }
268 |       };
269 |     });
270 | 
271 |     this.locks.set(key, lockPromise);
272 | 
273 |     // Set timeout to prevent stuck locks
274 |     const timeout = setTimeout(() => {
275 |       logger.warn('Cache lock timeout, forcefully releasing', { key: key.substring(0, 8) + '...' });
276 |       releaseLock!();
277 |     }, this.timeout);
278 |     this.lockTimeouts.set(key, timeout);
279 | 
280 |     return releaseLock!;
281 |   }
282 | 
283 |   /**
284 |    * Check if a key is currently locked
285 |    * @param key - The cache key to check
286 |    * @returns True if the key is locked
287 |    */
288 |   isLocked(key: string): boolean {
289 |     return this.locks.has(key);
290 |   }
291 | 
292 |   /**
293 |    * Clear all locks (use with caution)
294 |    */
295 |   clearAll(): void {
296 |     this.lockTimeouts.forEach(timeout => clearTimeout(timeout));
297 |     this.locks.clear();
298 |     this.lockTimeouts.clear();
299 |   }
300 | }
301 | 
302 | /**
303 |  * Retry configuration for API operations
304 |  */
305 | export interface RetryConfig {
306 |   maxAttempts: number;
307 |   baseDelayMs: number;
308 |   maxDelayMs: number;
309 |   jitterFactor: number;
310 | }
311 | 
312 | /**
313 |  * Default retry configuration
314 |  */
315 | export const DEFAULT_RETRY_CONFIG: RetryConfig = {
316 |   maxAttempts: 3,
317 |   baseDelayMs: 1000,
318 |   maxDelayMs: 10000,
319 |   jitterFactor: 0.3
320 | };
321 | 
322 | /**
323 |  * Calculate exponential backoff delay with jitter
324 |  * @param attempt - Current attempt number (0-based)
325 |  * @param config - Retry configuration
326 |  * @returns Delay in milliseconds
327 |  */
328 | export function calculateBackoffDelay(attempt: number, config: RetryConfig = DEFAULT_RETRY_CONFIG): number {
329 |   const exponentialDelay = Math.min(
330 |     config.baseDelayMs * Math.pow(2, attempt),
331 |     config.maxDelayMs
332 |   );
333 | 
334 |   // Add jitter to prevent thundering herd
335 |   const jitter = exponentialDelay * config.jitterFactor * Math.random();
336 | 
337 |   return Math.floor(exponentialDelay + jitter);
338 | }
339 | 
340 | /**
341 |  * Execute function with retry logic
342 |  * @param fn - Function to execute
343 |  * @param config - Retry configuration
344 |  * @param context - Optional context for logging
345 |  * @returns Result of the function
346 |  */
347 | export async function withRetry<T>(
348 |   fn: () => Promise<T>,
349 |   config: RetryConfig = DEFAULT_RETRY_CONFIG,
350 |   context?: string
351 | ): Promise<T> {
352 |   let lastError: Error;
353 | 
354 |   for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
355 |     try {
356 |       return await fn();
357 |     } catch (error) {
358 |       lastError = error as Error;
359 | 
360 |       // Check if error is retryable
361 |       if (!isRetryableError(error)) {
362 |         throw error;
363 |       }
364 | 
365 |       if (attempt < config.maxAttempts - 1) {
366 |         const delay = calculateBackoffDelay(attempt, config);
367 |         logger.debug('Retrying operation after delay', {
368 |           context,
369 |           attempt: attempt + 1,
370 |           maxAttempts: config.maxAttempts,
371 |           delayMs: delay,
372 |           error: lastError.message
373 |         });
374 |         await new Promise(resolve => setTimeout(resolve, delay));
375 |       }
376 |     }
377 |   }
378 | 
379 |   logger.error('All retry attempts exhausted', {
380 |     context,
381 |     attempts: config.maxAttempts,
382 |     lastError: lastError!.message
383 |   });
384 | 
385 |   throw lastError!;
386 | }
387 | 
388 | /**
389 |  * Check if an error is retryable
390 |  * @param error - The error to check
391 |  * @returns True if the error is retryable
392 |  */
393 | function isRetryableError(error: any): boolean {
394 |   // Network errors
395 |   if (error.code === 'ECONNREFUSED' ||
396 |       error.code === 'ECONNRESET' ||
397 |       error.code === 'ETIMEDOUT' ||
398 |       error.code === 'ENOTFOUND') {
399 |     return true;
400 |   }
401 | 
402 |   // HTTP status codes that are retryable
403 |   if (error.response?.status) {
404 |     const status = error.response.status;
405 |     return status === 429 || // Too Many Requests
406 |            status === 503 || // Service Unavailable
407 |            status === 504 || // Gateway Timeout
408 |            (status >= 500 && status < 600); // Server errors
409 |   }
410 | 
411 |   // Timeout errors
412 |   if (error.message && error.message.toLowerCase().includes('timeout')) {
413 |     return true;
414 |   }
415 | 
416 |   return false;
417 | }
418 | 
419 | /**
420 |  * Format cache statistics for logging or display
421 |  * @returns Formatted statistics string
422 |  */
423 | export function getCacheStatistics(): string {
424 |   const metrics = cacheMetrics.getMetrics();
425 |   const runtime = Date.now() - metrics.createdAt.getTime();
426 |   const runtimeMinutes = Math.floor(runtime / 60000);
427 | 
428 |   return `
429 | Cache Statistics:
430 |   Runtime: ${runtimeMinutes} minutes
431 |   Total Operations: ${metrics.hits + metrics.misses}
432 |   Hit Rate: ${(metrics.avgHitRate * 100).toFixed(2)}%
433 |   Current Size: ${metrics.size}/${metrics.maxSize}
434 |   Total Evictions: ${metrics.evictions}
435 |   Sets: ${metrics.sets}, Deletes: ${metrics.deletes}, Clears: ${metrics.clears}
436 |   `.trim();
437 | }
```

--------------------------------------------------------------------------------
/tests/setup/test-env.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Test Environment Configuration Loader
  3 |  * 
  4 |  * This module handles loading and validating test environment variables
  5 |  * with type safety and default values.
  6 |  */
  7 | 
  8 | import * as dotenv from 'dotenv';
  9 | import * as path from 'path';
 10 | import { existsSync } from 'fs';
 11 | 
 12 | // Load test environment variables
 13 | export function loadTestEnvironment(): void {
 14 |   // CI Debug logging
 15 |   const isCI = process.env.CI === 'true';
 16 | 
 17 |   // First, load the main .env file (for integration tests that need real credentials)
 18 |   const mainEnvPath = path.resolve(process.cwd(), '.env');
 19 |   if (existsSync(mainEnvPath)) {
 20 |     dotenv.config({ path: mainEnvPath });
 21 |     if (isCI) {
 22 |       console.log('[CI-DEBUG] Loaded .env file from:', mainEnvPath);
 23 |     }
 24 |   }
 25 | 
 26 |   // Load base test environment
 27 |   const testEnvPath = path.resolve(process.cwd(), '.env.test');
 28 | 
 29 |   if (isCI) {
 30 |     console.log('[CI-DEBUG] Looking for .env.test at:', testEnvPath);
 31 |     console.log('[CI-DEBUG] File exists?', existsSync(testEnvPath));
 32 |   }
 33 | 
 34 |   if (existsSync(testEnvPath)) {
 35 |     // Don't override values from .env
 36 |     const result = dotenv.config({ path: testEnvPath, override: false });
 37 |     if (isCI && result.error) {
 38 |       console.error('[CI-DEBUG] Failed to load .env.test:', result.error);
 39 |     } else if (isCI && result.parsed) {
 40 |       console.log('[CI-DEBUG] Successfully loaded', Object.keys(result.parsed).length, 'env vars from .env.test');
 41 |     }
 42 |   } else if (isCI) {
 43 |     console.warn('[CI-DEBUG] .env.test file not found, will use defaults only');
 44 |   }
 45 | 
 46 |   // Load local test overrides (for sensitive values)
 47 |   const localEnvPath = path.resolve(process.cwd(), '.env.test.local');
 48 |   if (existsSync(localEnvPath)) {
 49 |     dotenv.config({ path: localEnvPath, override: true });
 50 |   }
 51 | 
 52 |   // Set test-specific defaults (only if not already set)
 53 |   setTestDefaults();
 54 | 
 55 |   // Validate required environment variables
 56 |   validateTestEnvironment();
 57 | }
 58 | 
 59 | /**
 60 |  * Set default values for test environment variables
 61 |  */
 62 | function setTestDefaults(): void {
 63 |   // Ensure we're in test mode
 64 |   process.env.NODE_ENV = 'test';
 65 |   process.env.TEST_ENVIRONMENT = 'true';
 66 |   
 67 |   // Set defaults if not already set
 68 |   const defaults: Record<string, string> = {
 69 |     // Database
 70 |     NODE_DB_PATH: ':memory:',
 71 |     REBUILD_ON_START: 'false',
 72 |     
 73 |     // API
 74 |     N8N_API_URL: 'http://localhost:3001/mock-api',
 75 |     N8N_API_KEY: 'test-api-key-12345',
 76 |     
 77 |     // Server
 78 |     PORT: '3001',
 79 |     HOST: '127.0.0.1',
 80 |     
 81 |     // Logging
 82 |     LOG_LEVEL: 'error',
 83 |     DEBUG: 'false',
 84 |     TEST_LOG_VERBOSE: 'false',
 85 |     
 86 |     // Timeouts
 87 |     TEST_TIMEOUT_UNIT: '5000',
 88 |     TEST_TIMEOUT_INTEGRATION: '15000',
 89 |     TEST_TIMEOUT_E2E: '30000',
 90 |     TEST_TIMEOUT_GLOBAL: '30000', // Reduced from 60s to 30s to catch hangs faster
 91 |     
 92 |     // Test execution
 93 |     TEST_RETRY_ATTEMPTS: '2',
 94 |     TEST_RETRY_DELAY: '1000',
 95 |     TEST_PARALLEL: 'true',
 96 |     TEST_MAX_WORKERS: '4',
 97 |     
 98 |     // Features
 99 |     FEATURE_MOCK_EXTERNAL_APIS: 'true',
100 |     FEATURE_USE_TEST_CONTAINERS: 'false',
101 |     MSW_ENABLED: 'true',
102 |     MSW_API_DELAY: '0',
103 |     
104 |     // Paths
105 |     TEST_FIXTURES_PATH: './tests/fixtures',
106 |     TEST_DATA_PATH: './tests/data',
107 |     TEST_SNAPSHOTS_PATH: './tests/__snapshots__',
108 |     
109 |     // Performance
110 |     PERF_THRESHOLD_API_RESPONSE: '100',
111 |     PERF_THRESHOLD_DB_QUERY: '50',
112 |     PERF_THRESHOLD_NODE_PARSE: '200',
113 |     
114 |     // Caching
115 |     CACHE_TTL: '0',
116 |     CACHE_ENABLED: 'false',
117 |     
118 |     // Rate limiting
119 |     RATE_LIMIT_MAX: '0',
120 |     RATE_LIMIT_WINDOW: '0',
121 |     
122 |     // Error handling
123 |     ERROR_SHOW_STACK: 'true',
124 |     ERROR_SHOW_DETAILS: 'true',
125 |     
126 |     // Cleanup
127 |     TEST_CLEANUP_ENABLED: 'true',
128 |     TEST_CLEANUP_ON_FAILURE: 'false',
129 |     
130 |     // Database seeding
131 |     TEST_SEED_DATABASE: 'true',
132 |     TEST_SEED_TEMPLATES: 'true',
133 |     
134 |     // Network
135 |     NETWORK_TIMEOUT: '5000',
136 |     NETWORK_RETRY_COUNT: '0',
137 |     
138 |     // Memory
139 |     TEST_MEMORY_LIMIT: '512',
140 |     
141 |     // Coverage
142 |     COVERAGE_DIR: './coverage',
143 |     COVERAGE_REPORTER: 'lcov,html,text-summary'
144 |   };
145 | 
146 |   for (const [key, value] of Object.entries(defaults)) {
147 |     if (!process.env[key]) {
148 |       process.env[key] = value;
149 |     }
150 |   }
151 | }
152 | 
153 | /**
154 |  * Validate that required environment variables are set
155 |  */
156 | function validateTestEnvironment(): void {
157 |   const required = [
158 |     'NODE_ENV',
159 |     'NODE_DB_PATH',
160 |     'N8N_API_URL',
161 |     'N8N_API_KEY'
162 |   ];
163 | 
164 |   const missing = required.filter(key => !process.env[key]);
165 |   
166 |   if (missing.length > 0) {
167 |     throw new Error(
168 |       `Missing required test environment variables: ${missing.join(', ')}\n` +
169 |       'Please ensure .env.test is properly configured.'
170 |     );
171 |   }
172 | 
173 |   // Validate NODE_ENV is set to test
174 |   if (process.env.NODE_ENV !== 'test') {
175 |     throw new Error(
176 |       'NODE_ENV must be set to "test" when running tests.\n' +
177 |       'This prevents accidental execution against production systems.'
178 |     );
179 |   }
180 | }
181 | 
182 | /**
183 |  * Get typed test environment configuration
184 |  */
185 | export function getTestConfig() {
186 |   // Ensure defaults are set before accessing
187 |   if (!process.env.N8N_API_URL) {
188 |     setTestDefaults();
189 |   }
190 |   
191 |   return {
192 |     // Environment
193 |     nodeEnv: process.env.NODE_ENV || 'test',
194 |     isTest: process.env.TEST_ENVIRONMENT === 'true',
195 |     
196 |     // Database
197 |     database: {
198 |       path: process.env.NODE_DB_PATH || ':memory:',
199 |       rebuildOnStart: process.env.REBUILD_ON_START === 'true',
200 |       seedData: process.env.TEST_SEED_DATABASE === 'true',
201 |       seedTemplates: process.env.TEST_SEED_TEMPLATES === 'true'
202 |     },
203 |     
204 |     // API
205 |     api: {
206 |       url: process.env.N8N_API_URL || 'http://localhost:3001/mock-api',
207 |       key: process.env.N8N_API_KEY || 'test-api-key-12345',
208 |       webhookBaseUrl: process.env.N8N_WEBHOOK_BASE_URL,
209 |       webhookTestUrl: process.env.N8N_WEBHOOK_TEST_URL
210 |     },
211 |     
212 |     // Server
213 |     server: {
214 |       port: parseInt(process.env.PORT || '3001', 10),
215 |       host: process.env.HOST || '127.0.0.1',
216 |       corsOrigin: process.env.CORS_ORIGIN?.split(',') || []
217 |     },
218 |     
219 |     // Authentication
220 |     auth: {
221 |       token: process.env.AUTH_TOKEN,
222 |       mcpToken: process.env.MCP_AUTH_TOKEN
223 |     },
224 |     
225 |     // Logging
226 |     logging: {
227 |       level: process.env.LOG_LEVEL || 'error',
228 |       debug: process.env.DEBUG === 'true',
229 |       verbose: process.env.TEST_LOG_VERBOSE === 'true',
230 |       showStack: process.env.ERROR_SHOW_STACK === 'true',
231 |       showDetails: process.env.ERROR_SHOW_DETAILS === 'true'
232 |     },
233 |     
234 |     // Test execution
235 |     execution: {
236 |       timeouts: {
237 |         unit: parseInt(process.env.TEST_TIMEOUT_UNIT || '5000', 10),
238 |         integration: parseInt(process.env.TEST_TIMEOUT_INTEGRATION || '15000', 10),
239 |         e2e: parseInt(process.env.TEST_TIMEOUT_E2E || '30000', 10),
240 |         global: parseInt(process.env.TEST_TIMEOUT_GLOBAL || '60000', 10)
241 |       },
242 |       retry: {
243 |         attempts: parseInt(process.env.TEST_RETRY_ATTEMPTS || '2', 10),
244 |         delay: parseInt(process.env.TEST_RETRY_DELAY || '1000', 10)
245 |       },
246 |       parallel: process.env.TEST_PARALLEL === 'true',
247 |       maxWorkers: parseInt(process.env.TEST_MAX_WORKERS || '4', 10)
248 |     },
249 |     
250 |     // Features
251 |     features: {
252 |       coverage: process.env.FEATURE_TEST_COVERAGE === 'true',
253 |       screenshots: process.env.FEATURE_TEST_SCREENSHOTS === 'true',
254 |       videos: process.env.FEATURE_TEST_VIDEOS === 'true',
255 |       trace: process.env.FEATURE_TEST_TRACE === 'true',
256 |       mockExternalApis: process.env.FEATURE_MOCK_EXTERNAL_APIS === 'true',
257 |       useTestContainers: process.env.FEATURE_USE_TEST_CONTAINERS === 'true'
258 |     },
259 |     
260 |     // Mocking
261 |     mocking: {
262 |       msw: {
263 |         enabled: process.env.MSW_ENABLED === 'true',
264 |         apiDelay: parseInt(process.env.MSW_API_DELAY || '0', 10)
265 |       },
266 |       redis: {
267 |         enabled: process.env.REDIS_MOCK_ENABLED === 'true',
268 |         port: parseInt(process.env.REDIS_MOCK_PORT || '6380', 10)
269 |       },
270 |       elasticsearch: {
271 |         enabled: process.env.ELASTICSEARCH_MOCK_ENABLED === 'true',
272 |         port: parseInt(process.env.ELASTICSEARCH_MOCK_PORT || '9201', 10)
273 |       }
274 |     },
275 |     
276 |     // Paths
277 |     paths: {
278 |       fixtures: process.env.TEST_FIXTURES_PATH || './tests/fixtures',
279 |       data: process.env.TEST_DATA_PATH || './tests/data',
280 |       snapshots: process.env.TEST_SNAPSHOTS_PATH || './tests/__snapshots__'
281 |     },
282 |     
283 |     // Performance
284 |     performance: {
285 |       thresholds: {
286 |         apiResponse: parseInt(process.env.PERF_THRESHOLD_API_RESPONSE || '100', 10),
287 |         dbQuery: parseInt(process.env.PERF_THRESHOLD_DB_QUERY || '50', 10),
288 |         nodeParse: parseInt(process.env.PERF_THRESHOLD_NODE_PARSE || '200', 10)
289 |       }
290 |     },
291 |     
292 |     // Rate limiting
293 |     rateLimiting: {
294 |       max: parseInt(process.env.RATE_LIMIT_MAX || '0', 10),
295 |       window: parseInt(process.env.RATE_LIMIT_WINDOW || '0', 10)
296 |     },
297 |     
298 |     // Caching
299 |     cache: {
300 |       enabled: process.env.CACHE_ENABLED === 'true',
301 |       ttl: parseInt(process.env.CACHE_TTL || '0', 10)
302 |     },
303 |     
304 |     // Cleanup
305 |     cleanup: {
306 |       enabled: process.env.TEST_CLEANUP_ENABLED === 'true',
307 |       onFailure: process.env.TEST_CLEANUP_ON_FAILURE === 'true'
308 |     },
309 |     
310 |     // Network
311 |     network: {
312 |       timeout: parseInt(process.env.NETWORK_TIMEOUT || '5000', 10),
313 |       retryCount: parseInt(process.env.NETWORK_RETRY_COUNT || '0', 10)
314 |     },
315 |     
316 |     // Memory
317 |     memory: {
318 |       limit: parseInt(process.env.TEST_MEMORY_LIMIT || '512', 10)
319 |     },
320 |     
321 |     // Coverage
322 |     coverage: {
323 |       dir: process.env.COVERAGE_DIR || './coverage',
324 |       reporters: (process.env.COVERAGE_REPORTER || 'lcov,html,text-summary').split(',')
325 |     }
326 |   };
327 | }
328 | 
329 | // Export type for the test configuration
330 | export type TestConfig = ReturnType<typeof getTestConfig>;
331 | 
332 | /**
333 |  * Helper to check if we're in test mode
334 |  */
335 | export function isTestMode(): boolean {
336 |   return process.env.NODE_ENV === 'test' || process.env.TEST_ENVIRONMENT === 'true';
337 | }
338 | 
339 | /**
340 |  * Helper to get timeout for specific test type
341 |  */
342 | export function getTestTimeout(type: 'unit' | 'integration' | 'e2e' | 'global' = 'unit'): number {
343 |   const config = getTestConfig();
344 |   return config.execution.timeouts[type];
345 | }
346 | 
347 | /**
348 |  * Helper to check if a feature is enabled
349 |  */
350 | export function isFeatureEnabled(feature: keyof TestConfig['features']): boolean {
351 |   const config = getTestConfig();
352 |   return config.features[feature];
353 | }
354 | 
355 | /**
356 |  * Reset environment to defaults (useful for test isolation)
357 |  */
358 | export function resetTestEnvironment(): void {
359 |   // Clear all test-specific environment variables
360 |   const testKeys = Object.keys(process.env).filter(key => 
361 |     key.startsWith('TEST_') || 
362 |     key.startsWith('FEATURE_') ||
363 |     key.startsWith('MSW_') ||
364 |     key.startsWith('PERF_')
365 |   );
366 |   
367 |   testKeys.forEach(key => {
368 |     delete process.env[key];
369 |   });
370 |   
371 |   // Reload defaults
372 |   loadTestEnvironment();
373 | }
```

--------------------------------------------------------------------------------
/docs/RAILWAY_DEPLOYMENT.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Railway Deployment Guide for n8n-MCP
  2 | 
  3 | Deploy n8n-MCP to Railway's cloud platform with zero configuration and connect it to Claude Desktop from anywhere.
  4 | 
  5 | ## 🚀 Quick Deploy
  6 | 
  7 | Deploy n8n-MCP with one click:
  8 | 
  9 | [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/VY6UOG?referralCode=n8n-mcp)
 10 | 
 11 | ## 📋 Overview
 12 | 
 13 | Railway deployment provides:
 14 | - ☁️ **Instant cloud hosting** - No server setup required
 15 | - 🔒 **Secure by default** - HTTPS included, auth token warnings
 16 | - 🌐 **Global access** - Connect from any Claude Desktop
 17 | - ⚡ **Auto-scaling** - Railway handles the infrastructure
 18 | - 📊 **Built-in monitoring** - Logs and metrics included
 19 | 
 20 | ## 🎯 Step-by-Step Deployment
 21 | 
 22 | ### 1. Deploy to Railway
 23 | 
 24 | 1. **Click the Deploy button** above
 25 | 2. **Sign in to Railway** (or create account)
 26 | 3. **Configure your deployment**:
 27 |    - Project name (optional)
 28 |    - Environment (leave as "production")
 29 |    - Region (choose closest to you)
 30 | 4. **Click "Deploy"** and wait ~2-3 minutes
 31 | 
 32 | ### 2. Configure Security
 33 | 
 34 | **IMPORTANT**: The deployment includes a default AUTH_TOKEN for instant functionality, but you MUST change it:
 35 | 
 36 | ![Railway Dashboard - Variables Tab](./img/railway-variables.png)
 37 | 
 38 | 1. **Go to your Railway dashboard**
 39 | 2. **Click on your n8n-mcp service**
 40 | 3. **Navigate to "Variables" tab**
 41 | 4. **Find `AUTH_TOKEN`** 
 42 | 5. **Replace with secure token**:
 43 |    ```bash
 44 |    # Generate secure token locally:
 45 |    openssl rand -base64 32
 46 |    ```
 47 | 6. **Railway will automatically redeploy** with the new token
 48 | 
 49 | > ⚠️ **Security Warning**: The server displays warnings every 5 minutes until you change the default token!
 50 | 
 51 | ### 3. Get Your Service URL
 52 | 
 53 | ![Railway Dashboard - Domain Settings](./img/railway-domain.png)
 54 | 
 55 | 1. In Railway dashboard, click on your service
 56 | 2. Go to **"Settings"** tab
 57 | 3. Under **"Domains"**, you'll see your URL:
 58 |    ```
 59 |    https://your-app-name.up.railway.app
 60 |    ```
 61 | 4. Copy this URL for Claude Desktop configuration and add /mcp at the end
 62 | 
 63 | ### 4. Connect Claude Desktop
 64 | 
 65 | Add to your Claude Desktop configuration:
 66 | 
 67 | ```json
 68 | {
 69 |   "mcpServers": {
 70 |     "n8n-railway": {
 71 |       "command": "npx",
 72 |       "args": [
 73 |         "-y",
 74 |         "mcp-remote",
 75 |         "https://your-app-name.up.railway.app/mcp",
 76 |         "--header",
 77 |         "Authorization: Bearer YOUR_SECURE_TOKEN_HERE"
 78 |       ]
 79 |     }
 80 |   }
 81 | }
 82 | ```
 83 | 
 84 | **Configuration file locations:**
 85 | - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
 86 | - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
 87 | - **Linux**: `~/.config/Claude/claude_desktop_config.json`
 88 | 
 89 | **Restart Claude Desktop** after saving the configuration.
 90 | 
 91 | ## 🔧 Environment Variables
 92 | 
 93 | ### Default Variables (Pre-configured)
 94 | 
 95 | These are automatically set by the Railway template:
 96 | 
 97 | | Variable | Default Value | Description |
 98 | |----------|--------------|-------------|
 99 | | `AUTH_TOKEN` | `REPLACE_THIS...` | **⚠️ CHANGE IMMEDIATELY** |
100 | | `MCP_MODE` | `http` | Required for cloud deployment |
101 | | `USE_FIXED_HTTP` | `true` | Stable HTTP implementation |
102 | | `NODE_ENV` | `production` | Production optimizations |
103 | | `LOG_LEVEL` | `info` | Balanced logging |
104 | | `TRUST_PROXY` | `1` | Railway runs behind proxy |
105 | | `CORS_ORIGIN` | `*` | Allow any origin |
106 | | `HOST` | `0.0.0.0` | Listen on all interfaces |
107 | | `PORT` | (Railway provides) | Don't set manually |
108 | | `AUTH_RATE_LIMIT_WINDOW` | `900000` (15 min) | Rate limit window (v2.16.3+) |
109 | | `AUTH_RATE_LIMIT_MAX` | `20` | Max auth attempts (v2.16.3+) |
110 | | `WEBHOOK_SECURITY_MODE` | `strict` | SSRF protection mode (v2.16.3+) |
111 | 
112 | ### Optional Variables
113 | 
114 | | Variable | Default Value | Description |
115 | |----------|--------------|-------------|
116 | | `N8N_MODE` | `false` | Enable n8n integration mode for MCP Client Tool |
117 | | `N8N_API_URL` | - | URL of your n8n instance (for workflow management) |
118 | | `N8N_API_KEY` | - | API key from n8n Settings → API |
119 | 
120 | ### Optional: n8n Integration
121 | 
122 | #### For n8n MCP Client Tool Integration
123 | 
124 | To use n8n-MCP with n8n's MCP Client Tool node:
125 | 
126 | 1. **Go to Railway dashboard** → Your service → **Variables**
127 | 2. **Add this variable**:
128 |    - `N8N_MODE`: Set to `true` to enable n8n integration mode
129 | 3. **Save changes** - Railway will redeploy automatically
130 | 
131 | #### For n8n API Integration (Workflow Management)
132 | 
133 | To enable workflow management features:
134 | 
135 | 1. **Go to Railway dashboard** → Your service → **Variables**
136 | 2. **Add these variables**:
137 |    - `N8N_API_URL`: Your n8n instance URL (e.g., `https://n8n.example.com`)
138 |    - `N8N_API_KEY`: API key from n8n Settings → API
139 | 3. **Save changes** - Railway will redeploy automatically
140 | 
141 | ## 🏗️ Architecture Details
142 | 
143 | ### How It Works
144 | 
145 | ```
146 | Claude Desktop → mcp-remote → Railway (HTTPS) → n8n-MCP Server
147 | ```
148 | 
149 | 1. **Claude Desktop** uses `mcp-remote` as a bridge
150 | 2. **mcp-remote** converts stdio to HTTP requests
151 | 3. **Railway** provides HTTPS endpoint and infrastructure
152 | 4. **n8n-MCP** runs in HTTP mode on Railway
153 | 
154 | ### Single-Instance Design
155 | 
156 | **Important**: The n8n-MCP HTTP server is designed for single n8n instance deployment:
157 | - n8n API credentials are configured server-side via environment variables
158 | - All clients connecting to the server share the same n8n instance
159 | - For multi-tenant usage, deploy separate Railway instances
160 | 
161 | ### Security Model
162 | 
163 | - **Bearer Token Authentication**: All requests require the AUTH_TOKEN
164 | - **HTTPS by Default**: Railway provides SSL certificates
165 | - **Environment Isolation**: Each deployment is isolated
166 | - **No State Storage**: Server is stateless (database is read-only)
167 | 
168 | ## 🚨 Troubleshooting
169 | 
170 | ### Connection Issues
171 | 
172 | **"Invalid URL" error in Claude Desktop:**
173 | - Ensure you're using the exact configuration format shown above
174 | - Don't add "connect" or other arguments before the URL
175 | - The URL should end with `/mcp`
176 | 
177 | **"Unauthorized" error:**
178 | - Check that your AUTH_TOKEN matches exactly (no extra spaces)
179 | - Ensure the Authorization header format is correct: `Authorization: Bearer TOKEN`
180 | 
181 | **"Cannot connect to server":**
182 | - Verify your Railway deployment is running (check Railway dashboard)
183 | - Ensure the URL is correct and includes `https://`
184 | - Check Railway logs for any errors
185 | 
186 | **Windows: "The filename, directory name, or volume label syntax is incorrect" or npx command not found:**
187 | 
188 | This is a common Windows issue with spaces in Node.js installation paths. The error occurs because Claude Desktop can't properly execute npx.
189 | 
190 | **Solution 1: Use node directly (Recommended)**
191 | ```json
192 | {
193 |   "mcpServers": {
194 |     "n8n-railway": {
195 |       "command": "node",
196 |       "args": [
197 |         "C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npx-cli.js",
198 |         "-y",
199 |         "mcp-remote",
200 |         "https://your-app-name.up.railway.app/mcp",
201 |         "--header",
202 |         "Authorization: Bearer YOUR_SECURE_TOKEN_HERE"
203 |       ]
204 |     }
205 |   }
206 | }
207 | ```
208 | 
209 | **Solution 2: Use cmd wrapper**
210 | ```json
211 | {
212 |   "mcpServers": {
213 |     "n8n-railway": {
214 |       "command": "cmd",
215 |       "args": [
216 |         "/C",
217 |         "\"C:\\Program Files\\nodejs\\npx\" -y mcp-remote https://your-app-name.up.railway.app/mcp --header \"Authorization: Bearer YOUR_SECURE_TOKEN_HERE\""
218 |       ]
219 |     }
220 |   }
221 | }
222 | ```
223 | 
224 | To find your exact npx path, open Command Prompt and run: `where npx`
225 | 
226 | ### Railway-Specific Issues
227 | 
228 | **Build failures:**
229 | - Railway uses AMD64 architecture - the template is configured for this
230 | - Check build logs in Railway dashboard for specific errors
231 | 
232 | **Environment variable issues:**
233 | - Variables are case-sensitive
234 | - Don't include quotes in the Railway dashboard (only in JSON config)
235 | - Railway automatically restarts when you change variables
236 | 
237 | **Domain not working:**
238 | - It may take 1-2 minutes for the domain to become active
239 | - Check the "Deployments" tab to ensure the latest deployment succeeded
240 | 
241 | ## 📊 Monitoring & Logs
242 | 
243 | ### View Logs
244 | 
245 | 1. Go to Railway dashboard
246 | 2. Click on your n8n-mcp service
247 | 3. Click on **"Logs"** tab
248 | 4. You'll see real-time logs including:
249 |    - Server startup messages
250 |    - Authentication attempts
251 |    - API requests (without sensitive data)
252 |    - Any errors or warnings
253 | 
254 | ### Monitor Usage
255 | 
256 | Railway provides metrics for:
257 | - **Memory usage** (typically ~100-200MB)
258 | - **CPU usage** (minimal when idle)
259 | - **Network traffic**
260 | - **Response times**
261 | 
262 | ## 💰 Pricing & Limits
263 | 
264 | ### Railway Free Tier
265 | - **$5 free credit** monthly
266 | - **500 hours** of runtime
267 | - **Sufficient for personal use** of n8n-MCP
268 | 
269 | ### Estimated Costs
270 | - **n8n-MCP typically uses**: ~0.1 GB RAM
271 | - **Monthly cost**: ~$2-3 for 24/7 operation
272 | - **Well within free tier** for most users
273 | 
274 | ## 🔄 Updates & Maintenance
275 | 
276 | ### Manual Updates
277 | 
278 | Since the Railway template uses a specific Docker image tag, updates are manual:
279 | 
280 | 1. **Check for updates** on [GitHub](https://github.com/czlonkowski/n8n-mcp)
281 | 2. **Update image tag** in Railway:
282 |    - Go to Settings → Deploy → Docker Image
283 |    - Change tag from current to new version
284 |    - Click "Redeploy"
285 | 
286 | ### Automatic Updates (Not Recommended)
287 | 
288 | You could use the `latest` tag, but this may cause unexpected breaking changes.
289 | 
290 | ## 🔒 Security Features (v2.16.3+)
291 | 
292 | Railway deployments include enhanced security features:
293 | 
294 | ### Rate Limiting
295 | - **Automatic brute force protection** - 20 attempts per 15 minutes per IP
296 | - **Configurable limits** via `AUTH_RATE_LIMIT_WINDOW` and `AUTH_RATE_LIMIT_MAX`
297 | - **Standard rate limit headers** for client awareness
298 | 
299 | ### SSRF Protection
300 | - **Default strict mode** blocks localhost, private IPs, and cloud metadata
301 | - **Cloud metadata always blocked** (169.254.169.254, metadata.google.internal, etc.)
302 | - **Use `moderate` mode only if** connecting to local n8n instance
303 | 
304 | **Security Configuration:**
305 | ```bash
306 | # In Railway Variables tab:
307 | WEBHOOK_SECURITY_MODE=strict          # Production (recommended)
308 | # or
309 | WEBHOOK_SECURITY_MODE=moderate        # If using local n8n with port forwarding
310 | 
311 | # Rate limiting (defaults are good for most use cases)
312 | AUTH_RATE_LIMIT_WINDOW=900000         # 15 minutes
313 | AUTH_RATE_LIMIT_MAX=20                # 20 attempts per IP
314 | ```
315 | 
316 | ## 📝 Best Practices
317 | 
318 | 1. **Always change the default AUTH_TOKEN immediately**
319 | 2. **Use strong, unique tokens** (32+ characters)
320 | 3. **Monitor logs** for unauthorized access attempts
321 | 4. **Keep credentials secure** - never commit them to git
322 | 5. **Use environment variables** for all sensitive data
323 | 6. **Regular updates** - check for new versions monthly
324 | 
325 | ## 🆘 Getting Help
326 | 
327 | - **Railway Documentation**: [docs.railway.app](https://docs.railway.app)
328 | - **n8n-MCP Issues**: [GitHub Issues](https://github.com/czlonkowski/n8n-mcp/issues)
329 | - **Railway Community**: [Discord](https://discord.gg/railway)
330 | 
331 | ## 🎉 Success!
332 | 
333 | Once connected, you can use all n8n-MCP features from Claude Desktop:
334 | - Search and explore 500+ n8n nodes
335 | - Get node configurations and examples
336 | - Validate workflows before deployment
337 | - Manage n8n workflows (if API configured)
338 | 
339 | The cloud deployment means you can access your n8n knowledge base from any computer with Claude Desktop installed!
```
Page 15/59FirstPrevNextLast