#
tokens: 48263/50000 14/614 files (page 13/45)
lines: off (toggle) GitHub
raw markdown copy
This is page 13 of 45. 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-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
│   │   │   ├── template-node-configs.test.ts
│   │   │   ├── template-repository.test.ts
│   │   │   ├── test-utils.ts
│   │   │   └── transactions.test.ts
│   │   ├── database-integration.test.ts
│   │   ├── docker
│   │   │   ├── docker-config.test.ts
│   │   │   ├── docker-entrypoint.test.ts
│   │   │   └── test-helpers.ts
│   │   ├── flexible-instance-config.test.ts
│   │   ├── mcp
│   │   │   └── template-examples-e2e.test.ts
│   │   ├── mcp-protocol
│   │   │   ├── basic-connection.test.ts
│   │   │   ├── error-handling.test.ts
│   │   │   ├── performance.test.ts
│   │   │   ├── protocol-compliance.test.ts
│   │   │   ├── README.md
│   │   │   ├── session-management.test.ts
│   │   │   ├── test-helpers.ts
│   │   │   ├── tool-invocation.test.ts
│   │   │   └── workflow-error-validation.test.ts
│   │   ├── msw-setup.test.ts
│   │   ├── n8n-api
│   │   │   ├── executions
│   │   │   │   ├── delete-execution.test.ts
│   │   │   │   ├── get-execution.test.ts
│   │   │   │   ├── list-executions.test.ts
│   │   │   │   └── trigger-webhook.test.ts
│   │   │   ├── scripts
│   │   │   │   └── cleanup-orphans.ts
│   │   │   ├── system
│   │   │   │   ├── diagnostic.test.ts
│   │   │   │   ├── health-check.test.ts
│   │   │   │   └── list-tools.test.ts
│   │   │   ├── test-connection.ts
│   │   │   ├── types
│   │   │   │   └── mcp-responses.ts
│   │   │   ├── utils
│   │   │   │   ├── cleanup-helpers.ts
│   │   │   │   ├── credentials.ts
│   │   │   │   ├── factories.ts
│   │   │   │   ├── fixtures.ts
│   │   │   │   ├── mcp-context.ts
│   │   │   │   ├── n8n-client.ts
│   │   │   │   ├── node-repository.ts
│   │   │   │   ├── response-types.ts
│   │   │   │   ├── test-context.ts
│   │   │   │   └── webhook-workflows.ts
│   │   │   └── workflows
│   │   │       ├── autofix-workflow.test.ts
│   │   │       ├── create-workflow.test.ts
│   │   │       ├── delete-workflow.test.ts
│   │   │       ├── get-workflow-details.test.ts
│   │   │       ├── get-workflow-minimal.test.ts
│   │   │       ├── get-workflow-structure.test.ts
│   │   │       ├── get-workflow.test.ts
│   │   │       ├── list-workflows.test.ts
│   │   │       ├── smart-parameters.test.ts
│   │   │       ├── update-partial-workflow.test.ts
│   │   │       ├── update-workflow.test.ts
│   │   │       └── validate-workflow.test.ts
│   │   ├── security
│   │   │   ├── command-injection-prevention.test.ts
│   │   │   └── rate-limiting.test.ts
│   │   ├── setup
│   │   │   ├── integration-setup.ts
│   │   │   └── msw-test-server.ts
│   │   ├── telemetry
│   │   │   ├── docker-user-id-stability.test.ts
│   │   │   └── mcp-telemetry.test.ts
│   │   ├── templates
│   │   │   └── metadata-operations.test.ts
│   │   └── workflow-creation-node-type-format.test.ts
│   ├── logger.test.ts
│   ├── MOCKING_STRATEGY.md
│   ├── mocks
│   │   ├── n8n-api
│   │   │   ├── data
│   │   │   │   ├── credentials.ts
│   │   │   │   ├── executions.ts
│   │   │   │   └── workflows.ts
│   │   │   ├── handlers.ts
│   │   │   └── index.ts
│   │   └── README.md
│   ├── node-storage-export.json
│   ├── setup
│   │   ├── global-setup.ts
│   │   ├── msw-setup.ts
│   │   ├── TEST_ENV_DOCUMENTATION.md
│   │   └── test-env.ts
│   ├── test-database-extraction.js
│   ├── test-direct-extraction.js
│   ├── test-enhanced-documentation.js
│   ├── test-enhanced-integration.js
│   ├── test-mcp-extraction.js
│   ├── test-mcp-server-extraction.js
│   ├── test-mcp-tools-integration.js
│   ├── test-node-documentation-service.js
│   ├── test-node-list.js
│   ├── test-package-info.js
│   ├── test-parsing-operations.js
│   ├── test-slack-node-complete.js
│   ├── test-small-rebuild.js
│   ├── test-sqlite-search.js
│   ├── test-storage-system.js
│   ├── unit
│   │   ├── __mocks__
│   │   │   ├── n8n-nodes-base.test.ts
│   │   │   ├── n8n-nodes-base.ts
│   │   │   └── README.md
│   │   ├── database
│   │   │   ├── __mocks__
│   │   │   │   └── better-sqlite3.ts
│   │   │   ├── database-adapter-unit.test.ts
│   │   │   ├── node-repository-core.test.ts
│   │   │   ├── node-repository-operations.test.ts
│   │   │   ├── node-repository-outputs.test.ts
│   │   │   ├── README.md
│   │   │   └── template-repository-core.test.ts
│   │   ├── docker
│   │   │   ├── config-security.test.ts
│   │   │   ├── edge-cases.test.ts
│   │   │   ├── parse-config.test.ts
│   │   │   └── serve-command.test.ts
│   │   ├── errors
│   │   │   └── validation-service-error.test.ts
│   │   ├── examples
│   │   │   └── using-n8n-nodes-base-mock.test.ts
│   │   ├── flexible-instance-security-advanced.test.ts
│   │   ├── flexible-instance-security.test.ts
│   │   ├── http-server
│   │   │   └── multi-tenant-support.test.ts
│   │   ├── http-server-n8n-mode.test.ts
│   │   ├── http-server-n8n-reinit.test.ts
│   │   ├── http-server-session-management.test.ts
│   │   ├── loaders
│   │   │   └── node-loader.test.ts
│   │   ├── mappers
│   │   │   └── docs-mapper.test.ts
│   │   ├── mcp
│   │   │   ├── get-node-essentials-examples.test.ts
│   │   │   ├── handlers-n8n-manager-simple.test.ts
│   │   │   ├── handlers-n8n-manager.test.ts
│   │   │   ├── handlers-workflow-diff.test.ts
│   │   │   ├── lru-cache-behavior.test.ts
│   │   │   ├── multi-tenant-tool-listing.test.ts.disabled
│   │   │   ├── parameter-validation.test.ts
│   │   │   ├── search-nodes-examples.test.ts
│   │   │   ├── tools-documentation.test.ts
│   │   │   └── tools.test.ts
│   │   ├── monitoring
│   │   │   └── cache-metrics.test.ts
│   │   ├── MULTI_TENANT_TEST_COVERAGE.md
│   │   ├── multi-tenant-integration.test.ts
│   │   ├── parsers
│   │   │   ├── node-parser-outputs.test.ts
│   │   │   ├── node-parser.test.ts
│   │   │   ├── property-extractor.test.ts
│   │   │   └── simple-parser.test.ts
│   │   ├── scripts
│   │   │   └── fetch-templates-extraction.test.ts
│   │   ├── services
│   │   │   ├── ai-node-validator.test.ts
│   │   │   ├── ai-tool-validators.test.ts
│   │   │   ├── confidence-scorer.test.ts
│   │   │   ├── config-validator-basic.test.ts
│   │   │   ├── config-validator-edge-cases.test.ts
│   │   │   ├── config-validator-node-specific.test.ts
│   │   │   ├── config-validator-security.test.ts
│   │   │   ├── debug-validator.test.ts
│   │   │   ├── enhanced-config-validator-integration.test.ts
│   │   │   ├── enhanced-config-validator-operations.test.ts
│   │   │   ├── enhanced-config-validator.test.ts
│   │   │   ├── example-generator.test.ts
│   │   │   ├── execution-processor.test.ts
│   │   │   ├── expression-format-validator.test.ts
│   │   │   ├── expression-validator-edge-cases.test.ts
│   │   │   ├── expression-validator.test.ts
│   │   │   ├── fixed-collection-validation.test.ts
│   │   │   ├── loop-output-edge-cases.test.ts
│   │   │   ├── n8n-api-client.test.ts
│   │   │   ├── n8n-validation.test.ts
│   │   │   ├── node-similarity-service.test.ts
│   │   │   ├── node-specific-validators.test.ts
│   │   │   ├── operation-similarity-service-comprehensive.test.ts
│   │   │   ├── operation-similarity-service.test.ts
│   │   │   ├── property-dependencies.test.ts
│   │   │   ├── property-filter-edge-cases.test.ts
│   │   │   ├── property-filter.test.ts
│   │   │   ├── resource-similarity-service-comprehensive.test.ts
│   │   │   ├── resource-similarity-service.test.ts
│   │   │   ├── task-templates.test.ts
│   │   │   ├── template-service.test.ts
│   │   │   ├── universal-expression-validator.test.ts
│   │   │   ├── validation-fixes.test.ts
│   │   │   ├── workflow-auto-fixer.test.ts
│   │   │   ├── workflow-diff-engine.test.ts
│   │   │   ├── workflow-fixed-collection-validation.test.ts
│   │   │   ├── workflow-validator-comprehensive.test.ts
│   │   │   ├── workflow-validator-edge-cases.test.ts
│   │   │   ├── workflow-validator-error-outputs.test.ts
│   │   │   ├── workflow-validator-expression-format.test.ts
│   │   │   ├── workflow-validator-loops-simple.test.ts
│   │   │   ├── workflow-validator-loops.test.ts
│   │   │   ├── workflow-validator-mocks.test.ts
│   │   │   ├── workflow-validator-performance.test.ts
│   │   │   ├── workflow-validator-with-mocks.test.ts
│   │   │   └── workflow-validator.test.ts
│   │   ├── telemetry
│   │   │   ├── batch-processor.test.ts
│   │   │   ├── config-manager.test.ts
│   │   │   ├── event-tracker.test.ts
│   │   │   ├── event-validator.test.ts
│   │   │   ├── rate-limiter.test.ts
│   │   │   ├── telemetry-error.test.ts
│   │   │   ├── telemetry-manager.test.ts
│   │   │   ├── v2.18.3-fixes-verification.test.ts
│   │   │   └── workflow-sanitizer.test.ts
│   │   ├── templates
│   │   │   ├── batch-processor.test.ts
│   │   │   ├── metadata-generator.test.ts
│   │   │   ├── template-repository-metadata.test.ts
│   │   │   └── template-repository-security.test.ts
│   │   ├── test-env-example.test.ts
│   │   ├── test-infrastructure.test.ts
│   │   ├── types
│   │   │   ├── instance-context-coverage.test.ts
│   │   │   └── instance-context-multi-tenant.test.ts
│   │   ├── utils
│   │   │   ├── auth-timing-safe.test.ts
│   │   │   ├── cache-utils.test.ts
│   │   │   ├── console-manager.test.ts
│   │   │   ├── database-utils.test.ts
│   │   │   ├── fixed-collection-validator.test.ts
│   │   │   ├── n8n-errors.test.ts
│   │   │   ├── node-type-normalizer.test.ts
│   │   │   ├── node-type-utils.test.ts
│   │   │   ├── node-utils.test.ts
│   │   │   ├── simple-cache-memory-leak-fix.test.ts
│   │   │   ├── ssrf-protection.test.ts
│   │   │   └── template-node-resolver.test.ts
│   │   └── validation-fixes.test.ts
│   └── utils
│       ├── assertions.ts
│       ├── builders
│       │   └── workflow.builder.ts
│       ├── data-generators.ts
│       ├── database-utils.ts
│       ├── README.md
│       └── test-helpers.ts
├── thumbnail.png
├── tsconfig.build.json
├── tsconfig.json
├── types
│   ├── mcp.d.ts
│   └── test-env.d.ts
├── verify-telemetry-fix.js
├── versioned-nodes.md
├── vitest.config.benchmark.ts
├── vitest.config.integration.ts
└── vitest.config.ts
```

# Files

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

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

// Mock dependencies
vi.mock('@/database/node-repository');
vi.mock('@/services/enhanced-config-validator');

describe('WorkflowValidator - SplitInBatches Validation (Simplified)', () => {
  let validator: WorkflowValidator;
  let mockNodeRepository: any;
  let mockNodeValidator: any;

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

    mockNodeRepository = {
      getNode: vi.fn()
    };

    mockNodeValidator = {
      validateWithMode: vi.fn().mockReturnValue({
        errors: [],
        warnings: []
      })
    };

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

  describe('SplitInBatches node detection', () => {
    it('should identify SplitInBatches nodes in workflow', async () => {
      mockNodeRepository.getNode.mockReturnValue({
        nodeType: 'nodes-base.splitInBatches',
        properties: []
      });

      const workflow = {
        name: 'SplitInBatches Workflow',
        nodes: [
          {
            id: '1',
            name: 'Split In Batches',
            type: 'n8n-nodes-base.splitInBatches',
            position: [100, 100],
            parameters: { batchSize: 10 }
          },
          {
            id: '2',
            name: 'Process Item',
            type: 'n8n-nodes-base.set',
            position: [300, 100],
            parameters: {}
          }
        ],
        connections: {
          'Split In Batches': {
            main: [
              [], // Done output (0)
              [{ node: 'Process Item', type: 'main', index: 0 }] // Loop output (1)
            ]
          }
        }
      };

      const result = await validator.validateWorkflow(workflow as any);

      // Should complete validation without crashing
      expect(result).toBeDefined();
      expect(result.valid).toBeDefined();
    });

    it('should handle SplitInBatches with processing node name patterns', async () => {
      mockNodeRepository.getNode.mockReturnValue({
        nodeType: 'nodes-base.splitInBatches',
        properties: []
      });

      const processingNames = [
        'Process Item',
        'Transform Data',
        'Handle Each',
        'Function Node',
        'Code Block'
      ];

      for (const nodeName of processingNames) {
        const workflow = {
          name: 'Processing Pattern Test',
          nodes: [
            {
              id: '1',
              name: 'Split In Batches',
              type: 'n8n-nodes-base.splitInBatches',
              position: [100, 100],
              parameters: {}
            },
            {
              id: '2',
              name: nodeName,
              type: 'n8n-nodes-base.function',
              position: [300, 100],
              parameters: {}
            }
          ],
          connections: {
            'Split In Batches': {
              main: [
                [{ node: nodeName, type: 'main', index: 0 }], // Processing node on Done output
                []
              ]
            }
          }
        };

        const result = await validator.validateWorkflow(workflow as any);
        
        // Should identify potential processing nodes
        expect(result).toBeDefined();
      }
    });

    it('should handle final processing node patterns', async () => {
      mockNodeRepository.getNode.mockReturnValue({
        nodeType: 'nodes-base.splitInBatches',
        properties: []
      });

      const finalNames = [
        'Final Summary',
        'Send Email',
        'Complete Notification',
        'Final Report'
      ];

      for (const nodeName of finalNames) {
        const workflow = {
          name: 'Final Pattern Test',
          nodes: [
            {
              id: '1',
              name: 'Split In Batches',
              type: 'n8n-nodes-base.splitInBatches',
              position: [100, 100],
              parameters: {}
            },
            {
              id: '2',
              name: nodeName,
              type: 'n8n-nodes-base.emailSend',
              position: [300, 100],
              parameters: {}
            }
          ],
          connections: {
            'Split In Batches': {
              main: [
                [{ node: nodeName, type: 'main', index: 0 }], // Final node on Done output (correct)
                []
              ]
            }
          }
        };

        const result = await validator.validateWorkflow(workflow as any);
        
        // Should not warn about final nodes on done output
        expect(result).toBeDefined();
      }
    });
  });

  describe('Connection validation', () => {
    it('should validate connection indices', async () => {
      mockNodeRepository.getNode.mockReturnValue({
        nodeType: 'nodes-base.splitInBatches',
        properties: []
      });

      const workflow = {
        name: 'Connection Index Test',
        nodes: [
          {
            id: '1',
            name: 'Split In Batches',
            type: 'n8n-nodes-base.splitInBatches',
            position: [100, 100],
            parameters: {}
          },
          {
            id: '2',
            name: 'Target',
            type: 'n8n-nodes-base.set',
            position: [300, 100],
            parameters: {}
          }
        ],
        connections: {
          'Split In Batches': {
            main: [
              [{ node: 'Target', type: 'main', index: -1 }] // Invalid negative index
            ]
          }
        }
      };

      const result = await validator.validateWorkflow(workflow as any);

      const negativeIndexErrors = result.errors.filter(e => 
        e.message?.includes('Invalid connection index -1')
      );
      expect(negativeIndexErrors.length).toBeGreaterThan(0);
    });

    it('should handle non-existent target nodes', async () => {
      mockNodeRepository.getNode.mockReturnValue({
        nodeType: 'nodes-base.splitInBatches',
        properties: []
      });

      const workflow = {
        name: 'Missing Target Test',
        nodes: [
          {
            id: '1',
            name: 'Split In Batches',
            type: 'n8n-nodes-base.splitInBatches',
            position: [100, 100],
            parameters: {}
          }
        ],
        connections: {
          'Split In Batches': {
            main: [
              [{ node: 'NonExistentNode', type: 'main', index: 0 }]
            ]
          }
        }
      };

      const result = await validator.validateWorkflow(workflow as any);

      const missingNodeErrors = result.errors.filter(e => 
        e.message?.includes('non-existent node')
      );
      expect(missingNodeErrors.length).toBeGreaterThan(0);
    });
  });

  describe('Self-referencing connections', () => {
    it('should allow self-referencing for SplitInBatches nodes', async () => {
      mockNodeRepository.getNode.mockReturnValue({
        nodeType: 'nodes-base.splitInBatches',
        properties: []
      });

      const workflow = {
        name: 'Self Reference Test',
        nodes: [
          {
            id: '1',
            name: 'Split In Batches',
            type: 'n8n-nodes-base.splitInBatches',
            position: [100, 100],
            parameters: {}
          }
        ],
        connections: {
          'Split In Batches': {
            main: [
              [],
              [{ node: 'Split In Batches', type: 'main', index: 0 }] // Self-reference on loop output
            ]
          }
        }
      };

      const result = await validator.validateWorkflow(workflow as any);

      // Should not warn about self-reference for SplitInBatches
      const selfRefWarnings = result.warnings.filter(w => 
        w.message?.includes('self-referencing')
      );
      expect(selfRefWarnings).toHaveLength(0);
    });

    it('should warn about self-referencing for non-loop nodes', async () => {
      mockNodeRepository.getNode.mockReturnValue({
        nodeType: 'nodes-base.set',
        properties: []
      });

      const workflow = {
        name: 'Non-Loop Self Reference Test',
        nodes: [
          {
            id: '1',
            name: 'Set',
            type: 'n8n-nodes-base.set',
            position: [100, 100],
            parameters: {}
          }
        ],
        connections: {
          'Set': {
            main: [
              [{ node: 'Set', type: 'main', index: 0 }] // Self-reference on regular node
            ]
          }
        }
      };

      const result = await validator.validateWorkflow(workflow as any);

      // Should warn about self-reference for non-loop nodes
      const selfRefWarnings = result.warnings.filter(w => 
        w.message?.includes('self-referencing')
      );
      expect(selfRefWarnings.length).toBeGreaterThan(0);
    });
  });

  describe('Output connection validation', () => {
    it('should validate output connections for nodes with outputs', async () => {
      mockNodeRepository.getNode.mockReturnValue({
        nodeType: 'nodes-base.if',
        outputs: [
          { displayName: 'True', description: 'Items that match condition' },
          { displayName: 'False', description: 'Items that do not match condition' }
        ],
        outputNames: ['true', 'false'],
        properties: []
      });

      const workflow = {
        name: 'IF Node Test',
        nodes: [
          {
            id: '1',
            name: 'IF',
            type: 'n8n-nodes-base.if',
            position: [100, 100],
            parameters: {}
          },
          {
            id: '2',
            name: 'True Handler',
            type: 'n8n-nodes-base.set',
            position: [300, 50],
            parameters: {}
          },
          {
            id: '3',
            name: 'False Handler',
            type: 'n8n-nodes-base.set',
            position: [300, 150],
            parameters: {}
          }
        ],
        connections: {
          'IF': {
            main: [
              [{ node: 'True Handler', type: 'main', index: 0 }],   // True output (0)
              [{ node: 'False Handler', type: 'main', index: 0 }]   // False output (1)
            ]
          }
        }
      };

      const result = await validator.validateWorkflow(workflow as any);

      // Should validate without major errors
      expect(result).toBeDefined();
      expect(result.statistics.validConnections).toBe(2);
    });
  });

  describe('Error handling', () => {
    it('should handle nodes without outputs gracefully', async () => {
      mockNodeRepository.getNode.mockReturnValue({
        nodeType: 'nodes-base.httpRequest',
        outputs: null,
        outputNames: null,
        properties: []
      });

      const workflow = {
        name: 'No Outputs Test',
        nodes: [
          {
            id: '1',
            name: 'HTTP Request',
            type: 'n8n-nodes-base.httpRequest',
            position: [100, 100],
            parameters: {}
          }
        ],
        connections: {}
      };

      const result = await validator.validateWorkflow(workflow as any);

      // Should handle gracefully without crashing
      expect(result).toBeDefined();
    });

    it('should handle unknown node types gracefully', async () => {
      mockNodeRepository.getNode.mockReturnValue(null);

      const workflow = {
        name: 'Unknown Node Test',
        nodes: [
          {
            id: '1',
            name: 'Unknown',
            type: 'n8n-nodes-base.unknown',
            position: [100, 100],
            parameters: {}
          }
        ],
        connections: {}
      };

      const result = await validator.validateWorkflow(workflow as any);

      // Should report unknown node error
      const unknownErrors = result.errors.filter(e => 
        e.message?.includes('Unknown node type')
      );
      expect(unknownErrors.length).toBeGreaterThan(0);
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/fixtures/template-configs.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Test fixtures for template node configurations
 * Used across unit and integration tests for P0-R3 feature
 */

import * as zlib from 'zlib';

export interface TemplateConfigFixture {
  node_type: string;
  template_id: number;
  template_name: string;
  template_views: number;
  node_name: string;
  parameters_json: string;
  credentials_json: string | null;
  has_credentials: number;
  has_expressions: number;
  complexity: 'simple' | 'medium' | 'complex';
  use_cases: string;
  rank?: number;
}

export interface WorkflowFixture {
  id: string;
  name: string;
  nodes: any[];
  connections: Record<string, any>;
  settings?: Record<string, any>;
}

/**
 * Sample node configurations for common use cases
 */
export const sampleConfigs: Record<string, TemplateConfigFixture> = {
  simpleWebhook: {
    node_type: 'n8n-nodes-base.webhook',
    template_id: 1,
    template_name: 'Simple Webhook Trigger',
    template_views: 5000,
    node_name: 'Webhook',
    parameters_json: JSON.stringify({
      httpMethod: 'POST',
      path: 'webhook',
      responseMode: 'lastNode',
      alwaysOutputData: true
    }),
    credentials_json: null,
    has_credentials: 0,
    has_expressions: 0,
    complexity: 'simple',
    use_cases: JSON.stringify(['webhook processing', 'trigger automation']),
    rank: 1
  },

  webhookWithAuth: {
    node_type: 'n8n-nodes-base.webhook',
    template_id: 2,
    template_name: 'Authenticated Webhook',
    template_views: 3000,
    node_name: 'Webhook',
    parameters_json: JSON.stringify({
      httpMethod: 'POST',
      path: 'secure-webhook',
      responseMode: 'responseNode',
      authentication: 'headerAuth'
    }),
    credentials_json: JSON.stringify({
      httpHeaderAuth: {
        id: '1',
        name: 'Header Auth'
      }
    }),
    has_credentials: 1,
    has_expressions: 0,
    complexity: 'medium',
    use_cases: JSON.stringify(['secure webhook', 'authenticated triggers']),
    rank: 2
  },

  httpRequestBasic: {
    node_type: 'n8n-nodes-base.httpRequest',
    template_id: 3,
    template_name: 'Basic HTTP GET Request',
    template_views: 10000,
    node_name: 'HTTP Request',
    parameters_json: JSON.stringify({
      url: 'https://api.example.com/data',
      method: 'GET',
      responseFormat: 'json',
      options: {
        timeout: 10000,
        redirect: {
          followRedirects: true
        }
      }
    }),
    credentials_json: null,
    has_credentials: 0,
    has_expressions: 0,
    complexity: 'simple',
    use_cases: JSON.stringify(['API calls', 'data fetching']),
    rank: 1
  },

  httpRequestWithExpressions: {
    node_type: 'n8n-nodes-base.httpRequest',
    template_id: 4,
    template_name: 'Dynamic HTTP Request',
    template_views: 7500,
    node_name: 'HTTP Request',
    parameters_json: JSON.stringify({
      url: '={{ $json.apiUrl }}',
      method: 'POST',
      sendBody: true,
      bodyParameters: {
        values: [
          {
            name: 'userId',
            value: '={{ $json.userId }}'
          },
          {
            name: 'action',
            value: '={{ $json.action }}'
          }
        ]
      },
      options: {
        timeout: '={{ $json.timeout || 10000 }}'
      }
    }),
    credentials_json: null,
    has_credentials: 0,
    has_expressions: 1,
    complexity: 'complex',
    use_cases: JSON.stringify(['dynamic API calls', 'expression-based routing']),
    rank: 2
  },

  slackMessage: {
    node_type: 'n8n-nodes-base.slack',
    template_id: 5,
    template_name: 'Send Slack Message',
    template_views: 8000,
    node_name: 'Slack',
    parameters_json: JSON.stringify({
      resource: 'message',
      operation: 'post',
      channel: '#general',
      text: 'Hello from n8n!'
    }),
    credentials_json: JSON.stringify({
      slackApi: {
        id: '2',
        name: 'Slack API'
      }
    }),
    has_credentials: 1,
    has_expressions: 0,
    complexity: 'simple',
    use_cases: JSON.stringify(['notifications', 'team communication']),
    rank: 1
  },

  codeNodeTransform: {
    node_type: 'n8n-nodes-base.code',
    template_id: 6,
    template_name: 'Data Transformation',
    template_views: 6000,
    node_name: 'Code',
    parameters_json: JSON.stringify({
      mode: 'runOnceForAllItems',
      jsCode: `const items = $input.all();

