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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/tests/unit/services/property-filter.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { PropertyFilter } from '@/services/property-filter';
import type { SimplifiedProperty, FilteredProperties } from '@/services/property-filter';

// Mock the database
vi.mock('better-sqlite3');

describe('PropertyFilter', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('deduplicateProperties', () => {
    it('should remove duplicate properties with same name and conditions', () => {
      const properties = [
        { name: 'url', type: 'string', displayOptions: { show: { method: ['GET'] } } },
        { name: 'url', type: 'string', displayOptions: { show: { method: ['GET'] } } }, // Duplicate
        { name: 'url', type: 'string', displayOptions: { show: { method: ['POST'] } } }, // Different condition
      ];

      const result = PropertyFilter.deduplicateProperties(properties);

      expect(result).toHaveLength(2);
      expect(result[0].name).toBe('url');
      expect(result[1].name).toBe('url');
      expect(result[0].displayOptions).not.toEqual(result[1].displayOptions);
    });

    it('should handle properties without displayOptions', () => {
      const properties = [
        { name: 'timeout', type: 'number' },
        { name: 'timeout', type: 'number' }, // Duplicate
        { name: 'retries', type: 'number' },
      ];

      const result = PropertyFilter.deduplicateProperties(properties);

      expect(result).toHaveLength(2);
      expect(result.map(p => p.name)).toEqual(['timeout', 'retries']);
    });
  });

  describe('getEssentials', () => {
    it('should return configured essentials for HTTP Request node', () => {
      const properties = [
        { name: 'url', type: 'string', required: true },
        { name: 'method', type: 'options', options: ['GET', 'POST'] },
        { name: 'authentication', type: 'options' },
        { name: 'sendBody', type: 'boolean' },
        { name: 'contentType', type: 'options' },
        { name: 'sendHeaders', type: 'boolean' },
        { name: 'someRareOption', type: 'string' },
      ];

      const result = PropertyFilter.getEssentials(properties, 'nodes-base.httpRequest');

      expect(result.required).toHaveLength(1);
      expect(result.required[0].name).toBe('url');
      expect(result.required[0].required).toBe(true);
      
      expect(result.common).toHaveLength(5);
      expect(result.common.map(p => p.name)).toEqual([
        'method',
        'authentication',
        'sendBody',
        'contentType',
        'sendHeaders'
      ]);
    });

    it('should handle nested properties in collections', () => {
      const properties = [
        {
          name: 'assignments',
          type: 'collection',
          options: [
            { name: 'field', type: 'string' },
            { name: 'value', type: 'string' }
          ]
        }
      ];

      const result = PropertyFilter.getEssentials(properties, 'nodes-base.set');

      expect(result.common.some(p => p.name === 'assignments')).toBe(true);
    });

    it('should infer essentials for unconfigured nodes', () => {
      const properties = [
        { name: 'requiredField', type: 'string', required: true },
        { name: 'simpleField', type: 'string' },
        { name: 'conditionalField', type: 'string', displayOptions: { show: { mode: ['advanced'] } } },
        { name: 'complexField', type: 'collection' },
      ];

      const result = PropertyFilter.getEssentials(properties, 'nodes-base.unknownNode');

      expect(result.required).toHaveLength(1);
      expect(result.required[0].name).toBe('requiredField');
      
      // May include both simpleField and complexField (collection type)
      expect(result.common.length).toBeGreaterThanOrEqual(1);
      expect(result.common.some(p => p.name === 'simpleField')).toBe(true);
    });

    it('should include conditional properties when needed to reach minimum count', () => {
      const properties = [
        { name: 'field1', type: 'string' },
        { name: 'field2', type: 'string', displayOptions: { show: { mode: ['basic'] } } },
        { name: 'field3', type: 'string', displayOptions: { show: { mode: ['advanced'], type: ['custom'] } } },
      ];

      const result = PropertyFilter.getEssentials(properties, 'nodes-base.unknownNode');

      expect(result.common).toHaveLength(2);
      expect(result.common[0].name).toBe('field1');
      expect(result.common[1].name).toBe('field2'); // Single condition included
    });
  });

  describe('property simplification', () => {
    it('should simplify options properly', () => {
      const properties = [
        {
          name: 'method',
          type: 'options',
          displayName: 'HTTP Method',
          options: [
            { name: 'GET', value: 'GET' },
            { name: 'POST', value: 'POST' },
            { name: 'PUT', value: 'PUT' }
          ]
        }
      ];

      const result = PropertyFilter.getEssentials(properties, 'nodes-base.httpRequest');

      const methodProp = result.common.find(p => p.name === 'method');
      expect(methodProp?.options).toHaveLength(3);
      expect(methodProp?.options?.[0]).toEqual({ value: 'GET', label: 'GET' });
    });

    it('should handle string array options', () => {
      const properties = [
        {
          name: 'resource',
          type: 'options',
          options: ['user', 'post', 'comment']
        }
      ];

      const result = PropertyFilter.getEssentials(properties, 'nodes-base.unknownNode');

      const resourceProp = result.common.find(p => p.name === 'resource');
      expect(resourceProp?.options).toEqual([
        { value: 'user', label: 'user' },
        { value: 'post', label: 'post' },
        { value: 'comment', label: 'comment' }
      ]);
    });

    it('should include simple display conditions', () => {
      const properties = [
        {
          name: 'channel',
          type: 'string',
          displayOptions: {
            show: {
              resource: ['message'],
              operation: ['post']
            }
          }
        }
      ];

      const result = PropertyFilter.getEssentials(properties, 'nodes-base.slack');

      const channelProp = result.common.find(p => p.name === 'channel');
      expect(channelProp?.showWhen).toEqual({
        resource: ['message'],
        operation: ['post']
      });
    });

    it('should exclude complex display conditions', () => {
      const properties = [
        {
          name: 'complexField',
          type: 'string',
          displayOptions: {
            show: {
              mode: ['advanced'],
              type: ['custom'],
              enabled: [true],
              resource: ['special']
            }
          }
        }
      ];

      const result = PropertyFilter.getEssentials(properties, 'nodes-base.unknownNode');

      const complexProp = result.common.find(p => p.name === 'complexField');
      expect(complexProp?.showWhen).toBeUndefined();
    });

    it('should generate usage hints for common property types', () => {
      const properties = [
        { name: 'url', type: 'string' },
        { name: 'endpoint', type: 'string' },
        { name: 'authentication', type: 'options' },
        { name: 'jsonData', type: 'json' },
        { name: 'jsCode', type: 'code' },
        { name: 'enableFeature', type: 'boolean', displayOptions: { show: { mode: ['advanced'] } } }
      ];

      const result = PropertyFilter.getEssentials(properties, 'nodes-base.unknownNode');

      const urlProp = result.common.find(p => p.name === 'url');
      expect(urlProp?.usageHint).toBe('Enter the full URL including https://');

      const authProp = result.common.find(p => p.name === 'authentication');
      expect(authProp?.usageHint).toBe('Select authentication method or credentials');

      const jsonProp = result.common.find(p => p.name === 'jsonData');
      expect(jsonProp?.usageHint).toBe('Enter valid JSON data');
    });

    it('should extract descriptions from various fields', () => {
      const properties = [
        { name: 'field1', description: 'Primary description' },
        { name: 'field2', hint: 'Hint description' },
        { name: 'field3', placeholder: 'Placeholder description' },
        { name: 'field4', displayName: 'Display Name Only' },
        { name: 'url' } // Should generate description
      ];

      const result = PropertyFilter.getEssentials(properties, 'nodes-base.unknownNode');

      expect(result.common[0].description).toBe('Primary description');
      expect(result.common[1].description).toBe('Hint description');
      expect(result.common[2].description).toBe('Placeholder description');
      expect(result.common[3].description).toBe('Display Name Only');
      expect(result.common[4].description).toBe('The URL to make the request to');
    });
  });

  describe('searchProperties', () => {
    const testProperties = [
      { 
        name: 'url', 
        displayName: 'URL', 
        type: 'string',
        description: 'The endpoint URL for the request' 
      },
      { 
        name: 'urlParams', 
        displayName: 'URL Parameters', 
        type: 'collection' 
      },
      { 
        name: 'authentication', 
        displayName: 'Authentication', 
        type: 'options',
        description: 'Select the authentication method' 
      },
      {
        name: 'headers',
        type: 'collection',
        options: [
          { name: 'Authorization', type: 'string' },
          { name: 'Content-Type', type: 'string' }
        ]
      }
    ];

    it('should find exact name matches with highest score', () => {
      const results = PropertyFilter.searchProperties(testProperties, 'url');

      expect(results).toHaveLength(2);
      expect(results[0].name).toBe('url'); // Exact match
      expect(results[1].name).toBe('urlParams'); // Prefix match
    });

    it('should find properties by partial name match', () => {
      const results = PropertyFilter.searchProperties(testProperties, 'auth');

      // May match both 'authentication' and 'Authorization' in headers
      expect(results.length).toBeGreaterThanOrEqual(1);
      expect(results.some(r => r.name === 'authentication')).toBe(true);
    });

    it('should find properties by description match', () => {
      const results = PropertyFilter.searchProperties(testProperties, 'endpoint');

      expect(results).toHaveLength(1);
      expect(results[0].name).toBe('url');
    });

    it('should search nested properties in collections', () => {
      const results = PropertyFilter.searchProperties(testProperties, 'authorization');

      expect(results).toHaveLength(1);
      expect(results[0].name).toBe('Authorization');
      expect((results[0] as any).path).toBe('headers.Authorization');
    });

    it('should limit results to maxResults', () => {
      const manyProperties = Array.from({ length: 30 }, (_, i) => ({
        name: `authField${i}`,
        type: 'string'
      }));

      const results = PropertyFilter.searchProperties(manyProperties, 'auth', 5);

      expect(results).toHaveLength(5);
    });

    it('should handle empty query gracefully', () => {
      const results = PropertyFilter.searchProperties(testProperties, '');

      expect(results).toHaveLength(0);
    });

    it('should search in fixedCollection properties', () => {
      const properties = [
        {
          name: 'options',
          type: 'fixedCollection',
          options: [
            {
              name: 'advanced',
              values: [
                { name: 'timeout', type: 'number' },
                { name: 'retries', type: 'number' }
              ]
            }
          ]
        }
      ];

      const results = PropertyFilter.searchProperties(properties, 'timeout');

      expect(results).toHaveLength(1);
      expect(results[0].name).toBe('timeout');
      expect((results[0] as any).path).toBe('options.advanced.timeout');
    });
  });

  describe('edge cases', () => {
    it('should handle empty properties array', () => {
      const result = PropertyFilter.getEssentials([], 'nodes-base.httpRequest');

      expect(result.required).toHaveLength(0);
      expect(result.common).toHaveLength(0);
    });

    it('should handle properties with missing fields gracefully', () => {
      const properties = [
        { name: 'field1' }, // No type
        { type: 'string' }, // No name
        { name: 'field2', type: 'string' } // Valid
      ];

      const result = PropertyFilter.getEssentials(properties, 'nodes-base.unknownNode');

      expect(result.common.length).toBeGreaterThan(0);
      expect(result.common.every(p => p.name && p.type)).toBe(true);
    });

    it('should handle circular references in nested properties', () => {
      const circularProp: any = {
        name: 'circular',
        type: 'collection',
        options: []
      };
      circularProp.options.push(circularProp); // Create circular reference

      const properties = [circularProp, { name: 'normal', type: 'string' }];

      // Should not throw or hang
      expect(() => {
        PropertyFilter.getEssentials(properties, 'nodes-base.unknownNode');
      }).not.toThrow();
    });

    it('should preserve default values for simple types', () => {
      const properties = [
        { name: 'method', type: 'options', default: 'GET' },
        { name: 'timeout', type: 'number', default: 30000 },
        { name: 'enabled', type: 'boolean', default: true },
        { name: 'complex', type: 'collection', default: { key: 'value' } } // Should not include
      ];

      const result = PropertyFilter.getEssentials(properties, 'nodes-base.unknownNode');

      const method = result.common.find(p => p.name === 'method');
      expect(method?.default).toBe('GET');

      const timeout = result.common.find(p => p.name === 'timeout');
      expect(timeout?.default).toBe(30000);

      const enabled = result.common.find(p => p.name === 'enabled');
      expect(enabled?.default).toBe(true);

      const complex = result.common.find(p => p.name === 'complex');
      expect(complex?.default).toBeUndefined();
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/unit/utils/cache-utils.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Unit tests for cache utilities
 */

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
  createCacheKey,
  getCacheConfig,
  createInstanceCache,
  CacheMutex,
  calculateBackoffDelay,
  withRetry,
  getCacheStatistics,
  cacheMetrics,
  DEFAULT_RETRY_CONFIG
} from '../../../src/utils/cache-utils';

describe('cache-utils', () => {
  beforeEach(() => {
    // Reset environment variables
    delete process.env.INSTANCE_CACHE_MAX;
    delete process.env.INSTANCE_CACHE_TTL_MINUTES;
    // Reset cache metrics
    cacheMetrics.reset();
  });

  describe('createCacheKey', () => {
    it('should create consistent SHA-256 hash for same input', () => {
      const input = 'https://api.n8n.cloud:valid-key:instance1';
      const hash1 = createCacheKey(input);
      const hash2 = createCacheKey(input);

      expect(hash1).toBe(hash2);
      expect(hash1).toHaveLength(64); // SHA-256 produces 64 hex chars
      expect(hash1).toMatch(/^[a-f0-9]+$/); // Only hex characters
    });

    it('should produce different hashes for different inputs', () => {
      const hash1 = createCacheKey('input1');
      const hash2 = createCacheKey('input2');

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

    it('should use memoization for repeated inputs', () => {
      const input = 'memoized-input';

      // First call creates hash
      const hash1 = createCacheKey(input);

      // Second call should return memoized result
      const hash2 = createCacheKey(input);

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

    it('should limit memoization cache size', () => {
      // Create more than MAX_MEMO_SIZE (1000) unique hashes
      const hashes = new Set<string>();
      for (let i = 0; i < 1100; i++) {
        const hash = createCacheKey(`input-${i}`);
        hashes.add(hash);
      }

      // All hashes should be unique
      expect(hashes.size).toBe(1100);

      // Early entries should have been evicted from memo cache
      // but should still produce consistent results
      const earlyHash = createCacheKey('input-0');
      expect(earlyHash).toBe(hashes.values().next().value);
    });
  });

  describe('getCacheConfig', () => {
    it('should return default configuration when no env vars set', () => {
      const config = getCacheConfig();

      expect(config.max).toBe(100);
      expect(config.ttlMinutes).toBe(30);
    });

    it('should use environment variables when set', () => {
      process.env.INSTANCE_CACHE_MAX = '500';
      process.env.INSTANCE_CACHE_TTL_MINUTES = '60';

      const config = getCacheConfig();

      expect(config.max).toBe(500);
      expect(config.ttlMinutes).toBe(60);
    });

    it('should enforce minimum bounds', () => {
      process.env.INSTANCE_CACHE_MAX = '0';
      process.env.INSTANCE_CACHE_TTL_MINUTES = '0';

      const config = getCacheConfig();

      expect(config.max).toBe(1); // Min is 1
      expect(config.ttlMinutes).toBe(1); // Min is 1
    });

    it('should enforce maximum bounds', () => {
      process.env.INSTANCE_CACHE_MAX = '20000';
      process.env.INSTANCE_CACHE_TTL_MINUTES = '2000';

      const config = getCacheConfig();

      expect(config.max).toBe(10000); // Max is 10000
      expect(config.ttlMinutes).toBe(1440); // Max is 1440 (24 hours)
    });

    it('should handle invalid values gracefully', () => {
      process.env.INSTANCE_CACHE_MAX = 'invalid';
      process.env.INSTANCE_CACHE_TTL_MINUTES = 'not-a-number';

      const config = getCacheConfig();

      expect(config.max).toBe(100); // Falls back to default
      expect(config.ttlMinutes).toBe(30); // Falls back to default
    });
  });

  describe('createInstanceCache', () => {
    it('should create LRU cache with correct configuration', () => {
      process.env.INSTANCE_CACHE_MAX = '50';
      process.env.INSTANCE_CACHE_TTL_MINUTES = '15';

      const cache = createInstanceCache<{ data: string }>();

      // Add items to cache
      cache.set('key1', { data: 'value1' });
      cache.set('key2', { data: 'value2' });

      expect(cache.get('key1')).toEqual({ data: 'value1' });
      expect(cache.get('key2')).toEqual({ data: 'value2' });
      expect(cache.size).toBe(2);
    });

    it('should call dispose callback on eviction', () => {
      const disposeFn = vi.fn();
      const cache = createInstanceCache<{ data: string }>(disposeFn);

      // Set max to 2 for testing
      process.env.INSTANCE_CACHE_MAX = '2';
      const smallCache = createInstanceCache<{ data: string }>(disposeFn);

      smallCache.set('key1', { data: 'value1' });
      smallCache.set('key2', { data: 'value2' });
      smallCache.set('key3', { data: 'value3' }); // Should evict key1

      expect(disposeFn).toHaveBeenCalledWith({ data: 'value1' }, 'key1');
    });

    it('should update age on get', () => {
      const cache = createInstanceCache<{ data: string }>();

      cache.set('key1', { data: 'value1' });

      // Access should update age
      const value = cache.get('key1');
      expect(value).toEqual({ data: 'value1' });

      // Item should still be in cache
      expect(cache.has('key1')).toBe(true);
    });
  });

  describe('CacheMutex', () => {
    it('should prevent concurrent access to same key', async () => {
      const mutex = new CacheMutex();
      const key = 'test-key';
      const results: number[] = [];

      // First operation acquires lock
      const release1 = await mutex.acquire(key);

      // Second operation should wait
      const promise2 = mutex.acquire(key).then(release => {
        results.push(2);
        release();
      });

      // First operation completes
      results.push(1);
      release1();

      // Wait for second operation
      await promise2;

      expect(results).toEqual([1, 2]); // Operations executed in order
    });

    it('should allow concurrent access to different keys', async () => {
      const mutex = new CacheMutex();
      const results: string[] = [];

      const [release1, release2] = await Promise.all([
        mutex.acquire('key1'),
        mutex.acquire('key2')
      ]);

      results.push('both-acquired');
      release1();
      release2();

      expect(results).toEqual(['both-acquired']);
    });

    it('should check if key is locked', async () => {
      const mutex = new CacheMutex();
      const key = 'test-key';

      expect(mutex.isLocked(key)).toBe(false);

      const release = await mutex.acquire(key);
      expect(mutex.isLocked(key)).toBe(true);

      release();
      expect(mutex.isLocked(key)).toBe(false);
    });

    it('should clear all locks', async () => {
      const mutex = new CacheMutex();

      const release1 = await mutex.acquire('key1');
      const release2 = await mutex.acquire('key2');

      expect(mutex.isLocked('key1')).toBe(true);
      expect(mutex.isLocked('key2')).toBe(true);

      mutex.clearAll();

      expect(mutex.isLocked('key1')).toBe(false);
      expect(mutex.isLocked('key2')).toBe(false);

      // Should not throw when calling release after clear
      release1();
      release2();
    });

    it('should handle timeout for stuck locks', async () => {
      const mutex = new CacheMutex();
      const key = 'stuck-key';

      // Acquire lock but don't release
      await mutex.acquire(key);

      // Wait for timeout (mock the timeout)
      vi.useFakeTimers();

      // Try to acquire same lock
      const acquirePromise = mutex.acquire(key);

      // Fast-forward past timeout
      vi.advanceTimersByTime(6000); // Timeout is 5 seconds

      // Should be able to acquire after timeout
      const release = await acquirePromise;
      release();

      vi.useRealTimers();
    });
  });

  describe('calculateBackoffDelay', () => {
    it('should calculate exponential backoff correctly', () => {
      const config = { ...DEFAULT_RETRY_CONFIG, jitterFactor: 0 }; // No jitter for predictable tests

      expect(calculateBackoffDelay(0, config)).toBe(1000); // 1 * 1000
      expect(calculateBackoffDelay(1, config)).toBe(2000); // 2 * 1000
      expect(calculateBackoffDelay(2, config)).toBe(4000); // 4 * 1000
      expect(calculateBackoffDelay(3, config)).toBe(8000); // 8 * 1000
    });

    it('should respect max delay', () => {
      const config = {
        ...DEFAULT_RETRY_CONFIG,
        maxDelayMs: 5000,
        jitterFactor: 0
      };

      expect(calculateBackoffDelay(10, config)).toBe(5000); // Capped at max
    });

    it('should add jitter', () => {
      const config = {
        ...DEFAULT_RETRY_CONFIG,
        baseDelayMs: 1000,
        jitterFactor: 0.5
      };

      const delay = calculateBackoffDelay(0, config);

      // With 50% jitter, delay should be between 1000 and 1500
      expect(delay).toBeGreaterThanOrEqual(1000);
      expect(delay).toBeLessThanOrEqual(1500);
    });
  });

  describe('withRetry', () => {
    it('should succeed on first attempt', async () => {
      const fn = vi.fn().mockResolvedValue('success');

      const result = await withRetry(fn);

      expect(result).toBe('success');
      expect(fn).toHaveBeenCalledTimes(1);
    });

    it('should retry on failure and eventually succeed', async () => {
      // Create retryable errors (503 Service Unavailable)
      const retryableError1 = new Error('Service temporarily unavailable');
      (retryableError1 as any).response = { status: 503 };

      const retryableError2 = new Error('Another temporary failure');
      (retryableError2 as any).response = { status: 503 };

      const fn = vi.fn()
        .mockRejectedValueOnce(retryableError1)
        .mockRejectedValueOnce(retryableError2)
        .mockResolvedValue('success');

      const result = await withRetry(fn, {
        maxAttempts: 3,
        baseDelayMs: 10,
        maxDelayMs: 100,
        jitterFactor: 0
      });

      expect(result).toBe('success');
      expect(fn).toHaveBeenCalledTimes(3);
    });

    it('should throw after max attempts', async () => {
      // Create retryable error (503 Service Unavailable)
      const retryableError = new Error('Persistent failure');
      (retryableError as any).response = { status: 503 };

      const fn = vi.fn().mockRejectedValue(retryableError);

      await expect(withRetry(fn, {
        maxAttempts: 3,
        baseDelayMs: 10,
        maxDelayMs: 100,
        jitterFactor: 0
      })).rejects.toThrow('Persistent failure');

      expect(fn).toHaveBeenCalledTimes(3);
    });

    it('should not retry non-retryable errors', async () => {
      const error = new Error('Not retryable');
      (error as any).response = { status: 400 }; // Client error

      const fn = vi.fn().mockRejectedValue(error);

      await expect(withRetry(fn)).rejects.toThrow('Not retryable');
      expect(fn).toHaveBeenCalledTimes(1); // No retry
    });

    it('should retry network errors', async () => {
      const networkError = new Error('Network error');
      (networkError as any).code = 'ECONNREFUSED';

      const fn = vi.fn()
        .mockRejectedValueOnce(networkError)
        .mockResolvedValue('success');

      const result = await withRetry(fn, {
        maxAttempts: 2,
        baseDelayMs: 10,
        maxDelayMs: 100,
        jitterFactor: 0
      });

      expect(result).toBe('success');
      expect(fn).toHaveBeenCalledTimes(2);
    });

    it('should retry 429 Too Many Requests', async () => {
      const error = new Error('Rate limited');
      (error as any).response = { status: 429 };

      const fn = vi.fn()
        .mockRejectedValueOnce(error)
        .mockResolvedValue('success');

      const result = await withRetry(fn, {
        maxAttempts: 2,
        baseDelayMs: 10,
        maxDelayMs: 100,
        jitterFactor: 0
      });

      expect(result).toBe('success');
      expect(fn).toHaveBeenCalledTimes(2);
    });
  });

  describe('cacheMetrics', () => {
    it('should track cache operations', () => {
      cacheMetrics.recordHit();
      cacheMetrics.recordHit();
      cacheMetrics.recordMiss();
      cacheMetrics.recordSet();
      cacheMetrics.recordDelete();
      cacheMetrics.recordEviction();

      const metrics = cacheMetrics.getMetrics();

      expect(metrics.hits).toBe(2);
      expect(metrics.misses).toBe(1);
      expect(metrics.sets).toBe(1);
      expect(metrics.deletes).toBe(1);
      expect(metrics.evictions).toBe(1);
      expect(metrics.avgHitRate).toBeCloseTo(0.667, 2); // 2/3
    });

    it('should update cache size', () => {
      cacheMetrics.updateSize(50, 100);

      const metrics = cacheMetrics.getMetrics();

      expect(metrics.size).toBe(50);
      expect(metrics.maxSize).toBe(100);
    });

    it('should reset metrics', () => {
      cacheMetrics.recordHit();
      cacheMetrics.recordMiss();
      cacheMetrics.reset();

      const metrics = cacheMetrics.getMetrics();

      expect(metrics.hits).toBe(0);
      expect(metrics.misses).toBe(0);
      expect(metrics.avgHitRate).toBe(0);
    });

    it('should format metrics for logging', () => {
      cacheMetrics.recordHit();
      cacheMetrics.recordHit();
      cacheMetrics.recordMiss();
      cacheMetrics.updateSize(25, 100);
      cacheMetrics.recordEviction();

      const formatted = cacheMetrics.getFormattedMetrics();

      expect(formatted).toContain('Hits=2');
      expect(formatted).toContain('Misses=1');
      expect(formatted).toContain('HitRate=66.67%');
      expect(formatted).toContain('Size=25/100');
      expect(formatted).toContain('Evictions=1');
    });
  });

  describe('getCacheStatistics', () => {
    it('should return formatted statistics', () => {
      cacheMetrics.recordHit();
      cacheMetrics.recordHit();
      cacheMetrics.recordMiss();
      cacheMetrics.updateSize(30, 100);

      const stats = getCacheStatistics();

      expect(stats).toContain('Cache Statistics:');
      expect(stats).toContain('Total Operations: 3');
      expect(stats).toContain('Hit Rate: 66.67%');
      expect(stats).toContain('Current Size: 30/100');
    });

    it('should calculate runtime', () => {
      const stats = getCacheStatistics();

      expect(stats).toContain('Runtime:');
      expect(stats).toMatch(/Runtime: \d+ minutes/);
    });
  });
});
```

--------------------------------------------------------------------------------
/scripts/test-n8n-integration.sh:
--------------------------------------------------------------------------------

```bash
#!/bin/bash

# Script to test n8n integration with n8n-mcp server
set -e

# Check for command line arguments
if [ "$1" == "--clear-api-key" ] || [ "$1" == "-c" ]; then
    echo "🗑️  Clearing saved n8n API key..."
    rm -f "$HOME/.n8n-mcp-test/.n8n-api-key"
    echo "✅ API key cleared. You'll be prompted for a new key on next run."
    exit 0
fi

if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then
    echo "Usage: $0 [options]"
    echo ""
    echo "Options:"
    echo "  -h, --help           Show this help message"
    echo "  -c, --clear-api-key  Clear the saved n8n API key"
    echo ""
    echo "The script will save your n8n API key on first use and reuse it on"
    echo "subsequent runs. You can override the saved key at runtime or clear"
    echo "it with the --clear-api-key option."
    exit 0
fi

echo "🚀 Starting n8n integration test environment..."

# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# Configuration
N8N_PORT=5678
MCP_PORT=3001
AUTH_TOKEN="test-token-for-n8n-testing-minimum-32-chars"

# n8n data directory for persistence
N8N_DATA_DIR="$HOME/.n8n-mcp-test"
# API key storage file
API_KEY_FILE="$N8N_DATA_DIR/.n8n-api-key"

# Function to detect OS
detect_os() {
    if [[ "$OSTYPE" == "linux-gnu"* ]]; then
        if [ -f /etc/os-release ]; then
            . /etc/os-release
            echo "$ID"
        else
            echo "linux"
        fi
    elif [[ "$OSTYPE" == "darwin"* ]]; then
        echo "macos"
    elif [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]]; then
        echo "windows"
    else
        echo "unknown"
    fi
}

# Function to check if Docker is installed
check_docker() {
    if command -v docker &> /dev/null; then
        echo -e "${GREEN}✅ Docker is installed${NC}"
        # Check if Docker daemon is running
        if ! docker info &> /dev/null; then
            echo -e "${YELLOW}⚠️  Docker is installed but not running${NC}"
            echo -e "${YELLOW}Please start Docker and run this script again${NC}"
            exit 1
        fi
        return 0
    else
        return 1
    fi
}

# Function to install Docker based on OS
install_docker() {
    local os=$(detect_os)
    echo -e "${YELLOW}📦 Docker is not installed. Attempting to install...${NC}"
    
    case $os in
        "ubuntu"|"debian")
            echo -e "${BLUE}Installing Docker on Ubuntu/Debian...${NC}"
            echo "This requires sudo privileges."
            sudo apt-get update
            sudo apt-get install -y ca-certificates curl gnupg
            sudo install -m 0755 -d /etc/apt/keyrings
            curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
            sudo chmod a+r /etc/apt/keyrings/docker.gpg
            echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
            sudo apt-get update
            sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
            sudo usermod -aG docker $USER
            echo -e "${GREEN}✅ Docker installed successfully${NC}"
            echo -e "${YELLOW}⚠️  Please log out and back in for group changes to take effect${NC}"
            ;;
        "fedora"|"rhel"|"centos")
            echo -e "${BLUE}Installing Docker on Fedora/RHEL/CentOS...${NC}"
            echo "This requires sudo privileges."
            sudo dnf -y install dnf-plugins-core
            sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
            sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
            sudo systemctl start docker
            sudo systemctl enable docker
            sudo usermod -aG docker $USER
            echo -e "${GREEN}✅ Docker installed successfully${NC}"
            echo -e "${YELLOW}⚠️  Please log out and back in for group changes to take effect${NC}"
            ;;
        "macos")
            echo -e "${BLUE}Installing Docker on macOS...${NC}"
            if command -v brew &> /dev/null; then
                echo "Installing Docker Desktop via Homebrew..."
                brew install --cask docker
                echo -e "${GREEN}✅ Docker Desktop installed${NC}"
                echo -e "${YELLOW}⚠️  Please start Docker Desktop from Applications${NC}"
            else
                echo -e "${RED}❌ Homebrew not found${NC}"
                echo "Please install Docker Desktop manually from:"
                echo "https://www.docker.com/products/docker-desktop/"
            fi
            ;;
        "windows")
            echo -e "${RED}❌ Windows detected${NC}"
            echo "Please install Docker Desktop manually from:"
            echo "https://www.docker.com/products/docker-desktop/"
            ;;
        *)
            echo -e "${RED}❌ Unknown operating system: $os${NC}"
            echo "Please install Docker manually from https://docs.docker.com/get-docker/"
            ;;
    esac
    
    # If we installed Docker on Linux, we need to restart for group changes
    if [[ "$os" == "ubuntu" ]] || [[ "$os" == "debian" ]] || [[ "$os" == "fedora" ]] || [[ "$os" == "rhel" ]] || [[ "$os" == "centos" ]]; then
        echo -e "${YELLOW}Please run 'newgrp docker' or log out and back in, then run this script again${NC}"
        exit 0
    fi
    
    exit 1
}

# Check for Docker
if ! check_docker; then
    install_docker
fi

# Check for jq (optional but recommended)
if ! command -v jq &> /dev/null; then
    echo -e "${YELLOW}⚠️  jq is not installed (optional)${NC}"
    echo -e "${YELLOW}   Install it for pretty JSON output in tests${NC}"
fi

# Function to cleanup on exit
cleanup() {
    echo -e "\n${YELLOW}🧹 Cleaning up...${NC}"
    
    # Stop n8n container
    if docker ps -q -f name=n8n-test > /dev/null 2>&1; then
        echo "Stopping n8n container..."
        docker stop n8n-test >/dev/null 2>&1 || true
        docker rm n8n-test >/dev/null 2>&1 || true
    fi
    
    # Kill MCP server if running
    if [ -n "$MCP_PID" ] && kill -0 $MCP_PID 2>/dev/null; then
        echo "Stopping MCP server..."
        kill $MCP_PID 2>/dev/null || true
    fi
    
    echo -e "${GREEN}✅ Cleanup complete${NC}"
}

# Set trap to cleanup on exit
trap cleanup EXIT INT TERM

# Check if we're in the right directory
if [ ! -f "package.json" ] || [ ! -d "dist" ]; then
    echo -e "${RED}❌ Error: Must run from n8n-mcp directory${NC}"
    echo "Please cd to /Users/romualdczlonkowski/Pliki/n8n-mcp/n8n-mcp"
    exit 1
fi

# Always build the project to ensure latest changes
echo -e "${YELLOW}📦 Building project...${NC}"
npm run build

# Create n8n data directory if it doesn't exist
if [ ! -d "$N8N_DATA_DIR" ]; then
    echo -e "${YELLOW}📁 Creating n8n data directory: $N8N_DATA_DIR${NC}"
    mkdir -p "$N8N_DATA_DIR"
fi

# Start n8n in Docker with persistent volume
echo -e "\n${GREEN}🐳 Starting n8n container with persistent data...${NC}"
docker run -d \
  --name n8n-test \
  -p ${N8N_PORT}:5678 \
  -v "${N8N_DATA_DIR}:/home/node/.n8n" \
  -e N8N_BASIC_AUTH_ACTIVE=false \
  -e N8N_HOST=localhost \
  -e N8N_PORT=5678 \
  -e N8N_PROTOCOL=http \
  -e NODE_ENV=development \
  -e N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true \
  n8nio/n8n:latest

# Wait for n8n to be ready
echo -e "${YELLOW}⏳ Waiting for n8n to start...${NC}"
for i in {1..30}; do
    if curl -s http://localhost:${N8N_PORT}/ >/dev/null 2>&1; then
        echo -e "${GREEN}✅ n8n is ready!${NC}"
        break
    fi
    if [ $i -eq 30 ]; then
        echo -e "${RED}❌ n8n failed to start${NC}"
        exit 1
    fi
    sleep 1
done

# Check for saved API key
if [ -f "$API_KEY_FILE" ]; then
    # Read saved API key
    N8N_API_KEY=$(cat "$API_KEY_FILE" 2>/dev/null || echo "")
    
    if [ -n "$N8N_API_KEY" ]; then
        echo -e "\n${GREEN}✅ Using saved n8n API key${NC}"
        echo -e "${YELLOW}   To use a different key, delete: ${API_KEY_FILE}${NC}"
        
        # Give user a chance to override
        echo -e "\n${YELLOW}Press Enter to continue with saved key, or paste a new API key:${NC}"
        read -r NEW_API_KEY
        
        if [ -n "$NEW_API_KEY" ]; then
            N8N_API_KEY="$NEW_API_KEY"
            # Save the new key
            echo "$N8N_API_KEY" > "$API_KEY_FILE"
            chmod 600 "$API_KEY_FILE"
            echo -e "${GREEN}✅ New API key saved${NC}"
        fi
    else
        # File exists but is empty, remove it
        rm -f "$API_KEY_FILE"
    fi
fi

# If no saved key, prompt for one
if [ -z "$N8N_API_KEY" ]; then
    # Guide user to get API key
    echo -e "\n${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
    echo -e "${YELLOW}🔑 n8n API Key Setup${NC}"
    echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
    echo -e "\nTo enable n8n management tools, you need to create an API key:"
    echo -e "\n${GREEN}Steps:${NC}"
    echo -e "  1. Open n8n in your browser: ${BLUE}http://localhost:${N8N_PORT}${NC}"
    echo -e "  2. Click on your user menu (top right)"
    echo -e "  3. Go to 'Settings'"
    echo -e "  4. Navigate to 'API'"
    echo -e "  5. Click 'Create API Key'"
    echo -e "  6. Give it a name (e.g., 'n8n-mcp')"
    echo -e "  7. Copy the generated API key"
    echo -e "\n${YELLOW}Note: If this is your first time, you'll need to create an account first.${NC}"
    echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
    
    # Wait for API key input
    echo -e "\n${YELLOW}Please paste your n8n API key here (or press Enter to skip):${NC}"
    read -r N8N_API_KEY
    
    # Save the API key if provided
    if [ -n "$N8N_API_KEY" ]; then
        echo "$N8N_API_KEY" > "$API_KEY_FILE"
        chmod 600 "$API_KEY_FILE"
        echo -e "${GREEN}✅ API key saved for future use${NC}"
    fi
fi

# Check if API key was provided
if [ -z "$N8N_API_KEY" ]; then
    echo -e "${YELLOW}⚠️  No API key provided. n8n management tools will not be available.${NC}"
    echo -e "${YELLOW}   You can still use documentation and search tools.${NC}"
    N8N_API_KEY=""
    N8N_API_URL=""
else
    echo -e "${GREEN}✅ API key received${NC}"
    # Set the API URL for localhost access (MCP server runs on host, not in Docker)
    N8N_API_URL="http://localhost:${N8N_PORT}/api/v1"
fi

# Start MCP server
echo -e "\n${GREEN}🚀 Starting MCP server in n8n mode...${NC}"
if [ -n "$N8N_API_KEY" ]; then
    echo -e "${YELLOW}   With n8n management tools enabled${NC}"
fi

N8N_MODE=true \
MCP_MODE=http \
AUTH_TOKEN="${AUTH_TOKEN}" \
PORT=${MCP_PORT} \
N8N_API_KEY="${N8N_API_KEY}" \
N8N_API_URL="${N8N_API_URL}" \
node dist/mcp/index.js > /tmp/mcp-server.log 2>&1 &

MCP_PID=$!

# Show log file location
echo -e "${YELLOW}📄 MCP server logs: /tmp/mcp-server.log${NC}"

# Wait for MCP server to be ready
echo -e "${YELLOW}⏳ Waiting for MCP server to start...${NC}"
for i in {1..10}; do
    if curl -s http://localhost:${MCP_PORT}/health >/dev/null 2>&1; then
        echo -e "${GREEN}✅ MCP server is ready!${NC}"
        break
    fi
    if [ $i -eq 10 ]; then
        echo -e "${RED}❌ MCP server failed to start${NC}"
        exit 1
    fi
    sleep 1
done

# Show status and test endpoints
echo -e "\n${GREEN}🎉 Both services are running!${NC}"
echo -e "\n📍 Service URLs:"
echo -e "  • n8n:        http://localhost:${N8N_PORT}"
echo -e "  • MCP server: http://localhost:${MCP_PORT}"
echo -e "\n🔑 Auth token: ${AUTH_TOKEN}"
echo -e "\n💾 n8n data stored in: ${N8N_DATA_DIR}"
echo -e "   (Your workflows, credentials, and settings are preserved between runs)"

# Test MCP protocol endpoint
echo -e "\n${YELLOW}🧪 Testing MCP protocol endpoint...${NC}"
echo "Response from GET /mcp:"
curl -s http://localhost:${MCP_PORT}/mcp | jq '.' || curl -s http://localhost:${MCP_PORT}/mcp

# Test MCP initialization
echo -e "\n${YELLOW}🧪 Testing MCP initialization...${NC}"
echo "Response from POST /mcp (initialize):"
curl -s -X POST http://localhost:${MCP_PORT}/mcp \
  -H "Authorization: Bearer ${AUTH_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{}},"id":1}' \
  | jq '.' || echo "(Install jq for pretty JSON output)"

# Test available tools
echo -e "\n${YELLOW}🧪 Checking available MCP tools...${NC}"
if [ -n "$N8N_API_KEY" ]; then
    echo -e "${GREEN}✅ n8n Management Tools Available:${NC}"
    echo "   • n8n_list_workflows - List all workflows"
    echo "   • n8n_get_workflow - Get workflow details"
    echo "   • n8n_create_workflow - Create new workflows"
    echo "   • n8n_update_workflow - Update existing workflows"
    echo "   • n8n_delete_workflow - Delete workflows"
    echo "   • n8n_trigger_webhook_workflow - Trigger webhook workflows"
    echo "   • n8n_list_executions - List workflow executions"
    echo "   • And more..."
else
    echo -e "${YELLOW}⚠️  n8n Management Tools NOT Available${NC}"
    echo "   To enable, restart with an n8n API key"
fi

echo -e "\n${GREEN}✅ Documentation Tools Always Available:${NC}"
echo "   • list_nodes - List available n8n nodes"
echo "   • search_nodes - Search for specific nodes"
echo "   • get_node_info - Get detailed node information"
echo "   • validate_node_operation - Validate node configurations"
echo "   • And many more..."

echo -e "\n${GREEN}✅ Setup complete!${NC}"
echo -e "\n📝 Next steps:"
echo -e "  1. Open n8n at http://localhost:${N8N_PORT}"
echo -e "  2. Create a workflow with the AI Agent node"
echo -e "  3. Add MCP Client Tool node"
echo -e "  4. Configure it with:"
echo -e "     • Transport: HTTP"
echo -e "     • URL: http://host.docker.internal:${MCP_PORT}/mcp"
echo -e "     • Auth Token: ${BLUE}${AUTH_TOKEN}${NC}"
echo -e "\n${YELLOW}Press Ctrl+C to stop both services${NC}"
echo -e "\n${YELLOW}📋 To monitor MCP logs: tail -f /tmp/mcp-server.log${NC}"
echo -e "${YELLOW}📋 To monitor n8n logs: docker logs -f n8n-test${NC}"

# Wait for interrupt
wait $MCP_PID
```

--------------------------------------------------------------------------------
/src/database/node-repository.ts:
--------------------------------------------------------------------------------

```typescript
import { DatabaseAdapter } from './database-adapter';
import { ParsedNode } from '../parsers/node-parser';
import { SQLiteStorageService } from '../services/sqlite-storage-service';
import { NodeTypeNormalizer } from '../utils/node-type-normalizer';

export class NodeRepository {
  private db: DatabaseAdapter;
  
  constructor(dbOrService: DatabaseAdapter | SQLiteStorageService) {
    if (dbOrService instanceof SQLiteStorageService) {
      this.db = dbOrService.db;
      return;
    }

    this.db = dbOrService;
  }
  
  /**
   * Save node with proper JSON serialization
   */
  saveNode(node: ParsedNode): void {
    const stmt = this.db.prepare(`
      INSERT OR REPLACE INTO nodes (
        node_type, package_name, display_name, description,
        category, development_style, is_ai_tool, is_trigger,
        is_webhook, is_versioned, version, documentation,
        properties_schema, operations, credentials_required,
        outputs, output_names
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `);
    
    stmt.run(
      node.nodeType,
      node.packageName,
      node.displayName,
      node.description,
      node.category,
      node.style,
      node.isAITool ? 1 : 0,
      node.isTrigger ? 1 : 0,
      node.isWebhook ? 1 : 0,
      node.isVersioned ? 1 : 0,
      node.version,
      node.documentation || null,
      JSON.stringify(node.properties, null, 2),
      JSON.stringify(node.operations, null, 2),
      JSON.stringify(node.credentials, null, 2),
      node.outputs ? JSON.stringify(node.outputs, null, 2) : null,
      node.outputNames ? JSON.stringify(node.outputNames, null, 2) : null
    );
  }
  
  /**
   * Get node with proper JSON deserialization
   * Automatically normalizes node type to full form for consistent lookups
   */
  getNode(nodeType: string): any {
    // Normalize to full form first for consistent lookups
    const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType);

    const row = this.db.prepare(`
      SELECT * FROM nodes WHERE node_type = ?
    `).get(normalizedType) as any;

    // Fallback: try original type if normalization didn't help (e.g., community nodes)
    if (!row && normalizedType !== nodeType) {
      const originalRow = this.db.prepare(`
        SELECT * FROM nodes WHERE node_type = ?
      `).get(nodeType) as any;

      if (originalRow) {
        return this.parseNodeRow(originalRow);
      }
    }

    if (!row) return null;

    return this.parseNodeRow(row);
  }
  
  /**
   * Get AI tools with proper filtering
   */
  getAITools(): any[] {
    const rows = this.db.prepare(`
      SELECT node_type, display_name, description, package_name
      FROM nodes 
      WHERE is_ai_tool = 1
      ORDER BY display_name
    `).all() as any[];
    
    return rows.map(row => ({
      nodeType: row.node_type,
      displayName: row.display_name,
      description: row.description,
      package: row.package_name
    }));
  }
  
  private safeJsonParse(json: string, defaultValue: any): any {
    try {
      return JSON.parse(json);
    } catch {
      return defaultValue;
    }
  }

  // Additional methods for benchmarks
  upsertNode(node: ParsedNode): void {
    this.saveNode(node);
  }

  getNodeByType(nodeType: string): any {
    return this.getNode(nodeType);
  }

  getNodesByCategory(category: string): any[] {
    const rows = this.db.prepare(`
      SELECT * FROM nodes WHERE category = ?
      ORDER BY display_name
    `).all(category) as any[];
    
    return rows.map(row => this.parseNodeRow(row));
  }

  /**
   * Legacy LIKE-based search method for direct repository usage.
   *
   * NOTE: MCP tools do NOT use this method. They use MCPServer.searchNodes()
   * which automatically detects and uses FTS5 full-text search when available.
   * See src/mcp/server.ts:1135-1148 for FTS5 implementation.
   *
   * This method remains for:
   * - Direct repository access in scripts/benchmarks
   * - Fallback when FTS5 table doesn't exist
   * - Legacy compatibility
   */
  searchNodes(query: string, mode: 'OR' | 'AND' | 'FUZZY' = 'OR', limit: number = 20): any[] {
    let sql = '';
    const params: any[] = [];

    if (mode === 'FUZZY') {
      // Simple fuzzy search
      sql = `
        SELECT * FROM nodes 
        WHERE node_type LIKE ? OR display_name LIKE ? OR description LIKE ?
        ORDER BY display_name
        LIMIT ?
      `;
      const fuzzyQuery = `%${query}%`;
      params.push(fuzzyQuery, fuzzyQuery, fuzzyQuery, limit);
    } else {
      // OR/AND mode
      const words = query.split(/\s+/).filter(w => w.length > 0);
      const conditions = words.map(() => 
        '(node_type LIKE ? OR display_name LIKE ? OR description LIKE ?)'
      );
      const operator = mode === 'AND' ? ' AND ' : ' OR ';
      
      sql = `
        SELECT * FROM nodes 
        WHERE ${conditions.join(operator)}
        ORDER BY display_name
        LIMIT ?
      `;
      
      for (const word of words) {
        const searchTerm = `%${word}%`;
        params.push(searchTerm, searchTerm, searchTerm);
      }
      params.push(limit);
    }
    
    const rows = this.db.prepare(sql).all(...params) as any[];
    return rows.map(row => this.parseNodeRow(row));
  }

  getAllNodes(limit?: number): any[] {
    let sql = 'SELECT * FROM nodes ORDER BY display_name';
    if (limit) {
      sql += ` LIMIT ${limit}`;
    }
    
    const rows = this.db.prepare(sql).all() as any[];
    return rows.map(row => this.parseNodeRow(row));
  }

  getNodeCount(): number {
    const result = this.db.prepare('SELECT COUNT(*) as count FROM nodes').get() as any;
    return result.count;
  }

  getAIToolNodes(): any[] {
    return this.getAITools();
  }

  getNodesByPackage(packageName: string): any[] {
    const rows = this.db.prepare(`
      SELECT * FROM nodes WHERE package_name = ?
      ORDER BY display_name
    `).all(packageName) as any[];
    
    return rows.map(row => this.parseNodeRow(row));
  }

  searchNodeProperties(nodeType: string, query: string, maxResults: number = 20): any[] {
    const node = this.getNode(nodeType);
    if (!node || !node.properties) return [];
    
    const results: any[] = [];
    const searchLower = query.toLowerCase();
    
    function searchProperties(properties: any[], path: string[] = []) {
      for (const prop of properties) {
        if (results.length >= maxResults) break;
        
        const currentPath = [...path, prop.name || prop.displayName];
        const pathString = currentPath.join('.');
        
        if (prop.name?.toLowerCase().includes(searchLower) ||
            prop.displayName?.toLowerCase().includes(searchLower) ||
            prop.description?.toLowerCase().includes(searchLower)) {
          results.push({
            path: pathString,
            property: prop,
            description: prop.description
          });
        }
        
        // Search nested properties
        if (prop.options) {
          searchProperties(prop.options, currentPath);
        }
      }
    }
    
    searchProperties(node.properties);
    return results;
  }

  private parseNodeRow(row: any): any {
    return {
      nodeType: row.node_type,
      displayName: row.display_name,
      description: row.description,
      category: row.category,
      developmentStyle: row.development_style,
      package: row.package_name,
      isAITool: Number(row.is_ai_tool) === 1,
      isTrigger: Number(row.is_trigger) === 1,
      isWebhook: Number(row.is_webhook) === 1,
      isVersioned: Number(row.is_versioned) === 1,
      version: row.version,
      properties: this.safeJsonParse(row.properties_schema, []),
      operations: this.safeJsonParse(row.operations, []),
      credentials: this.safeJsonParse(row.credentials_required, []),
      hasDocumentation: !!row.documentation,
      outputs: row.outputs ? this.safeJsonParse(row.outputs, null) : null,
      outputNames: row.output_names ? this.safeJsonParse(row.output_names, null) : null
    };
  }

  /**
   * Get operations for a specific node, optionally filtered by resource
   */
  getNodeOperations(nodeType: string, resource?: string): any[] {
    const node = this.getNode(nodeType);
    if (!node) return [];

    const operations: any[] = [];

    // Parse operations field
    if (node.operations) {
      if (Array.isArray(node.operations)) {
        operations.push(...node.operations);
      } else if (typeof node.operations === 'object') {
        // Operations might be grouped by resource
        if (resource && node.operations[resource]) {
          return node.operations[resource];
        } else {
          // Return all operations
          Object.values(node.operations).forEach(ops => {
            if (Array.isArray(ops)) {
              operations.push(...ops);
            }
          });
        }
      }
    }

    // Also check properties for operation fields
    if (node.properties && Array.isArray(node.properties)) {
      for (const prop of node.properties) {
        if (prop.name === 'operation' && prop.options) {
          // If resource is specified, filter by displayOptions
          if (resource && prop.displayOptions?.show?.resource) {
            const allowedResources = Array.isArray(prop.displayOptions.show.resource)
              ? prop.displayOptions.show.resource
              : [prop.displayOptions.show.resource];
            if (!allowedResources.includes(resource)) {
              continue;
            }
          }

          // Add operations from this property
          operations.push(...prop.options);
        }
      }
    }

    return operations;
  }

  /**
   * Get all resources defined for a node
   */
  getNodeResources(nodeType: string): any[] {
    const node = this.getNode(nodeType);
    if (!node || !node.properties) return [];

    const resources: any[] = [];

    // Look for resource property
    for (const prop of node.properties) {
      if (prop.name === 'resource' && prop.options) {
        resources.push(...prop.options);
      }
    }

    return resources;
  }

  /**
   * Get operations that are valid for a specific resource
   */
  getOperationsForResource(nodeType: string, resource: string): any[] {
    const node = this.getNode(nodeType);
    if (!node || !node.properties) return [];

    const operations: any[] = [];

    // Find operation properties that are visible for this resource
    for (const prop of node.properties) {
      if (prop.name === 'operation' && prop.displayOptions?.show?.resource) {
        const allowedResources = Array.isArray(prop.displayOptions.show.resource)
          ? prop.displayOptions.show.resource
          : [prop.displayOptions.show.resource];

        if (allowedResources.includes(resource) && prop.options) {
          operations.push(...prop.options);
        }
      }
    }

    return operations;
  }

  /**
   * Get all operations across all nodes (for analysis)
   */
  getAllOperations(): Map<string, any[]> {
    const allOperations = new Map<string, any[]>();
    const nodes = this.getAllNodes();

    for (const node of nodes) {
      const operations = this.getNodeOperations(node.nodeType);
      if (operations.length > 0) {
        allOperations.set(node.nodeType, operations);
      }
    }

    return allOperations;
  }

  /**
   * Get all resources across all nodes (for analysis)
   */
  getAllResources(): Map<string, any[]> {
    const allResources = new Map<string, any[]>();
    const nodes = this.getAllNodes();

    for (const node of nodes) {
      const resources = this.getNodeResources(node.nodeType);
      if (resources.length > 0) {
        allResources.set(node.nodeType, resources);
      }
    }

    return allResources;
  }

  /**
   * Get default values for node properties
   */
  getNodePropertyDefaults(nodeType: string): Record<string, any> {
    try {
      const node = this.getNode(nodeType);
      if (!node || !node.properties) return {};

      const defaults: Record<string, any> = {};

      for (const prop of node.properties) {
        if (prop.name && prop.default !== undefined) {
          defaults[prop.name] = prop.default;
        }
      }

      return defaults;
    } catch (error) {
      // Log error and return empty defaults rather than throwing
      console.error(`Error getting property defaults for ${nodeType}:`, error);
      return {};
    }
  }

  /**
   * Get the default operation for a specific resource
   */
  getDefaultOperationForResource(nodeType: string, resource?: string): string | undefined {
    try {
      const node = this.getNode(nodeType);
      if (!node || !node.properties) return undefined;

      // Find operation property that's visible for this resource
      for (const prop of node.properties) {
        if (prop.name === 'operation') {
          // If there's a resource dependency, check if it matches
          if (resource && prop.displayOptions?.show?.resource) {
            // Validate displayOptions structure
            const resourceDep = prop.displayOptions.show.resource;
            if (!Array.isArray(resourceDep) && typeof resourceDep !== 'string') {
              continue; // Skip malformed displayOptions
            }

            const allowedResources = Array.isArray(resourceDep)
              ? resourceDep
              : [resourceDep];

            if (!allowedResources.includes(resource)) {
              continue; // This operation property doesn't apply to our resource
            }
          }

          // Return the default value if it exists
          if (prop.default !== undefined) {
            return prop.default;
          }

          // If no default but has options, return the first option's value
          if (prop.options && Array.isArray(prop.options) && prop.options.length > 0) {
            const firstOption = prop.options[0];
            return typeof firstOption === 'string' ? firstOption : firstOption.value;
          }
        }
      }
    } catch (error) {
      // Log error and return undefined rather than throwing
      // This ensures validation continues even with malformed node data
      console.error(`Error getting default operation for ${nodeType}:`, error);
      return undefined;
    }

    return undefined;
  }
}
```

--------------------------------------------------------------------------------
/src/telemetry/event-tracker.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Event Tracker for Telemetry (v2.18.3)
 * Handles all event tracking logic extracted from TelemetryManager
 * Now uses shared sanitization utilities to avoid code duplication
 */

import { TelemetryEvent, WorkflowTelemetry } from './telemetry-types';
import { WorkflowSanitizer } from './workflow-sanitizer';
import { TelemetryRateLimiter } from './rate-limiter';
import { TelemetryEventValidator } from './event-validator';
import { TelemetryError, TelemetryErrorType } from './telemetry-error';
import { logger } from '../utils/logger';
import { existsSync, readFileSync } from 'fs';
import { resolve } from 'path';
import { sanitizeErrorMessageCore } from './error-sanitization-utils';

export class TelemetryEventTracker {
  private rateLimiter: TelemetryRateLimiter;
  private validator: TelemetryEventValidator;
  private eventQueue: TelemetryEvent[] = [];
  private workflowQueue: WorkflowTelemetry[] = [];
  private previousTool?: string;
  private previousToolTimestamp: number = 0;
  private performanceMetrics: Map<string, number[]> = new Map();

  constructor(
    private getUserId: () => string,
    private isEnabled: () => boolean
  ) {
    this.rateLimiter = new TelemetryRateLimiter();
    this.validator = new TelemetryEventValidator();
  }

  /**
   * Track a tool usage event
   */
  trackToolUsage(toolName: string, success: boolean, duration?: number): void {
    if (!this.isEnabled()) return;

    // Check rate limit
    if (!this.rateLimiter.allow()) {
      logger.debug(`Rate limited: tool_used event for ${toolName}`);
      return;
    }

    // Track performance metrics
    if (duration !== undefined) {
      this.recordPerformanceMetric(toolName, duration);
    }

    const event: TelemetryEvent = {
      user_id: this.getUserId(),
      event: 'tool_used',
      properties: {
        tool: toolName.replace(/[^a-zA-Z0-9_-]/g, '_'),
        success,
        duration: duration || 0,
      }
    };

    // Validate and queue
    const validated = this.validator.validateEvent(event);
    if (validated) {
      this.eventQueue.push(validated);
    }
  }

  /**
   * Track workflow creation
   */
  async trackWorkflowCreation(workflow: any, validationPassed: boolean): Promise<void> {
    if (!this.isEnabled()) return;

    // Check rate limit
    if (!this.rateLimiter.allow()) {
      logger.debug('Rate limited: workflow creation event');
      return;
    }

    // Only store workflows that pass validation
    if (!validationPassed) {
      this.trackEvent('workflow_validation_failed', {
        nodeCount: workflow.nodes?.length || 0,
      });
      return;
    }

    try {
      const sanitized = WorkflowSanitizer.sanitizeWorkflow(workflow);

      const telemetryData: WorkflowTelemetry = {
        user_id: this.getUserId(),
        workflow_hash: sanitized.workflowHash,
        node_count: sanitized.nodeCount,
        node_types: sanitized.nodeTypes,
        has_trigger: sanitized.hasTrigger,
        has_webhook: sanitized.hasWebhook,
        complexity: sanitized.complexity,
        sanitized_workflow: {
          nodes: sanitized.nodes,
          connections: sanitized.connections,
        },
      };

      // Validate workflow telemetry
      const validated = this.validator.validateWorkflow(telemetryData);
      if (validated) {
        this.workflowQueue.push(validated);

        // Also track as event
        this.trackEvent('workflow_created', {
          nodeCount: sanitized.nodeCount,
          nodeTypes: sanitized.nodeTypes.length,
          complexity: sanitized.complexity,
          hasTrigger: sanitized.hasTrigger,
          hasWebhook: sanitized.hasWebhook,
        });
      }
    } catch (error) {
      logger.debug('Failed to track workflow creation:', error);
      throw new TelemetryError(
        TelemetryErrorType.VALIDATION_ERROR,
        'Failed to sanitize workflow',
        { error: error instanceof Error ? error.message : String(error) }
      );
    }
  }

  /**
   * Track an error event
   */
  trackError(errorType: string, context: string, toolName?: string, errorMessage?: string): void {
    if (!this.isEnabled()) return;

    // Don't rate limit error tracking - we want to see all errors
    this.trackEvent('error_occurred', {
      errorType: this.sanitizeErrorType(errorType),
      context: this.sanitizeContext(context),
      tool: toolName ? toolName.replace(/[^a-zA-Z0-9_-]/g, '_') : undefined,
      error: errorMessage ? this.sanitizeErrorMessage(errorMessage) : undefined,
      // Add environment context for better error analysis
      mcpMode: process.env.MCP_MODE || 'stdio',
      platform: process.platform
    }, false); // Skip rate limiting for errors
  }

  /**
   * Track a generic event
   */
  trackEvent(eventName: string, properties: Record<string, any>, checkRateLimit: boolean = true): void {
    if (!this.isEnabled()) return;

    // Check rate limit unless explicitly skipped
    if (checkRateLimit && !this.rateLimiter.allow()) {
      logger.debug(`Rate limited: ${eventName} event`);
      return;
    }

    const event: TelemetryEvent = {
      user_id: this.getUserId(),
      event: eventName,
      properties,
    };

    // Validate and queue
    const validated = this.validator.validateEvent(event);
    if (validated) {
      this.eventQueue.push(validated);
    }
  }

  /**
   * Track session start with optional startup tracking data (v2.18.2)
   */
  trackSessionStart(startupData?: {
    durationMs?: number;
    checkpoints?: string[];
    errorCount?: number;
  }): void {
    if (!this.isEnabled()) return;

    this.trackEvent('session_start', {
      version: this.getPackageVersion(),
      platform: process.platform,
      arch: process.arch,
      nodeVersion: process.version,
      isDocker: process.env.IS_DOCKER === 'true',
      cloudPlatform: this.detectCloudPlatform(),
      mcpMode: process.env.MCP_MODE || 'stdio',
      // NEW: Startup tracking fields (v2.18.2)
      startupDurationMs: startupData?.durationMs,
      checkpointsPassed: startupData?.checkpoints,
      startupErrorCount: startupData?.errorCount || 0,
    });
  }

  /**
   * Track startup completion (v2.18.2)
   * Called after first successful tool call to confirm server is functional
   */
  trackStartupComplete(): void {
    if (!this.isEnabled()) return;

    this.trackEvent('startup_completed', {
      version: this.getPackageVersion(),
    });
  }

  /**
   * Detect cloud platform from environment variables
   * Returns platform name or null if not in cloud
   */
  private detectCloudPlatform(): string | null {
    if (process.env.RAILWAY_ENVIRONMENT) return 'railway';
    if (process.env.RENDER) return 'render';
    if (process.env.FLY_APP_NAME) return 'fly';
    if (process.env.HEROKU_APP_NAME) return 'heroku';
    if (process.env.AWS_EXECUTION_ENV) return 'aws';
    if (process.env.KUBERNETES_SERVICE_HOST) return 'kubernetes';
    if (process.env.GOOGLE_CLOUD_PROJECT) return 'gcp';
    if (process.env.AZURE_FUNCTIONS_ENVIRONMENT) return 'azure';
    return null;
  }

  /**
   * Track search queries
   */
  trackSearchQuery(query: string, resultsFound: number, searchType: string): void {
    if (!this.isEnabled()) return;

    this.trackEvent('search_query', {
      query: query.substring(0, 100),
      resultsFound,
      searchType,
      hasResults: resultsFound > 0,
      isZeroResults: resultsFound === 0
    });
  }

  /**
   * Track validation details
   */
  trackValidationDetails(nodeType: string, errorType: string, details: Record<string, any>): void {
    if (!this.isEnabled()) return;

    this.trackEvent('validation_details', {
      nodeType: nodeType.replace(/[^a-zA-Z0-9_.-]/g, '_'),
      errorType: this.sanitizeErrorType(errorType),
      errorCategory: this.categorizeError(errorType),
      details
    });
  }

  /**
   * Track tool usage sequences
   */
  trackToolSequence(previousTool: string, currentTool: string, timeDelta: number): void {
    if (!this.isEnabled()) return;

    this.trackEvent('tool_sequence', {
      previousTool: previousTool.replace(/[^a-zA-Z0-9_-]/g, '_'),
      currentTool: currentTool.replace(/[^a-zA-Z0-9_-]/g, '_'),
      timeDelta: Math.min(timeDelta, 300000), // Cap at 5 minutes
      isSlowTransition: timeDelta > 10000,
      sequence: `${previousTool}->${currentTool}`
    });
  }

  /**
   * Track node configuration patterns
   */
  trackNodeConfiguration(nodeType: string, propertiesSet: number, usedDefaults: boolean): void {
    if (!this.isEnabled()) return;

    this.trackEvent('node_configuration', {
      nodeType: nodeType.replace(/[^a-zA-Z0-9_.-]/g, '_'),
      propertiesSet,
      usedDefaults,
      complexity: this.categorizeConfigComplexity(propertiesSet)
    });
  }

  /**
   * Track performance metrics
   */
  trackPerformanceMetric(operation: string, duration: number, metadata?: Record<string, any>): void {
    if (!this.isEnabled()) return;

    // Record for internal metrics
    this.recordPerformanceMetric(operation, duration);

    this.trackEvent('performance_metric', {
      operation: operation.replace(/[^a-zA-Z0-9_-]/g, '_'),
      duration,
      isSlow: duration > 1000,
      isVerySlow: duration > 5000,
      metadata
    });
  }

  /**
   * Update tool sequence tracking
   */
  updateToolSequence(toolName: string): void {
    if (this.previousTool) {
      const timeDelta = Date.now() - this.previousToolTimestamp;
      this.trackToolSequence(this.previousTool, toolName, timeDelta);
    }

    this.previousTool = toolName;
    this.previousToolTimestamp = Date.now();
  }

  /**
   * Get queued events
   */
  getEventQueue(): TelemetryEvent[] {
    return [...this.eventQueue];
  }

  /**
   * Get queued workflows
   */
  getWorkflowQueue(): WorkflowTelemetry[] {
    return [...this.workflowQueue];
  }

  /**
   * Clear event queue
   */
  clearEventQueue(): void {
    this.eventQueue = [];
  }

  /**
   * Clear workflow queue
   */
  clearWorkflowQueue(): void {
    this.workflowQueue = [];
  }

  /**
   * Get tracking statistics
   */
  getStats() {
    return {
      rateLimiter: this.rateLimiter.getStats(),
      validator: this.validator.getStats(),
      eventQueueSize: this.eventQueue.length,
      workflowQueueSize: this.workflowQueue.length,
      performanceMetrics: this.getPerformanceStats()
    };
  }

  /**
   * Record performance metric internally
   */
  private recordPerformanceMetric(operation: string, duration: number): void {
    if (!this.performanceMetrics.has(operation)) {
      this.performanceMetrics.set(operation, []);
    }

    const metrics = this.performanceMetrics.get(operation)!;
    metrics.push(duration);

    // Keep only last 100 measurements
    if (metrics.length > 100) {
      metrics.shift();
    }
  }

  /**
   * Get performance statistics
   */
  private getPerformanceStats() {
    const stats: Record<string, any> = {};

    for (const [operation, durations] of this.performanceMetrics.entries()) {
      if (durations.length === 0) continue;

      const sorted = [...durations].sort((a, b) => a - b);
      const sum = sorted.reduce((a, b) => a + b, 0);

      stats[operation] = {
        count: sorted.length,
        min: sorted[0],
        max: sorted[sorted.length - 1],
        avg: Math.round(sum / sorted.length),
        p50: sorted[Math.floor(sorted.length * 0.5)],
        p95: sorted[Math.floor(sorted.length * 0.95)],
        p99: sorted[Math.floor(sorted.length * 0.99)]
      };
    }

    return stats;
  }

  /**
   * Categorize error types
   */
  private categorizeError(errorType: string): string {
    const lowerError = errorType.toLowerCase();
    if (lowerError.includes('type')) return 'type_error';
    if (lowerError.includes('validation')) return 'validation_error';
    if (lowerError.includes('required')) return 'required_field_error';
    if (lowerError.includes('connection')) return 'connection_error';
    if (lowerError.includes('expression')) return 'expression_error';
    return 'other_error';
  }

  /**
   * Categorize configuration complexity
   */
  private categorizeConfigComplexity(propertiesSet: number): string {
    if (propertiesSet === 0) return 'defaults_only';
    if (propertiesSet <= 3) return 'simple';
    if (propertiesSet <= 10) return 'moderate';
    return 'complex';
  }

  /**
   * Get package version
   */
  private getPackageVersion(): string {
    try {
      const possiblePaths = [
        resolve(__dirname, '..', '..', 'package.json'),
        resolve(process.cwd(), 'package.json'),
        resolve(__dirname, '..', '..', '..', 'package.json')
      ];

      for (const packagePath of possiblePaths) {
        if (existsSync(packagePath)) {
          const packageJson = JSON.parse(readFileSync(packagePath, 'utf-8'));
          if (packageJson.version) {
            return packageJson.version;
          }
        }
      }

      return 'unknown';
    } catch (error) {
      logger.debug('Failed to get package version:', error);
      return 'unknown';
    }
  }

  /**
   * Sanitize error type
   */
  private sanitizeErrorType(errorType: string): string {
    return errorType.replace(/[^a-zA-Z0-9_-]/g, '_').substring(0, 50);
  }

  /**
   * Sanitize context
   */
  private sanitizeContext(context: string): string {
    // Sanitize in a specific order to preserve some structure
    let sanitized = context
      // First replace emails (before URLs eat them)
      .replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[EMAIL]')
      // Then replace long keys (32+ chars to match validator)
      .replace(/\b[a-zA-Z0-9_-]{32,}/g, '[KEY]')
      // Finally replace URLs but keep the path structure
      .replace(/(https?:\/\/)([^\s\/]+)(\/[^\s]*)?/gi, (match, protocol, domain, path) => {
        return '[URL]' + (path || '');
      });

    // Then truncate if needed
    if (sanitized.length > 100) {
      sanitized = sanitized.substring(0, 100);
    }
    return sanitized;
  }

  /**
   * Sanitize error message
   * Now uses shared sanitization core from error-sanitization-utils.ts (v2.18.3)
   * This eliminates code duplication and the ReDoS vulnerability
   */
  private sanitizeErrorMessage(errorMessage: string): string {
    return sanitizeErrorMessageCore(errorMessage);
  }
}
```

--------------------------------------------------------------------------------
/tests/unit/utils/ssrf-protection.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';

// Mock dns module before importing SSRFProtection
vi.mock('dns/promises', () => ({
  lookup: vi.fn(),
}));

import { SSRFProtection } from '../../../src/utils/ssrf-protection';
import * as dns from 'dns/promises';

/**
 * Unit tests for SSRFProtection with configurable security modes
 *
 * SECURITY: These tests verify SSRF protection blocks malicious URLs in all modes
 * See: https://github.com/czlonkowski/n8n-mcp/issues/265 (HIGH-03)
 */
describe('SSRFProtection', () => {
  const originalEnv = process.env.WEBHOOK_SECURITY_MODE;

  beforeEach(() => {
    // Clear all mocks before each test
    vi.clearAllMocks();
    // Default mock: simulate real DNS behavior - return the hostname as IP if it looks like an IP
    vi.mocked(dns.lookup).mockImplementation(async (hostname: any) => {
      // Handle special hostname "localhost"
      if (hostname === 'localhost') {
        return { address: '127.0.0.1', family: 4 } as any;
      }

      // If hostname is an IP address, return it as-is (simulating real DNS behavior)
      const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
      const ipv6Regex = /^([0-9a-fA-F]{0,4}:)+[0-9a-fA-F]{0,4}$/;

      if (ipv4Regex.test(hostname)) {
        return { address: hostname, family: 4 } as any;
      }
      if (ipv6Regex.test(hostname) || hostname === '::1') {
        return { address: hostname, family: 6 } as any;
      }

      // For actual hostnames, return a public IP by default
      return { address: '8.8.8.8', family: 4 } as any;
    });
  });

  afterEach(() => {
    // Restore original environment
    if (originalEnv) {
      process.env.WEBHOOK_SECURITY_MODE = originalEnv;
    } else {
      delete process.env.WEBHOOK_SECURITY_MODE;
    }
    vi.restoreAllMocks();
  });

  describe('Strict Mode (default)', () => {
    beforeEach(() => {
      delete process.env.WEBHOOK_SECURITY_MODE; // Use default strict
    });

    it('should block localhost', async () => {
      const localhostURLs = [
        'http://localhost:3000/webhook',
        'http://127.0.0.1/webhook',
        'http://[::1]/webhook',
      ];

      for (const url of localhostURLs) {
        const result = await SSRFProtection.validateWebhookUrl(url);
        expect(result.valid, `URL ${url} should be blocked but was valid`).toBe(false);
        expect(result.reason, `URL ${url} should have a reason`).toBeDefined();
      }
    });

    it('should block AWS metadata endpoint', async () => {
      const result = await SSRFProtection.validateWebhookUrl('http://169.254.169.254/latest/meta-data');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('Cloud metadata');
    });

    it('should block GCP metadata endpoint', async () => {
      const result = await SSRFProtection.validateWebhookUrl('http://metadata.google.internal/computeMetadata/v1/');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('Cloud metadata');
    });

    it('should block Alibaba Cloud metadata endpoint', async () => {
      const result = await SSRFProtection.validateWebhookUrl('http://100.100.100.200/latest/meta-data');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('Cloud metadata');
    });

    it('should block Oracle Cloud metadata endpoint', async () => {
      const result = await SSRFProtection.validateWebhookUrl('http://192.0.0.192/opc/v2/instance/');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('Cloud metadata');
    });

    it('should block private IP ranges', async () => {
      const privateIPs = [
        'http://10.0.0.1/webhook',
        'http://192.168.1.1/webhook',
        'http://172.16.0.1/webhook',
        'http://172.31.255.255/webhook',
      ];

      for (const url of privateIPs) {
        const result = await SSRFProtection.validateWebhookUrl(url);
        expect(result.valid).toBe(false);
        expect(result.reason).toContain('Private IP');
      }
    });

    it('should allow public URLs', async () => {
      const publicURLs = [
        'https://hooks.example.com/webhook',
        'https://api.external.com/callback',
        'http://public-service.com:8080/hook',
      ];

      for (const url of publicURLs) {
        const result = await SSRFProtection.validateWebhookUrl(url);
        expect(result.valid).toBe(true);
        expect(result.reason).toBeUndefined();
      }
    });

    it('should block non-HTTP protocols', async () => {
      const invalidProtocols = [
        'file:///etc/passwd',
        'ftp://internal-server/file',
        'gopher://old-service',
      ];

      for (const url of invalidProtocols) {
        const result = await SSRFProtection.validateWebhookUrl(url);
        expect(result.valid).toBe(false);
        expect(result.reason).toContain('protocol');
      }
    });
  });

  describe('Moderate Mode', () => {
    beforeEach(() => {
      process.env.WEBHOOK_SECURITY_MODE = 'moderate';
    });

    it('should allow localhost', async () => {
      const localhostURLs = [
        'http://localhost:5678/webhook',
        'http://127.0.0.1:5678/webhook',
        'http://[::1]:5678/webhook',
      ];

      for (const url of localhostURLs) {
        const result = await SSRFProtection.validateWebhookUrl(url);
        expect(result.valid).toBe(true);
      }
    });

    it('should still block private IPs', async () => {
      const privateIPs = [
        'http://10.0.0.1/webhook',
        'http://192.168.1.1/webhook',
        'http://172.16.0.1/webhook',
      ];

      for (const url of privateIPs) {
        const result = await SSRFProtection.validateWebhookUrl(url);
        expect(result.valid).toBe(false);
        expect(result.reason).toContain('Private IP');
      }
    });

    it('should still block cloud metadata', async () => {
      const metadataURLs = [
        'http://169.254.169.254/latest/meta-data',
        'http://metadata.google.internal/computeMetadata/v1/',
      ];

      for (const url of metadataURLs) {
        const result = await SSRFProtection.validateWebhookUrl(url);
        expect(result.valid).toBe(false);
        expect(result.reason).toContain('metadata');
      }
    });

    it('should allow public URLs', async () => {
      const result = await SSRFProtection.validateWebhookUrl('https://api.example.com/webhook');
      expect(result.valid).toBe(true);
    });
  });

  describe('Permissive Mode', () => {
    beforeEach(() => {
      process.env.WEBHOOK_SECURITY_MODE = 'permissive';
    });

    it('should allow localhost', async () => {
      const result = await SSRFProtection.validateWebhookUrl('http://localhost:5678/webhook');
      expect(result.valid).toBe(true);
    });

    it('should allow private IPs', async () => {
      const privateIPs = [
        'http://10.0.0.1/webhook',
        'http://192.168.1.1/webhook',
        'http://172.16.0.1/webhook',
      ];

      for (const url of privateIPs) {
        const result = await SSRFProtection.validateWebhookUrl(url);
        expect(result.valid).toBe(true);
      }
    });

    it('should still block cloud metadata', async () => {
      const metadataURLs = [
        'http://169.254.169.254/latest/meta-data',
        'http://metadata.google.internal/computeMetadata/v1/',
        'http://169.254.170.2/v2/metadata',
      ];

      for (const url of metadataURLs) {
        const result = await SSRFProtection.validateWebhookUrl(url);
        expect(result.valid).toBe(false);
        expect(result.reason).toContain('metadata');
      }
    });

    it('should allow public URLs', async () => {
      const result = await SSRFProtection.validateWebhookUrl('https://api.example.com/webhook');
      expect(result.valid).toBe(true);
    });
  });

  describe('DNS Rebinding Prevention', () => {
    it('should block hostname resolving to private IP (strict mode)', async () => {
      delete process.env.WEBHOOK_SECURITY_MODE; // strict

      // Mock DNS lookup to return private IP
      vi.mocked(dns.lookup).mockResolvedValue({ address: '10.0.0.1', family: 4 } as any);

      const result = await SSRFProtection.validateWebhookUrl('http://evil.example.com/webhook');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('Private IP');
    });

    it('should block hostname resolving to private IP (moderate mode)', async () => {
      process.env.WEBHOOK_SECURITY_MODE = 'moderate';

      // Mock DNS lookup to return private IP
      vi.mocked(dns.lookup).mockResolvedValue({ address: '192.168.1.100', family: 4 } as any);

      const result = await SSRFProtection.validateWebhookUrl('http://internal.company.com/webhook');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('Private IP');
    });

    it('should allow hostname resolving to private IP (permissive mode)', async () => {
      process.env.WEBHOOK_SECURITY_MODE = 'permissive';

      // Mock DNS lookup to return private IP
      vi.mocked(dns.lookup).mockResolvedValue({ address: '192.168.1.100', family: 4 } as any);

      const result = await SSRFProtection.validateWebhookUrl('http://internal.company.com/webhook');
      expect(result.valid).toBe(true);
    });

    it('should block hostname resolving to cloud metadata (all modes)', async () => {
      const modes = ['strict', 'moderate', 'permissive'];

      for (const mode of modes) {
        process.env.WEBHOOK_SECURITY_MODE = mode;

        // Mock DNS lookup to return cloud metadata IP
        vi.mocked(dns.lookup).mockResolvedValue({ address: '169.254.169.254', family: 4 } as any);

        const result = await SSRFProtection.validateWebhookUrl('http://evil-domain.com/webhook');
        expect(result.valid).toBe(false);
        expect(result.reason).toContain('metadata');
      }
    });

    it('should block hostname resolving to localhost IP (strict mode)', async () => {
      delete process.env.WEBHOOK_SECURITY_MODE; // strict

      // Mock DNS lookup to return localhost IP
      vi.mocked(dns.lookup).mockResolvedValue({ address: '127.0.0.1', family: 4 } as any);

      const result = await SSRFProtection.validateWebhookUrl('http://suspicious-domain.com/webhook');
      expect(result.valid).toBe(false);
      expect(result.reason).toBeDefined();
    });
  });

  describe('IPv6 Protection', () => {
    it('should block IPv6 localhost (strict mode)', async () => {
      delete process.env.WEBHOOK_SECURITY_MODE; // strict

      // Mock DNS to return IPv6 localhost
      vi.mocked(dns.lookup).mockResolvedValue({ address: '::1', family: 6 } as any);

      const result = await SSRFProtection.validateWebhookUrl('http://ipv6-test.com/webhook');
      expect(result.valid).toBe(false);
      // Updated: IPv6 localhost is now caught by the localhost check, not IPv6 check
      expect(result.reason).toContain('Localhost');
    });

    it('should block IPv6 link-local (strict mode)', async () => {
      delete process.env.WEBHOOK_SECURITY_MODE; // strict

      // Mock DNS to return IPv6 link-local
      vi.mocked(dns.lookup).mockResolvedValue({ address: 'fe80::1', family: 6 } as any);

      const result = await SSRFProtection.validateWebhookUrl('http://ipv6-local.com/webhook');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('IPv6 private');
    });

    it('should block IPv6 unique local (strict mode)', async () => {
      delete process.env.WEBHOOK_SECURITY_MODE; // strict

      // Mock DNS to return IPv6 unique local
      vi.mocked(dns.lookup).mockResolvedValue({ address: 'fc00::1', family: 6 } as any);

      const result = await SSRFProtection.validateWebhookUrl('http://ipv6-internal.com/webhook');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('IPv6 private');
    });

    it('should block IPv6 unique local fd00::/8 (strict mode)', async () => {
      delete process.env.WEBHOOK_SECURITY_MODE; // strict

      // Mock DNS to return IPv6 unique local fd00::/8
      vi.mocked(dns.lookup).mockResolvedValue({ address: 'fd00::1', family: 6 } as any);

      const result = await SSRFProtection.validateWebhookUrl('http://ipv6-fd00.com/webhook');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('IPv6 private');
    });

    it('should block IPv6 unspecified address (strict mode)', async () => {
      delete process.env.WEBHOOK_SECURITY_MODE; // strict

      // Mock DNS to return IPv6 unspecified address
      vi.mocked(dns.lookup).mockResolvedValue({ address: '::', family: 6 } as any);

      const result = await SSRFProtection.validateWebhookUrl('http://ipv6-unspecified.com/webhook');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('IPv6 private');
    });

    it('should block IPv4-mapped IPv6 addresses (strict mode)', async () => {
      delete process.env.WEBHOOK_SECURITY_MODE; // strict

      // Mock DNS to return IPv4-mapped IPv6 address
      vi.mocked(dns.lookup).mockResolvedValue({ address: '::ffff:127.0.0.1', family: 6 } as any);

      const result = await SSRFProtection.validateWebhookUrl('http://ipv4-mapped.com/webhook');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('IPv6 private');
    });
  });

  describe('DNS Resolution Failures', () => {
    it('should handle DNS resolution failure gracefully', async () => {
      // Mock DNS lookup to fail
      vi.mocked(dns.lookup).mockRejectedValue(new Error('ENOTFOUND'));

      const result = await SSRFProtection.validateWebhookUrl('http://non-existent-domain.invalid/webhook');
      expect(result.valid).toBe(false);
      expect(result.reason).toBe('DNS resolution failed');
    });
  });

  describe('Edge Cases', () => {
    it('should handle malformed URLs', async () => {
      const malformedURLs = [
        'not-a-url',
        'http://',
        '://missing-protocol.com',
      ];

      for (const url of malformedURLs) {
        const result = await SSRFProtection.validateWebhookUrl(url);
        expect(result.valid).toBe(false);
        expect(result.reason).toBe('Invalid URL format');
      }
    });

    it('should handle URL with special characters safely', async () => {
      const result = await SSRFProtection.validateWebhookUrl('https://example.com/webhook?param=value&other=123');
      expect(result.valid).toBe(true);
    });
  });
});

```

--------------------------------------------------------------------------------
/tests/unit/services/example-generator.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ExampleGenerator } from '@/services/example-generator';
import type { NodeExamples } from '@/services/example-generator';

// Mock the database
vi.mock('better-sqlite3');

describe('ExampleGenerator', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('getExamples', () => {
    it('should return curated examples for HTTP Request node', () => {
      const examples = ExampleGenerator.getExamples('nodes-base.httpRequest');

      expect(examples).toHaveProperty('minimal');
      expect(examples).toHaveProperty('common');
      expect(examples).toHaveProperty('advanced');

      // Check minimal example
      expect(examples.minimal).toEqual({
        url: 'https://api.example.com/data'
      });

      // Check common example has required fields
      expect(examples.common).toMatchObject({
        method: 'POST',
        url: 'https://api.example.com/users',
        sendBody: true,
        contentType: 'json'
      });

      // Check advanced example has error handling
      expect(examples.advanced).toMatchObject({
        method: 'POST',
        onError: 'continueRegularOutput',
        retryOnFail: true,
        maxTries: 3
      });
    });

    it('should return curated examples for Webhook node', () => {
      const examples = ExampleGenerator.getExamples('nodes-base.webhook');

      expect(examples.minimal).toMatchObject({
        path: 'my-webhook',
        httpMethod: 'POST'
      });

      expect(examples.common).toMatchObject({
        responseMode: 'lastNode',
        responseData: 'allEntries',
        responseCode: 200
      });
    });

    it('should return curated examples for Code node', () => {
      const examples = ExampleGenerator.getExamples('nodes-base.code');

      expect(examples.minimal).toMatchObject({
        language: 'javaScript',
        jsCode: 'return [{json: {result: "success"}}];'
      });

      expect(examples.common?.jsCode).toContain('items.map');
      expect(examples.common?.jsCode).toContain('DateTime.now()');

      expect(examples.advanced?.jsCode).toContain('try');
      expect(examples.advanced?.jsCode).toContain('catch');
    });

    it('should generate basic examples for unconfigured nodes', () => {
      const essentials = {
        required: [
          { name: 'url', type: 'string' },
          { name: 'method', type: 'options', options: [{ value: 'GET' }, { value: 'POST' }] }
        ],
        common: [
          { name: 'timeout', type: 'number' }
        ]
      };

      const examples = ExampleGenerator.getExamples('nodes-base.unknownNode', essentials);

      expect(examples.minimal).toEqual({
        url: 'https://api.example.com',
        method: 'GET'
      });

      expect(examples.common).toBeUndefined();
      expect(examples.advanced).toBeUndefined();
    });

    it('should use common property if no required fields exist', () => {
      const essentials = {
        required: [],
        common: [
          { name: 'name', type: 'string' }
        ]
      };

      const examples = ExampleGenerator.getExamples('nodes-base.unknownNode', essentials);

      expect(examples.minimal).toEqual({
        name: 'John Doe'
      });
    });

    it('should return empty minimal object if no essentials provided', () => {
      const examples = ExampleGenerator.getExamples('nodes-base.unknownNode');

      expect(examples.minimal).toEqual({});
    });
  });

  describe('special example nodes', () => {
    it('should provide webhook processing example', () => {
      const examples = ExampleGenerator.getExamples('nodes-base.code.webhookProcessing');

      expect(examples.minimal?.jsCode).toContain('const webhookData = items[0].json.body');
      expect(examples.minimal?.jsCode).toContain('// ❌ WRONG');
      expect(examples.minimal?.jsCode).toContain('// ✅ CORRECT');
    });

    it('should provide data transformation examples', () => {
      const examples = ExampleGenerator.getExamples('nodes-base.code.dataTransform');

      expect(examples.minimal?.jsCode).toContain('CSV-like data to JSON');
      expect(examples.minimal?.jsCode).toContain('split');
    });

    it('should provide aggregation example', () => {
      const examples = ExampleGenerator.getExamples('nodes-base.code.aggregation');

      expect(examples.minimal?.jsCode).toContain('items.reduce');
      expect(examples.minimal?.jsCode).toContain('totalAmount');
    });

    it('should provide JMESPath filtering example', () => {
      const examples = ExampleGenerator.getExamples('nodes-base.code.jmespathFiltering');

      expect(examples.minimal?.jsCode).toContain('$jmespath');
      expect(examples.minimal?.jsCode).toContain('`100`'); // Backticks for numeric literals
      expect(examples.minimal?.jsCode).toContain('✅ CORRECT');
    });

    it('should provide Python example', () => {
      const examples = ExampleGenerator.getExamples('nodes-base.code.pythonExample');

      expect(examples.minimal?.pythonCode).toContain('_input.all()');
      expect(examples.minimal?.pythonCode).toContain('to_py()');
      expect(examples.minimal?.pythonCode).toContain('import json');
    });

    it('should provide AI tool example', () => {
      const examples = ExampleGenerator.getExamples('nodes-base.code.aiTool');

      expect(examples.minimal?.mode).toBe('runOnceForEachItem');
      expect(examples.minimal?.jsCode).toContain('calculate discount');
      expect(examples.minimal?.jsCode).toContain('$json.quantity');
    });

    it('should provide crypto usage example', () => {
      const examples = ExampleGenerator.getExamples('nodes-base.code.crypto');

      expect(examples.minimal?.jsCode).toContain("require('crypto')");
      expect(examples.minimal?.jsCode).toContain('randomBytes');
      expect(examples.minimal?.jsCode).toContain('createHash');
    });

    it('should provide static data example', () => {
      const examples = ExampleGenerator.getExamples('nodes-base.code.staticData');

      expect(examples.minimal?.jsCode).toContain('$getWorkflowStaticData');
      expect(examples.minimal?.jsCode).toContain('processCount');
    });
  });

  describe('database node examples', () => {
    it('should provide PostgreSQL examples', () => {
      const examples = ExampleGenerator.getExamples('nodes-base.postgres');

      expect(examples.minimal).toMatchObject({
        operation: 'executeQuery',
        query: 'SELECT * FROM users LIMIT 10'
      });

      expect(examples.advanced?.query).toContain('ON CONFLICT');
      expect(examples.advanced?.retryOnFail).toBe(true);
    });

    it('should provide MongoDB examples', () => {
      const examples = ExampleGenerator.getExamples('nodes-base.mongoDb');

      expect(examples.minimal).toMatchObject({
        operation: 'find',
        collection: 'users'
      });

      expect(examples.common).toMatchObject({
        operation: 'findOneAndUpdate',
        options: {
          upsert: true,
          returnNewDocument: true
        }
      });
    });

    it('should provide MySQL examples', () => {
      const examples = ExampleGenerator.getExamples('nodes-base.mySql');

      expect(examples.minimal?.query).toContain('SELECT * FROM products');
      expect(examples.common?.operation).toBe('insert');
    });
  });

  describe('communication node examples', () => {
    it('should provide Slack examples', () => {
      const examples = ExampleGenerator.getExamples('nodes-base.slack');

      expect(examples.minimal).toMatchObject({
        resource: 'message',
        operation: 'post',
        channel: '#general',
        text: 'Hello from n8n!'
      });

      expect(examples.common?.attachments).toBeDefined();
      expect(examples.common?.retryOnFail).toBe(true);
    });

    it('should provide Email examples', () => {
      const examples = ExampleGenerator.getExamples('nodes-base.emailSend');

      expect(examples.minimal).toMatchObject({
        fromEmail: '[email protected]',
        toEmail: '[email protected]',
        subject: 'Test Email'
      });

      expect(examples.common?.html).toContain('<h1>Welcome!</h1>');
    });
  });

  describe('error handling patterns', () => {
    it('should provide modern error handling patterns', () => {
      const examples = ExampleGenerator.getExamples('error-handling.modern-patterns');

      expect(examples.minimal).toMatchObject({
        onError: 'continueRegularOutput'
      });

      expect(examples.advanced).toMatchObject({
        onError: 'stopWorkflow',
        retryOnFail: true,
        maxTries: 3
      });
    });

    it('should provide API retry patterns', () => {
      const examples = ExampleGenerator.getExamples('error-handling.api-with-retry');

      expect(examples.common?.retryOnFail).toBe(true);
      expect(examples.common?.maxTries).toBe(5);
      expect(examples.common?.alwaysOutputData).toBe(true);
    });

    it('should provide database error patterns', () => {
      const examples = ExampleGenerator.getExamples('error-handling.database-patterns');

      expect(examples.common).toMatchObject({
        retryOnFail: true,
        maxTries: 3,
        onError: 'stopWorkflow'
      });
    });

    it('should provide webhook error patterns', () => {
      const examples = ExampleGenerator.getExamples('error-handling.webhook-patterns');

      expect(examples.minimal?.alwaysOutputData).toBe(true);
      expect(examples.common?.responseCode).toBe(200);
    });
  });

  describe('getTaskExample', () => {
    it('should return minimal example for basic task', () => {
      const example = ExampleGenerator.getTaskExample('nodes-base.httpRequest', 'basic');

      expect(example).toEqual({
        url: 'https://api.example.com/data'
      });
    });

    it('should return common example for typical task', () => {
      const example = ExampleGenerator.getTaskExample('nodes-base.httpRequest', 'typical');

      expect(example).toMatchObject({
        method: 'POST',
        sendBody: true
      });
    });

    it('should return advanced example for complex task', () => {
      const example = ExampleGenerator.getTaskExample('nodes-base.httpRequest', 'complex');

      expect(example).toMatchObject({
        retryOnFail: true,
        maxTries: 3
      });
    });

    it('should default to common example for unknown task', () => {
      const example = ExampleGenerator.getTaskExample('nodes-base.httpRequest', 'unknown');

      expect(example).toMatchObject({
        method: 'POST' // This is from common example
      });
    });

    it('should return undefined for unknown node type', () => {
      const example = ExampleGenerator.getTaskExample('nodes-base.unknownNode', 'basic');

      expect(example).toBeUndefined();
    });
  });

  describe('default value generation', () => {
    it('should generate appropriate defaults for different property types', () => {
      const essentials = {
        required: [
          { name: 'url', type: 'string' },
          { name: 'port', type: 'number' },
          { name: 'enabled', type: 'boolean' },
          { name: 'method', type: 'options', options: [{ value: 'GET' }, { value: 'POST' }] },
          { name: 'data', type: 'json' }
        ],
        common: []
      };

      const examples = ExampleGenerator.getExamples('nodes-base.unknownNode', essentials);

      expect(examples.minimal).toEqual({
        url: 'https://api.example.com',
        port: 80,
        enabled: false,
        method: 'GET',
        data: '{\n  "key": "value"\n}'
      });
    });

    it('should use property defaults when available', () => {
      const essentials = {
        required: [
          { name: 'timeout', type: 'number', default: 5000 },
          { name: 'retries', type: 'number', default: 3 }
        ],
        common: []
      };

      const examples = ExampleGenerator.getExamples('nodes-base.unknownNode', essentials);

      expect(examples.minimal).toEqual({
        timeout: 5000,
        retries: 3
      });
    });

    it('should generate context-aware string defaults', () => {
      const essentials = {
        required: [
          { name: 'fromEmail', type: 'string' },
          { name: 'toEmail', type: 'string' },
          { name: 'webhookPath', type: 'string' },
          { name: 'username', type: 'string' },
          { name: 'apiKey', type: 'string' },
          { name: 'query', type: 'string' },
          { name: 'collection', type: 'string' }
        ],
        common: []
      };

      const examples = ExampleGenerator.getExamples('nodes-base.unknownNode', essentials);

      expect(examples.minimal).toEqual({
        fromEmail: '[email protected]',
        toEmail: '[email protected]',
        webhookPath: 'my-webhook',
        username: 'John Doe',
        apiKey: 'myKey',
        query: 'SELECT * FROM table_name LIMIT 10',
        collection: 'users'
      });
    });

    it('should use placeholder as fallback for string defaults', () => {
      const essentials = {
        required: [
          { name: 'customField', type: 'string', placeholder: 'Enter custom value' }
        ],
        common: []
      };

      const examples = ExampleGenerator.getExamples('nodes-base.unknownNode', essentials);

      expect(examples.minimal).toEqual({
        customField: 'Enter custom value'
      });
    });
  });

  describe('edge cases', () => {
    it('should handle empty essentials object', () => {
      const essentials = {
        required: [],
        common: []
      };

      const examples = ExampleGenerator.getExamples('nodes-base.unknownNode', essentials);

      expect(examples.minimal).toEqual({});
    });

    it('should handle properties with missing options', () => {
      const essentials = {
        required: [
          { name: 'choice', type: 'options' } // No options array
        ],
        common: []
      };

      const examples = ExampleGenerator.getExamples('nodes-base.unknownNode', essentials);

      expect(examples.minimal).toEqual({
        choice: ''
      });
    });

    it('should handle collection and fixedCollection types', () => {
      const essentials = {
        required: [
          { name: 'headers', type: 'collection' },
          { name: 'options', type: 'fixedCollection' }
        ],
        common: []
      };

      const examples = ExampleGenerator.getExamples('nodes-base.unknownNode', essentials);

      expect(examples.minimal).toEqual({
        headers: {},
        options: {}
      });
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/unit/services/property-filter-edge-cases.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { PropertyFilter } from '@/services/property-filter';
import type { SimplifiedProperty } from '@/services/property-filter';

// Mock the database
vi.mock('better-sqlite3');

describe('PropertyFilter - Edge Cases', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('Null and Undefined Handling', () => {
    it('should handle null properties gracefully', () => {
      const result = PropertyFilter.getEssentials(null as any, 'nodes-base.http');
      expect(result).toEqual({ required: [], common: [] });
    });

    it('should handle undefined properties gracefully', () => {
      const result = PropertyFilter.getEssentials(undefined as any, 'nodes-base.http');
      expect(result).toEqual({ required: [], common: [] });
    });

    it('should handle null nodeType gracefully', () => {
      const properties = [{ name: 'test', type: 'string' }];
      const result = PropertyFilter.getEssentials(properties, null as any);
      // Should fallback to inferEssentials
      expect(result.required).toBeDefined();
      expect(result.common).toBeDefined();
    });

    it('should handle properties with null values', () => {
      const properties = [
        { name: 'prop1', type: 'string', displayName: null, description: null },
        null,
        undefined,
        { name: null, type: 'string' },
        { name: 'prop2', type: null }
      ];
      
      const result = PropertyFilter.getEssentials(properties as any, 'nodes-base.test');
      expect(() => result).not.toThrow();
      expect(result.required).toBeDefined();
      expect(result.common).toBeDefined();
    });
  });

  describe('Boundary Value Testing', () => {
    it('should handle empty properties array', () => {
      const result = PropertyFilter.getEssentials([], 'nodes-base.http');
      expect(result).toEqual({ required: [], common: [] });
    });

    it('should handle very large properties array', () => {
      const largeProperties = Array(10000).fill(null).map((_, i) => ({
        name: `prop${i}`,
        type: 'string',
        displayName: `Property ${i}`,
        description: `Description for property ${i}`,
        required: i % 100 === 0
      }));
      
      const start = Date.now();
      const result = PropertyFilter.getEssentials(largeProperties, 'nodes-base.test');
      const duration = Date.now() - start;
      
      expect(result).toBeDefined();
      expect(duration).toBeLessThan(1000); // Should filter within 1 second
      // For unconfigured nodes, it uses inferEssentials which limits results
      expect(result.required.length + result.common.length).toBeLessThanOrEqual(30);
    });

    it('should handle properties with extremely long strings', () => {
      const properties = [
        {
          name: 'longProp',
          type: 'string',
          displayName: 'A'.repeat(1000),
          description: 'B'.repeat(10000),
          placeholder: 'C'.repeat(5000),
          required: true
        }
      ];
      
      const result = PropertyFilter.getEssentials(properties, 'nodes-base.test');
      // For unconfigured nodes, this might be included as required
      const allProps = [...result.required, ...result.common];
      const longProp = allProps.find(p => p.name === 'longProp');
      if (longProp) {
        expect(longProp.displayName).toBeDefined();
      }
    });

    it('should limit options array size', () => {
      const manyOptions = Array(1000).fill(null).map((_, i) => ({
        value: `option${i}`,
        name: `Option ${i}`
      }));
      
      const properties = [{
        name: 'selectProp',
        type: 'options',
        displayName: 'Select Property',
        options: manyOptions,
        required: true
      }];
      
      const result = PropertyFilter.getEssentials(properties, 'nodes-base.test');
      const allProps = [...result.required, ...result.common];
      const selectProp = allProps.find(p => p.name === 'selectProp');
      
      if (selectProp && selectProp.options) {
        // Should limit options to reasonable number
        expect(selectProp.options.length).toBeLessThanOrEqual(20);
      }
    });
  });

  describe('Property Type Handling', () => {
    it('should handle all n8n property types', () => {
      const propertyTypes = [
        'string', 'number', 'boolean', 'options', 'multiOptions',
        'collection', 'fixedCollection', 'json', 'notice', 'assignmentCollection',
        'resourceLocator', 'resourceMapper', 'filter', 'credentials'
      ];
      
      const properties = propertyTypes.map(type => ({
        name: `${type}Prop`,
        type,
        displayName: `${type} Property`,
        description: `A ${type} property`
      }));
      
      const result = PropertyFilter.getEssentials(properties, 'nodes-base.test');
      expect(result).toBeDefined();
      
      const allProps = [...result.required, ...result.common];
      // Should handle various types without crashing
      expect(allProps.length).toBeGreaterThan(0);
    });

    it('should handle nested collection properties', () => {
      const properties = [{
        name: 'collection',
        type: 'collection',
        displayName: 'Collection',
        options: [
          { name: 'nested1', type: 'string', displayName: 'Nested 1' },
          { name: 'nested2', type: 'number', displayName: 'Nested 2' }
        ]
      }];
      
      const result = PropertyFilter.getEssentials(properties, 'nodes-base.test');
      const allProps = [...result.required, ...result.common];
      
      // Should include the collection
      expect(allProps.some(p => p.name === 'collection')).toBe(true);
    });

    it('should handle fixedCollection properties', () => {
      const properties = [{
        name: 'headers',
        type: 'fixedCollection',
        displayName: 'Headers',
        typeOptions: { multipleValues: true },
        options: [{
          name: 'parameter',
          displayName: 'Parameter',
          values: [
            { name: 'name', type: 'string', displayName: 'Name' },
            { name: 'value', type: 'string', displayName: 'Value' }
          ]
        }]
      }];
      
      const result = PropertyFilter.getEssentials(properties, 'nodes-base.test');
      const allProps = [...result.required, ...result.common];
      
      // Should include the fixed collection
      expect(allProps.some(p => p.name === 'headers')).toBe(true);
    });
  });

  describe('Special Cases', () => {
    it('should handle circular references in properties', () => {
      const properties: any = [{
        name: 'circular',
        type: 'string',
        displayName: 'Circular'
      }];
      properties[0].self = properties[0];
      
      expect(() => {
        PropertyFilter.getEssentials(properties, 'nodes-base.test');
      }).not.toThrow();
    });

    it('should handle properties with special characters', () => {
      const properties = [
        { name: 'prop-with-dash', type: 'string', displayName: 'Prop With Dash' },
        { name: 'prop_with_underscore', type: 'string', displayName: 'Prop With Underscore' },
        { name: 'prop.with.dot', type: 'string', displayName: 'Prop With Dot' },
        { name: 'prop@special', type: 'string', displayName: 'Prop Special' }
      ];
      
      const result = PropertyFilter.getEssentials(properties, 'nodes-base.test');
      expect(result).toBeDefined();
    });

    it('should handle duplicate property names', () => {
      const properties = [
        { name: 'duplicate', type: 'string', displayName: 'First Duplicate' },
        { name: 'duplicate', type: 'number', displayName: 'Second Duplicate' },
        { name: 'duplicate', type: 'boolean', displayName: 'Third Duplicate' }
      ];
      
      const result = PropertyFilter.getEssentials(properties, 'nodes-base.test');
      const allProps = [...result.required, ...result.common];
      
      // Should deduplicate
      const duplicates = allProps.filter(p => p.name === 'duplicate');
      expect(duplicates.length).toBe(1);
    });
  });

  describe('Node-Specific Configurations', () => {
    it('should apply HTTP Request specific filtering', () => {
      const properties = [
        { name: 'url', type: 'string', required: true },
        { name: 'method', type: 'options', options: [{ value: 'GET' }, { value: 'POST' }] },
        { name: 'authentication', type: 'options' },
        { name: 'sendBody', type: 'boolean' },
        { name: 'contentType', type: 'options' },
        { name: 'sendHeaders', type: 'fixedCollection' },
        { name: 'someObscureOption', type: 'string' }
      ];
      
      const result = PropertyFilter.getEssentials(properties, 'nodes-base.httpRequest');
      
      expect(result.required.some(p => p.name === 'url')).toBe(true);
      expect(result.common.some(p => p.name === 'method')).toBe(true);
      expect(result.common.some(p => p.name === 'authentication')).toBe(true);
      
      // Should not include obscure option
      const allProps = [...result.required, ...result.common];
      expect(allProps.some(p => p.name === 'someObscureOption')).toBe(false);
    });

    it('should apply Slack specific filtering', () => {
      const properties = [
        { name: 'resource', type: 'options', required: true },
        { name: 'operation', type: 'options', required: true },
        { name: 'channel', type: 'string' },
        { name: 'text', type: 'string' },
        { name: 'attachments', type: 'collection' },
        { name: 'ts', type: 'string' },
        { name: 'advancedOption1', type: 'string' },
        { name: 'advancedOption2', type: 'boolean' }
      ];
      
      const result = PropertyFilter.getEssentials(properties, 'nodes-base.slack');
      
      // In the actual config, resource and operation are in common, not required
      expect(result.common.some(p => p.name === 'resource')).toBe(true);
      expect(result.common.some(p => p.name === 'operation')).toBe(true);
      expect(result.common.some(p => p.name === 'channel')).toBe(true);
      expect(result.common.some(p => p.name === 'text')).toBe(true);
    });
  });

  describe('Fallback Behavior', () => {
    it('should infer essentials for unconfigured nodes', () => {
      const properties = [
        { name: 'requiredProp', type: 'string', required: true },
        { name: 'commonProp', type: 'string', displayName: 'Common Property' },
        { name: 'advancedProp', type: 'json', displayName: 'Advanced Property' },
        { name: 'debugProp', type: 'boolean', displayName: 'Debug Mode' },
        { name: 'internalProp', type: 'hidden' }
      ];
      
      const result = PropertyFilter.getEssentials(properties, 'nodes-base.unknownNode');
      
      // Should include required properties
      expect(result.required.some(p => p.name === 'requiredProp')).toBe(true);
      
      // Should include some common properties
      expect(result.common.length).toBeGreaterThan(0);
      
      // Should not include internal/hidden properties
      const allProps = [...result.required, ...result.common];
      expect(allProps.some(p => p.name === 'internalProp')).toBe(false);
    });

    it('should handle nodes with only advanced properties', () => {
      const properties = [
        { name: 'advanced1', type: 'json', displayName: 'Advanced Option 1' },
        { name: 'advanced2', type: 'collection', displayName: 'Advanced Collection' },
        { name: 'advanced3', type: 'assignmentCollection', displayName: 'Advanced Assignment' }
      ];
      
      const result = PropertyFilter.getEssentials(properties, 'nodes-base.advancedNode');
      
      // Should still return some properties
      const allProps = [...result.required, ...result.common];
      expect(allProps.length).toBeGreaterThan(0);
    });
  });

  describe('Property Simplification', () => {
    it('should simplify complex property structures', () => {
      const properties = [{
        name: 'complexProp',
        type: 'options',
        displayName: 'Complex Property',
        description: 'A'.repeat(500), // Long description
        default: 'option1',
        placeholder: 'Select an option',
        hint: 'This is a hint',
        displayOptions: { show: { mode: ['advanced'] } },
        options: Array(50).fill(null).map((_, i) => ({
          value: `option${i}`,
          name: `Option ${i}`,
          description: `Description for option ${i}`
        }))
      }];
      
      const result = PropertyFilter.getEssentials(properties, 'nodes-base.test');
      const allProps = [...result.required, ...result.common];
      const simplified = allProps.find(p => p.name === 'complexProp');
      
      if (simplified) {
        // Should include essential fields
        expect(simplified.name).toBe('complexProp');
        expect(simplified.displayName).toBe('Complex Property');
        expect(simplified.type).toBe('options');
        
        // Should limit options
        if (simplified.options) {
          expect(simplified.options.length).toBeLessThanOrEqual(20);
        }
      }
    });

    it('should handle properties without display names', () => {
      const properties = [
        { name: 'prop_without_display', type: 'string', description: 'Property description' },
        { name: 'anotherProp', displayName: '', type: 'number' }
      ];
      
      const result = PropertyFilter.getEssentials(properties, 'nodes-base.test');
      const allProps = [...result.required, ...result.common];
      
      allProps.forEach(prop => {
        // Should have a displayName (fallback to name if needed)
        expect(prop.displayName).toBeTruthy();
        expect(prop.displayName.length).toBeGreaterThan(0);
      });
    });
  });

  describe('Performance', () => {
    it('should handle property filtering efficiently', () => {
      const nodeTypes = [
        'nodes-base.httpRequest',
        'nodes-base.webhook',
        'nodes-base.slack',
        'nodes-base.googleSheets',
        'nodes-base.postgres'
      ];
      
      const properties = Array(100).fill(null).map((_, i) => ({
        name: `prop${i}`,
        type: i % 2 === 0 ? 'string' : 'options',
        displayName: `Property ${i}`,
        required: i < 5
      }));
      
      const start = Date.now();
      nodeTypes.forEach(nodeType => {
        PropertyFilter.getEssentials(properties, nodeType);
      });
      const duration = Date.now() - start;
      
      // Should process multiple nodes quickly
      expect(duration).toBeLessThan(50);
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/integration/n8n-api/system/diagnostic.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Integration Tests: handleDiagnostic
 *
 * Tests system diagnostic functionality.
 * Covers environment checks, API status, and verbose mode.
 */

import { describe, it, expect, beforeEach } from 'vitest';
import { createMcpContext } from '../utils/mcp-context';
import { InstanceContext } from '../../../../src/types/instance-context';
import { handleDiagnostic } from '../../../../src/mcp/handlers-n8n-manager';
import { DiagnosticResponse } from '../utils/response-types';

describe('Integration: handleDiagnostic', () => {
  let mcpContext: InstanceContext;

  beforeEach(() => {
    mcpContext = createMcpContext();
  });

  // ======================================================================
  // Basic Diagnostic
  // ======================================================================

  describe('Basic Diagnostic', () => {
    it('should run basic diagnostic check', async () => {
      const response = await handleDiagnostic(
        { params: { arguments: {} } },
        mcpContext
      );

      expect(response.success).toBe(true);
      expect(response.data).toBeDefined();

      const data = response.data as DiagnosticResponse;

      // Verify core diagnostic fields
      expect(data).toHaveProperty('timestamp');
      expect(data).toHaveProperty('environment');
      expect(data).toHaveProperty('apiConfiguration');
      expect(data).toHaveProperty('toolsAvailability');
      expect(data).toHaveProperty('versionInfo');
      expect(data).toHaveProperty('performance');

      // Verify timestamp format
      expect(typeof data.timestamp).toBe('string');
      const timestamp = new Date(data.timestamp);
      expect(timestamp.toString()).not.toBe('Invalid Date');

      // Verify version info
      expect(data.versionInfo).toBeDefined();
      if (data.versionInfo) {
        expect(data.versionInfo).toHaveProperty('current');
        expect(data.versionInfo).toHaveProperty('upToDate');
        expect(typeof data.versionInfo.upToDate).toBe('boolean');
      }

      // Verify performance metrics
      expect(data.performance).toBeDefined();
      if (data.performance) {
        expect(data.performance).toHaveProperty('diagnosticResponseTimeMs');
        expect(typeof data.performance.diagnosticResponseTimeMs).toBe('number');
      }
    });

    it('should include environment variables', async () => {
      const response = await handleDiagnostic(
        { params: { arguments: {} } },
        mcpContext
      );

      const data = response.data as DiagnosticResponse;

      expect(data.environment).toBeDefined();
      expect(data.environment).toHaveProperty('N8N_API_URL');
      expect(data.environment).toHaveProperty('N8N_API_KEY');
      expect(data.environment).toHaveProperty('NODE_ENV');
      expect(data.environment).toHaveProperty('MCP_MODE');
      expect(data.environment).toHaveProperty('isDocker');
      expect(data.environment).toHaveProperty('cloudPlatform');
      expect(data.environment).toHaveProperty('nodeVersion');
      expect(data.environment).toHaveProperty('platform');

      // API key should be masked
      if (data.environment.N8N_API_KEY) {
        expect(data.environment.N8N_API_KEY).toBe('***configured***');
      }

      // Environment detection types
      expect(typeof data.environment.isDocker).toBe('boolean');
      expect(typeof data.environment.nodeVersion).toBe('string');
      expect(typeof data.environment.platform).toBe('string');
    });

    it('should check API configuration and connectivity', async () => {
      const response = await handleDiagnostic(
        { params: { arguments: {} } },
        mcpContext
      );

      const data = response.data as DiagnosticResponse;

      expect(data.apiConfiguration).toBeDefined();
      expect(data.apiConfiguration).toHaveProperty('configured');
      expect(data.apiConfiguration).toHaveProperty('status');

      // In test environment, API should be configured
      expect(data.apiConfiguration.configured).toBe(true);

      // Verify API status
      const status = data.apiConfiguration.status;
      expect(status).toHaveProperty('configured');
      expect(status).toHaveProperty('connected');

      // Should successfully connect to n8n API
      expect(status.connected).toBe(true);

      // If connected, should have version info
      if (status.connected) {
        expect(status).toHaveProperty('version');
      }

      // Config details should be present when configured
      if (data.apiConfiguration.configured) {
        expect(data.apiConfiguration).toHaveProperty('config');
        expect(data.apiConfiguration.config).toHaveProperty('baseUrl');
        expect(data.apiConfiguration.config).toHaveProperty('timeout');
        expect(data.apiConfiguration.config).toHaveProperty('maxRetries');
      }
    });

    it('should report tools availability', async () => {
      const response = await handleDiagnostic(
        { params: { arguments: {} } },
        mcpContext
      );

      const data = response.data as DiagnosticResponse;

      expect(data.toolsAvailability).toBeDefined();
      expect(data.toolsAvailability).toHaveProperty('documentationTools');
      expect(data.toolsAvailability).toHaveProperty('managementTools');
      expect(data.toolsAvailability).toHaveProperty('totalAvailable');

      // Documentation tools should always be available
      const docTools = data.toolsAvailability.documentationTools;
      expect(docTools.count).toBeGreaterThan(0);
      expect(docTools.enabled).toBe(true);
      expect(docTools.description).toBeDefined();

      // Management tools should be available when API configured
      const mgmtTools = data.toolsAvailability.managementTools;
      expect(mgmtTools).toHaveProperty('count');
      expect(mgmtTools).toHaveProperty('enabled');
      expect(mgmtTools).toHaveProperty('description');

      // In test environment, management tools should be enabled
      expect(mgmtTools.enabled).toBe(true);
      expect(mgmtTools.count).toBeGreaterThan(0);

      // Total should be sum of both
      expect(data.toolsAvailability.totalAvailable).toBe(
        docTools.count + mgmtTools.count
      );
    });

    it('should include troubleshooting information', async () => {
      const response = await handleDiagnostic(
        { params: { arguments: {} } },
        mcpContext
      );

      const data = response.data as DiagnosticResponse;

      // Should have either nextSteps (if API connected) or setupGuide (if not configured)
      const hasGuidance = data.nextSteps || data.setupGuide || data.troubleshooting;
      expect(hasGuidance).toBeDefined();

      if (data.nextSteps) {
        expect(data.nextSteps).toHaveProperty('message');
        expect(data.nextSteps).toHaveProperty('recommended');
        expect(Array.isArray(data.nextSteps.recommended)).toBe(true);
      }

      if (data.setupGuide) {
        expect(data.setupGuide).toHaveProperty('message');
        expect(data.setupGuide).toHaveProperty('whatYouCanDoNow');
        expect(data.setupGuide).toHaveProperty('whatYouCannotDo');
        expect(data.setupGuide).toHaveProperty('howToEnable');
      }

      if (data.troubleshooting) {
        expect(data.troubleshooting).toHaveProperty('issue');
        expect(data.troubleshooting).toHaveProperty('steps');
        expect(Array.isArray(data.troubleshooting.steps)).toBe(true);
      }
    });
  });

  // ======================================================================
  // Environment Detection
  // ======================================================================

  describe('Environment Detection', () => {
    it('should provide mode-specific debugging suggestions', async () => {
      const response = await handleDiagnostic(
        { params: { arguments: {} } },
        mcpContext
      );

      const data = response.data as DiagnosticResponse;

      // Mode-specific debug should always be present
      expect(data).toHaveProperty('modeSpecificDebug');
      expect(data.modeSpecificDebug).toBeDefined();
      expect(data.modeSpecificDebug).toHaveProperty('mode');
      expect(data.modeSpecificDebug).toHaveProperty('troubleshooting');
      expect(data.modeSpecificDebug).toHaveProperty('commonIssues');

      // Verify troubleshooting is an array with content
      expect(Array.isArray(data.modeSpecificDebug.troubleshooting)).toBe(true);
      expect(data.modeSpecificDebug.troubleshooting.length).toBeGreaterThan(0);

      // Verify common issues is an array with content
      expect(Array.isArray(data.modeSpecificDebug.commonIssues)).toBe(true);
      expect(data.modeSpecificDebug.commonIssues.length).toBeGreaterThan(0);

      // Mode should be either 'HTTP Server' or 'Standard I/O (Claude Desktop)'
      expect(['HTTP Server', 'Standard I/O (Claude Desktop)']).toContain(data.modeSpecificDebug.mode);
    });

    it('should include Docker debugging if IS_DOCKER is true', async () => {
      // Save original value
      const originalIsDocker = process.env.IS_DOCKER;

      try {
        // Set IS_DOCKER for this test
        process.env.IS_DOCKER = 'true';

        const response = await handleDiagnostic(
          { params: { arguments: {} } },
          mcpContext
        );

        const data = response.data as DiagnosticResponse;

        // Should have Docker debug section
        expect(data).toHaveProperty('dockerDebug');
        expect(data.dockerDebug).toBeDefined();
        expect(data.dockerDebug?.containerDetected).toBe(true);
        expect(data.dockerDebug?.troubleshooting).toBeDefined();
        expect(Array.isArray(data.dockerDebug?.troubleshooting)).toBe(true);
        expect(data.dockerDebug?.commonIssues).toBeDefined();
      } finally {
        // Restore original value
        if (originalIsDocker) {
          process.env.IS_DOCKER = originalIsDocker;
        } else {
          delete process.env.IS_DOCKER;
        }
      }
    });

    it('should not include Docker debugging if IS_DOCKER is false', async () => {
      // Save original value
      const originalIsDocker = process.env.IS_DOCKER;

      try {
        // Unset IS_DOCKER for this test
        delete process.env.IS_DOCKER;

        const response = await handleDiagnostic(
          { params: { arguments: {} } },
          mcpContext
        );

        const data = response.data as DiagnosticResponse;

        // Should not have Docker debug section
        expect(data.dockerDebug).toBeUndefined();
      } finally {
        // Restore original value
        if (originalIsDocker) {
          process.env.IS_DOCKER = originalIsDocker;
        }
      }
    });
  });

  // ======================================================================
  // Verbose Mode
  // ======================================================================

  describe('Verbose Mode', () => {
    it('should include additional debug info in verbose mode', async () => {
      const response = await handleDiagnostic(
        { params: { arguments: { verbose: true } } },
        mcpContext
      );

      expect(response.success).toBe(true);
      const data = response.data as DiagnosticResponse;

      // Verbose mode should add debug section
      expect(data).toHaveProperty('debug');
      expect(data.debug).toBeDefined();

      // Verify debug information
      expect(data.debug).toBeDefined();
      expect(data.debug).toHaveProperty('processEnv');
      expect(data.debug).toHaveProperty('nodeVersion');
      expect(data.debug).toHaveProperty('platform');
      expect(data.debug).toHaveProperty('workingDirectory');

      // Process env should list relevant environment variables
      expect(Array.isArray(data.debug?.processEnv)).toBe(true);

      // Node version should be a string
      expect(typeof data.debug?.nodeVersion).toBe('string');
      expect(data.debug?.nodeVersion).toMatch(/^v\d+\.\d+\.\d+/);

      // Platform should be a string (linux, darwin, win32, etc.)
      expect(typeof data.debug?.platform).toBe('string');
      expect(data.debug && data.debug.platform.length).toBeGreaterThan(0);

      // Working directory should be a path
      expect(typeof data.debug?.workingDirectory).toBe('string');
      expect(data.debug && data.debug.workingDirectory.length).toBeGreaterThan(0);
    });

    it('should not include debug info when verbose is false', async () => {
      const response = await handleDiagnostic(
        { params: { arguments: { verbose: false } } },
        mcpContext
      );

      expect(response.success).toBe(true);
      const data = response.data as DiagnosticResponse;

      // Debug section should not be present
      expect(data.debug).toBeUndefined();
    });

    it('should not include debug info by default', async () => {
      const response = await handleDiagnostic(
        { params: { arguments: {} } },
        mcpContext
      );

      expect(response.success).toBe(true);
      const data = response.data as DiagnosticResponse;

      // Debug section should not be present when verbose not specified
      expect(data.debug).toBeUndefined();
    });
  });

  // ======================================================================
  // Response Format Verification
  // ======================================================================

  describe('Response Format', () => {
    it('should return complete diagnostic response structure', async () => {
      const response = await handleDiagnostic(
        { params: { arguments: {} } },
        mcpContext
      );

      expect(response.success).toBe(true);
      expect(response.data).toBeDefined();

      const data = response.data as DiagnosticResponse;

      // Verify all required fields (always present)
      const requiredFields = [
        'timestamp',
        'environment',
        'apiConfiguration',
        'toolsAvailability',
        'versionInfo',
        'performance'
      ];

      requiredFields.forEach(field => {
        expect(data).toHaveProperty(field);
        expect(data[field]).toBeDefined();
      });

      // Context-specific fields (at least one should be present)
      const hasContextualGuidance = data.nextSteps || data.setupGuide || data.troubleshooting;
      expect(hasContextualGuidance).toBeDefined();

      // Verify data types
      expect(typeof data.timestamp).toBe('string');
      expect(typeof data.environment).toBe('object');
      expect(typeof data.apiConfiguration).toBe('object');
      expect(typeof data.toolsAvailability).toBe('object');
      expect(typeof data.versionInfo).toBe('object');
      expect(typeof data.performance).toBe('object');
    });
  });
});

```

--------------------------------------------------------------------------------
/tests/integration/mcp/template-examples-e2e.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { createDatabaseAdapter, DatabaseAdapter } from '../../../src/database/database-adapter';
import fs from 'fs';
import path from 'path';
import { sampleConfigs, compressWorkflow, sampleWorkflows } from '../../fixtures/template-configs';

/**
 * End-to-end integration tests for template-based examples feature
 * Tests the complete flow: database -> MCP server -> examples in response
 */

describe('Template Examples E2E Integration', () => {
  let db: DatabaseAdapter;

  beforeEach(async () => {
    // Create in-memory database
    db = await createDatabaseAdapter(':memory:');

    // Apply schema
    const schemaPath = path.join(__dirname, '../../../src/database/schema.sql');
    const schema = fs.readFileSync(schemaPath, 'utf-8');
    db.exec(schema);

    // Apply migration
    const migrationPath = path.join(__dirname, '../../../src/database/migrations/add-template-node-configs.sql');
    const migration = fs.readFileSync(migrationPath, 'utf-8');
    db.exec(migration);

    // Seed test data
    seedTemplateConfigs();
  });

  afterEach(() => {
    if ('close' in db && typeof db.close === 'function') {
      db.close();
    }
  });

  function seedTemplateConfigs() {
    // Insert sample templates first to satisfy foreign key constraints
    // The sampleConfigs use template_id 1-4, edge cases use 998-999
    const templateIds = [1, 2, 3, 4, 998, 999];
    for (const id of templateIds) {
      db.prepare(`
        INSERT INTO templates (
          id, workflow_id, name, description, views,
          nodes_used, created_at, updated_at
        ) VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
      `).run(
        id,
        id,
        `Test Template ${id}`,
        'Test Description',
        1000,
        JSON.stringify(['n8n-nodes-base.webhook', 'n8n-nodes-base.httpRequest'])
      );
    }

    // Insert webhook configs
    db.prepare(`
      INSERT INTO template_node_configs (
        node_type, template_id, template_name, template_views,
        node_name, parameters_json, credentials_json,
        has_credentials, has_expressions, complexity, use_cases, rank
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(
      ...Object.values(sampleConfigs.simpleWebhook)
    );

    db.prepare(`
      INSERT INTO template_node_configs (
        node_type, template_id, template_name, template_views,
        node_name, parameters_json, credentials_json,
        has_credentials, has_expressions, complexity, use_cases, rank
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(
      ...Object.values(sampleConfigs.webhookWithAuth)
    );

    // Insert HTTP request configs
    db.prepare(`
      INSERT INTO template_node_configs (
        node_type, template_id, template_name, template_views,
        node_name, parameters_json, credentials_json,
        has_credentials, has_expressions, complexity, use_cases, rank
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(
      ...Object.values(sampleConfigs.httpRequestBasic)
    );

    db.prepare(`
      INSERT INTO template_node_configs (
        node_type, template_id, template_name, template_views,
        node_name, parameters_json, credentials_json,
        has_credentials, has_expressions, complexity, use_cases, rank
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(
      ...Object.values(sampleConfigs.httpRequestWithExpressions)
    );
  }

  describe('Querying Examples Directly', () => {
    it('should fetch top 2 examples for webhook node', () => {
      const examples = db.prepare(`
        SELECT
          parameters_json,
          template_name,
          template_views
        FROM template_node_configs
        WHERE node_type = ?
        ORDER BY rank
        LIMIT 2
      `).all('n8n-nodes-base.webhook') as any[];

      expect(examples).toHaveLength(2);
      expect(examples[0].template_name).toBe('Simple Webhook Trigger');
      expect(examples[1].template_name).toBe('Authenticated Webhook');
    });

    it('should fetch top 3 examples with metadata for HTTP request node', () => {
      const examples = db.prepare(`
        SELECT
          parameters_json,
          template_name,
          template_views,
          complexity,
          use_cases,
          has_credentials,
          has_expressions
        FROM template_node_configs
        WHERE node_type = ?
        ORDER BY rank
        LIMIT 3
      `).all('n8n-nodes-base.httpRequest') as any[];

      expect(examples).toHaveLength(2); // Only 2 inserted
      expect(examples[0].template_name).toBe('Basic HTTP GET Request');
      expect(examples[0].complexity).toBe('simple');
      expect(examples[0].has_expressions).toBe(0);

      expect(examples[1].template_name).toBe('Dynamic HTTP Request');
      expect(examples[1].complexity).toBe('complex');
      expect(examples[1].has_expressions).toBe(1);
    });
  });

  describe('Example Data Structure Validation', () => {
    it('should have valid JSON in parameters_json', () => {
      const examples = db.prepare(`
        SELECT parameters_json
        FROM template_node_configs
        WHERE node_type = ?
        LIMIT 1
      `).all('n8n-nodes-base.webhook') as any[];

      expect(() => {
        const params = JSON.parse(examples[0].parameters_json);
        expect(params).toHaveProperty('httpMethod');
        expect(params).toHaveProperty('path');
      }).not.toThrow();
    });

    it('should have valid JSON in use_cases', () => {
      const examples = db.prepare(`
        SELECT use_cases
        FROM template_node_configs
        WHERE node_type = ?
        LIMIT 1
      `).all('n8n-nodes-base.webhook') as any[];

      expect(() => {
        const useCases = JSON.parse(examples[0].use_cases);
        expect(Array.isArray(useCases)).toBe(true);
      }).not.toThrow();
    });

    it('should have credentials_json when has_credentials is 1', () => {
      const examples = db.prepare(`
        SELECT credentials_json, has_credentials
        FROM template_node_configs
        WHERE has_credentials = 1
        LIMIT 1
      `).all() as any[];

      if (examples.length > 0) {
        expect(examples[0].credentials_json).not.toBeNull();
        expect(() => {
          JSON.parse(examples[0].credentials_json);
        }).not.toThrow();
      }
    });
  });

  describe('Ranked View Functionality', () => {
    it('should return only top 5 ranked configs per node type from view', () => {
      // Insert templates first to satisfy foreign key constraints
      // Note: seedTemplateConfigs already created templates 1-4, so start from 5
      for (let i = 5; i <= 14; i++) {
        db.prepare(`
          INSERT INTO templates (
            id, workflow_id, name, description, views,
            nodes_used, created_at, updated_at
          ) VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
        `).run(i, i, `Template ${i}`, 'Test', 1000 - (i * 50), '[]');
      }

      // Insert 10 configs for same node type
      for (let i = 5; i <= 14; i++) {
        db.prepare(`
          INSERT INTO template_node_configs (
            node_type, template_id, template_name, template_views,
            node_name, parameters_json, rank
          ) VALUES (?, ?, ?, ?, ?, ?, ?)
        `).run(
          'n8n-nodes-base.webhook',
          i,
          `Template ${i}`,
          1000 - (i * 50),
          'Webhook',
          '{}',
          i
        );
      }

      const rankedConfigs = db.prepare(`
        SELECT * FROM ranked_node_configs
        WHERE node_type = ?
      `).all('n8n-nodes-base.webhook') as any[];

      expect(rankedConfigs.length).toBeLessThanOrEqual(5);
    });
  });

  describe('Performance with Real-World Data Volume', () => {
    beforeEach(() => {
      // Insert templates first to satisfy foreign key constraints
      for (let i = 1; i <= 100; i++) {
        db.prepare(`
          INSERT INTO templates (
            id, workflow_id, name, description, views,
            nodes_used, created_at, updated_at
          ) VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
        `).run(i + 100, i + 100, `Template ${i}`, 'Test', Math.floor(Math.random() * 10000), '[]');
      }

      // Insert 100 configs across 10 different node types
      const nodeTypes = [
        'n8n-nodes-base.slack',
        'n8n-nodes-base.googleSheets',
        'n8n-nodes-base.code',
        'n8n-nodes-base.if',
        'n8n-nodes-base.switch',
        'n8n-nodes-base.set',
        'n8n-nodes-base.merge',
        'n8n-nodes-base.splitInBatches',
        'n8n-nodes-base.postgres',
        'n8n-nodes-base.gmail'
      ];

      for (let i = 1; i <= 100; i++) {
        const nodeType = nodeTypes[i % nodeTypes.length];
        db.prepare(`
          INSERT INTO template_node_configs (
            node_type, template_id, template_name, template_views,
            node_name, parameters_json, rank
          ) VALUES (?, ?, ?, ?, ?, ?, ?)
        `).run(
          nodeType,
          i + 100, // Offset template_id
          `Template ${i}`,
          Math.floor(Math.random() * 10000),
          'Node',
          '{}',
          (i % 10) + 1
        );
      }
    });

    it('should query specific node type examples quickly', () => {
      const start = Date.now();
      const examples = db.prepare(`
        SELECT * FROM template_node_configs
        WHERE node_type = ?
        ORDER BY rank
        LIMIT 3
      `).all('n8n-nodes-base.slack') as any[];
      const duration = Date.now() - start;

      expect(examples.length).toBeGreaterThan(0);
      expect(duration).toBeLessThan(5); // Should be very fast with index
    });

    it('should filter by complexity efficiently', () => {
      // Set complexity on configs
      db.exec(`UPDATE template_node_configs SET complexity = 'simple' WHERE id % 3 = 0`);
      db.exec(`UPDATE template_node_configs SET complexity = 'medium' WHERE id % 3 = 1`);

      const start = Date.now();
      const examples = db.prepare(`
        SELECT * FROM template_node_configs
        WHERE node_type = ? AND complexity = ?
        ORDER BY rank
        LIMIT 3
      `).all('n8n-nodes-base.code', 'simple') as any[];
      const duration = Date.now() - start;

      expect(duration).toBeLessThan(5);
    });
  });

  describe('Edge Cases', () => {
    it('should handle node types with no configs', () => {
      const examples = db.prepare(`
        SELECT * FROM template_node_configs
        WHERE node_type = ?
        LIMIT 2
      `).all('n8n-nodes-base.nonexistent') as any[];

      expect(examples).toHaveLength(0);
    });

    it('should handle very long parameters_json', () => {
      const longParams = JSON.stringify({
        options: {
          queryParameters: Array.from({ length: 100 }, (_, i) => ({
            name: `param${i}`,
            value: `value${i}`.repeat(10)
          }))
        }
      });

      db.prepare(`
        INSERT INTO template_node_configs (
          node_type, template_id, template_name, template_views,
          node_name, parameters_json, rank
        ) VALUES (?, ?, ?, ?, ?, ?, ?)
      `).run(
        'n8n-nodes-base.test',
        999,
        'Long Params Template',
        100,
        'Test',
        longParams,
        1
      );

      const example = db.prepare(`
        SELECT parameters_json FROM template_node_configs WHERE template_id = ?
      `).get(999) as any;

      expect(() => {
        const parsed = JSON.parse(example.parameters_json);
        expect(parsed.options.queryParameters).toHaveLength(100);
      }).not.toThrow();
    });

    it('should handle special characters in parameters', () => {
      const specialParams = JSON.stringify({
        message: "Test with 'quotes' and \"double quotes\"",
        unicode: "特殊文字 🎉 émojis",
        symbols: "!@#$%^&*()_+-={}[]|\\:;<>?,./"
      });

      db.prepare(`
        INSERT INTO template_node_configs (
          node_type, template_id, template_name, template_views,
          node_name, parameters_json, rank
        ) VALUES (?, ?, ?, ?, ?, ?, ?)
      `).run(
        'n8n-nodes-base.test',
        998,
        'Special Chars Template',
        100,
        'Test',
        specialParams,
        1
      );

      const example = db.prepare(`
        SELECT parameters_json FROM template_node_configs WHERE template_id = ?
      `).get(998) as any;

      expect(() => {
        const parsed = JSON.parse(example.parameters_json);
        expect(parsed.message).toContain("'quotes'");
        expect(parsed.unicode).toContain("🎉");
      }).not.toThrow();
    });
  });

  describe('Data Integrity', () => {
    it('should maintain referential integrity with templates table', () => {
      // Try to insert config with non-existent template_id (with FK enabled)
      db.exec('PRAGMA foreign_keys = ON');

      expect(() => {
        db.prepare(`
          INSERT INTO template_node_configs (
            node_type, template_id, template_name, template_views,
            node_name, parameters_json, rank
          ) VALUES (?, ?, ?, ?, ?, ?, ?)
        `).run(
          'n8n-nodes-base.test',
          999999, // Non-existent template_id
          'Test',
          100,
          'Node',
          '{}',
          1
        );
      }).toThrow(); // Should fail due to FK constraint
    });

    it('should cascade delete configs when template is deleted', () => {
      db.exec('PRAGMA foreign_keys = ON');

      // Insert a new template (use id 1000 to avoid conflicts with seedTemplateConfigs)
      db.prepare(`
        INSERT INTO templates (
          id, workflow_id, name, description, views,
          nodes_used, created_at, updated_at
        ) VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
      `).run(1000, 1000, 'Test Template 1000', 'Desc', 100, '[]');

      db.prepare(`
        INSERT INTO template_node_configs (
          node_type, template_id, template_name, template_views,
          node_name, parameters_json, rank
        ) VALUES (?, ?, ?, ?, ?, ?, ?)
      `).run(
        'n8n-nodes-base.test',
        1000,
        'Test',
        100,
        'Node',
        '{}',
        1
      );

      // Verify config exists
      let config = db.prepare('SELECT * FROM template_node_configs WHERE template_id = ?').get(1000);
      expect(config).toBeDefined();

      // Delete template
      db.prepare('DELETE FROM templates WHERE id = ?').run(1000);

      // Verify config is deleted (CASCADE)
      config = db.prepare('SELECT * FROM template_node_configs WHERE template_id = ?').get(1000);
      expect(config).toBeUndefined();
    });
  });
});

```

--------------------------------------------------------------------------------
/src/templates/batch-processor.ts:
--------------------------------------------------------------------------------

```typescript
import * as fs from 'fs';
import * as path from 'path';
import OpenAI from 'openai';
import { logger } from '../utils/logger';
import { MetadataGenerator, MetadataRequest, MetadataResult } from './metadata-generator';

export interface BatchProcessorOptions {
  apiKey: string;
  model?: string;
  batchSize?: number;
  outputDir?: string;
}

export interface BatchJob {
  id: string;
  status: 'validating' | 'in_progress' | 'finalizing' | 'completed' | 'failed' | 'expired' | 'cancelled';
  created_at: number;
  completed_at?: number;
  input_file_id: string;
  output_file_id?: string;
  error?: any;
}

export class BatchProcessor {
  private client: OpenAI;
  private generator: MetadataGenerator;
  private batchSize: number;
  private outputDir: string;
  
  constructor(options: BatchProcessorOptions) {
    this.client = new OpenAI({ apiKey: options.apiKey });
    this.generator = new MetadataGenerator(options.apiKey, options.model);
    this.batchSize = options.batchSize || 100;
    this.outputDir = options.outputDir || './temp';
    
    // Ensure output directory exists
    if (!fs.existsSync(this.outputDir)) {
      fs.mkdirSync(this.outputDir, { recursive: true });
    }
  }
  
  /**
   * Process templates in batches (parallel submission)
   */
  async processTemplates(
    templates: MetadataRequest[],
    progressCallback?: (message: string, current: number, total: number) => void
  ): Promise<Map<number, MetadataResult>> {
    const results = new Map<number, MetadataResult>();
    const batches = this.createBatches(templates);
    
    logger.info(`Processing ${templates.length} templates in ${batches.length} batches`);
    
    // Submit all batches in parallel
    console.log(`\n📤 Submitting ${batches.length} batch${batches.length > 1 ? 'es' : ''} to OpenAI...`);
    const batchJobs: Array<{ batchNum: number; jobPromise: Promise<any>; templates: MetadataRequest[] }> = [];
    
    for (let i = 0; i < batches.length; i++) {
      const batch = batches[i];
      const batchNum = i + 1;
      
      try {
        progressCallback?.(`Submitting batch ${batchNum}/${batches.length}`, i * this.batchSize, templates.length);
        
        // Submit batch (don't wait for completion)
        const jobPromise = this.submitBatch(batch, `batch_${batchNum}`);
        batchJobs.push({ batchNum, jobPromise, templates: batch });
        
        console.log(`   📨 Submitted batch ${batchNum}/${batches.length} (${batch.length} templates)`);
      } catch (error) {
        logger.error(`Error submitting batch ${batchNum}:`, error);
        console.error(`   ❌ Failed to submit batch ${batchNum}`);
      }
    }
    
    console.log(`\n⏳ All batches submitted. Waiting for completion...`);
    console.log(`   (Batches process in parallel - this is much faster than sequential processing)`);
    
    // Process all batches in parallel and collect results as they complete
    const batchPromises = batchJobs.map(async ({ batchNum, jobPromise, templates: batchTemplates }) => {
      try {
        const completedJob = await jobPromise;
        console.log(`\n📦 Retrieving results for batch ${batchNum}/${batches.length}...`);
        
        // Retrieve and parse results
        const batchResults = await this.retrieveResults(completedJob);
        
        logger.info(`Retrieved ${batchResults.length} results from batch ${batchNum}`);
        progressCallback?.(`Retrieved batch ${batchNum}/${batches.length}`, 
          Math.min(batchNum * this.batchSize, templates.length), templates.length);
        
        return { batchNum, results: batchResults };
      } catch (error) {
        logger.error(`Error processing batch ${batchNum}:`, error);
        console.error(`   ❌ Batch ${batchNum} failed:`, error);
        return { batchNum, results: [] };
      }
    });
    
    // Wait for all batches to complete
    const allBatchResults = await Promise.all(batchPromises);
    
    // Merge all results
    for (const { batchNum, results: batchResults } of allBatchResults) {
      for (const result of batchResults) {
        results.set(result.templateId, result);
      }
      if (batchResults.length > 0) {
        console.log(`   ✅ Merged ${batchResults.length} results from batch ${batchNum}`);
      }
    }
    
    logger.info(`Batch processing complete: ${results.size} results`);
    return results;
  }
  
  /**
   * Submit a batch without waiting for completion
   */
  private async submitBatch(templates: MetadataRequest[], batchName: string): Promise<any> {
    // Create JSONL file
    const inputFile = await this.createBatchFile(templates, batchName);
    
    try {
      // Upload file to OpenAI
      const uploadedFile = await this.uploadFile(inputFile);
      
      // Create batch job
      const batchJob = await this.createBatchJob(uploadedFile.id);
      
      // Start monitoring (returns promise that resolves when complete)
      const monitoringPromise = this.monitorBatchJob(batchJob.id);
      
      // Clean up input file immediately
      try {
        fs.unlinkSync(inputFile);
      } catch {}
      
      // Store file IDs for cleanup later
      monitoringPromise.then(async (completedJob) => {
        // Cleanup uploaded files after completion
        try {
          await this.client.files.del(uploadedFile.id);
          if (completedJob.output_file_id) {
            // Note: We'll delete output file after retrieving results
          }
        } catch (error) {
          logger.warn(`Failed to cleanup files for batch ${batchName}`, error);
        }
      });
      
      return monitoringPromise;
    } catch (error) {
      // Cleanup on error
      try {
        fs.unlinkSync(inputFile);
      } catch {}
      throw error;
    }
  }
  
  /**
   * Process a single batch
   */
  private async processBatch(templates: MetadataRequest[], batchName: string): Promise<MetadataResult[]> {
    // Create JSONL file
    const inputFile = await this.createBatchFile(templates, batchName);
    
    try {
      // Upload file to OpenAI
      const uploadedFile = await this.uploadFile(inputFile);
      
      // Create batch job
      const batchJob = await this.createBatchJob(uploadedFile.id);
      
      // Monitor job until completion
      const completedJob = await this.monitorBatchJob(batchJob.id);
      
      // Retrieve and parse results
      const results = await this.retrieveResults(completedJob);
      
      // Cleanup
      await this.cleanup(inputFile, uploadedFile.id, completedJob.output_file_id);
      
      return results;
    } catch (error) {
      // Cleanup on error
      try {
        fs.unlinkSync(inputFile);
      } catch {}
      throw error;
    }
  }
  
  /**
   * Create batches from templates
   */
  private createBatches(templates: MetadataRequest[]): MetadataRequest[][] {
    const batches: MetadataRequest[][] = [];
    
    for (let i = 0; i < templates.length; i += this.batchSize) {
      batches.push(templates.slice(i, i + this.batchSize));
    }
    
    return batches;
  }
  
  /**
   * Create JSONL batch file
   */
  private async createBatchFile(templates: MetadataRequest[], batchName: string): Promise<string> {
    const filename = path.join(this.outputDir, `${batchName}_${Date.now()}.jsonl`);
    const stream = fs.createWriteStream(filename);
    
    for (const template of templates) {
      const request = this.generator.createBatchRequest(template);
      stream.write(JSON.stringify(request) + '\n');
    }
    
    stream.end();
    
    // Wait for stream to finish
    await new Promise<void>((resolve, reject) => {
      stream.on('finish', () => resolve());
      stream.on('error', reject);
    });
    
    logger.debug(`Created batch file: ${filename} with ${templates.length} requests`);
    return filename;
  }
  
  /**
   * Upload file to OpenAI
   */
  private async uploadFile(filepath: string): Promise<any> {
    const file = fs.createReadStream(filepath);
    const uploadedFile = await this.client.files.create({
      file,
      purpose: 'batch'
    });
    
    logger.debug(`Uploaded file: ${uploadedFile.id}`);
    return uploadedFile;
  }
  
  /**
   * Create batch job
   */
  private async createBatchJob(fileId: string): Promise<any> {
    const batchJob = await this.client.batches.create({
      input_file_id: fileId,
      endpoint: '/v1/chat/completions',
      completion_window: '24h'
    });
    
    logger.info(`Created batch job: ${batchJob.id}`);
    return batchJob;
  }
  
  /**
   * Monitor batch job with fixed 1-minute polling interval
   */
  private async monitorBatchJob(batchId: string): Promise<any> {
    const pollInterval = 60; // Check every 60 seconds (1 minute)
    let attempts = 0;
    const maxAttempts = 120; // 120 minutes max (2 hours)
    const startTime = Date.now();
    let lastStatus = '';

    while (attempts < maxAttempts) {
      const batchJob = await this.client.batches.retrieve(batchId);
      const elapsedMinutes = Math.floor((Date.now() - startTime) / 60000);

      // Log status on every check (not just on change)
      const statusSymbol = batchJob.status === 'in_progress' ? '⚙️' :
                          batchJob.status === 'finalizing' ? '📦' :
                          batchJob.status === 'validating' ? '🔍' :
                          batchJob.status === 'completed' ? '✅' :
                          batchJob.status === 'failed' ? '❌' : '⏳';

      console.log(`   ${statusSymbol} Batch ${batchId.slice(-8)}: ${batchJob.status} (${elapsedMinutes} min, check ${attempts + 1})`);

      if (batchJob.status !== lastStatus) {
        logger.info(`Batch ${batchId} status changed: ${lastStatus} -> ${batchJob.status}`);
        lastStatus = batchJob.status;
      }

      if (batchJob.status === 'completed') {
        console.log(`   ✅ Batch ${batchId.slice(-8)} completed successfully in ${elapsedMinutes} minutes`);
        logger.info(`Batch job ${batchId} completed successfully`);
        return batchJob;
      }

      if (['failed', 'expired', 'cancelled'].includes(batchJob.status)) {
        logger.error(`Batch job ${batchId} failed with status: ${batchJob.status}`);
        throw new Error(`Batch job failed with status: ${batchJob.status}`);
      }

      // Wait before next check (always 1 minute)
      logger.debug(`Waiting ${pollInterval} seconds before next check...`);
      await this.sleep(pollInterval * 1000);

      attempts++;
    }

    throw new Error(`Batch job monitoring timed out after ${maxAttempts} minutes`);
  }
  
  /**
   * Retrieve and parse results
   */
  private async retrieveResults(batchJob: any): Promise<MetadataResult[]> {
    const results: MetadataResult[] = [];

    // Check if we have an output file (successful results)
    if (batchJob.output_file_id) {
      const fileResponse = await this.client.files.content(batchJob.output_file_id);
      const fileContent = await fileResponse.text();

      const lines = fileContent.trim().split('\n');
      for (const line of lines) {
        if (!line) continue;
        try {
          const result = JSON.parse(line);
          const parsed = this.generator.parseResult(result);
          results.push(parsed);
        } catch (error) {
          logger.error('Error parsing result line:', error);
        }
      }
      logger.info(`Retrieved ${results.length} successful results from batch job`);
    }

    // Check if we have an error file (failed results)
    if (batchJob.error_file_id) {
      logger.warn(`Batch job has error file: ${batchJob.error_file_id}`);

      try {
        const errorResponse = await this.client.files.content(batchJob.error_file_id);
        const errorContent = await errorResponse.text();

        // Save error file locally for debugging
        const errorFilePath = path.join(this.outputDir, `batch_${batchJob.id}_error.jsonl`);
        fs.writeFileSync(errorFilePath, errorContent);
        logger.warn(`Error file saved to: ${errorFilePath}`);

        // Parse errors and create default metadata for failed templates
        const errorLines = errorContent.trim().split('\n');
        logger.warn(`Found ${errorLines.length} failed requests in error file`);

        for (const line of errorLines) {
          if (!line) continue;
          try {
            const errorResult = JSON.parse(line);
            const templateId = parseInt(errorResult.custom_id?.replace('template-', '') || '0');

            if (templateId > 0) {
              const errorMessage = errorResult.response?.body?.error?.message ||
                                  errorResult.error?.message ||
                                  'Unknown error';

              logger.debug(`Template ${templateId} failed: ${errorMessage}`);

              // Use getDefaultMetadata() from generator (it's private but accessible via bracket notation)
              const defaultMeta = (this.generator as any).getDefaultMetadata();
              results.push({
                templateId,
                metadata: defaultMeta,
                error: errorMessage
              });
            }
          } catch (parseError) {
            logger.error('Error parsing error line:', parseError);
          }
        }
      } catch (error) {
        logger.error('Failed to process error file:', error);
      }
    }

    // If we have no results at all, something is very wrong
    if (results.length === 0 && !batchJob.output_file_id && !batchJob.error_file_id) {
      throw new Error('No output file or error file available for batch job');
    }

    logger.info(`Total results (successful + failed): ${results.length}`);
    return results;
  }
  
  /**
   * Cleanup temporary files
   */
  private async cleanup(localFile: string, inputFileId: string, outputFileId?: string): Promise<void> {
    // Delete local file
    try {
      fs.unlinkSync(localFile);
      logger.debug(`Deleted local file: ${localFile}`);
    } catch (error) {
      logger.warn(`Failed to delete local file: ${localFile}`, error);
    }
    
    // Delete uploaded files from OpenAI
    try {
      await this.client.files.del(inputFileId);
      logger.debug(`Deleted input file from OpenAI: ${inputFileId}`);
    } catch (error) {
      logger.warn(`Failed to delete input file from OpenAI: ${inputFileId}`, error);
    }
    
    if (outputFileId) {
      try {
        await this.client.files.del(outputFileId);
        logger.debug(`Deleted output file from OpenAI: ${outputFileId}`);
      } catch (error) {
        logger.warn(`Failed to delete output file from OpenAI: ${outputFileId}`, error);
      }
    }
  }
  
  /**
   * Sleep helper
   */
  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}
```

--------------------------------------------------------------------------------
/tests/unit/mcp/tools-documentation.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
  getToolDocumentation,
  getToolsOverview,
  searchToolDocumentation,
  getToolsByCategory,
  getAllCategories
} from '@/mcp/tools-documentation';

// Mock the tool-docs import
vi.mock('@/mcp/tool-docs', () => ({
  toolsDocumentation: {
    search_nodes: {
      name: 'search_nodes',
      category: 'discovery',
      essentials: {
        description: 'Search nodes by keywords',
        keyParameters: ['query', 'mode', 'limit'],
        example: 'search_nodes({query: "slack"})',
        performance: 'Instant (<10ms)',
        tips: ['Use single words for precision', 'Try FUZZY mode for typos']
      },
      full: {
        description: 'Full-text search across all n8n nodes with multiple matching modes',
        parameters: {
          query: {
            type: 'string',
            description: 'Search terms',
            required: true
          },
          mode: {
            type: 'string',
            description: 'Search mode',
            enum: ['OR', 'AND', 'FUZZY'],
            default: 'OR'
          },
          limit: {
            type: 'number',
            description: 'Max results',
            default: 20
          }
        },
        returns: 'Array of matching nodes with metadata',
        examples: [
          'search_nodes({query: "webhook"})',
          'search_nodes({query: "http request", mode: "AND"})'
        ],
        useCases: ['Finding integration nodes', 'Discovering available triggers'],
        performance: 'Instant - uses in-memory index',
        bestPractices: ['Start with single words', 'Use FUZZY for uncertain names'],
        pitfalls: ['Overly specific queries may return no results'],
        relatedTools: ['list_nodes', 'get_node_info']
      }
    },
    validate_workflow: {
      name: 'validate_workflow',
      category: 'validation',
      essentials: {
        description: 'Validate complete workflow structure',
        keyParameters: ['workflow', 'options'],
        example: 'validate_workflow(workflow)',
        performance: 'Moderate (100-500ms)',
        tips: ['Run before deployment', 'Check all validation types']
      },
      full: {
        description: 'Comprehensive workflow validation',
        parameters: {
          workflow: {
            type: 'object',
            description: 'Workflow JSON',
            required: true
          },
          options: {
            type: 'object',
            description: 'Validation options'
          }
        },
        returns: 'Validation results with errors and warnings',
        examples: ['validate_workflow(workflow)'],
        useCases: ['Pre-deployment checks', 'CI/CD validation'],
        performance: 'Depends on workflow complexity',
        bestPractices: ['Validate before saving', 'Fix errors first'],
        pitfalls: ['Large workflows may take time'],
        relatedTools: ['validate_node_operation']
      }
    },
    get_node_essentials: {
      name: 'get_node_essentials',
      category: 'configuration',
      essentials: {
        description: 'Get essential node properties only',
        keyParameters: ['nodeType'],
        example: 'get_node_essentials("nodes-base.slack")',
        performance: 'Fast (<100ms)',
        tips: ['Use this before get_node_info', 'Returns 95% smaller payload']
      },
      full: {
        description: 'Returns 10-20 most important properties',
        parameters: {
          nodeType: {
            type: 'string',
            description: 'Full node type with prefix',
            required: true
          }
        },
        returns: 'Essential properties with examples',
        examples: ['get_node_essentials("nodes-base.httpRequest")'],
        useCases: ['Quick configuration', 'Property discovery'],
        performance: 'Fast - pre-filtered data',
        bestPractices: ['Always try essentials first'],
        pitfalls: ['May not include all advanced options'],
        relatedTools: ['get_node_info']
      }
    }
  }
}));

// No need to mock package.json - let the actual module read it

describe('tools-documentation', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('getToolDocumentation', () => {
    describe('essentials mode', () => {
      it('should return essential documentation for existing tool', () => {
        const doc = getToolDocumentation('search_nodes', 'essentials');
        
        expect(doc).toContain('# search_nodes');
        expect(doc).toContain('Search nodes by keywords');
        expect(doc).toContain('**Example**: search_nodes({query: "slack"})');
        expect(doc).toContain('**Key parameters**: query, mode, limit');
        expect(doc).toContain('**Performance**: Instant (<10ms)');
        expect(doc).toContain('- Use single words for precision');
        expect(doc).toContain('- Try FUZZY mode for typos');
        expect(doc).toContain('For full documentation, use: tools_documentation({topic: "search_nodes", depth: "full"})');
      });

      it('should return error message for unknown tool', () => {
        const doc = getToolDocumentation('unknown_tool', 'essentials');
        expect(doc).toBe("Tool 'unknown_tool' not found. Use tools_documentation() to see available tools.");
      });

      it('should use essentials as default depth', () => {
        const docDefault = getToolDocumentation('search_nodes');
        const docEssentials = getToolDocumentation('search_nodes', 'essentials');
        expect(docDefault).toBe(docEssentials);
      });
    });

    describe('full mode', () => {
      it('should return complete documentation for existing tool', () => {
        const doc = getToolDocumentation('search_nodes', 'full');
        
        expect(doc).toContain('# search_nodes');
        expect(doc).toContain('Full-text search across all n8n nodes');
        expect(doc).toContain('## Parameters');
        expect(doc).toContain('- **query** (string, required): Search terms');
        expect(doc).toContain('- **mode** (string): Search mode');
        expect(doc).toContain('- **limit** (number): Max results');
        expect(doc).toContain('## Returns');
        expect(doc).toContain('Array of matching nodes with metadata');
        expect(doc).toContain('## Examples');
        expect(doc).toContain('search_nodes({query: "webhook"})');
        expect(doc).toContain('## Common Use Cases');
        expect(doc).toContain('- Finding integration nodes');
        expect(doc).toContain('## Performance');
        expect(doc).toContain('Instant - uses in-memory index');
        expect(doc).toContain('## Best Practices');
        expect(doc).toContain('- Start with single words');
        expect(doc).toContain('## Common Pitfalls');
        expect(doc).toContain('- Overly specific queries');
        expect(doc).toContain('## Related Tools');
        expect(doc).toContain('- list_nodes');
      });
    });

    describe('special documentation topics', () => {
      it('should return JavaScript Code node guide for javascript_code_node_guide', () => {
        const doc = getToolDocumentation('javascript_code_node_guide', 'essentials');
        expect(doc).toContain('# JavaScript Code Node Guide');
        expect(doc).toContain('$input.all()');
        expect(doc).toContain('DateTime');
      });

      it('should return Python Code node guide for python_code_node_guide', () => {
        const doc = getToolDocumentation('python_code_node_guide', 'essentials');
        expect(doc).toContain('# Python Code Node Guide');
        expect(doc).toContain('_input.all()');
        expect(doc).toContain('_json');
      });

      it('should return full JavaScript guide when requested', () => {
        const doc = getToolDocumentation('javascript_code_node_guide', 'full');
        expect(doc).toContain('# JavaScript Code Node Complete Guide');
        expect(doc).toContain('## Data Access Patterns');
        expect(doc).toContain('## Available Built-in Functions');
        expect(doc).toContain('$helpers.httpRequest');
      });

      it('should return full Python guide when requested', () => {
        const doc = getToolDocumentation('python_code_node_guide', 'full');
        expect(doc).toContain('# Python Code Node Complete Guide');
        expect(doc).toContain('## Available Built-in Modules');
        expect(doc).toContain('## Limitations & Workarounds');
        expect(doc).toContain('import json');
      });
    });
  });

  describe('getToolsOverview', () => {
    describe('essentials mode', () => {
      it('should return essential overview with categories', () => {
        const overview = getToolsOverview('essentials');
        
        expect(overview).toContain('# n8n MCP Tools Reference');
        expect(overview).toContain('## Important: Compatibility Notice');
        // The tools-documentation module dynamically reads version from package.json
        // so we need to read it the same way to match
        const packageJson = require('../../../package.json');
        const n8nVersion = packageJson.dependencies.n8n.replace(/[^0-9.]/g, '');
        expect(overview).toContain(`n8n version ${n8nVersion}`);
        expect(overview).toContain('## Code Node Configuration');
        expect(overview).toContain('## Standard Workflow Pattern');
        expect(overview).toContain('**Discovery Tools**');
        expect(overview).toContain('**Configuration Tools**');
        expect(overview).toContain('**Validation Tools**');
        expect(overview).toContain('## Performance Characteristics');
        expect(overview).toContain('- Instant (<10ms)');
        expect(overview).toContain('tools_documentation({topic: "tool_name", depth: "full"})');
      });

      it('should use essentials as default', () => {
        const overviewDefault = getToolsOverview();
        const overviewEssentials = getToolsOverview('essentials');
        expect(overviewDefault).toBe(overviewEssentials);
      });
    });

    describe('full mode', () => {
      it('should return complete overview with all tools', () => {
        const overview = getToolsOverview('full');
        
        expect(overview).toContain('# n8n MCP Tools - Complete Reference');
        expect(overview).toContain('## All Available Tools by Category');
        expect(overview).toContain('### Discovery');
        expect(overview).toContain('- **search_nodes**: Search nodes by keywords');
        expect(overview).toContain('### Validation');
        expect(overview).toContain('- **validate_workflow**: Validate complete workflow structure');
        expect(overview).toContain('## Usage Notes');
      });
    });
  });

  describe('searchToolDocumentation', () => {
    it('should find tools matching keyword in name', () => {
      const results = searchToolDocumentation('search');
      expect(results).toContain('search_nodes');
    });

    it('should find tools matching keyword in description', () => {
      const results = searchToolDocumentation('workflow');
      expect(results).toContain('validate_workflow');
    });

    it('should be case insensitive', () => {
      const resultsLower = searchToolDocumentation('search');
      const resultsUpper = searchToolDocumentation('SEARCH');
      expect(resultsLower).toEqual(resultsUpper);
    });

    it('should return empty array for no matches', () => {
      const results = searchToolDocumentation('nonexistentxyz123');
      expect(results).toEqual([]);
    });

    it('should search in both essentials and full descriptions', () => {
      const results = searchToolDocumentation('validation');
      expect(results.length).toBeGreaterThan(0);
    });
  });

  describe('getToolsByCategory', () => {
    it('should return tools for discovery category', () => {
      const tools = getToolsByCategory('discovery');
      expect(tools).toContain('search_nodes');
    });

    it('should return tools for validation category', () => {
      const tools = getToolsByCategory('validation');
      expect(tools).toContain('validate_workflow');
    });

    it('should return tools for configuration category', () => {
      const tools = getToolsByCategory('configuration');
      expect(tools).toContain('get_node_essentials');
    });

    it('should return empty array for unknown category', () => {
      const tools = getToolsByCategory('unknown_category');
      expect(tools).toEqual([]);
    });
  });

  describe('getAllCategories', () => {
    it('should return all unique categories', () => {
      const categories = getAllCategories();
      expect(categories).toContain('discovery');
      expect(categories).toContain('validation');
      expect(categories).toContain('configuration');
    });

    it('should not have duplicates', () => {
      const categories = getAllCategories();
      const uniqueCategories = new Set(categories);
      expect(categories.length).toBe(uniqueCategories.size);
    });

    it('should return non-empty array', () => {
      const categories = getAllCategories();
      expect(categories.length).toBeGreaterThan(0);
    });
  });

  describe('Error Handling', () => {
    it('should handle missing tool gracefully', () => {
      const doc = getToolDocumentation('missing_tool');
      expect(doc).toContain("Tool 'missing_tool' not found");
      expect(doc).toContain('Use tools_documentation()');
    });

    it('should handle empty search query', () => {
      const results = searchToolDocumentation('');
      // Should match all tools since empty string is in everything
      expect(results.length).toBeGreaterThan(0);
    });
  });

  describe('Documentation Quality', () => {
    it('should format parameters correctly in full mode', () => {
      const doc = getToolDocumentation('search_nodes', 'full');
      
      // Check parameter formatting
      expect(doc).toMatch(/- \*\*query\*\* \(string, required\): Search terms/);
      expect(doc).toMatch(/- \*\*mode\*\* \(string\): Search mode/);
      expect(doc).toMatch(/- \*\*limit\*\* \(number\): Max results/);
    });

    it('should include code blocks for examples', () => {
      const doc = getToolDocumentation('search_nodes', 'full');
      expect(doc).toContain('```javascript');
      expect(doc).toContain('```');
    });

    it('should have consistent section headers', () => {
      const doc = getToolDocumentation('search_nodes', 'full');
      const expectedSections = [
        '## Parameters',
        '## Returns',
        '## Examples',
        '## Common Use Cases',
        '## Performance',
        '## Best Practices',
        '## Common Pitfalls',
        '## Related Tools'
      ];
      
      expectedSections.forEach(section => {
        expect(doc).toContain(section);
      });
    });
  });
});
```
Page 16/46FirstPrevNextLast