return items.map(item => ({
  json: {
    id: item.json.id,
    name: item.json.name.toUpperCase(),
    timestamp: new Date().toISOString()
  }
}));`
    }),
    credentials_json: null,
    has_credentials: 0,
    has_expressions: 0,
    complexity: 'medium',
    use_cases: JSON.stringify(['data transformation', 'custom logic']),
    rank: 1
  },

  codeNodeWithExpressions: {
    node_type: 'n8n-nodes-base.code',
    template_id: 7,
    template_name: 'Advanced Code with Expressions',
    template_views: 4500,
    node_name: 'Code',
    parameters_json: JSON.stringify({
      mode: 'runOnceForEachItem',
      jsCode: `const data = $input.item.json;
const previousNode = $('HTTP Request').first().json;

return {
  json: {
    combined: data.value + previousNode.value,
    nodeRef: $node
  }
};`
    }),
    credentials_json: null,
    has_credentials: 0,
    has_expressions: 1,
    complexity: 'complex',
    use_cases: JSON.stringify(['advanced transformations', 'node references']),
    rank: 2
  }
};

/**
 * Sample workflows for testing extraction
 */
export const sampleWorkflows: Record<string, WorkflowFixture> = {
  webhookToSlack: {
    id: '1',
    name: 'Webhook to Slack Notification',
    nodes: [
      {
        id: 'webhook1',
        name: 'Webhook',
        type: 'n8n-nodes-base.webhook',
        typeVersion: 1,
        position: [250, 300],
        parameters: {
          httpMethod: 'POST',
          path: 'alert',
          responseMode: 'lastNode'
        }
      },
      {
        id: 'slack1',
        name: 'Slack',
        type: 'n8n-nodes-base.slack',
        typeVersion: 1,
        position: [450, 300],
        parameters: {
          resource: 'message',
          operation: 'post',
          channel: '#alerts',
          text: '={{ $json.message }}'
        },
        credentials: {
          slackApi: {
            id: '1',
            name: 'Slack API'
          }
        }
      }
    ],
    connections: {
      webhook1: {
        main: [[{ node: 'slack1', type: 'main', index: 0 }]]
      }
    },
    settings: {}
  },

  apiWorkflow: {
    id: '2',
    name: 'API Data Processing',
    nodes: [
      {
        id: 'http1',
        name: 'Fetch Data',
        type: 'n8n-nodes-base.httpRequest',
        typeVersion: 3,
        position: [250, 300],
        parameters: {
          url: 'https://api.example.com/users',
          method: 'GET',
          responseFormat: 'json'
        }
      },
      {
        id: 'code1',
        name: 'Transform',
        type: 'n8n-nodes-base.code',
        typeVersion: 2,
        position: [450, 300],
        parameters: {
          mode: 'runOnceForAllItems',
          jsCode: 'return $input.all().map(item => ({ json: { ...item.json, processed: true } }));'
        }
      },
      {
        id: 'http2',
        name: 'Send Results',
        type: 'n8n-nodes-base.httpRequest',
        typeVersion: 3,
        position: [650, 300],
        parameters: {
          url: '={{ $json.callbackUrl }}',
          method: 'POST',
          sendBody: true,
          bodyParameters: {
            values: [
              { name: 'data', value: '={{ JSON.stringify($json) }}' }
            ]
          }
        }
      }
    ],
    connections: {
      http1: {
        main: [[{ node: 'code1', type: 'main', index: 0 }]]
      },
      code1: {
        main: [[{ node: 'http2', type: 'main', index: 0 }]]
      }
    },
    settings: {}
  },

  complexWorkflow: {
    id: '3',
    name: 'Complex Multi-Node Workflow',
    nodes: [
      {
        id: 'webhook1',
        name: 'Start',
        type: 'n8n-nodes-base.webhook',
        typeVersion: 1,
        position: [100, 300],
        parameters: {
          httpMethod: 'POST',
          path: 'start'
        }
      },
      {
        id: 'sticky1',
        name: 'Note',
        type: 'n8n-nodes-base.stickyNote',
        typeVersion: 1,
        position: [100, 200],
        parameters: {
          content: 'This workflow processes incoming data'
        }
      },
      {
        id: 'if1',
        name: 'Check Type',
        type: 'n8n-nodes-base.if',
        typeVersion: 1,
        position: [300, 300],
        parameters: {
          conditions: {
            boolean: [
              {
                value1: '={{ $json.type }}',
                value2: 'premium'
              }
            ]
          }
        }
      },
      {
        id: 'http1',
        name: 'Premium API',
        type: 'n8n-nodes-base.httpRequest',
        typeVersion: 3,
        position: [500, 200],
        parameters: {
          url: 'https://api.example.com/premium',
          method: 'POST'
        }
      },
      {
        id: 'http2',
        name: 'Standard API',
        type: 'n8n-nodes-base.httpRequest',
        typeVersion: 3,
        position: [500, 400],
        parameters: {
          url: 'https://api.example.com/standard',
          method: 'POST'
        }
      }
    ],
    connections: {
      webhook1: {
        main: [[{ node: 'if1', type: 'main', index: 0 }]]
      },
      if1: {
        main: [
          [{ node: 'http1', type: 'main', index: 0 }],
          [{ node: 'http2', type: 'main', index: 0 }]
        ]
      }
    },
    settings: {}
  }
};

/**
 * Compress workflow to base64 (mimics n8n template format)
 */
export function compressWorkflow(workflow: WorkflowFixture): string {
  const json = JSON.stringify(workflow);
  return zlib.gzipSync(Buffer.from(json, 'utf-8')).toString('base64');
}

/**
 * Create template metadata
 */
export function createTemplateMetadata(complexity: 'simple' | 'medium' | 'complex', useCases: string[]) {
  return {
    complexity,
    use_cases: useCases
  };
}

/**
 * Batch create configs for testing
 */
export function createConfigBatch(nodeType: string, count: number): TemplateConfigFixture[] {
  return Array.from({ length: count }, (_, i) => ({
    node_type: nodeType,
    template_id: i + 1,
    template_name: `Template ${i + 1}`,
    template_views: 1000 - (i * 50),
    node_name: `Node ${i + 1}`,
    parameters_json: JSON.stringify({ index: i }),
    credentials_json: null,
    has_credentials: 0,
    has_expressions: 0,
    complexity: (['simple', 'medium', 'complex'] as const)[i % 3],
    use_cases: JSON.stringify(['test use case']),
    rank: i + 1
  }));
}

/**
 * Get config by complexity
 */
export function getConfigByComplexity(complexity: 'simple' | 'medium' | 'complex'): TemplateConfigFixture {
  const configs = Object.values(sampleConfigs);
  const match = configs.find(c => c.complexity === complexity);
  return match || configs[0];
}

/**
 * Get configs with expressions
 */
export function getConfigsWithExpressions(): TemplateConfigFixture[] {
  return Object.values(sampleConfigs).filter(c => c.has_expressions === 1);
}

/**
 * Get configs with credentials
 */
export function getConfigsWithCredentials(): TemplateConfigFixture[] {
  return Object.values(sampleConfigs).filter(c => c.has_credentials === 1);
}

/**
 * Mock database insert helper
 */
export function createInsertStatement(config: TemplateConfigFixture): string {
  return `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 (
    '${config.node_type}',
    ${config.template_id},
    '${config.template_name}',
    ${config.template_views},
    '${config.node_name}',
    '${config.parameters_json.replace(/'/g, "''")}',
    ${config.credentials_json ? `'${config.credentials_json.replace(/'/g, "''")}'` : 'NULL'},
    ${config.has_credentials},
    ${config.has_expressions},
    '${config.complexity}',
    '${config.use_cases.replace(/'/g, "''")}',
    ${config.rank || 0}
  )`;
}

```

--------------------------------------------------------------------------------
/src/services/n8n-api-client.ts:
--------------------------------------------------------------------------------

```typescript
import axios, { AxiosInstance, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios';
import { logger } from '../utils/logger';
import {
  Workflow,
  WorkflowListParams,
  WorkflowListResponse,
  Execution,
  ExecutionListParams,
  ExecutionListResponse,
  Credential,
  CredentialListParams,
  CredentialListResponse,
  Tag,
  TagListParams,
  TagListResponse,
  HealthCheckResponse,
  Variable,
  WebhookRequest,
  WorkflowExport,
  WorkflowImport,
  SourceControlStatus,
  SourceControlPullResult,
  SourceControlPushResult,
} from '../types/n8n-api';
import { handleN8nApiError, logN8nError } from '../utils/n8n-errors';
import { cleanWorkflowForCreate, cleanWorkflowForUpdate } from './n8n-validation';

export interface N8nApiClientConfig {
  baseUrl: string;
  apiKey: string;
  timeout?: number;
  maxRetries?: number;
}

export class N8nApiClient {
  private client: AxiosInstance;
  private maxRetries: number;

  constructor(config: N8nApiClientConfig) {
    const { baseUrl, apiKey, timeout = 30000, maxRetries = 3 } = config;

    this.maxRetries = maxRetries;

    // Ensure baseUrl ends with /api/v1
    const apiUrl = baseUrl.endsWith('/api/v1') 
      ? baseUrl 
      : `${baseUrl.replace(/\/$/, '')}/api/v1`;

    this.client = axios.create({
      baseURL: apiUrl,
      timeout,
      headers: {
        'X-N8N-API-KEY': apiKey,
        'Content-Type': 'application/json',
      },
    });

    // Request interceptor for logging
    this.client.interceptors.request.use(
      (config: InternalAxiosRequestConfig) => {
        logger.debug(`n8n API Request: ${config.method?.toUpperCase()} ${config.url}`, {
          params: config.params,
          data: config.data,
        });
        return config;
      },
      (error: unknown) => {
        logger.error('n8n API Request Error:', error);
        return Promise.reject(error);
      }
    );

    // Response interceptor for logging
    this.client.interceptors.response.use(
      (response: any) => {
        logger.debug(`n8n API Response: ${response.status} ${response.config.url}`);
        return response;
      },
      (error: unknown) => {
        const n8nError = handleN8nApiError(error);
        logN8nError(n8nError, 'n8n API Response');
        return Promise.reject(n8nError);
      }
    );
  }

  // Health check to verify API connectivity
  async healthCheck(): Promise<HealthCheckResponse> {
    try {
      // Try the standard healthz endpoint (available on all n8n instances)
      const baseUrl = this.client.defaults.baseURL || '';
      const healthzUrl = baseUrl.replace(/\/api\/v\d+\/?$/, '') + '/healthz';
      
      const response = await axios.get(healthzUrl, {
        timeout: 5000,
        validateStatus: (status) => status < 500
      });
      
      if (response.status === 200 && response.data?.status === 'ok') {
        return { 
          status: 'ok',
          features: {} // Features detection would require additional endpoints
        };
      }
      
      // If healthz doesn't work, fall back to API check
      throw new Error('healthz endpoint not available');
    } catch (error) {
      // If healthz endpoint doesn't exist, try listing workflows with limit 1
      // This is a fallback for older n8n versions
      try {
        await this.client.get('/workflows', { params: { limit: 1 } });
        return { 
          status: 'ok',
          features: {}
        };
      } catch (fallbackError) {
        throw handleN8nApiError(fallbackError);
      }
    }
  }

  // Workflow Management
  async createWorkflow(workflow: Partial<Workflow>): Promise<Workflow> {
    try {
      const cleanedWorkflow = cleanWorkflowForCreate(workflow);
      const response = await this.client.post('/workflows', cleanedWorkflow);
      return response.data;
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  async getWorkflow(id: string): Promise<Workflow> {
    try {
      const response = await this.client.get(`/workflows/${id}`);
      return response.data;
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  async updateWorkflow(id: string, workflow: Partial<Workflow>): Promise<Workflow> {
    try {
      // First, try PUT method (newer n8n versions)
      const cleanedWorkflow = cleanWorkflowForUpdate(workflow as Workflow);
      try {
        const response = await this.client.put(`/workflows/${id}`, cleanedWorkflow);
        return response.data;
      } catch (putError: any) {
        // If PUT fails with 405 (Method Not Allowed), try PATCH
        if (putError.response?.status === 405) {
          logger.debug('PUT method not supported, falling back to PATCH');
          const response = await this.client.patch(`/workflows/${id}`, cleanedWorkflow);
          return response.data;
        }
        throw putError;
      }
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  async deleteWorkflow(id: string): Promise<Workflow> {
    try {
      const response = await this.client.delete(`/workflows/${id}`);
      return response.data;
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  async listWorkflows(params: WorkflowListParams = {}): Promise<WorkflowListResponse> {
    try {
      const response = await this.client.get('/workflows', { params });
      return response.data;
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  // Execution Management
  async getExecution(id: string, includeData = false): Promise<Execution> {
    try {
      const response = await this.client.get(`/executions/${id}`, {
        params: { includeData },
      });
      return response.data;
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  async listExecutions(params: ExecutionListParams = {}): Promise<ExecutionListResponse> {
    try {
      const response = await this.client.get('/executions', { params });
      return response.data;
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  async deleteExecution(id: string): Promise<void> {
    try {
      await this.client.delete(`/executions/${id}`);
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  // Webhook Execution
  async triggerWebhook(request: WebhookRequest): Promise<any> {
    try {
      const { webhookUrl, httpMethod, data, headers, waitForResponse = true } = request;

      // SECURITY: Validate URL for SSRF protection (includes DNS resolution)
      // See: https://github.com/czlonkowski/n8n-mcp/issues/265 (HIGH-03)
      const { SSRFProtection } = await import('../utils/ssrf-protection');
      const validation = await SSRFProtection.validateWebhookUrl(webhookUrl);

      if (!validation.valid) {
        throw new Error(`SSRF protection: ${validation.reason}`);
      }

      // Extract path from webhook URL
      const url = new URL(webhookUrl);
      const webhookPath = url.pathname;
      
      // Make request directly to webhook endpoint
      const config: AxiosRequestConfig = {
        method: httpMethod,
        url: webhookPath,
        headers: {
          ...headers,
          // Don't override API key header for webhook endpoints
          'X-N8N-API-KEY': undefined,
        },
        data: httpMethod !== 'GET' ? data : undefined,
        params: httpMethod === 'GET' ? data : undefined,
        // Webhooks might take longer
        timeout: waitForResponse ? 120000 : 30000,
      };

      // Create a new axios instance for webhook requests to avoid API interceptors
      const webhookClient = axios.create({
        baseURL: new URL('/', webhookUrl).toString(),
        validateStatus: (status) => status < 500, // Don't throw on 4xx
      });

      const response = await webhookClient.request(config);
      
      return {
        status: response.status,
        statusText: response.statusText,
        data: response.data,
        headers: response.headers,
      };
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  // Credential Management
  async listCredentials(params: CredentialListParams = {}): Promise<CredentialListResponse> {
    try {
      const response = await this.client.get('/credentials', { params });
      return response.data;
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  async getCredential(id: string): Promise<Credential> {
    try {
      const response = await this.client.get(`/credentials/${id}`);
      return response.data;
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  async createCredential(credential: Partial<Credential>): Promise<Credential> {
    try {
      const response = await this.client.post('/credentials', credential);
      return response.data;
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  async updateCredential(id: string, credential: Partial<Credential>): Promise<Credential> {
    try {
      const response = await this.client.patch(`/credentials/${id}`, credential);
      return response.data;
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  async deleteCredential(id: string): Promise<void> {
    try {
      await this.client.delete(`/credentials/${id}`);
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  // Tag Management
  async listTags(params: TagListParams = {}): Promise<TagListResponse> {
    try {
      const response = await this.client.get('/tags', { params });
      return response.data;
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  async createTag(tag: Partial<Tag>): Promise<Tag> {
    try {
      const response = await this.client.post('/tags', tag);
      return response.data;
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  async updateTag(id: string, tag: Partial<Tag>): Promise<Tag> {
    try {
      const response = await this.client.patch(`/tags/${id}`, tag);
      return response.data;
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  async deleteTag(id: string): Promise<void> {
    try {
      await this.client.delete(`/tags/${id}`);
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  // Source Control Management (Enterprise feature)
  async getSourceControlStatus(): Promise<SourceControlStatus> {
    try {
      const response = await this.client.get('/source-control/status');
      return response.data;
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  async pullSourceControl(force = false): Promise<SourceControlPullResult> {
    try {
      const response = await this.client.post('/source-control/pull', { force });
      return response.data;
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  async pushSourceControl(
    message: string,
    fileNames?: string[]
  ): Promise<SourceControlPushResult> {
    try {
      const response = await this.client.post('/source-control/push', {
        message,
        fileNames,
      });
      return response.data;
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  // Variable Management (via Source Control API)
  async getVariables(): Promise<Variable[]> {
    try {
      const response = await this.client.get('/variables');
      return response.data.data || [];
    } catch (error) {
      // Variables might not be available in all n8n versions
      logger.warn('Variables API not available, returning empty array');
      return [];
    }
  }

  async createVariable(variable: Partial<Variable>): Promise<Variable> {
    try {
      const response = await this.client.post('/variables', variable);
      return response.data;
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  async updateVariable(id: string, variable: Partial<Variable>): Promise<Variable> {
    try {
      const response = await this.client.patch(`/variables/${id}`, variable);
      return response.data;
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }

  async deleteVariable(id: string): Promise<void> {
    try {
      await this.client.delete(`/variables/${id}`);
    } catch (error) {
      throw handleN8nApiError(error);
    }
  }
}
```

--------------------------------------------------------------------------------
/tests/unit/monitoring/cache-metrics.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Unit tests for cache metrics monitoring functionality
 */

import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import {
  getInstanceCacheMetrics,
  getN8nApiClient,
  clearInstanceCache
} from '../../../src/mcp/handlers-n8n-manager';
import {
  cacheMetrics,
  getCacheStatistics
} from '../../../src/utils/cache-utils';
import { InstanceContext } from '../../../src/types/instance-context';

// Mock the N8nApiClient
vi.mock('../../../src/clients/n8n-api-client', () => ({
  N8nApiClient: vi.fn().mockImplementation((config) => ({
    config,
    getWorkflows: vi.fn().mockResolvedValue([]),
    getWorkflow: vi.fn().mockResolvedValue({}),
    isConnected: vi.fn().mockReturnValue(true)
  }))
}));

// Mock logger to reduce noise in tests
vi.mock('../../../src/utils/logger', () => {
  const mockLogger = {
    debug: vi.fn(),
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn()
  };

  return {
    Logger: vi.fn().mockImplementation(() => mockLogger),
    logger: mockLogger
  };
});

describe('Cache Metrics Monitoring', () => {
  beforeEach(() => {
    // Clear cache before each test
    clearInstanceCache();
    cacheMetrics.reset();

    // Reset environment variables
    delete process.env.N8N_API_URL;
    delete process.env.N8N_API_KEY;
    delete process.env.INSTANCE_CACHE_MAX;
    delete process.env.INSTANCE_CACHE_TTL_MINUTES;
  });

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

  describe('getInstanceCacheStatistics', () => {
    it('should return initial statistics', () => {
      const stats = getInstanceCacheMetrics();

      expect(stats).toBeDefined();
      expect(stats.hits).toBe(0);
      expect(stats.misses).toBe(0);
      expect(stats.size).toBe(0);
      expect(stats.avgHitRate).toBe(0);
    });

    it('should track cache hits and misses', () => {
      const context1: InstanceContext = {
        n8nApiUrl: 'https://api1.n8n.cloud',
        n8nApiKey: 'key1',
        instanceId: 'instance1'
      };

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

      // First access - cache miss
      getN8nApiClient(context1);
      let stats = getInstanceCacheMetrics();
      expect(stats.misses).toBe(1);
      expect(stats.hits).toBe(0);
      expect(stats.size).toBe(1);

      // Second access same context - cache hit
      getN8nApiClient(context1);
      stats = getInstanceCacheMetrics();
      expect(stats.hits).toBe(1);
      expect(stats.misses).toBe(1);
      expect(stats.avgHitRate).toBe(0.5); // 1 hit / 2 total

      // Third access different context - cache miss
      getN8nApiClient(context2);
      stats = getInstanceCacheMetrics();
      expect(stats.hits).toBe(1);
      expect(stats.misses).toBe(2);
      expect(stats.size).toBe(2);
      expect(stats.avgHitRate).toBeCloseTo(0.333, 2); // 1 hit / 3 total
    });

    it('should track evictions when cache is full', () => {
      // Note: Cache is created with default size (100), so we need many items to trigger evictions
      // This test verifies that eviction tracking works, even if we don't hit the limit in practice
      const initialStats = getInstanceCacheMetrics();

      // The cache dispose callback should track evictions when items are removed
      // For this test, we'll verify the eviction tracking mechanism exists
      expect(initialStats.evictions).toBeGreaterThanOrEqual(0);

      // Add a few items to cache
      const contexts = [
        { n8nApiUrl: 'https://api1.n8n.cloud', n8nApiKey: 'key1' },
        { n8nApiUrl: 'https://api2.n8n.cloud', n8nApiKey: 'key2' },
        { n8nApiUrl: 'https://api3.n8n.cloud', n8nApiKey: 'key3' }
      ];

      contexts.forEach(ctx => getN8nApiClient(ctx));

      const stats = getInstanceCacheMetrics();
      expect(stats.size).toBe(3); // All items should fit in default cache (max: 100)
    });

    it('should track cache operations over time', () => {
      const context: InstanceContext = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'test-key'
      };

      // Simulate multiple operations
      for (let i = 0; i < 10; i++) {
        getN8nApiClient(context);
      }

      const stats = getInstanceCacheMetrics();
      expect(stats.hits).toBe(9); // First is miss, rest are hits
      expect(stats.misses).toBe(1);
      expect(stats.avgHitRate).toBe(0.9); // 9/10
      expect(stats.sets).toBeGreaterThanOrEqual(1);
    });

    it('should include timestamp information', () => {
      const stats = getInstanceCacheMetrics();

      expect(stats.createdAt).toBeInstanceOf(Date);
      expect(stats.lastResetAt).toBeInstanceOf(Date);
      expect(stats.createdAt.getTime()).toBeLessThanOrEqual(Date.now());
    });

    it('should track cache clear operations', () => {
      const context: InstanceContext = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'test-key'
      };

      // Add some clients
      getN8nApiClient(context);

      // Clear cache
      clearInstanceCache();

      const stats = getInstanceCacheMetrics();
      expect(stats.clears).toBe(1);
      expect(stats.size).toBe(0);
    });
  });

  describe('Cache Metrics with Different Scenarios', () => {
    it('should handle rapid successive requests', () => {
      const context: InstanceContext = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'rapid-test'
      };

      // Simulate rapid requests
      const promises = [];
      for (let i = 0; i < 50; i++) {
        promises.push(Promise.resolve(getN8nApiClient(context)));
      }

      return Promise.all(promises).then(() => {
        const stats = getInstanceCacheMetrics();
        expect(stats.hits).toBe(49); // First is miss
        expect(stats.misses).toBe(1);
        expect(stats.avgHitRate).toBe(0.98); // 49/50
      });
    });

    it('should track metrics for fallback to environment variables', () => {
      // Note: Singleton mode (no context) doesn't use the instance cache
      // This test verifies that cache metrics are not affected by singleton usage
      const initialStats = getInstanceCacheMetrics();

      process.env.N8N_API_URL = 'https://env.n8n.cloud';
      process.env.N8N_API_KEY = 'env-key';

      // Calls without context use singleton mode (no cache metrics)
      getN8nApiClient();
      getN8nApiClient();

      const stats = getInstanceCacheMetrics();
      expect(stats.hits).toBe(initialStats.hits);
      expect(stats.misses).toBe(initialStats.misses);
    });

    it('should maintain separate metrics for different instances', () => {
      const contexts = Array.from({ length: 5 }, (_, i) => ({
        n8nApiUrl: `https://api${i}.n8n.cloud`,
        n8nApiKey: `key${i}`,
        instanceId: `instance${i}`
      }));

      // Access each instance twice
      contexts.forEach(ctx => {
        getN8nApiClient(ctx); // Miss
        getN8nApiClient(ctx); // Hit
      });

      const stats = getInstanceCacheMetrics();
      expect(stats.hits).toBe(5);
      expect(stats.misses).toBe(5);
      expect(stats.size).toBe(5);
      expect(stats.avgHitRate).toBe(0.5);
    });

    it('should handle cache with TTL expiration', () => {
      // Note: TTL configuration is set when cache is created, not dynamically
      // This test verifies that TTL-related cache behavior can be tracked
      const context: InstanceContext = {
        n8nApiUrl: 'https://ttl-test.n8n.cloud',
        n8nApiKey: 'ttl-key'
      };

      // First access - miss
      getN8nApiClient(context);

      // Second access - hit (within TTL)
      getN8nApiClient(context);

      const stats = getInstanceCacheMetrics();
      expect(stats.hits).toBe(1);
      expect(stats.misses).toBe(1);
    });
  });

  describe('getCacheStatistics (formatted)', () => {
    it('should return human-readable statistics', () => {
      const context: InstanceContext = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'test-key'
      };

      // Generate some activity
      getN8nApiClient(context);
      getN8nApiClient(context);
      getN8nApiClient({ ...context, instanceId: 'different' });

      const formattedStats = getCacheStatistics();

      expect(formattedStats).toContain('Cache Statistics:');
      expect(formattedStats).toContain('Runtime:');
      expect(formattedStats).toContain('Total Operations:');
      expect(formattedStats).toContain('Hit Rate:');
      expect(formattedStats).toContain('Current Size:');
      expect(formattedStats).toContain('Total Evictions:');
    });

    it('should show runtime in minutes', () => {
      const stats = getCacheStatistics();
      expect(stats).toMatch(/Runtime: \d+ minutes/);
    });

    it('should show operation counts', () => {
      const context: InstanceContext = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'test-key'
      };

      // Generate operations
      getN8nApiClient(context); // Set
      getN8nApiClient(context); // Hit
      clearInstanceCache(); // Clear

      const stats = getCacheStatistics();
      expect(stats).toContain('Sets: 1');
      expect(stats).toContain('Clears: 1');
    });
  });

  describe('Monitoring Performance Impact', () => {
    it('should have minimal performance overhead', () => {
      const context: InstanceContext = {
        n8nApiUrl: 'https://perf-test.n8n.cloud',
        n8nApiKey: 'perf-key'
      };

      const startTime = performance.now();

      // Perform many operations
      for (let i = 0; i < 1000; i++) {
        getN8nApiClient(context);
      }

      const endTime = performance.now();
      const totalTime = endTime - startTime;

      // Should complete quickly (< 100ms for 1000 operations)
      expect(totalTime).toBeLessThan(100);

      // Verify metrics were tracked
      const stats = getInstanceCacheMetrics();
      expect(stats.hits).toBe(999);
      expect(stats.misses).toBe(1);
    });

    it('should handle concurrent metric updates', async () => {
      const contexts = Array.from({ length: 10 }, (_, i) => ({
        n8nApiUrl: `https://concurrent${i}.n8n.cloud`,
        n8nApiKey: `key${i}`
      }));

      // Concurrent requests
      const promises = contexts.map(ctx =>
        Promise.resolve(getN8nApiClient(ctx))
      );

      await Promise.all(promises);

      const stats = getInstanceCacheMetrics();
      expect(stats.misses).toBe(10);
      expect(stats.size).toBe(10);
    });
  });

  describe('Edge Cases and Error Conditions', () => {
    it('should handle metrics when cache operations fail', () => {
      const invalidContext = {
        n8nApiUrl: '',
        n8nApiKey: ''
      } as InstanceContext;

      // This should fail validation but metrics should still work
      const client = getN8nApiClient(invalidContext);
      expect(client).toBeNull();

      // Metrics should not be affected by validation failures
      const stats = getInstanceCacheMetrics();
      expect(stats).toBeDefined();
    });

    it('should maintain metrics integrity after reset', () => {
      const context: InstanceContext = {
        n8nApiUrl: 'https://reset-test.n8n.cloud',
        n8nApiKey: 'reset-key'
      };

      // Generate some metrics
      getN8nApiClient(context);
      getN8nApiClient(context);

      // Reset metrics
      cacheMetrics.reset();

      // New operations should start fresh
      getN8nApiClient(context);
      const stats = getInstanceCacheMetrics();

      expect(stats.hits).toBe(1); // Cache still has item from before reset
      expect(stats.misses).toBe(0);
      expect(stats.lastResetAt.getTime()).toBeGreaterThan(stats.createdAt.getTime());
    });

    it('should handle maximum cache size correctly', () => {
      // Note: Cache uses default configuration (max: 100) since it's created at module load
      const contexts = Array.from({ length: 5 }, (_, i) => ({
        n8nApiUrl: `https://max${i}.n8n.cloud`,
        n8nApiKey: `key${i}`
      }));

      // Add items within default cache size
      contexts.forEach(ctx => getN8nApiClient(ctx));

      const stats = getInstanceCacheMetrics();
      expect(stats.size).toBe(5); // Should fit in default cache
      expect(stats.maxSize).toBe(100); // Default max size
    });
  });
});
```

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

```typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { NodeRepository } from '../../../src/database/node-repository';
import { DatabaseAdapter, PreparedStatement, RunResult } from '../../../src/database/database-adapter';
import { ParsedNode } from '../../../src/parsers/node-parser';

// Create a complete mock for DatabaseAdapter
class MockDatabaseAdapter implements DatabaseAdapter {
  private statements = new Map<string, MockPreparedStatement>();
  private mockData = new Map<string, any>();
  
  prepare = vi.fn((sql: string) => {
    if (!this.statements.has(sql)) {
      this.statements.set(sql, new MockPreparedStatement(sql, this.mockData));
    }
    return this.statements.get(sql)!;
  });
  
  exec = vi.fn();
  close = vi.fn();
  pragma = vi.fn();
  transaction = vi.fn((fn: () => any) => fn());
  checkFTS5Support = vi.fn(() => true);
  inTransaction = false;
  
  // Test helper to set mock data
  _setMockData(key: string, value: any) {
    this.mockData.set(key, value);
  }
  
  // Test helper to get statement by SQL
  _getStatement(sql: string) {
    return this.statements.get(sql);
  }
}

class MockPreparedStatement implements PreparedStatement {
  run = vi.fn((...params: any[]): RunResult => ({ changes: 1, lastInsertRowid: 1 }));
  get = vi.fn();
  all = vi.fn(() => []);
  iterate = vi.fn();
  pluck = vi.fn(() => this);
  expand = vi.fn(() => this);
  raw = vi.fn(() => this);
  columns = vi.fn(() => []);
  bind = vi.fn(() => this);
  
  constructor(private sql: string, private mockData: Map<string, any>) {
    // Configure get() based on SQL pattern
    if (sql.includes('SELECT * FROM nodes WHERE node_type = ?')) {
      this.get = vi.fn((nodeType: string) => this.mockData.get(`node:${nodeType}`));
    }
    
    // Configure all() for getAITools
    if (sql.includes('WHERE is_ai_tool = 1')) {
      this.all = vi.fn(() => this.mockData.get('ai_tools') || []);
    }
  }
}

describe('NodeRepository - Core Functionality', () => {
  let repository: NodeRepository;
  let mockAdapter: MockDatabaseAdapter;
  
  beforeEach(() => {
    mockAdapter = new MockDatabaseAdapter();
    repository = new NodeRepository(mockAdapter);
  });
  
  describe('saveNode', () => {
    it('should save a node with proper JSON serialization', () => {
      const parsedNode: ParsedNode = {
        nodeType: 'nodes-base.httpRequest',
        displayName: 'HTTP Request',
        description: 'Makes HTTP requests',
        category: 'transform',
        style: 'declarative',
        packageName: 'n8n-nodes-base',
        properties: [{ name: 'url', type: 'string' }],
        operations: [{ name: 'execute', displayName: 'Execute' }],
        credentials: [{ name: 'httpBasicAuth' }],
        isAITool: false,
        isTrigger: false,
        isWebhook: false,
        isVersioned: true,
        version: '1.0',
        documentation: 'HTTP Request documentation',
        outputs: undefined,
        outputNames: undefined
      };
      
      repository.saveNode(parsedNode);
      
      // Verify prepare was called with correct SQL
      expect(mockAdapter.prepare).toHaveBeenCalledWith(expect.stringContaining('INSERT OR REPLACE INTO nodes'));
      
      // Get the prepared statement and verify run was called
      const stmt = mockAdapter._getStatement(mockAdapter.prepare.mock.lastCall?.[0] || '');
      expect(stmt?.run).toHaveBeenCalledWith(
        'nodes-base.httpRequest',
        'n8n-nodes-base',
        'HTTP Request',
        'Makes HTTP requests',
        'transform',
        'declarative',
        0, // isAITool
        0, // isTrigger
        0, // isWebhook
        1, // isVersioned
        '1.0',
        'HTTP Request documentation',
        JSON.stringify([{ name: 'url', type: 'string' }], null, 2),
        JSON.stringify([{ name: 'execute', displayName: 'Execute' }], null, 2),
        JSON.stringify([{ name: 'httpBasicAuth' }], null, 2),
        null, // outputs
        null  // outputNames
      );
    });
    
    it('should handle nodes without optional fields', () => {
      const minimalNode: ParsedNode = {
        nodeType: 'nodes-base.simple',
        displayName: 'Simple Node',
        category: 'core',
        style: 'programmatic',
        packageName: 'n8n-nodes-base',
        properties: [],
        operations: [],
        credentials: [],
        isAITool: true,
        isTrigger: true,
        isWebhook: true,
        isVersioned: false,
        outputs: undefined,
        outputNames: undefined
      };
      
      repository.saveNode(minimalNode);
      
      const stmt = mockAdapter._getStatement(mockAdapter.prepare.mock.lastCall?.[0] || '');
      const runCall = stmt?.run.mock.lastCall;
      
      expect(runCall?.[2]).toBe('Simple Node'); // displayName
      expect(runCall?.[3]).toBeUndefined(); // description
      expect(runCall?.[10]).toBeUndefined(); // version
      expect(runCall?.[11]).toBeNull(); // documentation
    });
  });
  
  describe('getNode', () => {
    it('should retrieve and deserialize a node correctly', () => {
      const mockRow = {
        node_type: 'nodes-base.httpRequest',
        display_name: 'HTTP Request',
        description: 'Makes HTTP requests',
        category: 'transform',
        development_style: 'declarative',
        package_name: 'n8n-nodes-base',
        is_ai_tool: 0,
        is_trigger: 0,
        is_webhook: 0,
        is_versioned: 1,
        version: '1.0',
        properties_schema: JSON.stringify([{ name: 'url', type: 'string' }]),
        operations: JSON.stringify([{ name: 'execute' }]),
        credentials_required: JSON.stringify([{ name: 'httpBasicAuth' }]),
        documentation: 'HTTP docs',
        outputs: null,
        output_names: null
      };
      
      mockAdapter._setMockData('node:nodes-base.httpRequest', mockRow);
      
      const result = repository.getNode('nodes-base.httpRequest');
      
      expect(result).toEqual({
        nodeType: 'nodes-base.httpRequest',
        displayName: 'HTTP Request',
        description: 'Makes HTTP requests',
        category: 'transform',
        developmentStyle: 'declarative',
        package: 'n8n-nodes-base',
        isAITool: false,
        isTrigger: false,
        isWebhook: false,
        isVersioned: true,
        version: '1.0',
        properties: [{ name: 'url', type: 'string' }],
        operations: [{ name: 'execute' }],
        credentials: [{ name: 'httpBasicAuth' }],
        hasDocumentation: true,
        outputs: null,
        outputNames: null
      });
    });
    
    it('should return null for non-existent nodes', () => {
      const result = repository.getNode('non-existent');
      expect(result).toBeNull();
    });
    
    it('should handle invalid JSON gracefully', () => {
      const mockRow = {
        node_type: 'nodes-base.broken',
        display_name: 'Broken Node',
        description: 'Node with broken JSON',
        category: 'transform',
        development_style: 'declarative',
        package_name: 'n8n-nodes-base',
        is_ai_tool: 0,
        is_trigger: 0,
        is_webhook: 0,
        is_versioned: 0,
        version: null,
        properties_schema: '{invalid json',
        operations: 'not json at all',
        credentials_required: '{"valid": "json"}',
        documentation: null,
        outputs: null,
        output_names: null
      };
      
      mockAdapter._setMockData('node:nodes-base.broken', mockRow);
      
      const result = repository.getNode('nodes-base.broken');
      
      expect(result?.properties).toEqual([]); // defaultValue from safeJsonParse
      expect(result?.operations).toEqual([]); // defaultValue from safeJsonParse
      expect(result?.credentials).toEqual({ valid: 'json' }); // successfully parsed
    });
  });
  
  describe('getAITools', () => {
    it('should retrieve all AI tools sorted by display name', () => {
      const mockAITools = [
        {
          node_type: 'nodes-base.openai',
          display_name: 'OpenAI',
          description: 'OpenAI integration',
          package_name: 'n8n-nodes-base'
        },
        {
          node_type: 'nodes-base.agent',
          display_name: 'AI Agent',
          description: 'AI Agent node',
          package_name: '@n8n/n8n-nodes-langchain'
        }
      ];
      
      mockAdapter._setMockData('ai_tools', mockAITools);
      
      const result = repository.getAITools();
      
      expect(result).toEqual([
        {
          nodeType: 'nodes-base.openai',
          displayName: 'OpenAI',
          description: 'OpenAI integration',
          package: 'n8n-nodes-base'
        },
        {
          nodeType: 'nodes-base.agent',
          displayName: 'AI Agent',
          description: 'AI Agent node',
          package: '@n8n/n8n-nodes-langchain'
        }
      ]);
    });
    
    it('should return empty array when no AI tools exist', () => {
      mockAdapter._setMockData('ai_tools', []);
      
      const result = repository.getAITools();
      
      expect(result).toEqual([]);
    });
  });
  
  describe('safeJsonParse', () => {
    it('should parse valid JSON', () => {
      // Access private method through the class
      const parseMethod = (repository as any).safeJsonParse.bind(repository);
      
      const validJson = '{"key": "value", "number": 42}';
      const result = parseMethod(validJson, {});
      
      expect(result).toEqual({ key: 'value', number: 42 });
    });
    
    it('should return default value for invalid JSON', () => {
      const parseMethod = (repository as any).safeJsonParse.bind(repository);
      
      const invalidJson = '{invalid json}';
      const defaultValue = { default: true };
      const result = parseMethod(invalidJson, defaultValue);
      
      expect(result).toEqual(defaultValue);
    });
    
    it('should handle empty strings', () => {
      const parseMethod = (repository as any).safeJsonParse.bind(repository);
      
      const result = parseMethod('', []);
      expect(result).toEqual([]);
    });
    
    it('should handle null and undefined', () => {
      const parseMethod = (repository as any).safeJsonParse.bind(repository);
      
      // JSON.parse(null) returns null, not an error
      expect(parseMethod(null, 'default')).toBe(null);
      expect(parseMethod(undefined, 'default')).toBe('default');
    });
  });
  
  describe('Edge Cases', () => {
    it('should handle very large JSON properties', () => {
      const largeProperties = Array(1000).fill(null).map((_, i) => ({
        name: `prop${i}`,
        type: 'string',
        description: 'A'.repeat(100)
      }));
      
      const node: ParsedNode = {
        nodeType: 'nodes-base.large',
        displayName: 'Large Node',
        category: 'test',
        style: 'declarative',
        packageName: 'test',
        properties: largeProperties,
        operations: [],
        credentials: [],
        isAITool: false,
        isTrigger: false,
        isWebhook: false,
        isVersioned: false,
        outputs: undefined,
        outputNames: undefined
      };
      
      repository.saveNode(node);
      
      const stmt = mockAdapter._getStatement(mockAdapter.prepare.mock.lastCall?.[0] || '');
      const runCall = stmt?.run.mock.lastCall;
      const savedProperties = runCall?.[12];
      
      expect(savedProperties).toBe(JSON.stringify(largeProperties, null, 2));
    });
    
    it('should handle boolean conversion for integer fields', () => {
      const mockRow = {
        node_type: 'nodes-base.bool-test',
        display_name: 'Bool Test',
        description: 'Testing boolean conversion',
        category: 'test',
        development_style: 'declarative',
        package_name: 'test',
        is_ai_tool: 1,
        is_trigger: 0,
        is_webhook: '1', // String that should be converted
        is_versioned: '0', // String that should be converted
        version: null,
        properties_schema: '[]',
        operations: '[]',
        credentials_required: '[]',
        documentation: null,
        outputs: null,
        output_names: null
      };
      
      mockAdapter._setMockData('node:nodes-base.bool-test', mockRow);
      
      const result = repository.getNode('nodes-base.bool-test');
      
      expect(result?.isAITool).toBe(true);
      expect(result?.isTrigger).toBe(false);
      expect(result?.isWebhook).toBe(true);
      expect(result?.isVersioned).toBe(false);
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/integration/ai-validation/ai-tool-validation.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Integration Tests: AI Tool Validation
 *
 * Tests AI tool node validation against real n8n instance.
 * Covers HTTP Request Tool, Code Tool, Vector Store Tool, Workflow Tool, Calculator Tool.
 */

import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest';
import { createTestContext, TestContext, createTestWorkflowName } from '../n8n-api/utils/test-context';
import { getTestN8nClient } from '../n8n-api/utils/n8n-client';
import { N8nApiClient } from '../../../src/services/n8n-api-client';
import { cleanupOrphanedWorkflows } from '../n8n-api/utils/cleanup-helpers';
import { createMcpContext } from '../n8n-api/utils/mcp-context';
import { InstanceContext } from '../../../src/types/instance-context';
import { handleValidateWorkflow } from '../../../src/mcp/handlers-n8n-manager';
import { getNodeRepository, closeNodeRepository } from '../n8n-api/utils/node-repository';
import { NodeRepository } from '../../../src/database/node-repository';
import { ValidationResponse } from '../n8n-api/types/mcp-responses';
import {
  createHTTPRequestToolNode,
  createCodeToolNode,
  createVectorStoreToolNode,
  createWorkflowToolNode,
  createCalculatorToolNode,
  createAIWorkflow
} from './helpers';

describe('Integration: AI Tool Validation', () => {
  let context: TestContext;
  let client: N8nApiClient;
  let mcpContext: InstanceContext;
  let repository: NodeRepository;

  beforeEach(async () => {
    context = createTestContext();
    client = getTestN8nClient();
    mcpContext = createMcpContext();
    repository = await getNodeRepository();
  });

  afterEach(async () => {
    await context.cleanup();
  });

  afterAll(async () => {
    await closeNodeRepository();
    if (!process.env.CI) {
      await cleanupOrphanedWorkflows();
    }
  });

  // ======================================================================
  // HTTP Request Tool Tests
  // ======================================================================

  describe('HTTP Request Tool', () => {
    it('should detect missing toolDescription', async () => {
      const httpTool = createHTTPRequestToolNode({
        name: 'HTTP Request Tool',
        toolDescription: '', // Missing
        url: 'https://api.example.com/data',
        method: 'GET'
      });

      const workflow = createAIWorkflow(
        [httpTool],
        {},
        {
          name: createTestWorkflowName('HTTP Tool - No Description'),
          tags: ['mcp-integration-test', 'ai-validation']
        }
      );

      const created = await client.createWorkflow(workflow);
      context.trackWorkflow(created.id!);

      const response = await handleValidateWorkflow(
        { id: created.id },
        repository,
        mcpContext
      );

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

      expect(data.valid).toBe(false);
      expect(data.errors).toBeDefined();

      const errorCodes = data.errors!.map(e => e.details?.code || e.code);
      expect(errorCodes).toContain('MISSING_TOOL_DESCRIPTION');
    });

    it('should detect missing URL', async () => {
      const httpTool = createHTTPRequestToolNode({
        name: 'HTTP Request Tool',
        toolDescription: 'Fetches data from API',
        url: '', // Missing
        method: 'GET'
      });

      const workflow = createAIWorkflow(
        [httpTool],
        {},
        {
          name: createTestWorkflowName('HTTP Tool - No URL'),
          tags: ['mcp-integration-test', 'ai-validation']
        }
      );

      const created = await client.createWorkflow(workflow);
      context.trackWorkflow(created.id!);

      const response = await handleValidateWorkflow(
        { id: created.id },
        repository,
        mcpContext
      );

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

      expect(data.valid).toBe(false);
      expect(data.errors).toBeDefined();

      const errorCodes = data.errors!.map(e => e.details?.code || e.code);
      expect(errorCodes).toContain('MISSING_URL');
    });

    it('should validate valid HTTP Request Tool', async () => {
      const httpTool = createHTTPRequestToolNode({
        name: 'HTTP Request Tool',
        toolDescription: 'Fetches weather data from the weather API',
        url: 'https://api.weather.com/current',
        method: 'GET'
      });

      const workflow = createAIWorkflow(
        [httpTool],
        {},
        {
          name: createTestWorkflowName('HTTP Tool - Valid'),
          tags: ['mcp-integration-test', 'ai-validation']
        }
      );

      const created = await client.createWorkflow(workflow);
      context.trackWorkflow(created.id!);

      const response = await handleValidateWorkflow(
        { id: created.id },
        repository,
        mcpContext
      );

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

      expect(data.valid).toBe(true);
      expect(data.errors).toBeUndefined();
    });
  });

  // ======================================================================
  // Code Tool Tests
  // ======================================================================

  describe('Code Tool', () => {
    it('should detect missing code', async () => {
      const codeTool = createCodeToolNode({
        name: 'Code Tool',
        toolDescription: 'Processes data with custom logic',
        code: '' // Missing
      });

      const workflow = createAIWorkflow(
        [codeTool],
        {},
        {
          name: createTestWorkflowName('Code Tool - No Code'),
          tags: ['mcp-integration-test', 'ai-validation']
        }
      );

      const created = await client.createWorkflow(workflow);
      context.trackWorkflow(created.id!);

      const response = await handleValidateWorkflow(
        { id: created.id },
        repository,
        mcpContext
      );

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

      expect(data.valid).toBe(false);
      expect(data.errors).toBeDefined();

      const errorCodes = data.errors!.map(e => e.details?.code || e.code);
      expect(errorCodes).toContain('MISSING_CODE');
    });

    it('should validate valid Code Tool', async () => {
      const codeTool = createCodeToolNode({
        name: 'Code Tool',
        toolDescription: 'Calculates the sum of two numbers',
        code: 'return { sum: Number(a) + Number(b) };'
      });

      const workflow = createAIWorkflow(
        [codeTool],
        {},
        {
          name: createTestWorkflowName('Code Tool - Valid'),
          tags: ['mcp-integration-test', 'ai-validation']
        }
      );

      const created = await client.createWorkflow(workflow);
      context.trackWorkflow(created.id!);

      const response = await handleValidateWorkflow(
        { id: created.id },
        repository,
        mcpContext
      );

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

      expect(data.valid).toBe(true);
      expect(data.errors).toBeUndefined();
    });
  });

  // ======================================================================
  // Vector Store Tool Tests
  // ======================================================================

  describe('Vector Store Tool', () => {
    it('should detect missing toolDescription', async () => {
      const vectorTool = createVectorStoreToolNode({
        name: 'Vector Store Tool',
        toolDescription: '' // Missing
      });

      const workflow = createAIWorkflow(
        [vectorTool],
        {},
        {
          name: createTestWorkflowName('Vector Tool - No Description'),
          tags: ['mcp-integration-test', 'ai-validation']
        }
      );

      const created = await client.createWorkflow(workflow);
      context.trackWorkflow(created.id!);

      const response = await handleValidateWorkflow(
        { id: created.id },
        repository,
        mcpContext
      );

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

      expect(data.valid).toBe(false);
      expect(data.errors).toBeDefined();

      const errorCodes = data.errors!.map(e => e.details?.code || e.code);
      expect(errorCodes).toContain('MISSING_TOOL_DESCRIPTION');
    });

    it('should validate valid Vector Store Tool', async () => {
      const vectorTool = createVectorStoreToolNode({
        name: 'Vector Store Tool',
        toolDescription: 'Searches documentation in vector database'
      });

      const workflow = createAIWorkflow(
        [vectorTool],
        {},
        {
          name: createTestWorkflowName('Vector Tool - Valid'),
          tags: ['mcp-integration-test', 'ai-validation']
        }
      );

      const created = await client.createWorkflow(workflow);
      context.trackWorkflow(created.id!);

      const response = await handleValidateWorkflow(
        { id: created.id },
        repository,
        mcpContext
      );

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

      expect(data.valid).toBe(true);
      expect(data.errors).toBeUndefined();
    });
  });

  // ======================================================================
  // Workflow Tool Tests
  // ======================================================================

  describe('Workflow Tool', () => {
    it('should detect missing workflowId', async () => {
      const workflowTool = createWorkflowToolNode({
        name: 'Workflow Tool',
        toolDescription: 'Executes a sub-workflow',
        workflowId: '' // Missing
      });

      const workflow = createAIWorkflow(
        [workflowTool],
        {},
        {
          name: createTestWorkflowName('Workflow Tool - No ID'),
          tags: ['mcp-integration-test', 'ai-validation']
        }
      );

      const created = await client.createWorkflow(workflow);
      context.trackWorkflow(created.id!);

      const response = await handleValidateWorkflow(
        { id: created.id },
        repository,
        mcpContext
      );

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

      expect(data.valid).toBe(false);
      expect(data.errors).toBeDefined();

      const errorCodes = data.errors!.map(e => e.details?.code || e.code);
      expect(errorCodes).toContain('MISSING_WORKFLOW_ID');
    });

    it('should validate valid Workflow Tool', async () => {
      const workflowTool = createWorkflowToolNode({
        name: 'Workflow Tool',
        toolDescription: 'Processes customer data through validation workflow',
        workflowId: '123'
      });

      const workflow = createAIWorkflow(
        [workflowTool],
        {},
        {
          name: createTestWorkflowName('Workflow Tool - Valid'),
          tags: ['mcp-integration-test', 'ai-validation']
        }
      );

      const created = await client.createWorkflow(workflow);
      context.trackWorkflow(created.id!);

      const response = await handleValidateWorkflow(
        { id: created.id },
        repository,
        mcpContext
      );

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

      expect(data.valid).toBe(true);
      expect(data.errors).toBeUndefined();
    });
  });

  // ======================================================================
  // Calculator Tool Tests
  // ======================================================================

  describe('Calculator Tool', () => {
    it('should validate Calculator Tool (no configuration needed)', async () => {
      const calcTool = createCalculatorToolNode({
        name: 'Calculator'
      });

      const workflow = createAIWorkflow(
        [calcTool],
        {},
        {
          name: createTestWorkflowName('Calculator Tool - Valid'),
          tags: ['mcp-integration-test', 'ai-validation']
        }
      );

      const created = await client.createWorkflow(workflow);
      context.trackWorkflow(created.id!);

      const response = await handleValidateWorkflow(
        { id: created.id },
        repository,
        mcpContext
      );

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

      // Calculator has no required configuration
      expect(data.valid).toBe(true);
      expect(data.errors).toBeUndefined();
    });
  });
});

```

--------------------------------------------------------------------------------
/tests/unit/services/workflow-auto-fixer.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { WorkflowAutoFixer, isNodeFormatIssue } from '@/services/workflow-auto-fixer';
import { NodeRepository } from '@/database/node-repository';
import type { WorkflowValidationResult } from '@/services/workflow-validator';
import type { ExpressionFormatIssue } from '@/services/expression-format-validator';
import type { Workflow, WorkflowNode } from '@/types/n8n-api';

vi.mock('@/database/node-repository');
vi.mock('@/services/node-similarity-service');

describe('WorkflowAutoFixer', () => {
  let autoFixer: WorkflowAutoFixer;
  let mockRepository: NodeRepository;

  const createMockWorkflow = (nodes: WorkflowNode[]): Workflow => ({
    id: 'test-workflow',
    name: 'Test Workflow',
    active: false,
    nodes,
    connections: {},
    settings: {},
    createdAt: '',
    updatedAt: ''
  });

  const createMockNode = (id: string, type: string, parameters: any = {}): WorkflowNode => ({
    id,
    name: id,
    type,
    typeVersion: 1,
    position: [0, 0],
    parameters
  });

  beforeEach(() => {
    vi.clearAllMocks();
    mockRepository = new NodeRepository({} as any);
    autoFixer = new WorkflowAutoFixer(mockRepository);
  });

  describe('Type Guards', () => {
    it('should identify NodeFormatIssue correctly', () => {
      const validIssue: ExpressionFormatIssue = {
        fieldPath: 'url',
        currentValue: '{{ $json.url }}',
        correctedValue: '={{ $json.url }}',
        issueType: 'missing-prefix',
        severity: 'error',
        explanation: 'Missing = prefix'
      } as any;
      (validIssue as any).nodeName = 'httpRequest';
      (validIssue as any).nodeId = 'node-1';

      const invalidIssue: ExpressionFormatIssue = {
        fieldPath: 'url',
        currentValue: '{{ $json.url }}',
        correctedValue: '={{ $json.url }}',
        issueType: 'missing-prefix',
        severity: 'error',
        explanation: 'Missing = prefix'
      };

      expect(isNodeFormatIssue(validIssue)).toBe(true);
      expect(isNodeFormatIssue(invalidIssue)).toBe(false);
    });
  });

  describe('Expression Format Fixes', () => {
    it('should fix missing prefix in expressions', () => {
      const workflow = createMockWorkflow([
        createMockNode('node-1', 'nodes-base.httpRequest', {
          url: '{{ $json.url }}',
          method: 'GET'
        })
      ]);

      const formatIssues: ExpressionFormatIssue[] = [{
        fieldPath: 'url',
        currentValue: '{{ $json.url }}',
        correctedValue: '={{ $json.url }}',
        issueType: 'missing-prefix',
        severity: 'error',
        explanation: 'Expression must start with =',
        nodeName: 'node-1',
        nodeId: 'node-1'
      } as any];

      const validationResult: WorkflowValidationResult = {
        valid: false,
        errors: [],
        warnings: [],
        statistics: {
          totalNodes: 1,
          enabledNodes: 1,
          triggerNodes: 0,
          validConnections: 0,
          invalidConnections: 0,
          expressionsValidated: 0
        },
        suggestions: []
      };

      const result = autoFixer.generateFixes(workflow, validationResult, formatIssues);

      expect(result.fixes).toHaveLength(1);
      expect(result.fixes[0].type).toBe('expression-format');
      expect(result.fixes[0].before).toBe('{{ $json.url }}');
      expect(result.fixes[0].after).toBe('={{ $json.url }}');
      expect(result.fixes[0].confidence).toBe('high');

      expect(result.operations).toHaveLength(1);
      expect(result.operations[0].type).toBe('updateNode');
    });

    it('should handle multiple expression fixes in same node', () => {
      const workflow = createMockWorkflow([
        createMockNode('node-1', 'nodes-base.httpRequest', {
          url: '{{ $json.url }}',
          body: '{{ $json.body }}'
        })
      ]);

      const formatIssues: ExpressionFormatIssue[] = [
        {
          fieldPath: 'url',
          currentValue: '{{ $json.url }}',
          correctedValue: '={{ $json.url }}',
          issueType: 'missing-prefix',
          severity: 'error',
          explanation: 'Expression must start with =',
          nodeName: 'node-1',
          nodeId: 'node-1'
        } as any,
        {
          fieldPath: 'body',
          currentValue: '{{ $json.body }}',
          correctedValue: '={{ $json.body }}',
          issueType: 'missing-prefix',
          severity: 'error',
          explanation: 'Expression must start with =',
          nodeName: 'node-1',
          nodeId: 'node-1'
        } as any
      ];

      const validationResult: WorkflowValidationResult = {
        valid: false,
        errors: [],
        warnings: [],
        statistics: {
          totalNodes: 1,
          enabledNodes: 1,
          triggerNodes: 0,
          validConnections: 0,
          invalidConnections: 0,
          expressionsValidated: 0
        },
        suggestions: []
      };

      const result = autoFixer.generateFixes(workflow, validationResult, formatIssues);

      expect(result.fixes).toHaveLength(2);
      expect(result.operations).toHaveLength(1); // Single update operation for the node
    });
  });

  describe('TypeVersion Fixes', () => {
    it('should fix typeVersion exceeding maximum', () => {
      const workflow = createMockWorkflow([
        createMockNode('node-1', 'nodes-base.httpRequest', {})
      ]);

      const validationResult: WorkflowValidationResult = {
        valid: false,
        errors: [{
          type: 'error',
          nodeId: 'node-1',
          nodeName: 'node-1',
          message: 'typeVersion 3.5 exceeds maximum supported version 2.0'
        }],
        warnings: [],
        statistics: {
          totalNodes: 1,
          enabledNodes: 1,
          triggerNodes: 0,
          validConnections: 0,
          invalidConnections: 0,
          expressionsValidated: 0
        },
        suggestions: []
      };

      const result = autoFixer.generateFixes(workflow, validationResult, []);

      expect(result.fixes).toHaveLength(1);
      expect(result.fixes[0].type).toBe('typeversion-correction');
      expect(result.fixes[0].before).toBe(3.5);
      expect(result.fixes[0].after).toBe(2);
      expect(result.fixes[0].confidence).toBe('medium');
    });
  });

  describe('Error Output Configuration Fixes', () => {
    it('should remove conflicting onError setting', () => {
      const workflow = createMockWorkflow([
        createMockNode('node-1', 'nodes-base.httpRequest', {})
      ]);
      workflow.nodes[0].onError = 'continueErrorOutput';

      const validationResult: WorkflowValidationResult = {
        valid: false,
        errors: [{
          type: 'error',
          nodeId: 'node-1',
          nodeName: 'node-1',
          message: "Node has onError: 'continueErrorOutput' but no error output connections"
        }],
        warnings: [],
        statistics: {
          totalNodes: 1,
          enabledNodes: 1,
          triggerNodes: 0,
          validConnections: 0,
          invalidConnections: 0,
          expressionsValidated: 0
        },
        suggestions: []
      };

      const result = autoFixer.generateFixes(workflow, validationResult, []);

      expect(result.fixes).toHaveLength(1);
      expect(result.fixes[0].type).toBe('error-output-config');
      expect(result.fixes[0].before).toBe('continueErrorOutput');
      expect(result.fixes[0].after).toBeUndefined();
      expect(result.fixes[0].confidence).toBe('medium');
    });
  });

  describe('setNestedValue Validation', () => {
    it('should throw error for non-object target', () => {
      expect(() => {
        autoFixer['setNestedValue'](null, ['field'], 'value');
      }).toThrow('Cannot set value on non-object');

      expect(() => {
        autoFixer['setNestedValue']('string', ['field'], 'value');
      }).toThrow('Cannot set value on non-object');
    });

    it('should throw error for empty path', () => {
      expect(() => {
        autoFixer['setNestedValue']({}, [], 'value');
      }).toThrow('Cannot set value with empty path');
    });

    it('should handle nested paths correctly', () => {
      const obj = { level1: { level2: { level3: 'old' } } };
      autoFixer['setNestedValue'](obj, ['level1', 'level2', 'level3'], 'new');
      expect(obj.level1.level2.level3).toBe('new');
    });

    it('should create missing nested objects', () => {
      const obj = {};
      autoFixer['setNestedValue'](obj, ['level1', 'level2', 'level3'], 'value');
      expect(obj).toEqual({
        level1: {
          level2: {
            level3: 'value'
          }
        }
      });
    });

    it('should handle array indices in paths', () => {
      const obj: any = { items: [] };
      autoFixer['setNestedValue'](obj, ['items[0]', 'name'], 'test');
      expect(obj.items[0].name).toBe('test');
    });

    it('should throw error for invalid array notation', () => {
      const obj = {};
      expect(() => {
        autoFixer['setNestedValue'](obj, ['field[abc]'], 'value');
      }).toThrow('Invalid array notation: field[abc]');
    });

    it('should throw when trying to traverse non-object', () => {
      const obj = { field: 'string' };
      expect(() => {
        autoFixer['setNestedValue'](obj, ['field', 'nested'], 'value');
      }).toThrow('Cannot traverse through string at field');
    });
  });

  describe('Confidence Filtering', () => {
    it('should filter fixes by confidence level', () => {
      const workflow = createMockWorkflow([
        createMockNode('node-1', 'nodes-base.httpRequest', { url: '{{ $json.url }}' })
      ]);

      const formatIssues: ExpressionFormatIssue[] = [{
        fieldPath: 'url',
        currentValue: '{{ $json.url }}',
        correctedValue: '={{ $json.url }}',
        issueType: 'missing-prefix',
        severity: 'error',
        explanation: 'Expression must start with =',
        nodeName: 'node-1',
        nodeId: 'node-1'
      } as any];

      const validationResult: WorkflowValidationResult = {
        valid: false,
        errors: [],
        warnings: [],
        statistics: {
          totalNodes: 1,
          enabledNodes: 1,
          triggerNodes: 0,
          validConnections: 0,
          invalidConnections: 0,
          expressionsValidated: 0
        },
        suggestions: []
      };

      const result = autoFixer.generateFixes(workflow, validationResult, formatIssues, {
        confidenceThreshold: 'low'
      });

      expect(result.fixes.length).toBeGreaterThan(0);
      expect(result.fixes.every(f => ['high', 'medium', 'low'].includes(f.confidence))).toBe(true);
    });
  });

  describe('Summary Generation', () => {
    it('should generate appropriate summary for fixes', () => {
      const workflow = createMockWorkflow([
        createMockNode('node-1', 'nodes-base.httpRequest', { url: '{{ $json.url }}' })
      ]);

      const formatIssues: ExpressionFormatIssue[] = [{
        fieldPath: 'url',
        currentValue: '{{ $json.url }}',
        correctedValue: '={{ $json.url }}',
        issueType: 'missing-prefix',
        severity: 'error',
        explanation: 'Expression must start with =',
        nodeName: 'node-1',
        nodeId: 'node-1'
      } as any];

      const validationResult: WorkflowValidationResult = {
        valid: false,
        errors: [],
        warnings: [],
        statistics: {
          totalNodes: 1,
          enabledNodes: 1,
          triggerNodes: 0,
          validConnections: 0,
          invalidConnections: 0,
          expressionsValidated: 0
        },
        suggestions: []
      };

      const result = autoFixer.generateFixes(workflow, validationResult, formatIssues);

      expect(result.summary).toContain('expression format');
      expect(result.stats.total).toBe(1);
      expect(result.stats.byType['expression-format']).toBe(1);
    });

    it('should handle empty fixes gracefully', () => {
      const workflow = createMockWorkflow([]);
      const validationResult: WorkflowValidationResult = {
        valid: true,
        errors: [],
        warnings: [],
        statistics: {
          totalNodes: 0,
          enabledNodes: 0,
          triggerNodes: 0,
          validConnections: 0,
          invalidConnections: 0,
          expressionsValidated: 0
        },
        suggestions: []
      };

      const result = autoFixer.generateFixes(workflow, validationResult, []);

      expect(result.summary).toBe('No fixes available');
      expect(result.stats.total).toBe(0);
      expect(result.operations).toEqual([]);
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/unit/utils/node-type-normalizer.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tests for NodeTypeNormalizer
 *
 * Comprehensive test suite for the node type normalization utility
 * that fixes the critical issue of AI agents producing short-form node types
 */

import { describe, it, expect } from 'vitest';
import { NodeTypeNormalizer } from '../../../src/utils/node-type-normalizer';

describe('NodeTypeNormalizer', () => {
  describe('normalizeToFullForm', () => {
    describe('Base nodes', () => {
      it('should normalize full base form to short form', () => {
        expect(NodeTypeNormalizer.normalizeToFullForm('n8n-nodes-base.webhook'))
          .toBe('nodes-base.webhook');
      });

      it('should normalize full base form with different node names', () => {
        expect(NodeTypeNormalizer.normalizeToFullForm('n8n-nodes-base.httpRequest'))
          .toBe('nodes-base.httpRequest');
        expect(NodeTypeNormalizer.normalizeToFullForm('n8n-nodes-base.set'))
          .toBe('nodes-base.set');
        expect(NodeTypeNormalizer.normalizeToFullForm('n8n-nodes-base.slack'))
          .toBe('nodes-base.slack');
      });

      it('should leave short base form unchanged', () => {
        expect(NodeTypeNormalizer.normalizeToFullForm('nodes-base.webhook'))
          .toBe('nodes-base.webhook');
        expect(NodeTypeNormalizer.normalizeToFullForm('nodes-base.httpRequest'))
          .toBe('nodes-base.httpRequest');
      });
    });

    describe('LangChain nodes', () => {
      it('should normalize full langchain form to short form', () => {
        expect(NodeTypeNormalizer.normalizeToFullForm('@n8n/n8n-nodes-langchain.agent'))
          .toBe('nodes-langchain.agent');
        expect(NodeTypeNormalizer.normalizeToFullForm('@n8n/n8n-nodes-langchain.openAi'))
          .toBe('nodes-langchain.openAi');
      });

      it('should normalize langchain form with n8n- prefix but missing @n8n/', () => {
        expect(NodeTypeNormalizer.normalizeToFullForm('n8n-nodes-langchain.agent'))
          .toBe('nodes-langchain.agent');
      });

      it('should leave short langchain form unchanged', () => {
        expect(NodeTypeNormalizer.normalizeToFullForm('nodes-langchain.agent'))
          .toBe('nodes-langchain.agent');
        expect(NodeTypeNormalizer.normalizeToFullForm('nodes-langchain.openAi'))
          .toBe('nodes-langchain.openAi');
      });
    });

    describe('Edge cases', () => {
      it('should handle empty string', () => {
        expect(NodeTypeNormalizer.normalizeToFullForm('')).toBe('');
      });

      it('should handle null', () => {
        expect(NodeTypeNormalizer.normalizeToFullForm(null as any)).toBe(null);
      });

      it('should handle undefined', () => {
        expect(NodeTypeNormalizer.normalizeToFullForm(undefined as any)).toBe(undefined);
      });

      it('should handle non-string input', () => {
        expect(NodeTypeNormalizer.normalizeToFullForm(123 as any)).toBe(123);
        expect(NodeTypeNormalizer.normalizeToFullForm({} as any)).toEqual({});
      });

      it('should leave community nodes unchanged', () => {
        expect(NodeTypeNormalizer.normalizeToFullForm('custom-package.myNode'))
          .toBe('custom-package.myNode');
      });

      it('should leave nodes without prefixes unchanged', () => {
        expect(NodeTypeNormalizer.normalizeToFullForm('someRandomNode'))
          .toBe('someRandomNode');
      });
    });
  });

  describe('normalizeWithDetails', () => {
    it('should return normalization details for full base form', () => {
      const result = NodeTypeNormalizer.normalizeWithDetails('n8n-nodes-base.webhook');

      expect(result).toEqual({
        original: 'n8n-nodes-base.webhook',
        normalized: 'nodes-base.webhook',
        wasNormalized: true,
        package: 'base'
      });
    });

    it('should return normalization details for already short form', () => {
      const result = NodeTypeNormalizer.normalizeWithDetails('nodes-base.webhook');

      expect(result).toEqual({
        original: 'nodes-base.webhook',
        normalized: 'nodes-base.webhook',
        wasNormalized: false,
        package: 'base'
      });
    });

    it('should detect langchain package', () => {
      const result = NodeTypeNormalizer.normalizeWithDetails('@n8n/n8n-nodes-langchain.agent');

      expect(result).toEqual({
        original: '@n8n/n8n-nodes-langchain.agent',
        normalized: 'nodes-langchain.agent',
        wasNormalized: true,
        package: 'langchain'
      });
    });

    it('should detect community package', () => {
      const result = NodeTypeNormalizer.normalizeWithDetails('custom-package.myNode');

      expect(result).toEqual({
        original: 'custom-package.myNode',
        normalized: 'custom-package.myNode',
        wasNormalized: false,
        package: 'community'
      });
    });

    it('should detect unknown package', () => {
      const result = NodeTypeNormalizer.normalizeWithDetails('unknownNode');

      expect(result).toEqual({
        original: 'unknownNode',
        normalized: 'unknownNode',
        wasNormalized: false,
        package: 'unknown'
      });
    });
  });

  describe('normalizeBatch', () => {
    it('should normalize multiple node types', () => {
      const types = ['n8n-nodes-base.webhook', 'n8n-nodes-base.set', '@n8n/n8n-nodes-langchain.agent'];
      const result = NodeTypeNormalizer.normalizeBatch(types);

      expect(result.size).toBe(3);
      expect(result.get('n8n-nodes-base.webhook')).toBe('nodes-base.webhook');
      expect(result.get('n8n-nodes-base.set')).toBe('nodes-base.set');
      expect(result.get('@n8n/n8n-nodes-langchain.agent')).toBe('nodes-langchain.agent');
    });

    it('should handle empty array', () => {
      const result = NodeTypeNormalizer.normalizeBatch([]);
      expect(result.size).toBe(0);
    });

    it('should handle mixed forms', () => {
      const types = [
        'n8n-nodes-base.webhook',
        'nodes-base.set',
        '@n8n/n8n-nodes-langchain.agent',
        'nodes-langchain.openAi'
      ];
      const result = NodeTypeNormalizer.normalizeBatch(types);

      expect(result.size).toBe(4);
      expect(result.get('n8n-nodes-base.webhook')).toBe('nodes-base.webhook');
      expect(result.get('nodes-base.set')).toBe('nodes-base.set');
      expect(result.get('@n8n/n8n-nodes-langchain.agent')).toBe('nodes-langchain.agent');
      expect(result.get('nodes-langchain.openAi')).toBe('nodes-langchain.openAi');
    });
  });

  describe('normalizeWorkflowNodeTypes', () => {
    it('should normalize all nodes in workflow', () => {
      const workflow = {
        nodes: [
          { type: 'n8n-nodes-base.webhook', id: '1', name: 'Webhook', parameters: {}, position: [0, 0] },
          { type: 'n8n-nodes-base.set', id: '2', name: 'Set', parameters: {}, position: [100, 100] }
        ],
        connections: {}
      };

      const result = NodeTypeNormalizer.normalizeWorkflowNodeTypes(workflow);

      expect(result.nodes[0].type).toBe('nodes-base.webhook');
      expect(result.nodes[1].type).toBe('nodes-base.set');
    });

    it('should preserve all other node properties', () => {
      const workflow = {
        nodes: [
          {
            type: 'n8n-nodes-base.webhook',
            id: 'test-id',
            name: 'Test Webhook',
            parameters: { path: '/test' },
            position: [250, 300],
            credentials: { webhookAuth: { id: '1', name: 'Test' } }
          }
        ],
        connections: {}
      };

      const result = NodeTypeNormalizer.normalizeWorkflowNodeTypes(workflow);

      expect(result.nodes[0]).toEqual({
        type: 'nodes-base.webhook', // normalized to short form
        id: 'test-id', // preserved
        name: 'Test Webhook', // preserved
        parameters: { path: '/test' }, // preserved
        position: [250, 300], // preserved
        credentials: { webhookAuth: { id: '1', name: 'Test' } } // preserved
      });
    });

    it('should preserve workflow properties', () => {
      const workflow = {
        name: 'Test Workflow',
        active: true,
        nodes: [
          { type: 'n8n-nodes-base.webhook', id: '1', name: 'Webhook', parameters: {}, position: [0, 0] }
        ],
        connections: {
          '1': { main: [[{ node: '2', type: 'main', index: 0 }]] }
        }
      };

      const result = NodeTypeNormalizer.normalizeWorkflowNodeTypes(workflow);

      expect(result.name).toBe('Test Workflow');
      expect(result.active).toBe(true);
      expect(result.connections).toEqual({
        '1': { main: [[{ node: '2', type: 'main', index: 0 }]] }
      });
    });

    it('should handle workflow without nodes', () => {
      const workflow = { connections: {} };
      const result = NodeTypeNormalizer.normalizeWorkflowNodeTypes(workflow);
      expect(result).toEqual(workflow);
    });

    it('should handle null workflow', () => {
      const result = NodeTypeNormalizer.normalizeWorkflowNodeTypes(null);
      expect(result).toBe(null);
    });

    it('should handle workflow with empty nodes array', () => {
      const workflow = { nodes: [], connections: {} };
      const result = NodeTypeNormalizer.normalizeWorkflowNodeTypes(workflow);
      expect(result.nodes).toEqual([]);
    });
  });

  describe('isFullForm', () => {
    it('should return true for full base form', () => {
      expect(NodeTypeNormalizer.isFullForm('n8n-nodes-base.webhook')).toBe(true);
    });

    it('should return true for full langchain form', () => {
      expect(NodeTypeNormalizer.isFullForm('@n8n/n8n-nodes-langchain.agent')).toBe(true);
      expect(NodeTypeNormalizer.isFullForm('n8n-nodes-langchain.agent')).toBe(true);
    });

    it('should return false for short base form', () => {
      expect(NodeTypeNormalizer.isFullForm('nodes-base.webhook')).toBe(false);
    });

    it('should return false for short langchain form', () => {
      expect(NodeTypeNormalizer.isFullForm('nodes-langchain.agent')).toBe(false);
    });

    it('should return false for community nodes', () => {
      expect(NodeTypeNormalizer.isFullForm('custom-package.myNode')).toBe(false);
    });

    it('should return false for null/undefined', () => {
      expect(NodeTypeNormalizer.isFullForm(null as any)).toBe(false);
      expect(NodeTypeNormalizer.isFullForm(undefined as any)).toBe(false);
    });
  });

  describe('isShortForm', () => {
    it('should return true for short base form', () => {
      expect(NodeTypeNormalizer.isShortForm('nodes-base.webhook')).toBe(true);
    });

    it('should return true for short langchain form', () => {
      expect(NodeTypeNormalizer.isShortForm('nodes-langchain.agent')).toBe(true);
    });

    it('should return false for full base form', () => {
      expect(NodeTypeNormalizer.isShortForm('n8n-nodes-base.webhook')).toBe(false);
    });

    it('should return false for full langchain form', () => {
      expect(NodeTypeNormalizer.isShortForm('@n8n/n8n-nodes-langchain.agent')).toBe(false);
      expect(NodeTypeNormalizer.isShortForm('n8n-nodes-langchain.agent')).toBe(false);
    });

    it('should return false for community nodes', () => {
      expect(NodeTypeNormalizer.isShortForm('custom-package.myNode')).toBe(false);
    });

    it('should return false for null/undefined', () => {
      expect(NodeTypeNormalizer.isShortForm(null as any)).toBe(false);
      expect(NodeTypeNormalizer.isShortForm(undefined as any)).toBe(false);
    });
  });

  describe('Integration scenarios', () => {
    it('should handle the critical use case from P0-R1', () => {
      // This is the exact scenario - normalize full form to match database
      const fullFormType = 'n8n-nodes-base.webhook'; // External source produces this
      const normalized = NodeTypeNormalizer.normalizeToFullForm(fullFormType);

      expect(normalized).toBe('nodes-base.webhook'); // Database stores in short form
    });

    it('should work correctly in a workflow validation scenario', () => {
      const workflow = {
        nodes: [
          { type: 'n8n-nodes-base.webhook', id: '1', name: 'Webhook', parameters: {}, position: [0, 0] },
          { type: 'n8n-nodes-base.httpRequest', id: '2', name: 'HTTP', parameters: {}, position: [200, 0] },
          { type: 'nodes-base.set', id: '3', name: 'Set', parameters: {}, position: [400, 0] }
        ],
        connections: {}
      };

      const normalized = NodeTypeNormalizer.normalizeWorkflowNodeTypes(workflow);

      // All node types should now be in short form for database lookup
      expect(normalized.nodes.every((n: any) => n.type.startsWith('nodes-base.'))).toBe(true);
    });
  });
});

```

--------------------------------------------------------------------------------
/tests/unit/services/workflow-fixed-collection-validation.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Workflow Fixed Collection Validation Tests
 * Tests that workflow validation catches fixedCollection structure errors at the workflow level
 */

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

describe('Workflow FixedCollection Validation', () => {
  let validator: WorkflowValidator;
  let mockNodeRepository: any;

  beforeEach(() => {
    // Create mock repository that returns basic node info for common nodes
    mockNodeRepository = {
      getNode: vi.fn().mockImplementation((type: string) => {
        const normalizedType = type.replace('n8n-nodes-base.', '').replace('nodes-base.', '');
        switch (normalizedType) {
          case 'webhook':
            return {
              nodeType: 'nodes-base.webhook',
              displayName: 'Webhook',
              properties: [
                { name: 'path', type: 'string', required: true },
                { name: 'httpMethod', type: 'options' }
              ]
            };
          case 'switch':
            return {
              nodeType: 'nodes-base.switch',
              displayName: 'Switch',
              properties: [
                { name: 'rules', type: 'fixedCollection', required: true }
              ]
            };
          case 'if':
            return {
              nodeType: 'nodes-base.if',
              displayName: 'If',
              properties: [
                { name: 'conditions', type: 'filter', required: true }
              ]
            };
          case 'filter':
            return {
              nodeType: 'nodes-base.filter',
              displayName: 'Filter',
              properties: [
                { name: 'conditions', type: 'filter', required: true }
              ]
            };
          default:
            return null;
        }
      })
    };
    
    validator = new WorkflowValidator(mockNodeRepository, EnhancedConfigValidator);
  });

  test('should catch invalid Switch node structure in workflow validation', async () => {
    const workflow = {
      name: 'Test Workflow with Invalid Switch',
      nodes: [
        {
          id: 'webhook',
          name: 'Webhook',
          type: 'n8n-nodes-base.webhook',
          position: [0, 0] as [number, number],
          parameters: {
            path: 'test-webhook'
          }
        },
        {
          id: 'switch',
          name: 'Switch',
          type: 'n8n-nodes-base.switch',
          position: [200, 0] as [number, number],
          parameters: {
            // This is the problematic structure that causes "propertyValues[itemName] is not iterable"
            rules: {
              conditions: {
                values: [
                  {
                    value1: '={{$json.status}}',
                    operation: 'equals',
                    value2: 'active'
                  }
                ]
              }
            }
          }
        }
      ],
      connections: {
        Webhook: {
          main: [[{ node: 'Switch', type: 'main', index: 0 }]]
        }
      }
    };

    const result = await validator.validateWorkflow(workflow, {
      validateNodes: true,
      profile: 'ai-friendly'
    });

    expect(result.valid).toBe(false);
    expect(result.errors).toHaveLength(1);
    
    const switchError = result.errors.find(e => e.nodeId === 'switch');
    expect(switchError).toBeDefined();
    expect(switchError!.message).toContain('propertyValues[itemName] is not iterable');
    expect(switchError!.message).toContain('Invalid structure for nodes-base.switch node');
  });

  test('should catch invalid If node structure in workflow validation', async () => {
    const workflow = {
      name: 'Test Workflow with Invalid If',
      nodes: [
        {
          id: 'webhook',
          name: 'Webhook',
          type: 'n8n-nodes-base.webhook',
          position: [0, 0] as [number, number],
          parameters: {
            path: 'test-webhook'
          }
        },
        {
          id: 'if',
          name: 'If',
          type: 'n8n-nodes-base.if',
          position: [200, 0] as [number, number],
          parameters: {
            // This is the problematic structure
            conditions: {
              values: [
                {
                  value1: '={{$json.age}}',
                  operation: 'largerEqual',
                  value2: 18
                }
              ]
            }
          }
        }
      ],
      connections: {
        Webhook: {
          main: [[{ node: 'If', type: 'main', index: 0 }]]
        }
      }
    };

    const result = await validator.validateWorkflow(workflow, {
      validateNodes: true,
      profile: 'ai-friendly'
    });

    expect(result.valid).toBe(false);
    expect(result.errors).toHaveLength(1);
    
    const ifError = result.errors.find(e => e.nodeId === 'if');
    expect(ifError).toBeDefined();
    expect(ifError!.message).toContain('Invalid structure for nodes-base.if node');
  });

  test('should accept valid Switch node structure in workflow validation', async () => {
    const workflow = {
      name: 'Test Workflow with Valid Switch',
      nodes: [
        {
          id: 'webhook',
          name: 'Webhook',
          type: 'n8n-nodes-base.webhook',
          position: [0, 0] as [number, number],
          parameters: {
            path: 'test-webhook'
          }
        },
        {
          id: 'switch',
          name: 'Switch',
          type: 'n8n-nodes-base.switch',
          position: [200, 0] as [number, number],
          parameters: {
            // This is the correct structure
            rules: {
              values: [
                {
                  conditions: {
                    value1: '={{$json.status}}',
                    operation: 'equals',
                    value2: 'active'
                  },
                  outputKey: 'active'
                }
              ]
            }
          }
        }
      ],
      connections: {
        Webhook: {
          main: [[{ node: 'Switch', type: 'main', index: 0 }]]
        }
      }
    };

    const result = await validator.validateWorkflow(workflow, {
      validateNodes: true,
      profile: 'ai-friendly'
    });

    // Should not have fixedCollection structure errors
    const hasFixedCollectionError = result.errors.some(e => 
      e.message.includes('propertyValues[itemName] is not iterable')
    );
    expect(hasFixedCollectionError).toBe(false);
  });

  test('should catch multiple fixedCollection errors in a single workflow', async () => {
    const workflow = {
      name: 'Test Workflow with Multiple Invalid Structures',
      nodes: [
        {
          id: 'webhook',
          name: 'Webhook',
          type: 'n8n-nodes-base.webhook',
          position: [0, 0] as [number, number],
          parameters: {
            path: 'test-webhook'
          }
        },
        {
          id: 'switch',
          name: 'Switch',
          type: 'n8n-nodes-base.switch',
          position: [200, 0] as [number, number],
          parameters: {
            rules: {
              conditions: {
                values: [{ value1: 'test', operation: 'equals', value2: 'test' }]
              }
            }
          }
        },
        {
          id: 'if',
          name: 'If',
          type: 'n8n-nodes-base.if',
          position: [400, 0] as [number, number],
          parameters: {
            conditions: {
              values: [{ value1: 'test', operation: 'equals', value2: 'test' }]
            }
          }
        },
        {
          id: 'filter',
          name: 'Filter',
          type: 'n8n-nodes-base.filter',
          position: [600, 0] as [number, number],
          parameters: {
            conditions: {
              values: [{ value1: 'test', operation: 'equals', value2: 'test' }]
            }
          }
        }
      ],
      connections: {
        Webhook: {
          main: [[{ node: 'Switch', type: 'main', index: 0 }]]
        },
        Switch: {
          main: [
            [{ node: 'If', type: 'main', index: 0 }],
            [{ node: 'Filter', type: 'main', index: 0 }]
          ]
        }
      }
    };

    const result = await validator.validateWorkflow(workflow, {
      validateNodes: true,
      profile: 'ai-friendly'
    });

    expect(result.valid).toBe(false);
    expect(result.errors.length).toBeGreaterThanOrEqual(3); // At least one error for each problematic node
    
    // Check that each problematic node has an error
    const switchError = result.errors.find(e => e.nodeId === 'switch');
    const ifError = result.errors.find(e => e.nodeId === 'if');
    const filterError = result.errors.find(e => e.nodeId === 'filter');
    
    expect(switchError).toBeDefined();
    expect(ifError).toBeDefined();
    expect(filterError).toBeDefined();
  });

  test('should provide helpful statistics about fixedCollection errors', async () => {
    const workflow = {
      name: 'Test Workflow Statistics',
      nodes: [
        {
          id: 'webhook',
          name: 'Webhook',
          type: 'n8n-nodes-base.webhook',
          position: [0, 0] as [number, number],
          parameters: { path: 'test' }
        },
        {
          id: 'bad-switch',
          name: 'Bad Switch',
          type: 'n8n-nodes-base.switch',
          position: [200, 0] as [number, number],
          parameters: {
            rules: {
              conditions: { values: [{ value1: 'test', operation: 'equals', value2: 'test' }] }
            }
          }
        },
        {
          id: 'good-switch',
          name: 'Good Switch',
          type: 'n8n-nodes-base.switch',
          position: [400, 0] as [number, number],
          parameters: {
            rules: {
              values: [{ conditions: { value1: 'test', operation: 'equals', value2: 'test' }, outputKey: 'out' }]
            }
          }
        }
      ],
      connections: {
        Webhook: {
          main: [
            [{ node: 'Bad Switch', type: 'main', index: 0 }],
            [{ node: 'Good Switch', type: 'main', index: 0 }]
          ]
        }
      }
    };

    const result = await validator.validateWorkflow(workflow, {
      validateNodes: true,
      profile: 'ai-friendly'
    });

    expect(result.statistics.totalNodes).toBe(3);
    expect(result.statistics.enabledNodes).toBe(3);
    expect(result.valid).toBe(false); // Should be invalid due to the bad switch
    
    // Should have at least one error for the bad switch
    const badSwitchError = result.errors.find(e => e.nodeId === 'bad-switch');
    expect(badSwitchError).toBeDefined();
    
    // Should not have errors for the good switch or webhook
    const goodSwitchError = result.errors.find(e => e.nodeId === 'good-switch');
    const webhookError = result.errors.find(e => e.nodeId === 'webhook');
    
    // These might have other validation errors, but not fixedCollection errors
    if (goodSwitchError) {
      expect(goodSwitchError.message).not.toContain('propertyValues[itemName] is not iterable');
    }
    if (webhookError) {
      expect(webhookError.message).not.toContain('propertyValues[itemName] is not iterable');
    }
  });

  test('should work with different validation profiles', async () => {
    const workflow = {
      name: 'Test Profile Compatibility',
      nodes: [
        {
          id: 'switch',
          name: 'Switch',
          type: 'n8n-nodes-base.switch',
          position: [0, 0] as [number, number],
          parameters: {
            rules: {
              conditions: {
                values: [{ value1: 'test', operation: 'equals', value2: 'test' }]
              }
            }
          }
        }
      ],
      connections: {}
    };

    const profiles: Array<'strict' | 'runtime' | 'ai-friendly' | 'minimal'> = 
      ['strict', 'runtime', 'ai-friendly', 'minimal'];

    for (const profile of profiles) {
      const result = await validator.validateWorkflow(workflow, {
        validateNodes: true,
        profile
      });

      // All profiles should catch this critical error
      const hasCriticalError = result.errors.some(e => 
        e.message.includes('propertyValues[itemName] is not iterable')
      );
      
      expect(hasCriticalError, `Profile ${profile} should catch critical fixedCollection error`).toBe(true);
      expect(result.valid, `Profile ${profile} should mark workflow as invalid`).toBe(false);
    }
  });
});
```

--------------------------------------------------------------------------------
/tests/unit/services/fixed-collection-validation.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Fixed Collection Validation Tests
 * Tests for the fix of issue #90: "propertyValues[itemName] is not iterable" error
 * 
 * This ensures AI agents cannot create invalid fixedCollection structures that break n8n UI
 */

import { describe, test, expect } from 'vitest';
import { EnhancedConfigValidator } from '../../../src/services/enhanced-config-validator';

describe('FixedCollection Validation', () => {
  describe('Switch Node v2/v3 Validation', () => {
    test('should detect invalid nested conditions structure', () => {
      const invalidConfig = {
        rules: {
          conditions: {
            values: [
              {
                value1: '={{$json.status}}',
                operation: 'equals',
                value2: 'active'
              }
            ]
          }
        }
      };

      const result = EnhancedConfigValidator.validateWithMode(
        'nodes-base.switch',
        invalidConfig,
        [],
        'operation',
        'ai-friendly'
      );

      expect(result.valid).toBe(false);
      expect(result.errors).toHaveLength(1);
      expect(result.errors[0].type).toBe('invalid_value');
      expect(result.errors[0].property).toBe('rules');
      expect(result.errors[0].message).toContain('propertyValues[itemName] is not iterable');
      expect(result.errors[0].fix).toContain('{ "rules": { "values": [{ "conditions": {...}, "outputKey": "output1" }] } }');
    });

    test('should detect direct conditions in rules (another invalid pattern)', () => {
      const invalidConfig = {
        rules: {
          conditions: {
            value1: '={{$json.status}}',
            operation: 'equals',
            value2: 'active'
          }
        }
      };

      const result = EnhancedConfigValidator.validateWithMode(
        'nodes-base.switch',
        invalidConfig,
        [],
        'operation',
        'ai-friendly'
      );

      expect(result.valid).toBe(false);
      expect(result.errors).toHaveLength(1);
      expect(result.errors[0].message).toContain('Invalid structure for nodes-base.switch node');
    });

    test('should provide auto-fix for invalid switch structure', () => {
      const invalidConfig = {
        rules: {
          conditions: {
            values: [
              {
                value1: '={{$json.status}}',
                operation: 'equals',
                value2: 'active'
              }
            ]
          }
        }
      };

      const result = EnhancedConfigValidator.validateWithMode(
        'nodes-base.switch',
        invalidConfig,
        [],
        'operation',
        'ai-friendly'
      );

      expect(result.autofix).toBeDefined();
      expect(result.autofix!.rules).toBeDefined();
      expect(result.autofix!.rules.values).toBeInstanceOf(Array);
      expect(result.autofix!.rules.values).toHaveLength(1);
      expect(result.autofix!.rules.values[0]).toHaveProperty('conditions');
      expect(result.autofix!.rules.values[0]).toHaveProperty('outputKey');
    });

    test('should accept valid switch structure', () => {
      const validConfig = {
        rules: {
          values: [
            {
              conditions: {
                value1: '={{$json.status}}',
                operation: 'equals',
                value2: 'active'
              },
              outputKey: 'active'
            }
          ]
        }
      };

      const result = EnhancedConfigValidator.validateWithMode(
        'nodes-base.switch',
        validConfig,
        [],
        'operation',
        'ai-friendly'
      );

      // Should not have the specific fixedCollection error
      const hasFixedCollectionError = result.errors.some(e => 
        e.message.includes('propertyValues[itemName] is not iterable')
      );
      expect(hasFixedCollectionError).toBe(false);
    });

    test('should warn about missing outputKey in valid structure', () => {
      const configMissingOutputKey = {
        rules: {
          values: [
            {
              conditions: {
                value1: '={{$json.status}}',
                operation: 'equals',
                value2: 'active'
              }
              // Missing outputKey
            }
          ]
        }
      };

      const result = EnhancedConfigValidator.validateWithMode(
        'nodes-base.switch',
        configMissingOutputKey,
        [],
        'operation',
        'ai-friendly'
      );

      const hasOutputKeyWarning = result.warnings.some(w => 
        w.message.includes('missing "outputKey" property')
      );
      expect(hasOutputKeyWarning).toBe(true);
    });
  });

  describe('If Node Validation', () => {
    test('should detect invalid nested values structure', () => {
      const invalidConfig = {
        conditions: {
          values: [
            {
              value1: '={{$json.age}}',
              operation: 'largerEqual',
              value2: 18
            }
          ]
        }
      };

      const result = EnhancedConfigValidator.validateWithMode(
        'nodes-base.if',
        invalidConfig,
        [],
        'operation',
        'ai-friendly'
      );

      expect(result.valid).toBe(false);
      expect(result.errors).toHaveLength(1);
      expect(result.errors[0].type).toBe('invalid_value');
      expect(result.errors[0].property).toBe('conditions');
      expect(result.errors[0].message).toContain('Invalid structure for nodes-base.if node');
      expect(result.errors[0].fix).toBe('Use: { "conditions": {...} } or { "conditions": [...] } directly, not nested under "values"');
    });

    test('should provide auto-fix for invalid if structure', () => {
      const invalidConfig = {
        conditions: {
          values: [
            {
              value1: '={{$json.age}}',
              operation: 'largerEqual',
              value2: 18
            }
          ]
        }
      };

      const result = EnhancedConfigValidator.validateWithMode(
        'nodes-base.if',
        invalidConfig,
        [],
        'operation',
        'ai-friendly'
      );

      expect(result.autofix).toBeDefined();
      expect(result.autofix!.conditions).toEqual(invalidConfig.conditions.values);
    });

    test('should accept valid if structure', () => {
      const validConfig = {
        conditions: {
          value1: '={{$json.age}}',
          operation: 'largerEqual',
          value2: 18
        }
      };

      const result = EnhancedConfigValidator.validateWithMode(
        'nodes-base.if',
        validConfig,
        [],
        'operation',
        'ai-friendly'
      );

      // Should not have the specific structure error
      const hasStructureError = result.errors.some(e => 
        e.message.includes('should be a filter object/array directly')
      );
      expect(hasStructureError).toBe(false);
    });
  });

  describe('Filter Node Validation', () => {
    test('should detect invalid nested values structure', () => {
      const invalidConfig = {
        conditions: {
          values: [
            {
              value1: '={{$json.score}}',
              operation: 'larger',
              value2: 80
            }
          ]
        }
      };

      const result = EnhancedConfigValidator.validateWithMode(
        'nodes-base.filter',
        invalidConfig,
        [],
        'operation',
        'ai-friendly'
      );

      expect(result.valid).toBe(false);
      expect(result.errors).toHaveLength(1);
      expect(result.errors[0].type).toBe('invalid_value');
      expect(result.errors[0].property).toBe('conditions');
      expect(result.errors[0].message).toContain('Invalid structure for nodes-base.filter node');
    });

    test('should accept valid filter structure', () => {
      const validConfig = {
        conditions: {
          value1: '={{$json.score}}',
          operation: 'larger',
          value2: 80
        }
      };

      const result = EnhancedConfigValidator.validateWithMode(
        'nodes-base.filter',
        validConfig,
        [],
        'operation',
        'ai-friendly'
      );

      // Should not have the specific structure error
      const hasStructureError = result.errors.some(e => 
        e.message.includes('should be a filter object/array directly')
      );
      expect(hasStructureError).toBe(false);
    });
  });

  describe('Edge Cases', () => {
    test('should not validate non-problematic nodes', () => {
      const config = {
        someProperty: {
          conditions: {
            values: ['should', 'not', 'trigger', 'validation']
          }
        }
      };

      const result = EnhancedConfigValidator.validateWithMode(
        'nodes-base.httpRequest',
        config,
        [],
        'operation',
        'ai-friendly'
      );

      // Should not have fixedCollection errors for non-problematic nodes
      const hasFixedCollectionError = result.errors.some(e => 
        e.message.includes('propertyValues[itemName] is not iterable')
      );
      expect(hasFixedCollectionError).toBe(false);
    });

    test('should handle empty config gracefully', () => {
      const result = EnhancedConfigValidator.validateWithMode(
        'nodes-base.switch',
        {},
        [],
        'operation',
        'ai-friendly'
      );

      // Should not crash or produce false positives
      expect(result).toBeDefined();
      expect(result.errors).toBeInstanceOf(Array);
    });

    test('should handle non-object property values', () => {
      const config = {
        rules: 'not an object'
      };

      const result = EnhancedConfigValidator.validateWithMode(
        'nodes-base.switch',
        config,
        [],
        'operation',
        'ai-friendly'
      );

      // Should not crash on non-object values
      expect(result).toBeDefined();
      expect(result.errors).toBeInstanceOf(Array);
    });
  });

  describe('Real-world AI Agent Patterns', () => {
    test('should catch common ChatGPT/Claude switch patterns', () => {
      // This is a pattern commonly generated by AI agents
      const aiGeneratedConfig = {
        rules: {
          conditions: {
            values: [
              {
                "value1": "={{$json.status}}",
                "operation": "equals", 
                "value2": "active"
              },
              {
                "value1": "={{$json.priority}}",
                "operation": "equals",
                "value2": "high"
              }
            ]
          }
        }
      };

      const result = EnhancedConfigValidator.validateWithMode(
        'nodes-base.switch',
        aiGeneratedConfig,
        [],
        'operation',
        'ai-friendly'
      );

      expect(result.valid).toBe(false);
      expect(result.errors).toHaveLength(1);
      expect(result.errors[0].message).toContain('propertyValues[itemName] is not iterable');
      
      // Check auto-fix generates correct structure
      expect(result.autofix!.rules.values).toHaveLength(2);
      result.autofix!.rules.values.forEach((rule: any) => {
        expect(rule).toHaveProperty('conditions');
        expect(rule).toHaveProperty('outputKey');
      });
    });

    test('should catch common AI if/filter patterns', () => {
      const aiGeneratedIfConfig = {
        conditions: {
          values: {
            "value1": "={{$json.age}}",
            "operation": "largerEqual",
            "value2": 21
          }
        }
      };

      const result = EnhancedConfigValidator.validateWithMode(
        'nodes-base.if',
        aiGeneratedIfConfig,
        [],
        'operation',
        'ai-friendly'
      );

      expect(result.valid).toBe(false);
      expect(result.errors[0].message).toContain('Invalid structure for nodes-base.if node');
    });
  });

  describe('Version Compatibility', () => {
    test('should work across different validation profiles', () => {
      const invalidConfig = {
        rules: {
          conditions: {
            values: [{ value1: 'test', operation: 'equals', value2: 'test' }]
          }
        }
      };

      const profiles: Array<'strict' | 'runtime' | 'ai-friendly' | 'minimal'> = 
        ['strict', 'runtime', 'ai-friendly', 'minimal'];

      profiles.forEach(profile => {
        const result = EnhancedConfigValidator.validateWithMode(
          'nodes-base.switch',
          invalidConfig,
          [],
          'operation',
          profile
        );

        // All profiles should catch this critical error
        const hasCriticalError = result.errors.some(e => 
          e.message.includes('propertyValues[itemName] is not iterable')
        );
        
        expect(hasCriticalError, `Profile ${profile} should catch critical fixedCollection error`).toBe(true);
      });
    });
  });
});
```

--------------------------------------------------------------------------------
/src/telemetry/config-manager.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Telemetry Configuration Manager
 * Handles telemetry settings, opt-in/opt-out, and first-run detection
 */

import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
import { join, resolve, dirname } from 'path';
import { homedir } from 'os';
import { createHash } from 'crypto';
import { hostname, platform, arch } from 'os';

export interface TelemetryConfig {
  enabled: boolean;
  userId: string;
  firstRun?: string;
  lastModified?: string;
  version?: string;
}

export class TelemetryConfigManager {
  private static instance: TelemetryConfigManager;
  private readonly configDir: string;
  private readonly configPath: string;
  private config: TelemetryConfig | null = null;

  private constructor() {
    this.configDir = join(homedir(), '.n8n-mcp');
    this.configPath = join(this.configDir, 'telemetry.json');
  }

  static getInstance(): TelemetryConfigManager {
    if (!TelemetryConfigManager.instance) {
      TelemetryConfigManager.instance = new TelemetryConfigManager();
    }
    return TelemetryConfigManager.instance;
  }

  /**
   * Generate a deterministic anonymous user ID based on machine characteristics
   * Uses Docker/cloud-specific method for containerized environments
   */
  private generateUserId(): string {
    // Use boot_id for all Docker/cloud environments (stable across container updates)
    if (process.env.IS_DOCKER === 'true' || this.isCloudEnvironment()) {
      return this.generateDockerStableId();
    }

    // Local installations use file-based method with hostname
    const machineId = `${hostname()}-${platform()}-${arch()}-${homedir()}`;
    return createHash('sha256').update(machineId).digest('hex').substring(0, 16);
  }

  /**
   * Generate stable user ID for Docker/cloud environments
   * Priority: boot_id → combined signals → generic fallback
   */
  private generateDockerStableId(): string {
    // Priority 1: Try boot_id (stable across container recreations)
    const bootId = this.readBootId();
    if (bootId) {
      const fingerprint = `${bootId}-${platform()}-${arch()}`;
      return createHash('sha256').update(fingerprint).digest('hex').substring(0, 16);
    }

    // Priority 2: Try combined host signals
    const combinedFingerprint = this.generateCombinedFingerprint();
    if (combinedFingerprint) {
      return combinedFingerprint;
    }

    // Priority 3: Generic Docker ID (allows aggregate statistics)
    const genericId = `docker-${platform()}-${arch()}`;
    return createHash('sha256').update(genericId).digest('hex').substring(0, 16);
  }

  /**
   * Read host boot_id from /proc (available in Linux containers)
   * Returns null if not available or invalid format
   */
  private readBootId(): string | null {
    try {
      const bootIdPath = '/proc/sys/kernel/random/boot_id';

      if (!existsSync(bootIdPath)) {
        return null;
      }

      const bootId = readFileSync(bootIdPath, 'utf-8').trim();

      // Validate UUID format (8-4-4-4-12 hex digits)
      const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
      if (!uuidRegex.test(bootId)) {
        return null;
      }

      return bootId;
    } catch (error) {
      // File not readable or other error
      return null;
    }
  }

  /**
   * Generate fingerprint from combined host signals
   * Fallback for environments where boot_id is not available
   */
  private generateCombinedFingerprint(): string | null {
    try {
      const signals: string[] = [];

      // CPU cores (stable)
      if (existsSync('/proc/cpuinfo')) {
        const cpuinfo = readFileSync('/proc/cpuinfo', 'utf-8');
        const cores = (cpuinfo.match(/processor\s*:/g) || []).length;
        if (cores > 0) {
          signals.push(`cores:${cores}`);
        }
      }

      // Memory (stable)
      if (existsSync('/proc/meminfo')) {
        const meminfo = readFileSync('/proc/meminfo', 'utf-8');
        const totalMatch = meminfo.match(/MemTotal:\s+(\d+)/);
        if (totalMatch) {
          signals.push(`mem:${totalMatch[1]}`);
        }
      }

      // Kernel version (stable)
      if (existsSync('/proc/version')) {
        const version = readFileSync('/proc/version', 'utf-8');
        const kernelMatch = version.match(/Linux version ([\d.]+)/);
        if (kernelMatch) {
          signals.push(`kernel:${kernelMatch[1]}`);
        }
      }

      // Platform and arch
      signals.push(platform(), arch());

      // Need at least 3 signals for reasonable uniqueness
      if (signals.length < 3) {
        return null;
      }

      const fingerprint = signals.join('-');
      return createHash('sha256').update(fingerprint).digest('hex').substring(0, 16);
    } catch (error) {
      return null;
    }
  }

  /**
   * Check if running in a cloud environment
   */
  private isCloudEnvironment(): boolean {
    return !!(
      process.env.RAILWAY_ENVIRONMENT ||
      process.env.RENDER ||
      process.env.FLY_APP_NAME ||
      process.env.HEROKU_APP_NAME ||
      process.env.AWS_EXECUTION_ENV ||
      process.env.KUBERNETES_SERVICE_HOST ||
      process.env.GOOGLE_CLOUD_PROJECT ||
      process.env.AZURE_FUNCTIONS_ENVIRONMENT
    );
  }

  /**
   * Load configuration from disk or create default
   */
  loadConfig(): TelemetryConfig {
    if (this.config) {
      return this.config;
    }

    if (!existsSync(this.configPath)) {
      // First run - create default config
      const version = this.getPackageVersion();

      // Check if telemetry is disabled via environment variable
      const envDisabled = this.isDisabledByEnvironment();

      this.config = {
        enabled: !envDisabled, // Respect env var on first run
        userId: this.generateUserId(),
        firstRun: new Date().toISOString(),
        version
      };

      this.saveConfig();

      // Only show notice if not disabled via environment
      if (!envDisabled) {
        this.showFirstRunNotice();
      }

      return this.config;
    }

    try {
      const rawConfig = readFileSync(this.configPath, 'utf-8');
      this.config = JSON.parse(rawConfig);

      // Ensure userId exists (for upgrades from older versions)
      if (!this.config!.userId) {
        this.config!.userId = this.generateUserId();
        this.saveConfig();
      }

      return this.config!;
    } catch (error) {
      console.error('Failed to load telemetry config, using defaults:', error);
      this.config = {
        enabled: false,
        userId: this.generateUserId()
      };
      return this.config;
    }
  }

  /**
   * Save configuration to disk
   */
  private saveConfig(): void {
    if (!this.config) return;

    try {
      if (!existsSync(this.configDir)) {
        mkdirSync(this.configDir, { recursive: true });
      }

      this.config.lastModified = new Date().toISOString();
      writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
    } catch (error) {
      console.error('Failed to save telemetry config:', error);
    }
  }

  /**
   * Check if telemetry is enabled
   * Priority: Environment variable > Config file > Default (true)
   */
  isEnabled(): boolean {
    // Check environment variables first (for Docker users)
    if (this.isDisabledByEnvironment()) {
      return false;
    }

    const config = this.loadConfig();
    return config.enabled;
  }

  /**
   * Check if telemetry is disabled via environment variable
   */
  private isDisabledByEnvironment(): boolean {
    const envVars = [
      'N8N_MCP_TELEMETRY_DISABLED',
      'TELEMETRY_DISABLED',
      'DISABLE_TELEMETRY'
    ];

    for (const varName of envVars) {
      const value = process.env[varName];
      if (value !== undefined) {
        const normalized = value.toLowerCase().trim();

        // Warn about invalid values
        if (!['true', 'false', '1', '0', ''].includes(normalized)) {
          console.warn(
            `⚠️  Invalid telemetry environment variable value: ${varName}="${value}"\n` +
            `   Use "true" to disable or "false" to enable telemetry.`
          );
        }

        // Accept common truthy values
        if (normalized === 'true' || normalized === '1') {
          return true;
        }
      }
    }

    return false;
  }

  /**
   * Get the anonymous user ID
   */
  getUserId(): string {
    const config = this.loadConfig();
    return config.userId;
  }

  /**
   * Check if this is the first run
   */
  isFirstRun(): boolean {
    return !existsSync(this.configPath);
  }

  /**
   * Enable telemetry
   */
  enable(): void {
    const config = this.loadConfig();
    config.enabled = true;
    this.config = config;
    this.saveConfig();
    console.log('✓ Anonymous telemetry enabled');
  }

  /**
   * Disable telemetry
   */
  disable(): void {
    const config = this.loadConfig();
    config.enabled = false;
    this.config = config;
    this.saveConfig();
    console.log('✓ Anonymous telemetry disabled');
  }

  /**
   * Get current status
   */
  getStatus(): string {
    const config = this.loadConfig();

    // Check if disabled by environment
    const envDisabled = this.isDisabledByEnvironment();

    let status = config.enabled ? 'ENABLED' : 'DISABLED';
    if (envDisabled) {
      status = 'DISABLED (via environment variable)';
    }

    return `
Telemetry Status: ${status}
Anonymous ID: ${config.userId}
First Run: ${config.firstRun || 'Unknown'}
Config Path: ${this.configPath}

To opt-out: npx n8n-mcp telemetry disable
To opt-in:  npx n8n-mcp telemetry enable

For Docker: Set N8N_MCP_TELEMETRY_DISABLED=true
`;
  }

  /**
   * Show first-run notice to user
   */
  private showFirstRunNotice(): void {
    console.log(`
╔════════════════════════════════════════════════════════════╗
║              Anonymous Usage Statistics                     ║
╠════════════════════════════════════════════════════════════╣
║                                                             ║
║  n8n-mcp collects anonymous usage data to improve the      ║
║  tool and understand how it's being used.                  ║
║                                                             ║
║  We track:                                                 ║
║  • Which MCP tools are used (no parameters)                ║
║  • Workflow structures (sanitized, no sensitive data)      ║
║  • Error patterns (hashed, no details)                     ║
║  • Performance metrics (timing, success rates)             ║
║                                                             ║
║  We NEVER collect:                                         ║
║  • URLs, API keys, or credentials                          ║
║  • Workflow content or actual data                         ║
║  • Personal or identifiable information                    ║
║  • n8n instance details or locations                       ║
║                                                             ║
║  Your anonymous ID: ${this.config?.userId || 'generating...'}          ║
║                                                             ║
║  This helps me understand usage patterns and improve       ║
║  n8n-mcp for everyone. Thank you for your support!         ║
║                                                             ║
║  To opt-out at any time:                                   ║
║  npx n8n-mcp telemetry disable                            ║
║                                                             ║
║  Data deletion requests:                                   ║
║  Email [email protected] with your anonymous ID          ║
║                                                             ║
║  Learn more:                                               ║
║  https://github.com/czlonkowski/n8n-mcp/blob/main/PRIVACY.md ║
║                                                             ║
╚════════════════════════════════════════════════════════════╝
`);
  }

  /**
   * Get package version safely
   */
  private getPackageVersion(): string {
    try {
      // Try multiple approaches to find package.json
      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;
          }
        }
      }

      // Fallback: try require (works in some environments)
      try {
        const packageJson = require('../../package.json');
        return packageJson.version || 'unknown';
      } catch {
        // Ignore require error
      }

      return 'unknown';
    } catch (error) {
      return 'unknown';
    }
  }
}
```

--------------------------------------------------------------------------------
/scripts/prepare-release.js:
--------------------------------------------------------------------------------

```javascript
#!/usr/bin/env node

/**
 * Pre-release preparation script
 * Validates and prepares everything needed for a successful release
 */

const fs = require('fs');
const path = require('path');
const { execSync, spawnSync } = require('child_process');
const readline = require('readline');

// Color codes
const colors = {
  reset: '\x1b[0m',
  red: '\x1b[31m',
  green: '\x1b[32m',
  yellow: '\x1b[33m',
  blue: '\x1b[34m',
  magenta: '\x1b[35m',
  cyan: '\x1b[36m'
};

function log(message, color = 'reset') {
  console.log(`${colors[color]}${message}${colors.reset}`);
}

function success(message) {
  log(`✅ ${message}`, 'green');
}

function warning(message) {
  log(`⚠️  ${message}`, 'yellow');
}

function error(message) {
  log(`❌ ${message}`, 'red');
}

function info(message) {
  log(`ℹ️  ${message}`, 'blue');
}

function header(title) {
  log(`\n${'='.repeat(60)}`, 'cyan');
  log(`🚀 ${title}`, 'cyan');
  log(`${'='.repeat(60)}`, 'cyan');
}

class ReleasePreparation {
  constructor() {
    this.rootDir = path.resolve(__dirname, '..');
    this.rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout
    });
  }

  async askQuestion(question) {
    return new Promise((resolve) => {
      this.rl.question(question, resolve);
    });
  }

  /**
   * Get current version and ask for new version
   */
  async getVersionInfo() {
    const packageJson = require(path.join(this.rootDir, 'package.json'));
    const currentVersion = packageJson.version;
    
    log(`\nCurrent version: ${currentVersion}`, 'blue');
    
    const newVersion = await this.askQuestion('\nEnter new version (e.g., 2.10.0): ');
    
    if (!newVersion || !this.isValidSemver(newVersion)) {
      error('Invalid semantic version format');
      throw new Error('Invalid version');
    }
    
    if (this.compareVersions(newVersion, currentVersion) <= 0) {
      error('New version must be greater than current version');
      throw new Error('Version not incremented');
    }
    
    return { currentVersion, newVersion };
  }

  /**
   * Validate semantic version format (strict semver compliance)
   */
  isValidSemver(version) {
    // Strict semantic versioning regex
    const semverRegex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
    return semverRegex.test(version);
  }

  /**
   * Compare two semantic versions
   */
  compareVersions(v1, v2) {
    const parseVersion = (v) => v.split('-')[0].split('.').map(Number);
    const [v1Parts, v2Parts] = [parseVersion(v1), parseVersion(v2)];
    
    for (let i = 0; i < 3; i++) {
      if (v1Parts[i] > v2Parts[i]) return 1;
      if (v1Parts[i] < v2Parts[i]) return -1;
    }
    return 0;
  }

  /**
   * Update version in package files
   */
  updateVersions(newVersion) {
    log('\n📝 Updating version in package files...', 'blue');
    
    // Update package.json
    const packageJsonPath = path.join(this.rootDir, 'package.json');
    const packageJson = require(packageJsonPath);
    packageJson.version = newVersion;
    fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
    success('Updated package.json');
    
    // Sync to runtime package
    try {
      execSync('npm run sync:runtime-version', { cwd: this.rootDir, stdio: 'pipe' });
      success('Synced package.runtime.json');
    } catch (err) {
      warning('Could not sync runtime version automatically');
      
      // Manual sync
      const runtimeJsonPath = path.join(this.rootDir, 'package.runtime.json');
      if (fs.existsSync(runtimeJsonPath)) {
        const runtimeJson = require(runtimeJsonPath);
        runtimeJson.version = newVersion;
        fs.writeFileSync(runtimeJsonPath, JSON.stringify(runtimeJson, null, 2) + '\n');
        success('Manually synced package.runtime.json');
      }
    }
  }

  /**
   * Update changelog
   */
  async updateChangelog(newVersion) {
    const changelogPath = path.join(this.rootDir, 'docs/CHANGELOG.md');
    
    if (!fs.existsSync(changelogPath)) {
      warning('Changelog file not found, skipping update');
      return;
    }
    
    log('\n📋 Updating changelog...', 'blue');
    
    const content = fs.readFileSync(changelogPath, 'utf8');
    const today = new Date().toISOString().split('T')[0];
    
    // Check if version already exists in changelog
    const versionRegex = new RegExp(`^## \\[${newVersion.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]`, 'm');
    if (versionRegex.test(content)) {
      info(`Version ${newVersion} already exists in changelog`);
      return;
    }
    
    // Find the Unreleased section
    const unreleasedMatch = content.match(/^## \[Unreleased\]\s*\n([\s\S]*?)(?=\n## \[|$)/m);
    
    if (unreleasedMatch) {
      const unreleasedContent = unreleasedMatch[1].trim();
      
      if (unreleasedContent) {
        log('\nFound content in Unreleased section:', 'blue');
        log(unreleasedContent.substring(0, 200) + '...', 'yellow');
        
        const moveContent = await this.askQuestion('\nMove this content to the new version? (y/n): ');
        
        if (moveContent.toLowerCase() === 'y') {
          // Move unreleased content to new version
          const newVersionSection = `## [${newVersion}] - ${today}\n\n${unreleasedContent}\n\n`;
          const updatedContent = content.replace(
            /^## \[Unreleased\]\s*\n[\s\S]*?(?=\n## \[)/m,
            `## [Unreleased]\n\n${newVersionSection}## [`
          );
          
          fs.writeFileSync(changelogPath, updatedContent);
          success(`Moved unreleased content to version ${newVersion}`);
        } else {
          // Just add empty version section
          const newVersionSection = `## [${newVersion}] - ${today}\n\n### Added\n- \n\n### Changed\n- \n\n### Fixed\n- \n\n`;
          const updatedContent = content.replace(
            /^## \[Unreleased\]\s*\n/m,
            `## [Unreleased]\n\n${newVersionSection}`
          );
          
          fs.writeFileSync(changelogPath, updatedContent);
          warning(`Added empty version section for ${newVersion} - please fill in the changes`);
        }
      } else {
        // Add empty version section
        const newVersionSection = `## [${newVersion}] - ${today}\n\n### Added\n- \n\n### Changed\n- \n\n### Fixed\n- \n\n`;
        const updatedContent = content.replace(
          /^## \[Unreleased\]\s*\n/m,
          `## [Unreleased]\n\n${newVersionSection}`
        );
        
        fs.writeFileSync(changelogPath, updatedContent);
        warning(`Added empty version section for ${newVersion} - please fill in the changes`);
      }
    } else {
      warning('Could not find Unreleased section in changelog');
    }
    
    info('Please review and edit the changelog before committing');
  }

  /**
   * Run tests and build
   */
  async runChecks() {
    log('\n🧪 Running pre-release checks...', 'blue');
    
    try {
      // Run tests
      log('Running tests...', 'blue');
      execSync('npm test', { cwd: this.rootDir, stdio: 'inherit' });
      success('All tests passed');
      
      // Run build
      log('Building project...', 'blue');
      execSync('npm run build', { cwd: this.rootDir, stdio: 'inherit' });
      success('Build completed');
      
      // Rebuild database
      log('Rebuilding database...', 'blue');
      execSync('npm run rebuild', { cwd: this.rootDir, stdio: 'inherit' });
      success('Database rebuilt');
      
      // Run type checking
      log('Type checking...', 'blue');
      execSync('npm run typecheck', { cwd: this.rootDir, stdio: 'inherit' });
      success('Type checking passed');
      
    } catch (err) {
      error('Pre-release checks failed');
      throw err;
    }
  }

  /**
   * Create git commit
   */
  async createCommit(newVersion) {
    log('\n📝 Creating git commit...', 'blue');
    
    try {
      // Check git status
      const status = execSync('git status --porcelain', { 
        cwd: this.rootDir, 
        encoding: 'utf8' 
      });
      
      if (!status.trim()) {
        info('No changes to commit');
        return;
      }
      
      // Show what will be committed
      log('\nFiles to be committed:', 'blue');
      execSync('git diff --name-only', { cwd: this.rootDir, stdio: 'inherit' });
      
      const commit = await this.askQuestion('\nCreate commit for release? (y/n): ');
      
      if (commit.toLowerCase() === 'y') {
        // Add files
        execSync('git add package.json package.runtime.json docs/CHANGELOG.md', { 
          cwd: this.rootDir, 
          stdio: 'pipe' 
        });
        
        // Create commit
        const commitMessage = `chore: release v${newVersion}

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>`;
        
        const result = spawnSync('git', ['commit', '-m', commitMessage], { 
          cwd: this.rootDir, 
          stdio: 'pipe',
          encoding: 'utf8'
        });
        
        if (result.error || result.status !== 0) {
          throw new Error(`Git commit failed: ${result.stderr || result.error?.message}`);
        }
        
        success(`Created commit for v${newVersion}`);
        
        const push = await this.askQuestion('\nPush to trigger release workflow? (y/n): ');
        
        if (push.toLowerCase() === 'y') {
          // Add confirmation for destructive operation
          warning('\n⚠️  DESTRUCTIVE OPERATION WARNING ⚠️');
          warning('This will trigger a PUBLIC RELEASE that cannot be undone!');
          warning('The following will happen automatically:');
          warning('• Create GitHub release with tag');
          warning('• Publish package to NPM registry');
          warning('• Build and push Docker images');
          warning('• Update documentation');
          
          const confirmation = await this.askQuestion('\nType "RELEASE" (all caps) to confirm: ');
          
          if (confirmation === 'RELEASE') {
            execSync('git push', { cwd: this.rootDir, stdio: 'inherit' });
            success('Pushed to remote repository');
            log('\n🎉 Release workflow will be triggered automatically!', 'green');
            log('Monitor progress at: https://github.com/czlonkowski/n8n-mcp/actions', 'blue');
          } else {
            warning('Release cancelled. Commit created but not pushed.');
            info('You can push manually later to trigger the release.');
          }
        } else {
          info('Commit created but not pushed. Push manually to trigger release.');
        }
      }
      
    } catch (err) {
      error(`Git operations failed: ${err.message}`);
      throw err;
    }
  }

  /**
   * Display final instructions
   */
  displayInstructions(newVersion) {
    header('Release Preparation Complete');
    
    log('📋 What happens next:', 'blue');
    log(`1. The GitHub Actions workflow will detect the version change to v${newVersion}`, 'green');
    log('2. It will automatically:', 'green');
    log('   • Create a GitHub release with changelog content', 'green');
    log('   • Publish the npm package', 'green');
    log('   • Build and push Docker images', 'green');
    log('   • Update documentation badges', 'green');
    log('\n🔍 Monitor the release at:', 'blue');
    log('   • GitHub Actions: https://github.com/czlonkowski/n8n-mcp/actions', 'blue');
    log('   • NPM Package: https://www.npmjs.com/package/n8n-mcp', 'blue');
    log('   • Docker Images: https://github.com/czlonkowski/n8n-mcp/pkgs/container/n8n-mcp', 'blue');
    
    log('\n✅ Release preparation completed successfully!', 'green');
  }

  /**
   * Main execution flow
   */
  async run() {
    try {
      header('n8n-MCP Release Preparation');
      
      // Get version information
      const { currentVersion, newVersion } = await this.getVersionInfo();
      
      log(`\n🔄 Preparing release: ${currentVersion} → ${newVersion}`, 'magenta');
      
      // Update versions
      this.updateVersions(newVersion);
      
      // Update changelog
      await this.updateChangelog(newVersion);
      
      // Run pre-release checks
      await this.runChecks();
      
      // Create git commit
      await this.createCommit(newVersion);
      
      // Display final instructions
      this.displayInstructions(newVersion);
      
    } catch (err) {
      error(`Release preparation failed: ${err.message}`);
      process.exit(1);
    } finally {
      this.rl.close();
    }
  }
}

// Run the script
if (require.main === module) {
  const preparation = new ReleasePreparation();
  preparation.run().catch(err => {
    console.error('Release preparation failed:', err);
    process.exit(1);
  });
}

module.exports = ReleasePreparation;
```

--------------------------------------------------------------------------------
/tests/unit/mcp/search-nodes-examples.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { N8NDocumentationMCPServer } from '../../../src/mcp/server';
import { createDatabaseAdapter } from '../../../src/database/database-adapter';
import path from 'path';
import fs from 'fs';

/**
 * Unit tests for search_nodes with includeExamples parameter
 * Testing P0-R3 feature: Template-based configuration examples
 */

describe('search_nodes with includeExamples', () => {
  let server: N8NDocumentationMCPServer;
  let dbPath: string;

  beforeEach(async () => {
    // Use in-memory database for testing
    process.env.NODE_DB_PATH = ':memory:';
    server = new N8NDocumentationMCPServer();
    await (server as any).initialized;

    // Populate in-memory database with test nodes
    // NOTE: Database stores nodes in SHORT form (nodes-base.xxx, not n8n-nodes-base.xxx)
    const testNodes = [
      {
        node_type: 'nodes-base.webhook',
        package_name: 'n8n-nodes-base',
        display_name: 'Webhook',
        description: 'Starts workflow on webhook call',
        category: 'Core Nodes',
        is_ai_tool: 0,
        is_trigger: 1,
        is_webhook: 1,
        is_versioned: 1,
        version: '1',
        properties_schema: JSON.stringify([]),
        operations: JSON.stringify([])
      },
      {
        node_type: 'nodes-base.httpRequest',
        package_name: 'n8n-nodes-base',
        display_name: 'HTTP Request',
        description: 'Makes an HTTP request',
        category: 'Core Nodes',
        is_ai_tool: 0,
        is_trigger: 0,
        is_webhook: 0,
        is_versioned: 1,
        version: '1',
        properties_schema: JSON.stringify([]),
        operations: JSON.stringify([])
      }
    ];

    // Insert test nodes into the in-memory database
    const db = (server as any).db;
    if (db) {
      const insertStmt = db.prepare(`
        INSERT INTO nodes (
          node_type, package_name, display_name, description, category,
          is_ai_tool, is_trigger, is_webhook, is_versioned, version,
          properties_schema, operations
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
      `);

      for (const node of testNodes) {
        insertStmt.run(
          node.node_type,
          node.package_name,
          node.display_name,
          node.description,
          node.category,
          node.is_ai_tool,
          node.is_trigger,
          node.is_webhook,
          node.is_versioned,
          node.version,
          node.properties_schema,
          node.operations
        );
      }
      // Note: FTS table is not created in test environment
      // searchNodes will fall back to LIKE search when FTS doesn't exist
    }
  });

  afterEach(() => {
    delete process.env.NODE_DB_PATH;
  });

  describe('includeExamples parameter', () => {
    it('should not include examples when includeExamples is false', async () => {
      const result = await (server as any).searchNodes('webhook', 5, { includeExamples: false });

      expect(result.results).toBeDefined();
      if (result.results.length > 0) {
        result.results.forEach((node: any) => {
          expect(node.examples).toBeUndefined();
        });
      }
    });

    it('should not include examples when includeExamples is undefined', async () => {
      const result = await (server as any).searchNodes('webhook', 5, {});

      expect(result.results).toBeDefined();
      if (result.results.length > 0) {
        result.results.forEach((node: any) => {
          expect(node.examples).toBeUndefined();
        });
      }
    });

    it('should include examples when includeExamples is true', async () => {
      const result = await (server as any).searchNodes('webhook', 5, { includeExamples: true });

      expect(result.results).toBeDefined();
      // Note: In-memory test database may not have template configs
      // This test validates the parameter is processed correctly
    });

    it('should handle nodes without examples gracefully', async () => {
      const result = await (server as any).searchNodes('nonexistent', 5, { includeExamples: true });

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

    it('should limit examples to top 2 per node', async () => {
      // This test would need a database with actual template_node_configs data
      // In a real scenario, we'd verify that only 2 examples are returned
      const result = await (server as any).searchNodes('http', 5, { includeExamples: true });

      expect(result.results).toBeDefined();
      if (result.results.length > 0) {
        result.results.forEach((node: any) => {
          if (node.examples) {
            expect(node.examples.length).toBeLessThanOrEqual(2);
          }
        });
      }
    });
  });

  describe('example data structure', () => {
    it('should return examples with correct structure when present', async () => {
      // Mock database to return example data
      const mockDb = (server as any).db;
      if (mockDb) {
        const originalPrepare = mockDb.prepare.bind(mockDb);
        mockDb.prepare = vi.fn((query: string) => {
          if (query.includes('template_node_configs')) {
            return {
              all: vi.fn(() => [
                {
                  parameters_json: JSON.stringify({
                    httpMethod: 'POST',
                    path: 'webhook-test'
                  }),
                  template_name: 'Test Template',
                  template_views: 1000
                },
                {
                  parameters_json: JSON.stringify({
                    httpMethod: 'GET',
                    path: 'webhook-get'
                  }),
                  template_name: 'Another Template',
                  template_views: 500
                }
              ])
            };
          }
          return originalPrepare(query);
        });

        const result = await (server as any).searchNodes('webhook', 5, { includeExamples: true });

        if (result.results.length > 0 && result.results[0].examples) {
          const example = result.results[0].examples[0];
          expect(example).toHaveProperty('configuration');
          expect(example).toHaveProperty('template');
          expect(example).toHaveProperty('views');
          expect(typeof example.configuration).toBe('object');
          expect(typeof example.template).toBe('string');
          expect(typeof example.views).toBe('number');
        }
      }
    });
  });

  describe('backward compatibility', () => {
    it('should maintain backward compatibility when includeExamples not specified', async () => {
      const resultWithoutParam = await (server as any).searchNodes('http', 5);
      const resultWithFalse = await (server as any).searchNodes('http', 5, { includeExamples: false });

      expect(resultWithoutParam.results).toBeDefined();
      expect(resultWithFalse.results).toBeDefined();

      // Both should have same structure (no examples)
      if (resultWithoutParam.results.length > 0) {
        expect(resultWithoutParam.results[0].examples).toBeUndefined();
      }
      if (resultWithFalse.results.length > 0) {
        expect(resultWithFalse.results[0].examples).toBeUndefined();
      }
    });
  });

  describe('performance considerations', () => {
    it('should not significantly impact performance when includeExamples is false', async () => {
      const startWithout = Date.now();
      await (server as any).searchNodes('http', 20, { includeExamples: false });
      const durationWithout = Date.now() - startWithout;

      const startWith = Date.now();
      await (server as any).searchNodes('http', 20, { includeExamples: true });
      const durationWith = Date.now() - startWith;

      // Both should complete quickly (under 100ms)
      expect(durationWithout).toBeLessThan(100);
      expect(durationWith).toBeLessThan(200);
    });
  });

  describe('error handling', () => {
    it('should continue to work even if example fetch fails', async () => {
      // Mock database to throw error on example fetch
      const mockDb = (server as any).db;
      if (mockDb) {
        const originalPrepare = mockDb.prepare.bind(mockDb);
        mockDb.prepare = vi.fn((query: string) => {
          if (query.includes('template_node_configs')) {
            throw new Error('Database error');
          }
          return originalPrepare(query);
        });

        // Should not throw, should return results without examples
        const result = await (server as any).searchNodes('webhook', 5, { includeExamples: true });

        expect(result.results).toBeDefined();
        // Examples should be undefined due to error
        if (result.results.length > 0) {
          expect(result.results[0].examples).toBeUndefined();
        }
      }
    });

    it('should handle malformed parameters_json gracefully', async () => {
      const mockDb = (server as any).db;
      if (mockDb) {
        const originalPrepare = mockDb.prepare.bind(mockDb);
        mockDb.prepare = vi.fn((query: string) => {
          if (query.includes('template_node_configs')) {
            return {
              all: vi.fn(() => [
                {
                  parameters_json: 'invalid json',
                  template_name: 'Test Template',
                  template_views: 1000
                }
              ])
            };
          }
          return originalPrepare(query);
        });

        // Should not throw
        const result = await (server as any).searchNodes('webhook', 5, { includeExamples: true });
        expect(result).toBeDefined();
      }
    });
  });
});

describe('searchNodesLIKE with includeExamples', () => {
  let server: N8NDocumentationMCPServer;

  beforeEach(async () => {
    process.env.NODE_DB_PATH = ':memory:';
    server = new N8NDocumentationMCPServer();
    await (server as any).initialized;

    // Populate in-memory database with test nodes
    const testNodes = [
      {
        node_type: 'nodes-base.webhook',
        package_name: 'n8n-nodes-base',
        display_name: 'Webhook',
        description: 'Starts workflow on webhook call',
        category: 'Core Nodes',
        is_ai_tool: 0,
        is_trigger: 1,
        is_webhook: 1,
        is_versioned: 1,
        version: '1',
        properties_schema: JSON.stringify([]),
        operations: JSON.stringify([])
      }
    ];

    const db = (server as any).db;
    if (db) {
      const insertStmt = db.prepare(`
        INSERT INTO nodes (
          node_type, package_name, display_name, description, category,
          is_ai_tool, is_trigger, is_webhook, is_versioned, version,
          properties_schema, operations
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
      `);

      for (const node of testNodes) {
        insertStmt.run(
          node.node_type,
          node.package_name,
          node.display_name,
          node.description,
          node.category,
          node.is_ai_tool,
          node.is_trigger,
          node.is_webhook,
          node.is_versioned,
          node.version,
          node.properties_schema,
          node.operations
        );
      }
    }
  });

  afterEach(() => {
    delete process.env.NODE_DB_PATH;
  });

  it('should support includeExamples in LIKE search', async () => {
    const result = await (server as any).searchNodesLIKE('webhook', 5, { includeExamples: true });

    expect(result).toBeDefined();
    expect(result.results).toBeDefined();
    expect(Array.isArray(result.results)).toBe(true);
  });

  it('should not include examples when includeExamples is false', async () => {
    const result = await (server as any).searchNodesLIKE('webhook', 5, { includeExamples: false });

    expect(result).toBeDefined();
    expect(result.results).toBeDefined();
    if (result.results.length > 0) {
      result.results.forEach((node: any) => {
        expect(node.examples).toBeUndefined();
      });
    }
  });
});

describe('searchNodesFTS with includeExamples', () => {
  let server: N8NDocumentationMCPServer;

  beforeEach(async () => {
    process.env.NODE_DB_PATH = ':memory:';
    server = new N8NDocumentationMCPServer();
    await (server as any).initialized;
  });

  afterEach(() => {
    delete process.env.NODE_DB_PATH;
  });

  it('should support includeExamples in FTS search', async () => {
    const result = await (server as any).searchNodesFTS('webhook', 5, 'OR', { includeExamples: true });

    expect(result.results).toBeDefined();
    expect(Array.isArray(result.results)).toBe(true);
  });

  it('should pass options to example fetching logic', async () => {
    const result = await (server as any).searchNodesFTS('http', 5, 'AND', { includeExamples: true });

    expect(result).toBeDefined();
    expect(result.results).toBeDefined();
  });
});

```

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

```typescript
import { describe, it, expect } from 'vitest';
import { ExpressionFormatValidator } from '../../../src/services/expression-format-validator';

describe('ExpressionFormatValidator', () => {
  describe('validateAndFix', () => {
    const context = {
      nodeType: 'n8n-nodes-base.httpRequest',
      nodeName: 'HTTP Request',
      nodeId: 'test-id-1'
    };

    describe('Simple string expressions', () => {
      it('should detect missing = prefix for expression', () => {
        const value = '{{ $env.API_KEY }}';
        const issue = ExpressionFormatValidator.validateAndFix(value, 'apiKey', context);

        expect(issue).toBeTruthy();
        expect(issue?.issueType).toBe('missing-prefix');
        expect(issue?.correctedValue).toBe('={{ $env.API_KEY }}');
        expect(issue?.severity).toBe('error');
      });

      it('should accept expression with = prefix', () => {
        const value = '={{ $env.API_KEY }}';
        const issue = ExpressionFormatValidator.validateAndFix(value, 'apiKey', context);

        expect(issue).toBeNull();
      });

      it('should detect mixed content without prefix', () => {
        const value = 'Bearer {{ $env.TOKEN }}';
        const issue = ExpressionFormatValidator.validateAndFix(value, 'authorization', context);

        expect(issue).toBeTruthy();
        expect(issue?.issueType).toBe('missing-prefix');
        expect(issue?.correctedValue).toBe('=Bearer {{ $env.TOKEN }}');
      });

      it('should accept mixed content with prefix', () => {
        const value = '=Bearer {{ $env.TOKEN }}';
        const issue = ExpressionFormatValidator.validateAndFix(value, 'authorization', context);

        expect(issue).toBeNull();
      });

      it('should ignore plain strings without expressions', () => {
        const value = 'https://api.example.com';
        const issue = ExpressionFormatValidator.validateAndFix(value, 'url', context);

        expect(issue).toBeNull();
      });
    });

    describe('Resource Locator fields', () => {
      const githubContext = {
        nodeType: 'n8n-nodes-base.github',
        nodeName: 'GitHub',
        nodeId: 'github-1'
      };

      it('should detect expression in owner field needing resource locator', () => {
        const value = '{{ $vars.GITHUB_OWNER }}';
        const issue = ExpressionFormatValidator.validateAndFix(value, 'owner', githubContext);

        expect(issue).toBeTruthy();
        expect(issue?.issueType).toBe('needs-resource-locator');
        expect(issue?.correctedValue).toEqual({
          __rl: true,
          value: '={{ $vars.GITHUB_OWNER }}',
          mode: 'expression'
        });
        expect(issue?.severity).toBe('error');
      });

      it('should accept resource locator with expression', () => {
        const value = {
          __rl: true,
          value: '={{ $vars.GITHUB_OWNER }}',
          mode: 'expression'
        };
        const issue = ExpressionFormatValidator.validateAndFix(value, 'owner', githubContext);

        expect(issue).toBeNull();
      });

      it('should detect missing prefix in resource locator value', () => {
        const value = {
          __rl: true,
          value: '{{ $vars.GITHUB_OWNER }}',
          mode: 'expression'
        };
        const issue = ExpressionFormatValidator.validateAndFix(value, 'owner', githubContext);

        expect(issue).toBeTruthy();
        expect(issue?.issueType).toBe('missing-prefix');
        expect(issue?.correctedValue.value).toBe('={{ $vars.GITHUB_OWNER }}');
      });

      it('should warn if expression has prefix but should use RL format', () => {
        const value = '={{ $vars.GITHUB_OWNER }}';
        const issue = ExpressionFormatValidator.validateAndFix(value, 'owner', githubContext);

        expect(issue).toBeTruthy();
        expect(issue?.issueType).toBe('needs-resource-locator');
        expect(issue?.severity).toBe('warning');
      });
    });

    describe('Multiple expressions', () => {
      it('should detect multiple expressions without prefix', () => {
        const value = '{{ $json.first }} - {{ $json.last }}';
        const issue = ExpressionFormatValidator.validateAndFix(value, 'fullName', context);

        expect(issue).toBeTruthy();
        expect(issue?.issueType).toBe('missing-prefix');
        expect(issue?.correctedValue).toBe('={{ $json.first }} - {{ $json.last }}');
      });

      it('should accept multiple expressions with prefix', () => {
        const value = '={{ $json.first }} - {{ $json.last }}';
        const issue = ExpressionFormatValidator.validateAndFix(value, 'fullName', context);

        expect(issue).toBeNull();
      });
    });

    describe('Edge cases', () => {
      it('should handle null values', () => {
        const issue = ExpressionFormatValidator.validateAndFix(null, 'field', context);
        expect(issue).toBeNull();
      });

      it('should handle undefined values', () => {
        const issue = ExpressionFormatValidator.validateAndFix(undefined, 'field', context);
        expect(issue).toBeNull();
      });

      it('should handle empty strings', () => {
        const issue = ExpressionFormatValidator.validateAndFix('', 'field', context);
        expect(issue).toBeNull();
      });

      it('should handle numbers', () => {
        const issue = ExpressionFormatValidator.validateAndFix(42, 'field', context);
        expect(issue).toBeNull();
      });

      it('should handle booleans', () => {
        const issue = ExpressionFormatValidator.validateAndFix(true, 'field', context);
        expect(issue).toBeNull();
      });

      it('should handle arrays', () => {
        const issue = ExpressionFormatValidator.validateAndFix(['item1', 'item2'], 'field', context);
        expect(issue).toBeNull();
      });
    });
  });

  describe('validateNodeParameters', () => {
    const context = {
      nodeType: 'n8n-nodes-base.emailSend',
      nodeName: 'Send Email',
      nodeId: 'email-1'
    };

    it('should validate all parameters recursively', () => {
      const parameters = {
        fromEmail: '{{ $env.SENDER_EMAIL }}',
        toEmail: '[email protected]',
        subject: 'Test {{ $json.type }}',
        body: {
          html: '<p>Hello {{ $json.name }}</p>',
          text: 'Hello {{ $json.name }}'
        },
        options: {
          replyTo: '={{ $env.REPLY_EMAIL }}'
        }
      };

      const issues = ExpressionFormatValidator.validateNodeParameters(parameters, context);

      expect(issues).toHaveLength(4);
      expect(issues.map(i => i.fieldPath)).toContain('fromEmail');
      expect(issues.map(i => i.fieldPath)).toContain('subject');
      expect(issues.map(i => i.fieldPath)).toContain('body.html');
      expect(issues.map(i => i.fieldPath)).toContain('body.text');
    });

    it('should handle arrays with expressions', () => {
      const parameters = {
        recipients: [
          '{{ $json.email1 }}',
          '[email protected]',
          '={{ $json.email2 }}'
        ]
      };

      const issues = ExpressionFormatValidator.validateNodeParameters(parameters, context);

      expect(issues).toHaveLength(1);
      expect(issues[0].fieldPath).toBe('recipients[0]');
      expect(issues[0].correctedValue).toBe('={{ $json.email1 }}');
    });

    it('should handle nested objects', () => {
      const parameters = {
        config: {
          database: {
            host: '{{ $env.DB_HOST }}',
            port: 5432,
            name: 'mydb'
          }
        }
      };

      const issues = ExpressionFormatValidator.validateNodeParameters(parameters, context);

      expect(issues).toHaveLength(1);
      expect(issues[0].fieldPath).toBe('config.database.host');
    });

    it('should skip circular references', () => {
      const circular: any = { a: 1 };
      circular.self = circular;

      const parameters = {
        normal: '{{ $json.value }}',
        circular
      };

      const issues = ExpressionFormatValidator.validateNodeParameters(parameters, context);

      // Should only find the issue in 'normal', not crash on circular
      expect(issues).toHaveLength(1);
      expect(issues[0].fieldPath).toBe('normal');
    });

    it('should handle maximum recursion depth', () => {
      // Create a deeply nested object (105 levels deep, exceeding the limit of 100)
      let deepObject: any = { value: '{{ $json.data }}' };
      let current = deepObject;
      for (let i = 0; i < 105; i++) {
        current.nested = { value: `{{ $json.level${i} }}` };
        current = current.nested;
      }

      const parameters = {
        deep: deepObject
      };

      const issues = ExpressionFormatValidator.validateNodeParameters(parameters, context);

      // Should find expression format issues up to the depth limit
      const depthWarning = issues.find(i => i.explanation.includes('Maximum recursion depth'));
      expect(depthWarning).toBeTruthy();
      expect(depthWarning?.severity).toBe('warning');

      // Should still find some expression format errors before hitting the limit
      const formatErrors = issues.filter(i => i.issueType === 'missing-prefix');
      expect(formatErrors.length).toBeGreaterThan(0);
      expect(formatErrors.length).toBeLessThanOrEqual(100); // Should not exceed the depth limit
    });
  });

  describe('formatErrorMessage', () => {
    const context = {
      nodeType: 'n8n-nodes-base.github',
      nodeName: 'Create Issue',
      nodeId: 'github-1'
    };

    it('should format error message for missing prefix', () => {
      const issue = {
        fieldPath: 'title',
        currentValue: '{{ $json.title }}',
        correctedValue: '={{ $json.title }}',
        issueType: 'missing-prefix' as const,
        explanation: "Expression missing required '=' prefix.",
        severity: 'error' as const
      };

      const message = ExpressionFormatValidator.formatErrorMessage(issue, context);

      expect(message).toContain("Expression format error in node 'Create Issue'");
      expect(message).toContain('Field \'title\'');
      expect(message).toContain('Current (incorrect):');
      expect(message).toContain('"title": "{{ $json.title }}"');
      expect(message).toContain('Fixed (correct):');
      expect(message).toContain('"title": "={{ $json.title }}"');
    });

    it('should format error message for resource locator', () => {
      const issue = {
        fieldPath: 'owner',
        currentValue: '{{ $vars.OWNER }}',
        correctedValue: {
          __rl: true,
          value: '={{ $vars.OWNER }}',
          mode: 'expression'
        },
        issueType: 'needs-resource-locator' as const,
        explanation: 'Field needs resource locator format.',
        severity: 'error' as const
      };

      const message = ExpressionFormatValidator.formatErrorMessage(issue, context);

      expect(message).toContain("Expression format error in node 'Create Issue'");
      expect(message).toContain('Current (incorrect):');
      expect(message).toContain('"owner": "{{ $vars.OWNER }}"');
      expect(message).toContain('Fixed (correct):');
      expect(message).toContain('"__rl": true');
      expect(message).toContain('"value": "={{ $vars.OWNER }}"');
      expect(message).toContain('"mode": "expression"');
    });
  });

  describe('Real-world examples', () => {
    it('should validate Email Send node example', () => {
      const context = {
        nodeType: 'n8n-nodes-base.emailSend',
        nodeName: 'Error Handler',
        nodeId: 'b9dd1cfd-ee66-4049-97e7-1af6d976a4e0'
      };

      const parameters = {
        fromEmail: '{{ $env.ADMIN_EMAIL }}',
        toEmail: '[email protected]',
        subject: 'GitHub Issue Workflow Error - HIGH PRIORITY',
        options: {}
      };

      const issues = ExpressionFormatValidator.validateNodeParameters(parameters, context);

      expect(issues).toHaveLength(1);
      expect(issues[0].fieldPath).toBe('fromEmail');
      expect(issues[0].correctedValue).toBe('={{ $env.ADMIN_EMAIL }}');
    });

    it('should validate GitHub node example', () => {
      const context = {
        nodeType: 'n8n-nodes-base.github',
        nodeName: 'Send Welcome Comment',
        nodeId: '3c742ca1-af8f-4d80-a47e-e68fb1ced491'
      };

      const parameters = {
        operation: 'createComment',
        owner: '{{ $vars.GITHUB_OWNER }}',
        repository: '{{ $vars.GITHUB_REPO }}',
        issueNumber: null,
        body: '👋 Hi @{{ $(\'Extract Issue Data\').first().json.author }}!\n\nThank you for creating this issue.'
      };

      const issues = ExpressionFormatValidator.validateNodeParameters(parameters, context);

      expect(issues.length).toBeGreaterThan(0);
      expect(issues.some(i => i.fieldPath === 'owner')).toBe(true);
      expect(issues.some(i => i.fieldPath === 'repository')).toBe(true);
      expect(issues.some(i => i.fieldPath === 'body')).toBe(true);
    });
  });
});
```
Page 13/45FirstPrevNextLast