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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/src/mcp/tools-n8n-manager.ts:
--------------------------------------------------------------------------------

```typescript
import { ToolDefinition } from '../types';

/**
 * n8n Management Tools
 * 
 * These tools enable AI agents to manage n8n workflows through the n8n API.
 * They require N8N_API_URL and N8N_API_KEY to be configured.
 */
export const n8nManagementTools: ToolDefinition[] = [
  // Workflow Management Tools
  {
    name: 'n8n_create_workflow',
    description: `Create workflow. Requires: name, nodes[], connections{}. Created inactive. Returns workflow with ID.`,
    inputSchema: {
      type: 'object',
      properties: {
        name: { 
          type: 'string', 
          description: 'Workflow name (required)' 
        },
        nodes: { 
          type: 'array', 
          description: 'Array of workflow nodes. Each node must have: id, name, type, typeVersion, position, and parameters',
          items: {
            type: 'object',
            required: ['id', 'name', 'type', 'typeVersion', 'position', 'parameters'],
            properties: {
              id: { type: 'string' },
              name: { type: 'string' },
              type: { type: 'string' },
              typeVersion: { type: 'number' },
              position: { 
                type: 'array',
                items: { type: 'number' },
                minItems: 2,
                maxItems: 2
              },
              parameters: { type: 'object' },
              credentials: { type: 'object' },
              disabled: { type: 'boolean' },
              notes: { type: 'string' },
              continueOnFail: { type: 'boolean' },
              retryOnFail: { type: 'boolean' },
              maxTries: { type: 'number' },
              waitBetweenTries: { type: 'number' }
            }
          }
        },
        connections: { 
          type: 'object', 
          description: 'Workflow connections object. Keys are source node IDs, values define output connections' 
        },
        settings: {
          type: 'object',
          description: 'Optional workflow settings (execution order, timezone, error handling)',
          properties: {
            executionOrder: { type: 'string', enum: ['v0', 'v1'] },
            timezone: { type: 'string' },
            saveDataErrorExecution: { type: 'string', enum: ['all', 'none'] },
            saveDataSuccessExecution: { type: 'string', enum: ['all', 'none'] },
            saveManualExecutions: { type: 'boolean' },
            saveExecutionProgress: { type: 'boolean' },
            executionTimeout: { type: 'number' },
            errorWorkflow: { type: 'string' }
          }
        }
      },
      required: ['name', 'nodes', 'connections']
    }
  },
  {
    name: 'n8n_get_workflow',
    description: `Get a workflow by ID. Returns the complete workflow including nodes, connections, and settings.`,
    inputSchema: {
      type: 'object',
      properties: {
        id: { 
          type: 'string', 
          description: 'Workflow ID' 
        }
      },
      required: ['id']
    }
  },
  {
    name: 'n8n_get_workflow_details',
    description: `Get workflow details with metadata, version, execution stats. More info than get_workflow.`,
    inputSchema: {
      type: 'object',
      properties: {
        id: { 
          type: 'string', 
          description: 'Workflow ID' 
        }
      },
      required: ['id']
    }
  },
  {
    name: 'n8n_get_workflow_structure',
    description: `Get workflow structure: nodes and connections only. No parameter details.`,
    inputSchema: {
      type: 'object',
      properties: {
        id: { 
          type: 'string', 
          description: 'Workflow ID' 
        }
      },
      required: ['id']
    }
  },
  {
    name: 'n8n_get_workflow_minimal',
    description: `Get minimal info: ID, name, active status, tags. Fast for listings.`,
    inputSchema: {
      type: 'object',
      properties: {
        id: { 
          type: 'string', 
          description: 'Workflow ID' 
        }
      },
      required: ['id']
    }
  },
  {
    name: 'n8n_update_full_workflow',
    description: `Full workflow update. Requires complete nodes[] and connections{}. For incremental use n8n_update_partial_workflow.`,
    inputSchema: {
      type: 'object',
      properties: {
        id: { 
          type: 'string', 
          description: 'Workflow ID to update' 
        },
        name: { 
          type: 'string', 
          description: 'New workflow name' 
        },
        nodes: { 
          type: 'array', 
          description: 'Complete array of workflow nodes (required if modifying workflow structure)',
          items: {
            type: 'object',
            additionalProperties: true
          }
        },
        connections: { 
          type: 'object', 
          description: 'Complete connections object (required if modifying workflow structure)' 
        },
        settings: { 
          type: 'object', 
          description: 'Workflow settings to update' 
        }
      },
      required: ['id']
    }
  },
  {
    name: 'n8n_update_partial_workflow',
    description: `Update workflow incrementally with diff operations. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, updateSettings, updateName, add/removeTag. See tools_documentation("n8n_update_partial_workflow", "full") for details.`,
    inputSchema: {
      type: 'object',
      additionalProperties: true,  // Allow any extra properties Claude Desktop might add
      properties: {
        id: { 
          type: 'string', 
          description: 'Workflow ID to update' 
        },
        operations: {
          type: 'array',
          description: 'Array of diff operations to apply. Each operation must have a "type" field and relevant properties for that operation type.',
          items: {
            type: 'object',
            additionalProperties: true
          }
        },
        validateOnly: {
          type: 'boolean',
          description: 'If true, only validate operations without applying them'
        },
        continueOnError: {
          type: 'boolean',
          description: 'If true, apply valid operations even if some fail (best-effort mode). Returns applied and failed operation indices. Default: false (atomic)'
        }
      },
      required: ['id', 'operations']
    }
  },
  {
    name: 'n8n_delete_workflow',
    description: `Permanently delete a workflow. This action cannot be undone.`,
    inputSchema: {
      type: 'object',
      properties: {
        id: { 
          type: 'string', 
          description: 'Workflow ID to delete' 
        }
      },
      required: ['id']
    }
  },
  {
    name: 'n8n_list_workflows',
    description: `List workflows (minimal metadata only). Returns id/name/active/dates/tags. Check hasMore/nextCursor for pagination.`,
    inputSchema: {
      type: 'object',
      properties: {
        limit: { 
          type: 'number', 
          description: 'Number of workflows to return (1-100, default: 100)' 
        },
        cursor: { 
          type: 'string', 
          description: 'Pagination cursor from previous response' 
        },
        active: { 
          type: 'boolean', 
          description: 'Filter by active status' 
        },
        tags: { 
          type: 'array', 
          items: { type: 'string' },
          description: 'Filter by tags (exact match)' 
        },
        projectId: { 
          type: 'string', 
          description: 'Filter by project ID (enterprise feature)' 
        },
        excludePinnedData: { 
          type: 'boolean', 
          description: 'Exclude pinned data from response (default: true)' 
        }
      }
    }
  },
  {
    name: 'n8n_validate_workflow',
    description: `Validate workflow by ID. Checks nodes, connections, expressions. Returns errors/warnings/suggestions.`,
    inputSchema: {
      type: 'object',
      properties: {
        id: { 
          type: 'string', 
          description: 'Workflow ID to validate' 
        },
        options: {
          type: 'object',
          description: 'Validation options',
          properties: {
            validateNodes: { 
              type: 'boolean', 
              description: 'Validate node configurations (default: true)' 
            },
            validateConnections: { 
              type: 'boolean', 
              description: 'Validate workflow connections (default: true)' 
            },
            validateExpressions: { 
              type: 'boolean', 
              description: 'Validate n8n expressions (default: true)' 
            },
            profile: { 
              type: 'string', 
              enum: ['minimal', 'runtime', 'ai-friendly', 'strict'],
              description: 'Validation profile to use (default: runtime)' 
            }
          }
        }
      },
      required: ['id']
    }
  },
  {
    name: 'n8n_autofix_workflow',
    description: `Automatically fix common workflow validation errors. Preview fixes or apply them. Fixes expression format, typeVersion, error output config, webhook paths.`,
    inputSchema: {
      type: 'object',
      properties: {
        id: {
          type: 'string',
          description: 'Workflow ID to fix'
        },
        applyFixes: {
          type: 'boolean',
          description: 'Apply fixes to workflow (default: false - preview mode)'
        },
        fixTypes: {
          type: 'array',
          description: 'Types of fixes to apply (default: all)',
          items: {
            type: 'string',
            enum: ['expression-format', 'typeversion-correction', 'error-output-config', 'node-type-correction', 'webhook-missing-path']
          }
        },
        confidenceThreshold: {
          type: 'string',
          enum: ['high', 'medium', 'low'],
          description: 'Minimum confidence level for fixes (default: medium)'
        },
        maxFixes: {
          type: 'number',
          description: 'Maximum number of fixes to apply (default: 50)'
        }
      },
      required: ['id']
    }
  },

  // Execution Management Tools
  {
    name: 'n8n_trigger_webhook_workflow',
    description: `Trigger workflow via webhook. Must be ACTIVE with Webhook node. Method must match config.`,
    inputSchema: {
      type: 'object',
      properties: {
        webhookUrl: { 
          type: 'string', 
          description: 'Full webhook URL from n8n workflow (e.g., https://n8n.example.com/webhook/abc-def-ghi)' 
        },
        httpMethod: { 
          type: 'string', 
          enum: ['GET', 'POST', 'PUT', 'DELETE'],
          description: 'HTTP method (must match webhook configuration, often GET)' 
        },
        data: { 
          type: 'object', 
          description: 'Data to send with the webhook request' 
        },
        headers: { 
          type: 'object', 
          description: 'Additional HTTP headers' 
        },
        waitForResponse: { 
          type: 'boolean', 
          description: 'Wait for workflow completion (default: true)' 
        }
      },
      required: ['webhookUrl']
    }
  },
  {
    name: 'n8n_get_execution',
    description: `Get execution details with smart filtering. RECOMMENDED: Use mode='preview' first to assess data size.
Examples:
- {id, mode:'preview'} - Structure & counts (fast, no data)
- {id, mode:'summary'} - 2 samples per node (default)
- {id, mode:'filtered', itemsLimit:5} - 5 items per node
- {id, nodeNames:['HTTP Request']} - Specific node only
- {id, mode:'full'} - Complete data (use with caution)`,
    inputSchema: {
      type: 'object',
      properties: {
        id: {
          type: 'string',
          description: 'Execution ID'
        },
        mode: {
          type: 'string',
          enum: ['preview', 'summary', 'filtered', 'full'],
          description: 'Data retrieval mode: preview=structure only, summary=2 items, filtered=custom, full=all data'
        },
        nodeNames: {
          type: 'array',
          items: { type: 'string' },
          description: 'Filter to specific nodes by name (for filtered mode)'
        },
        itemsLimit: {
          type: 'number',
          description: 'Items per node: 0=structure only, 2=default, -1=unlimited (for filtered mode)'
        },
        includeInputData: {
          type: 'boolean',
          description: 'Include input data in addition to output (default: false)'
        },
        includeData: {
          type: 'boolean',
          description: 'Legacy: Include execution data. Maps to mode=summary if true (deprecated, use mode instead)'
        }
      },
      required: ['id']
    }
  },
  {
    name: 'n8n_list_executions',
    description: `List workflow executions (returns up to limit). Check hasMore/nextCursor for pagination.`,
    inputSchema: {
      type: 'object',
      properties: {
        limit: { 
          type: 'number', 
          description: 'Number of executions to return (1-100, default: 100)' 
        },
        cursor: { 
          type: 'string', 
          description: 'Pagination cursor from previous response' 
        },
        workflowId: { 
          type: 'string', 
          description: 'Filter by workflow ID' 
        },
        projectId: { 
          type: 'string', 
          description: 'Filter by project ID (enterprise feature)' 
        },
        status: { 
          type: 'string', 
          enum: ['success', 'error', 'waiting'],
          description: 'Filter by execution status' 
        },
        includeData: { 
          type: 'boolean', 
          description: 'Include execution data (default: false)' 
        }
      }
    }
  },
  {
    name: 'n8n_delete_execution',
    description: `Delete an execution record. This only removes the execution history, not any data processed.`,
    inputSchema: {
      type: 'object',
      properties: {
        id: { 
          type: 'string', 
          description: 'Execution ID to delete' 
        }
      },
      required: ['id']
    }
  },

  // System Tools
  {
    name: 'n8n_health_check',
    description: `Check n8n instance health and API connectivity. Returns status and available features.`,
    inputSchema: {
      type: 'object',
      properties: {}
    }
  },
  {
    name: 'n8n_list_available_tools',
    description: `List available n8n tools and capabilities.`,
    inputSchema: {
      type: 'object',
      properties: {}
    }
  },
  {
    name: 'n8n_diagnostic',
    description: `Diagnose n8n API config. Shows tool status, API connectivity, env vars. Helps troubleshoot missing tools.`,
    inputSchema: {
      type: 'object',
      properties: {
        verbose: {
          type: 'boolean',
          description: 'Include detailed debug information (default: false)'
        }
      }
    }
  }
];
```

--------------------------------------------------------------------------------
/src/utils/fixed-collection-validator.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Generic utility for validating and fixing fixedCollection structures in n8n nodes
 * Prevents the "propertyValues[itemName] is not iterable" error
 */

// Type definitions for node configurations
export type NodeConfigValue = string | number | boolean | null | undefined | NodeConfig | NodeConfigValue[];

export interface NodeConfig {
  [key: string]: NodeConfigValue;
}

export interface FixedCollectionPattern {
  nodeType: string;
  property: string;
  subProperty?: string;
  expectedStructure: string;
  invalidPatterns: string[];
}

export interface FixedCollectionValidationResult {
  isValid: boolean;
  errors: Array<{
    pattern: string;
    message: string;
    fix: string;
  }>;
  autofix?: NodeConfig | NodeConfigValue[];
}

export class FixedCollectionValidator {
  /**
   * Type guard to check if value is a NodeConfig
   */
  private static isNodeConfig(value: NodeConfigValue): value is NodeConfig {
    return typeof value === 'object' && value !== null && !Array.isArray(value);
  }

  /**
   * Safely get nested property value
   */
  private static getNestedValue(obj: NodeConfig, path: string): NodeConfigValue | undefined {
    const parts = path.split('.');
    let current: NodeConfigValue = obj;

    for (const part of parts) {
      if (!this.isNodeConfig(current)) {
        return undefined;
      }
      current = current[part];
    }

    return current;
  }
  /**
   * Known problematic patterns for various n8n nodes
   */
  private static readonly KNOWN_PATTERNS: FixedCollectionPattern[] = [
    // Conditional nodes (already fixed)
    {
      nodeType: 'switch',
      property: 'rules',
      expectedStructure: 'rules.values array',
      invalidPatterns: ['rules.conditions', 'rules.conditions.values']
    },
    {
      nodeType: 'if',
      property: 'conditions',
      expectedStructure: 'conditions array/object',
      invalidPatterns: ['conditions.values']
    },
    {
      nodeType: 'filter',
      property: 'conditions',
      expectedStructure: 'conditions array/object',
      invalidPatterns: ['conditions.values']
    },
    // New nodes identified by research
    {
      nodeType: 'summarize',
      property: 'fieldsToSummarize',
      subProperty: 'values',
      expectedStructure: 'fieldsToSummarize.values array',
      invalidPatterns: ['fieldsToSummarize.values.values']
    },
    {
      nodeType: 'comparedatasets',
      property: 'mergeByFields',
      subProperty: 'values',
      expectedStructure: 'mergeByFields.values array',
      invalidPatterns: ['mergeByFields.values.values']
    },
    {
      nodeType: 'sort',
      property: 'sortFieldsUi',
      subProperty: 'sortField',
      expectedStructure: 'sortFieldsUi.sortField array',
      invalidPatterns: ['sortFieldsUi.sortField.values']
    },
    {
      nodeType: 'aggregate',
      property: 'fieldsToAggregate',
      subProperty: 'fieldToAggregate',
      expectedStructure: 'fieldsToAggregate.fieldToAggregate array',
      invalidPatterns: ['fieldsToAggregate.fieldToAggregate.values']
    },
    {
      nodeType: 'set',
      property: 'fields',
      subProperty: 'values',
      expectedStructure: 'fields.values array',
      invalidPatterns: ['fields.values.values']
    },
    {
      nodeType: 'html',
      property: 'extractionValues',
      subProperty: 'values',
      expectedStructure: 'extractionValues.values array',
      invalidPatterns: ['extractionValues.values.values']
    },
    {
      nodeType: 'httprequest',
      property: 'body',
      subProperty: 'parameters',
      expectedStructure: 'body.parameters array',
      invalidPatterns: ['body.parameters.values']
    },
    {
      nodeType: 'airtable',
      property: 'sort',
      subProperty: 'sortField',
      expectedStructure: 'sort.sortField array',
      invalidPatterns: ['sort.sortField.values']
    }
  ];

  /**
   * Validate a node configuration for fixedCollection issues
   * Includes protection against circular references
   */
  static validate(
    nodeType: string,
    config: NodeConfig
  ): FixedCollectionValidationResult {
    // Early return for non-object configs
    if (typeof config !== 'object' || config === null || Array.isArray(config)) {
      return { isValid: true, errors: [] };
    }
    
    const normalizedNodeType = this.normalizeNodeType(nodeType);
    const pattern = this.getPatternForNode(normalizedNodeType);
    
    if (!pattern) {
      return { isValid: true, errors: [] };
    }

    const result: FixedCollectionValidationResult = {
      isValid: true,
      errors: []
    };

    // Check for invalid patterns
    for (const invalidPattern of pattern.invalidPatterns) {
      if (this.hasInvalidStructure(config, invalidPattern)) {
        result.isValid = false;
        result.errors.push({
          pattern: invalidPattern,
          message: `Invalid structure for nodes-base.${pattern.nodeType} node: found nested "${invalidPattern}" but expected "${pattern.expectedStructure}". This causes "propertyValues[itemName] is not iterable" error in n8n.`,
          fix: this.generateFixMessage(pattern)
        });

        // Generate autofix
        if (!result.autofix) {
          result.autofix = this.generateAutofix(config, pattern);
        }
      }
    }

    return result;
  }

  /**
   * Apply autofix to a configuration
   */
  static applyAutofix(
    config: NodeConfig,
    pattern: FixedCollectionPattern
  ): NodeConfig | NodeConfigValue[] {
    const fixedConfig = this.generateAutofix(config, pattern);
    // For If/Filter nodes, the autofix might return just the values array
    if (pattern.nodeType === 'if' || pattern.nodeType === 'filter') {
      const conditions = config.conditions;
      if (conditions && typeof conditions === 'object' && !Array.isArray(conditions) && 'values' in conditions) {
        const values = conditions.values;
        if (values !== undefined && values !== null && 
            (Array.isArray(values) || typeof values === 'object')) {
          return values as NodeConfig | NodeConfigValue[];
        }
      }
    }
    return fixedConfig;
  }

  /**
   * Normalize node type to handle various formats
   */
  private static normalizeNodeType(nodeType: string): string {
    return nodeType
      .replace('n8n-nodes-base.', '')
      .replace('nodes-base.', '')
      .replace('@n8n/n8n-nodes-langchain.', '')
      .toLowerCase();
  }

  /**
   * Get pattern configuration for a specific node type
   */
  private static getPatternForNode(nodeType: string): FixedCollectionPattern | undefined {
    return this.KNOWN_PATTERNS.find(p => p.nodeType === nodeType);
  }

  /**
   * Check if configuration has an invalid structure
   * Includes circular reference protection
   */
  private static hasInvalidStructure(
    config: NodeConfig,
    pattern: string
  ): boolean {
    const parts = pattern.split('.');
    let current: NodeConfigValue = config;
    const visited = new WeakSet<object>();

    for (const part of parts) {
      // Check for null/undefined
      if (current === null || current === undefined) {
        return false;
      }
      
      // Check if it's an object (but not an array for property access)
      if (typeof current !== 'object' || Array.isArray(current)) {
        return false;
      }
      
      // Check for circular reference
      if (visited.has(current)) {
        return false; // Circular reference detected, invalid structure
      }
      visited.add(current);
      
      // Check if property exists (using hasOwnProperty to avoid prototype pollution)
      if (!Object.prototype.hasOwnProperty.call(current, part)) {
        return false;
      }
      
      const nextValue = (current as NodeConfig)[part];
      if (typeof nextValue !== 'object' || nextValue === null) {
        // If we have more parts to traverse but current value is not an object, invalid structure
        if (parts.indexOf(part) < parts.length - 1) {
          return false;
        }
      }
      current = nextValue as NodeConfig;
    }

    return true;
  }

  /**
   * Generate a fix message for the specific pattern
   */
  private static generateFixMessage(pattern: FixedCollectionPattern): string {
    switch (pattern.nodeType) {
      case 'switch':
        return 'Use: { "rules": { "values": [{ "conditions": {...}, "outputKey": "output1" }] } }';
      case 'if':
      case 'filter':
        return 'Use: { "conditions": {...} } or { "conditions": [...] } directly, not nested under "values"';
      case 'summarize':
        return 'Use: { "fieldsToSummarize": { "values": [...] } } not nested values.values';
      case 'comparedatasets':
        return 'Use: { "mergeByFields": { "values": [...] } } not nested values.values';
      case 'sort':
        return 'Use: { "sortFieldsUi": { "sortField": [...] } } not sortField.values';
      case 'aggregate':
        return 'Use: { "fieldsToAggregate": { "fieldToAggregate": [...] } } not fieldToAggregate.values';
      case 'set':
        return 'Use: { "fields": { "values": [...] } } not nested values.values';
      case 'html':
        return 'Use: { "extractionValues": { "values": [...] } } not nested values.values';
      case 'httprequest':
        return 'Use: { "body": { "parameters": [...] } } not parameters.values';
      case 'airtable':
        return 'Use: { "sort": { "sortField": [...] } } not sortField.values';
      default:
        return `Use ${pattern.expectedStructure} structure`;
    }
  }

  /**
   * Generate autofix for invalid structures
   */
  private static generateAutofix(
    config: NodeConfig,
    pattern: FixedCollectionPattern
  ): NodeConfig | NodeConfigValue[] {
    const fixedConfig = { ...config };

    switch (pattern.nodeType) {
      case 'switch': {
        const rules = config.rules;
        if (this.isNodeConfig(rules)) {
          const conditions = rules.conditions;
          if (this.isNodeConfig(conditions) && 'values' in conditions) {
            const values = conditions.values;
            fixedConfig.rules = {
              values: Array.isArray(values)
                ? values.map((condition, index) => ({
                    conditions: condition,
                    outputKey: `output${index + 1}`
                  }))
                : [{
                    conditions: values,
                    outputKey: 'output1'
                  }]
            };
          } else if (conditions) {
            fixedConfig.rules = {
              values: [{
                conditions: conditions,
                outputKey: 'output1'
              }]
            };
          }
        }
        break;
      }

      case 'if':
      case 'filter': {
        const conditions = config.conditions;
        if (this.isNodeConfig(conditions) && 'values' in conditions) {
          const values = conditions.values;
          if (values !== undefined && values !== null && 
              (Array.isArray(values) || typeof values === 'object')) {
            return values as NodeConfig | NodeConfigValue[];
          }
        }
        break;
      }

      case 'summarize': {
        const fieldsToSummarize = config.fieldsToSummarize;
        if (this.isNodeConfig(fieldsToSummarize)) {
          const values = fieldsToSummarize.values;
          if (this.isNodeConfig(values) && 'values' in values) {
            fixedConfig.fieldsToSummarize = {
              values: values.values
            };
          }
        }
        break;
      }

      case 'comparedatasets': {
        const mergeByFields = config.mergeByFields;
        if (this.isNodeConfig(mergeByFields)) {
          const values = mergeByFields.values;
          if (this.isNodeConfig(values) && 'values' in values) {
            fixedConfig.mergeByFields = {
              values: values.values
            };
          }
        }
        break;
      }

      case 'sort': {
        const sortFieldsUi = config.sortFieldsUi;
        if (this.isNodeConfig(sortFieldsUi)) {
          const sortField = sortFieldsUi.sortField;
          if (this.isNodeConfig(sortField) && 'values' in sortField) {
            fixedConfig.sortFieldsUi = {
              sortField: sortField.values
            };
          }
        }
        break;
      }

      case 'aggregate': {
        const fieldsToAggregate = config.fieldsToAggregate;
        if (this.isNodeConfig(fieldsToAggregate)) {
          const fieldToAggregate = fieldsToAggregate.fieldToAggregate;
          if (this.isNodeConfig(fieldToAggregate) && 'values' in fieldToAggregate) {
            fixedConfig.fieldsToAggregate = {
              fieldToAggregate: fieldToAggregate.values
            };
          }
        }
        break;
      }

      case 'set': {
        const fields = config.fields;
        if (this.isNodeConfig(fields)) {
          const values = fields.values;
          if (this.isNodeConfig(values) && 'values' in values) {
            fixedConfig.fields = {
              values: values.values
            };
          }
        }
        break;
      }

      case 'html': {
        const extractionValues = config.extractionValues;
        if (this.isNodeConfig(extractionValues)) {
          const values = extractionValues.values;
          if (this.isNodeConfig(values) && 'values' in values) {
            fixedConfig.extractionValues = {
              values: values.values
            };
          }
        }
        break;
      }

      case 'httprequest': {
        const body = config.body;
        if (this.isNodeConfig(body)) {
          const parameters = body.parameters;
          if (this.isNodeConfig(parameters) && 'values' in parameters) {
            fixedConfig.body = {
              ...body,
              parameters: parameters.values
            };
          }
        }
        break;
      }

      case 'airtable': {
        const sort = config.sort;
        if (this.isNodeConfig(sort)) {
          const sortField = sort.sortField;
          if (this.isNodeConfig(sortField) && 'values' in sortField) {
            fixedConfig.sort = {
              sortField: sortField.values
            };
          }
        }
        break;
      }
    }

    return fixedConfig;
  }

  /**
   * Get all known patterns (for testing and documentation)
   * Returns a deep copy to prevent external modifications
   */
  static getAllPatterns(): FixedCollectionPattern[] {
    return this.KNOWN_PATTERNS.map(pattern => ({
      ...pattern,
      invalidPatterns: [...pattern.invalidPatterns]
    }));
  }

  /**
   * Check if a node type is susceptible to fixedCollection issues
   */
  static isNodeSusceptible(nodeType: string): boolean {
    const normalizedType = this.normalizeNodeType(nodeType);
    return this.KNOWN_PATTERNS.some(p => p.nodeType === normalizedType);
  }
}
```

--------------------------------------------------------------------------------
/tests/unit/parsers/node-parser-outputs.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { NodeParser } from '@/parsers/node-parser';
import { PropertyExtractor } from '@/parsers/property-extractor';

// Mock PropertyExtractor
vi.mock('@/parsers/property-extractor');

describe('NodeParser - Output Extraction', () => {
  let parser: NodeParser;
  let mockPropertyExtractor: any;

  beforeEach(() => {
    vi.clearAllMocks();
    
    mockPropertyExtractor = {
      extractProperties: vi.fn().mockReturnValue([]),
      extractCredentials: vi.fn().mockReturnValue([]),
      detectAIToolCapability: vi.fn().mockReturnValue(false),
      extractOperations: vi.fn().mockReturnValue([])
    };
    
    (PropertyExtractor as any).mockImplementation(() => mockPropertyExtractor);
    
    parser = new NodeParser();
  });

  describe('extractOutputs method', () => {
    it('should extract outputs array from base description', () => {
      const outputs = [
        { displayName: 'Done', description: 'Final results when loop completes' },
        { displayName: 'Loop', description: 'Current batch data during iteration' }
      ];
      
      const nodeDescription = {
        name: 'splitInBatches',
        displayName: 'Split In Batches',
        outputs
      };
      
      const NodeClass = class {
        description = nodeDescription;
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.outputs).toEqual(outputs);
      expect(result.outputNames).toBeUndefined();
    });

    it('should extract outputNames array from base description', () => {
      const outputNames = ['done', 'loop'];
      
      const nodeDescription = {
        name: 'splitInBatches',
        displayName: 'Split In Batches',
        outputNames
      };
      
      const NodeClass = class {
        description = nodeDescription;
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.outputNames).toEqual(outputNames);
      expect(result.outputs).toBeUndefined();
    });

    it('should extract both outputs and outputNames when both are present', () => {
      const outputs = [
        { displayName: 'Done', description: 'Final results when loop completes' },
        { displayName: 'Loop', description: 'Current batch data during iteration' }
      ];
      const outputNames = ['done', 'loop'];
      
      const nodeDescription = {
        name: 'splitInBatches',
        displayName: 'Split In Batches',
        outputs,
        outputNames
      };
      
      const NodeClass = class {
        description = nodeDescription;
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.outputs).toEqual(outputs);
      expect(result.outputNames).toEqual(outputNames);
    });

    it('should convert single output to array format', () => {
      const singleOutput = { displayName: 'Output', description: 'Single output' };
      
      const nodeDescription = {
        name: 'singleOutputNode',
        displayName: 'Single Output Node',
        outputs: singleOutput
      };
      
      const NodeClass = class {
        description = nodeDescription;
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.outputs).toEqual([singleOutput]);
    });

    it('should convert single outputName to array format', () => {
      const nodeDescription = {
        name: 'singleOutputNode',
        displayName: 'Single Output Node',
        outputNames: 'main'
      };
      
      const NodeClass = class {
        description = nodeDescription;
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.outputNames).toEqual(['main']);
    });

    it('should extract outputs from versioned node when not in base description', () => {
      const versionedOutputs = [
        { displayName: 'True', description: 'Items that match condition' },
        { displayName: 'False', description: 'Items that do not match condition' }
      ];
      
      const NodeClass = class {
        description = {
          name: 'if',
          displayName: 'IF'
          // No outputs in base description
        };
        
        nodeVersions = {
          1: {
            description: {
              outputs: versionedOutputs
            }
          },
          2: {
            description: {
              outputs: versionedOutputs,
              outputNames: ['true', 'false']
            }
          }
        };
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      // Should get outputs from latest version (2)
      expect(result.outputs).toEqual(versionedOutputs);
      expect(result.outputNames).toEqual(['true', 'false']);
    });

    it('should handle node instantiation failure gracefully', () => {
      const NodeClass = class {
        // Static description that can be accessed when instantiation fails
        static description = {
          name: 'problematic',
          displayName: 'Problematic Node'
        };
        
        constructor() {
          throw new Error('Cannot instantiate');
        }
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.outputs).toBeUndefined();
      expect(result.outputNames).toBeUndefined();
    });

    it('should return empty result when no outputs found anywhere', () => {
      const nodeDescription = {
        name: 'noOutputs',
        displayName: 'No Outputs Node'
        // No outputs or outputNames
      };
      
      const NodeClass = class {
        description = nodeDescription;
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.outputs).toBeUndefined();
      expect(result.outputNames).toBeUndefined();
    });

    it('should handle complex versioned node structure', () => {
      const NodeClass = class VersionedNodeType {
        baseDescription = {
          name: 'complexVersioned',
          displayName: 'Complex Versioned Node',
          defaultVersion: 3
        };
        
        nodeVersions = {
          1: {
            description: {
              outputs: [{ displayName: 'V1 Output' }]
            }
          },
          2: {
            description: {
              outputs: [
                { displayName: 'V2 Output 1' },
                { displayName: 'V2 Output 2' }
              ]
            }
          },
          3: {
            description: {
              outputs: [
                { displayName: 'V3 True', description: 'True branch' },
                { displayName: 'V3 False', description: 'False branch' }
              ],
              outputNames: ['true', 'false']
            }
          }
        };
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      // Should use latest version (3)
      expect(result.outputs).toEqual([
        { displayName: 'V3 True', description: 'True branch' },
        { displayName: 'V3 False', description: 'False branch' }
      ]);
      expect(result.outputNames).toEqual(['true', 'false']);
    });

    it('should prefer base description outputs over versioned when both exist', () => {
      const baseOutputs = [{ displayName: 'Base Output' }];
      const versionedOutputs = [{ displayName: 'Versioned Output' }];
      
      const NodeClass = class {
        description = {
          name: 'preferBase',
          displayName: 'Prefer Base',
          outputs: baseOutputs
        };
        
        nodeVersions = {
          1: {
            description: {
              outputs: versionedOutputs
            }
          }
        };
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.outputs).toEqual(baseOutputs);
    });

    it('should handle IF node with typical output structure', () => {
      const ifOutputs = [
        { displayName: 'True', description: 'Items that match the condition' },
        { displayName: 'False', description: 'Items that do not match the condition' }
      ];
      
      const NodeClass = class {
        description = {
          name: 'if',
          displayName: 'IF',
          outputs: ifOutputs,
          outputNames: ['true', 'false']
        };
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.outputs).toEqual(ifOutputs);
      expect(result.outputNames).toEqual(['true', 'false']);
    });

    it('should handle SplitInBatches node with counterintuitive output structure', () => {
      const splitInBatchesOutputs = [
        { displayName: 'Done', description: 'Final results when loop completes' },
        { displayName: 'Loop', description: 'Current batch data during iteration' }
      ];
      
      const NodeClass = class {
        description = {
          name: 'splitInBatches',
          displayName: 'Split In Batches',
          outputs: splitInBatchesOutputs,
          outputNames: ['done', 'loop']
        };
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.outputs).toEqual(splitInBatchesOutputs);
      expect(result.outputNames).toEqual(['done', 'loop']);
      
      // Verify the counterintuitive order: done=0, loop=1
      expect(result.outputs).toBeDefined();
      expect(result.outputNames).toBeDefined();
      expect(result.outputs![0].displayName).toBe('Done');
      expect(result.outputs![1].displayName).toBe('Loop');
      expect(result.outputNames![0]).toBe('done');
      expect(result.outputNames![1]).toBe('loop');
    });

    it('should handle Switch node with multiple outputs', () => {
      const switchOutputs = [
        { displayName: 'Output 1', description: 'First branch' },
        { displayName: 'Output 2', description: 'Second branch' },
        { displayName: 'Output 3', description: 'Third branch' },
        { displayName: 'Fallback', description: 'Default branch when no conditions match' }
      ];
      
      const NodeClass = class {
        description = {
          name: 'switch',
          displayName: 'Switch',
          outputs: switchOutputs,
          outputNames: ['0', '1', '2', 'fallback']
        };
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.outputs).toEqual(switchOutputs);
      expect(result.outputNames).toEqual(['0', '1', '2', 'fallback']);
    });

    it('should handle empty outputs array', () => {
      const NodeClass = class {
        description = {
          name: 'emptyOutputs',
          displayName: 'Empty Outputs',
          outputs: [],
          outputNames: []
        };
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.outputs).toEqual([]);
      expect(result.outputNames).toEqual([]);
    });

    it('should handle mismatched outputs and outputNames arrays', () => {
      const outputs = [
        { displayName: 'Output 1' },
        { displayName: 'Output 2' }
      ];
      const outputNames = ['first', 'second', 'third']; // One extra
      
      const NodeClass = class {
        description = {
          name: 'mismatched',
          displayName: 'Mismatched Arrays',
          outputs,
          outputNames
        };
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.outputs).toEqual(outputs);
      expect(result.outputNames).toEqual(outputNames);
    });
  });

  describe('real-world node structures', () => {
    it('should handle actual n8n SplitInBatches node structure', () => {
      // This mimics the actual structure from n8n-nodes-base
      const NodeClass = class {
        description = {
          name: 'splitInBatches',
          displayName: 'Split In Batches',
          description: 'Split data into batches and iterate over each batch',
          icon: 'fa:th-large',
          group: ['transform'],
          version: 3,
          outputs: [
            {
              displayName: 'Done',
              name: 'done',
              type: 'main',
              hint: 'Receives the final data after all batches have been processed'
            },
            {
              displayName: 'Loop',
              name: 'loop', 
              type: 'main',
              hint: 'Receives the current batch data during each iteration'
            }
          ],
          outputNames: ['done', 'loop']
        };
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.outputs).toHaveLength(2);
      expect(result.outputs).toBeDefined();
      expect(result.outputs![0].displayName).toBe('Done');
      expect(result.outputs![1].displayName).toBe('Loop');
      expect(result.outputNames).toEqual(['done', 'loop']);
    });

    it('should handle actual n8n IF node structure', () => {
      // This mimics the actual structure from n8n-nodes-base
      const NodeClass = class {
        description = {
          name: 'if',
          displayName: 'IF',
          description: 'Route items to different outputs based on conditions',
          icon: 'fa:map-signs',
          group: ['transform'],
          version: 2,
          outputs: [
            {
              displayName: 'True',
              name: 'true',
              type: 'main',
              hint: 'Items that match the condition'
            },
            {
              displayName: 'False',
              name: 'false',
              type: 'main',
              hint: 'Items that do not match the condition'
            }
          ],
          outputNames: ['true', 'false']
        };
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.outputs).toHaveLength(2);
      expect(result.outputs).toBeDefined();
      expect(result.outputs![0].displayName).toBe('True');
      expect(result.outputs![1].displayName).toBe('False');
      expect(result.outputNames).toEqual(['true', 'false']);
    });

    it('should handle single-output nodes like HTTP Request', () => {
      const NodeClass = class {
        description = {
          name: 'httpRequest',
          displayName: 'HTTP Request',
          description: 'Make HTTP requests',
          icon: 'fa:at',
          group: ['input'],
          version: 4
          // No outputs specified - single main output implied
        };
      };
      
      const result = parser.parse(NodeClass as any, 'n8n-nodes-base');
      
      expect(result.outputs).toBeUndefined();
      expect(result.outputNames).toBeUndefined();
    });
  });
});
```

--------------------------------------------------------------------------------
/src/data/canonical-ai-tool-examples.json:
--------------------------------------------------------------------------------

```json
{
  "description": "Canonical configuration examples for critical AI tools based on FINAL_AI_VALIDATION_SPEC.md",
  "version": "1.0.0",
  "examples": [
    {
      "node_type": "@n8n/n8n-nodes-langchain.toolHttpRequest",
      "display_name": "HTTP Request Tool",
      "examples": [
        {
          "name": "Weather API Tool",
          "use_case": "Fetch current weather data for AI Agent",
          "complexity": "simple",
          "parameters": {
            "method": "GET",
            "url": "https://api.weatherapi.com/v1/current.json?key={{$credentials.weatherApiKey}}&q={city}",
            "toolDescription": "Get current weather conditions for a city. Provide the city name (e.g., 'London', 'New York') and receive temperature, humidity, wind speed, and conditions.",
            "placeholderDefinitions": {
              "values": [
                {
                  "name": "city",
                  "description": "Name of the city to get weather for",
                  "type": "string"
                }
              ]
            },
            "authentication": "predefinedCredentialType",
            "nodeCredentialType": "weatherApiApi"
          },
          "credentials": {
            "weatherApiApi": {
              "id": "1",
              "name": "Weather API account"
            }
          },
          "notes": "Example shows proper toolDescription, URL with placeholder, and credential configuration"
        },
        {
          "name": "GitHub Issues Tool",
          "use_case": "Create GitHub issues from AI Agent conversations",
          "complexity": "medium",
          "parameters": {
            "method": "POST",
            "url": "https://api.github.com/repos/{owner}/{repo}/issues",
            "toolDescription": "Create a new GitHub issue. Requires owner (repo owner username), repo (repository name), title, and body. Returns the created issue URL and number.",
            "placeholderDefinitions": {
              "values": [
                {
                  "name": "owner",
                  "description": "GitHub repository owner username",
                  "type": "string"
                },
                {
                  "name": "repo",
                  "description": "Repository name",
                  "type": "string"
                },
                {
                  "name": "title",
                  "description": "Issue title",
                  "type": "string"
                },
                {
                  "name": "body",
                  "description": "Issue description and details",
                  "type": "string"
                }
              ]
            },
            "sendBody": true,
            "specifyBody": "json",
            "jsonBody": "={{ { \"title\": $json.title, \"body\": $json.body } }}",
            "authentication": "predefinedCredentialType",
            "nodeCredentialType": "githubApi"
          },
          "credentials": {
            "githubApi": {
              "id": "2",
              "name": "GitHub credentials"
            }
          },
          "notes": "Example shows POST request with JSON body, multiple placeholders, and expressions"
        },
        {
          "name": "Slack Message Tool",
          "use_case": "Send Slack messages from AI Agent",
          "complexity": "simple",
          "parameters": {
            "method": "POST",
            "url": "https://slack.com/api/chat.postMessage",
            "toolDescription": "Send a message to a Slack channel. Provide channel ID or name (e.g., '#general', 'C1234567890') and message text.",
            "placeholderDefinitions": {
              "values": [
                {
                  "name": "channel",
                  "description": "Channel ID or name (e.g., #general)",
                  "type": "string"
                },
                {
                  "name": "text",
                  "description": "Message text to send",
                  "type": "string"
                }
              ]
            },
            "sendHeaders": true,
            "headerParameters": {
              "parameters": [
                {
                  "name": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "name": "Authorization",
                  "value": "=Bearer {{$credentials.slackApi.accessToken}}"
                }
              ]
            },
            "sendBody": true,
            "specifyBody": "json",
            "jsonBody": "={{ { \"channel\": $json.channel, \"text\": $json.text } }}",
            "authentication": "predefinedCredentialType",
            "nodeCredentialType": "slackApi"
          },
          "credentials": {
            "slackApi": {
              "id": "3",
              "name": "Slack account"
            }
          },
          "notes": "Example shows headers with credential expressions and JSON body construction"
        }
      ]
    },
    {
      "node_type": "@n8n/n8n-nodes-langchain.toolCode",
      "display_name": "Code Tool",
      "examples": [
        {
          "name": "Calculate Shipping Cost",
          "use_case": "Calculate shipping costs based on weight and distance",
          "complexity": "simple",
          "parameters": {
            "name": "calculate_shipping_cost",
            "description": "Calculate shipping cost based on package weight (in kg) and distance (in km). Returns the cost in USD.",
            "language": "javaScript",
            "code": "const baseRate = 5;\nconst perKgRate = 2;\nconst perKmRate = 0.1;\n\nconst weight = $input.weight || 0;\nconst distance = $input.distance || 0;\n\nconst cost = baseRate + (weight * perKgRate) + (distance * perKmRate);\n\nreturn { cost: parseFloat(cost.toFixed(2)), currency: 'USD' };",
            "specifyInputSchema": true,
            "schemaType": "manual",
            "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"weight\": {\n      \"type\": \"number\",\n      \"description\": \"Package weight in kilograms\"\n    },\n    \"distance\": {\n      \"type\": \"number\",\n      \"description\": \"Shipping distance in kilometers\"\n    }\n  },\n  \"required\": [\"weight\", \"distance\"]\n}"
          },
          "notes": "Example shows proper function naming, detailed description, input schema, and return value"
        },
        {
          "name": "Format Customer Data",
          "use_case": "Transform and validate customer information",
          "complexity": "medium",
          "parameters": {
            "name": "format_customer_data",
            "description": "Format and validate customer data. Takes raw customer info (name, email, phone) and returns formatted object with validation status.",
            "language": "javaScript",
            "code": "const { name, email, phone } = $input;\n\n// Validation\nconst emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst phoneRegex = /^\\+?[1-9]\\d{1,14}$/;\n\nconst errors = [];\nif (!emailRegex.test(email)) errors.push('Invalid email format');\nif (!phoneRegex.test(phone)) errors.push('Invalid phone format');\n\n// Formatting\nconst formatted = {\n  name: name.trim(),\n  email: email.toLowerCase().trim(),\n  phone: phone.replace(/\\s/g, ''),\n  valid: errors.length === 0,\n  errors: errors\n};\n\nreturn formatted;",
            "specifyInputSchema": true,
            "schemaType": "manual",
            "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"name\": {\n      \"type\": \"string\",\n      \"description\": \"Customer full name\"\n    },\n    \"email\": {\n      \"type\": \"string\",\n      \"description\": \"Customer email address\"\n    },\n    \"phone\": {\n      \"type\": \"string\",\n      \"description\": \"Customer phone number\"\n    }\n  },\n  \"required\": [\"name\", \"email\", \"phone\"]\n}"
          },
          "notes": "Example shows data validation, formatting, and structured error handling"
        },
        {
          "name": "Parse Date Range",
          "use_case": "Convert natural language date ranges to ISO format",
          "complexity": "medium",
          "parameters": {
            "name": "parse_date_range",
            "description": "Parse natural language date ranges (e.g., 'last 7 days', 'this month', 'Q1 2024') into start and end dates in ISO format.",
            "language": "javaScript",
            "code": "const input = $input.dateRange || '';\nconst now = new Date();\nlet start, end;\n\nif (input.includes('last') && input.includes('days')) {\n  const days = parseInt(input.match(/\\d+/)[0]);\n  start = new Date(now.getTime() - (days * 24 * 60 * 60 * 1000));\n  end = now;\n} else if (input === 'this month') {\n  start = new Date(now.getFullYear(), now.getMonth(), 1);\n  end = new Date(now.getFullYear(), now.getMonth() + 1, 0);\n} else if (input === 'this year') {\n  start = new Date(now.getFullYear(), 0, 1);\n  end = new Date(now.getFullYear(), 11, 31);\n} else {\n  throw new Error('Unsupported date range format');\n}\n\nreturn {\n  startDate: start.toISOString().split('T')[0],\n  endDate: end.toISOString().split('T')[0],\n  daysCount: Math.ceil((end - start) / (24 * 60 * 60 * 1000))\n};",
            "specifyInputSchema": true,
            "schemaType": "manual",
            "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"dateRange\": {\n      \"type\": \"string\",\n      \"description\": \"Natural language date range (e.g., 'last 7 days', 'this month')\"\n    }\n  },\n  \"required\": [\"dateRange\"]\n}"
          },
          "notes": "Example shows complex logic, error handling, and date manipulation"
        }
      ]
    },
    {
      "node_type": "@n8n/n8n-nodes-langchain.agentTool",
      "display_name": "AI Agent Tool",
      "examples": [
        {
          "name": "Research Specialist Agent",
          "use_case": "Specialized sub-agent for in-depth research tasks",
          "complexity": "medium",
          "parameters": {
            "name": "research_specialist",
            "description": "Expert research agent that can search multiple sources, synthesize information, and provide comprehensive analysis on any topic. Use this when you need detailed, well-researched information.",
            "promptType": "define",
            "text": "You are a research specialist. Your role is to:\n1. Search for relevant information from multiple sources\n2. Synthesize findings into a coherent analysis\n3. Cite your sources\n4. Highlight key insights and patterns\n\nProvide thorough, well-structured research that answers the user's question comprehensively.",
            "systemMessage": "You are a meticulous researcher focused on accuracy and completeness. Always cite sources and acknowledge limitations in available information."
          },
          "connections": {
            "ai_languageModel": [
              {
                "node": "OpenAI GPT-4",
                "type": "ai_languageModel",
                "index": 0
              }
            ],
            "ai_tool": [
              {
                "node": "SerpApi Tool",
                "type": "ai_tool",
                "index": 0
              },
              {
                "node": "Wikipedia Tool",
                "type": "ai_tool",
                "index": 0
              }
            ]
          },
          "notes": "Example shows specialized sub-agent with custom prompt, specific system message, and multiple search tools"
        },
        {
          "name": "Data Analysis Agent",
          "use_case": "Sub-agent for analyzing and visualizing data",
          "complexity": "complex",
          "parameters": {
            "name": "data_analyst",
            "description": "Data analysis specialist that can process datasets, calculate statistics, identify trends, and generate insights. Use for any data analysis or statistical questions.",
            "promptType": "auto",
            "systemMessage": "You are a data analyst with expertise in statistics and data interpretation. Break down complex datasets into understandable insights. Use the Code Tool to perform calculations when needed.",
            "maxIterations": 10
          },
          "connections": {
            "ai_languageModel": [
              {
                "node": "Anthropic Claude",
                "type": "ai_languageModel",
                "index": 0
              }
            ],
            "ai_tool": [
              {
                "node": "Code Tool - Stats",
                "type": "ai_tool",
                "index": 0
              },
              {
                "node": "HTTP Request Tool - Data API",
                "type": "ai_tool",
                "index": 0
              }
            ]
          },
          "notes": "Example shows auto prompt type with specialized system message and analytical tools"
        }
      ]
    },
    {
      "node_type": "@n8n/n8n-nodes-langchain.mcpClientTool",
      "display_name": "MCP Client Tool",
      "examples": [
        {
          "name": "Filesystem MCP Tool",
          "use_case": "Access filesystem operations via MCP protocol",
          "complexity": "medium",
          "parameters": {
            "description": "Access file system operations through MCP. Can read files, list directories, create files, and search for content.",
            "mcpServer": {
              "transport": "stdio",
              "command": "npx",
              "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/directory"]
            },
            "tool": "read_file"
          },
          "notes": "Example shows stdio transport MCP server with filesystem access tool"
        },
        {
          "name": "Puppeteer MCP Tool",
          "use_case": "Browser automation via MCP for AI Agents",
          "complexity": "complex",
          "parameters": {
            "description": "Control a web browser to navigate pages, take screenshots, and extract content. Useful for web scraping and automated testing.",
            "mcpServer": {
              "transport": "stdio",
              "command": "npx",
              "args": ["-y", "@modelcontextprotocol/server-puppeteer"]
            },
            "tool": "puppeteer_navigate"
          },
          "notes": "Example shows Puppeteer MCP server for browser automation"
        },
        {
          "name": "Database MCP Tool",
          "use_case": "Query databases via MCP protocol",
          "complexity": "complex",
          "parameters": {
            "description": "Execute SQL queries and retrieve data from PostgreSQL databases. Supports SELECT, INSERT, UPDATE operations with proper escaping.",
            "mcpServer": {
              "transport": "sse",
              "url": "https://mcp-server.example.com/database"
            },
            "tool": "execute_query"
          },
          "notes": "Example shows SSE transport MCP server for remote database access"
        }
      ]
    }
  ]
}

```

--------------------------------------------------------------------------------
/tests/unit/services/workflow-validator-with-mocks.test.ts:
--------------------------------------------------------------------------------

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

// Mock logger to prevent console output
vi.mock('@/utils/logger', () => ({
  Logger: vi.fn().mockImplementation(() => ({
    error: vi.fn(),
    warn: vi.fn(),
    info: vi.fn()
  }))
}));

describe('WorkflowValidator - Simple Unit Tests', () => {
  let validator: WorkflowValidator;
  
  // Create a simple mock repository
  const createMockRepository = (nodeData: Record<string, any>) => ({
    getNode: vi.fn((type: string) => nodeData[type] || null),
    findSimilarNodes: vi.fn().mockReturnValue([])
  });

  // Create a simple mock validator class
  const createMockValidatorClass = (validationResult: any) => ({
    validateWithMode: vi.fn().mockReturnValue(validationResult)
  });

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

  describe('Basic validation scenarios', () => {
    it('should pass validation for a webhook workflow with single node', async () => {
      // Arrange
      const nodeData = {
        'n8n-nodes-base.webhook': {
          type: 'nodes-base.webhook',
          displayName: 'Webhook',
          name: 'webhook',
          version: 1,
          isVersioned: true,
          properties: []
        },
        'nodes-base.webhook': {
          type: 'nodes-base.webhook',
          displayName: 'Webhook',
          name: 'webhook',
          version: 1,
          isVersioned: true,
          properties: []
        }
      };

      const mockRepository = createMockRepository(nodeData);
      const mockValidatorClass = createMockValidatorClass({
        valid: true,
        errors: [],
        warnings: [],
        suggestions: []
      });

      validator = new WorkflowValidator(mockRepository as any, mockValidatorClass as any);

      const workflow = {
        name: 'Webhook Workflow',
        nodes: [
          {
            id: '1',
            name: 'Webhook',
            type: 'n8n-nodes-base.webhook',
            typeVersion: 1,
            position: [250, 300] as [number, number],
            parameters: {}
          }
        ],
        connections: {}
      };

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

      // Assert
      expect(result.valid).toBe(true);
      expect(result.errors).toHaveLength(0);
      // Single webhook node should just have a warning about no connections
      expect(result.warnings.some(w => w.message.includes('no connections'))).toBe(true);
    });

    it('should fail validation for unknown node types', async () => {
      // Arrange
      const mockRepository = createMockRepository({}); // Empty node data
      const mockValidatorClass = createMockValidatorClass({
        valid: true,
        errors: [],
        warnings: [],
        suggestions: []
      });

      validator = new WorkflowValidator(mockRepository as any, mockValidatorClass as any);

      const workflow = {
        name: 'Test Workflow',
        nodes: [
          {
            id: '1',
            name: 'Unknown',
            type: 'n8n-nodes-base.unknownNode',
            position: [250, 300] as [number, number],
            parameters: {}
          }
        ],
        connections: {}
      };

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

      // Assert
      expect(result.valid).toBe(false);
      // Check for either the error message or valid being false
      const hasUnknownNodeError = result.errors.some(e =>
        e.message && (e.message.includes('Unknown node type') || e.message.includes('unknown-node-type'))
      );
      expect(result.errors.length > 0 || hasUnknownNodeError).toBe(true);
    });

    it('should detect duplicate node names', async () => {
      // Arrange
      const mockRepository = createMockRepository({});
      const mockValidatorClass = createMockValidatorClass({
        valid: true,
        errors: [],
        warnings: [],
        suggestions: []
      });

      validator = new WorkflowValidator(mockRepository as any, mockValidatorClass as any);

      const workflow = {
        name: 'Duplicate Names',
        nodes: [
          {
            id: '1',
            name: 'HTTP Request',
            type: 'n8n-nodes-base.httpRequest',
            position: [250, 300] as [number, number],
            parameters: {}
          },
          {
            id: '2',
            name: 'HTTP Request', // Duplicate name
            type: 'n8n-nodes-base.httpRequest',
            position: [450, 300] as [number, number],
            parameters: {}
          }
        ],
        connections: {}
      };

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

      // Assert
      expect(result.valid).toBe(false);
      expect(result.errors.some(e => e.message.includes('Duplicate node name'))).toBe(true);
    });

    it('should validate connections properly', async () => {
      // Arrange
      const nodeData = {
        'n8n-nodes-base.manualTrigger': {
          type: 'nodes-base.manualTrigger',
          displayName: 'Manual Trigger',
          isVersioned: false,
          properties: []
        },
        'nodes-base.manualTrigger': {
          type: 'nodes-base.manualTrigger',
          displayName: 'Manual Trigger',
          isVersioned: false,
          properties: []
        },
        'n8n-nodes-base.set': {
          type: 'nodes-base.set',
          displayName: 'Set',
          version: 2,
          isVersioned: true,
          properties: []
        },
        'nodes-base.set': {
          type: 'nodes-base.set',
          displayName: 'Set',
          version: 2,
          isVersioned: true,
          properties: []
        }
      };

      const mockRepository = createMockRepository(nodeData);
      const mockValidatorClass = createMockValidatorClass({
        valid: true,
        errors: [],
        warnings: [],
        suggestions: []
      });

      validator = new WorkflowValidator(mockRepository as any, mockValidatorClass as any);

      const workflow = {
        name: 'Connected Workflow',
        nodes: [
          {
            id: '1',
            name: 'Manual Trigger',
            type: 'n8n-nodes-base.manualTrigger',
            position: [250, 300] as [number, number],
            parameters: {}
          },
          {
            id: '2',
            name: 'Set',
            type: 'n8n-nodes-base.set',
            typeVersion: 2,
            position: [450, 300] as [number, number],
            parameters: {}
          }
        ],
        connections: {
          'Manual Trigger': {
            main: [[{ node: 'Set', type: 'main', index: 0 }]]
          }
        }
      };

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

      // Assert
      expect(result.valid).toBe(true);
      expect(result.statistics.validConnections).toBe(1);
      expect(result.statistics.invalidConnections).toBe(0);
    });

    it('should detect workflow cycles', async () => {
      // Arrange
      const nodeData = {
        'n8n-nodes-base.set': {
          type: 'nodes-base.set',
          displayName: 'Set',
          isVersioned: true,
          version: 2,
          properties: []
        },
        'nodes-base.set': {
          type: 'nodes-base.set',
          displayName: 'Set',
          isVersioned: true,
          version: 2,
          properties: []
        }
      };

      const mockRepository = createMockRepository(nodeData);
      const mockValidatorClass = createMockValidatorClass({
        valid: true,
        errors: [],
        warnings: [],
        suggestions: []
      });

      validator = new WorkflowValidator(mockRepository as any, mockValidatorClass as any);

      const workflow = {
        name: 'Cyclic Workflow',
        nodes: [
          {
            id: '1',
            name: 'Node A',
            type: 'n8n-nodes-base.set',
            typeVersion: 2,
            position: [250, 300] as [number, number],
            parameters: {}
          },
          {
            id: '2',
            name: 'Node B',
            type: 'n8n-nodes-base.set',
            typeVersion: 2,
            position: [450, 300] as [number, number],
            parameters: {}
          }
        ],
        connections: {
          'Node A': {
            main: [[{ node: 'Node B', type: 'main', index: 0 }]]
          },
          'Node B': {
            main: [[{ node: 'Node A', type: 'main', index: 0 }]] // Creates a cycle
          }
        }
      };

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

      // Assert
      expect(result.valid).toBe(false);
      expect(result.errors.some(e => e.message.includes('cycle'))).toBe(true);
    });

    it('should handle null workflow gracefully', async () => {
      // Arrange
      const mockRepository = createMockRepository({});
      const mockValidatorClass = createMockValidatorClass({
        valid: true,
        errors: [],
        warnings: [],
        suggestions: []
      });

      validator = new WorkflowValidator(mockRepository as any, mockValidatorClass as any);

      // Act
      const result = await validator.validateWorkflow(null as any);

      // Assert
      expect(result.valid).toBe(false);
      expect(result.errors[0].message).toContain('workflow is null or undefined');
    });

    it('should require connections for multi-node workflows', async () => {
      // Arrange
      const nodeData = {
        'n8n-nodes-base.manualTrigger': {
          type: 'nodes-base.manualTrigger',
          displayName: 'Manual Trigger',
          properties: []
        },
        'nodes-base.manualTrigger': {
          type: 'nodes-base.manualTrigger',
          displayName: 'Manual Trigger',
          properties: []
        },
        'n8n-nodes-base.set': {
          type: 'nodes-base.set',
          displayName: 'Set',
          version: 2,
          isVersioned: true,
          properties: []
        },
        'nodes-base.set': {
          type: 'nodes-base.set',
          displayName: 'Set',
          version: 2,
          isVersioned: true,
          properties: []
        }
      };

      const mockRepository = createMockRepository(nodeData);
      const mockValidatorClass = createMockValidatorClass({
        valid: true,
        errors: [],
        warnings: [],
        suggestions: []
      });

      validator = new WorkflowValidator(mockRepository as any, mockValidatorClass as any);

      const workflow = {
        name: 'No Connections',
        nodes: [
          {
            id: '1',
            name: 'Manual Trigger',
            type: 'n8n-nodes-base.manualTrigger',
            position: [250, 300] as [number, number],
            parameters: {}
          },
          {
            id: '2',
            name: 'Set',
            type: 'n8n-nodes-base.set',
            typeVersion: 2,
            position: [450, 300] as [number, number],
            parameters: {}
          }
        ],
        connections: {} // No connections between nodes
      };

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

      // Assert
      expect(result.valid).toBe(false);
      expect(result.errors.some(e => e.message.includes('Multi-node workflow has no connections'))).toBe(true);
    });

    it('should validate typeVersion for versioned nodes', async () => {
      // Arrange
      const nodeData = {
        'n8n-nodes-base.httpRequest': {
          type: 'nodes-base.httpRequest',
          displayName: 'HTTP Request',
          isVersioned: true,
          version: 3, // Latest version is 3
          properties: []
        },
        'nodes-base.httpRequest': {
          type: 'nodes-base.httpRequest',
          displayName: 'HTTP Request',
          isVersioned: true,
          version: 3,
          properties: []
        }
      };

      const mockRepository = createMockRepository(nodeData);
      const mockValidatorClass = createMockValidatorClass({
        valid: true,
        errors: [],
        warnings: [],
        suggestions: []
      });

      validator = new WorkflowValidator(mockRepository as any, mockValidatorClass as any);

      const workflow = {
        name: 'Version Test',
        nodes: [
          {
            id: '1',
            name: 'HTTP Request',
            type: 'n8n-nodes-base.httpRequest',
            typeVersion: 2, // Outdated version
            position: [250, 300] as [number, number],
            parameters: {}
          }
        ],
        connections: {}
      };

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

      // Assert
      expect(result.warnings.some(w => w.message.includes('Outdated typeVersion'))).toBe(true);
    });

    it('should normalize and validate nodes-base prefix to find the node', async () => {
      // Arrange - Test that full-form types are normalized to short form to find the node
      // The repository only has the node under the SHORT normalized key (database format)
      const nodeData = {
        'nodes-base.webhook': {  // Repository has it under SHORT form (database format)
          type: 'nodes-base.webhook',
          displayName: 'Webhook',
          isVersioned: true,
          version: 2,
          properties: []
        }
      };

      // Mock repository that simulates the normalization behavior
      // After our changes, getNode is called with the already-normalized type (short form)
      const mockRepository = {
        getNode: vi.fn((type: string) => {
          // The validator now normalizes to short form before calling getNode
          // So getNode receives 'nodes-base.webhook'
          if (type === 'nodes-base.webhook') {
            return nodeData['nodes-base.webhook'];
          }
          return null;
        }),
        findSimilarNodes: vi.fn().mockReturnValue([])
      };

      const mockValidatorClass = createMockValidatorClass({
        valid: true,
        errors: [],
        warnings: [],
        suggestions: []
      });

      validator = new WorkflowValidator(mockRepository as any, mockValidatorClass as any);

      const workflow = {
        name: 'Valid Alternative Prefix',
        nodes: [
          {
            id: '1',
            name: 'Webhook',
            type: 'n8n-nodes-base.webhook', // Using the full-form prefix (will be normalized to short)
            position: [250, 300] as [number, number],
            parameters: {},
            typeVersion: 2
          }
        ],
        connections: {}
      };

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

      // Assert - The node should be found through normalization
      expect(result.valid).toBe(true);
      expect(result.errors).toHaveLength(0);

      // Verify the repository was called (once with original, once with normalized)
      expect(mockRepository.getNode).toHaveBeenCalled();
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/unit/mcp/lru-cache-behavior.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Comprehensive unit tests for LRU cache behavior in handlers-n8n-manager.ts
 *
 * This test file focuses specifically on cache behavior, TTL, eviction, and dispose callbacks
 */

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

// Mock dependencies
vi.mock('../../../src/services/n8n-api-client');
vi.mock('../../../src/config/n8n-api');
vi.mock('../../../src/utils/logger');
vi.mock('../../../src/types/instance-context', async () => {
  const actual = await vi.importActual('../../../src/types/instance-context');
  return {
    ...actual,
    validateInstanceContext: vi.fn()
  };
});

describe('LRU Cache Behavior Tests', () => {
  let mockN8nApiClient: Mock;
  let mockGetN8nApiConfigFromContext: Mock;
  let mockLogger: any; // Logger mock has complex type
  let mockValidateInstanceContext: Mock;

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

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

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

    // Default mock returns valid context validation
    mockValidateInstanceContext.mockReturnValue({
      valid: true,
      errors: undefined
    });

    // Force re-import of the module to get fresh cache state
    vi.resetModules();
  });

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

  describe('Cache Key Generation and Collision', () => {
    it('should generate different cache keys for different contexts', () => {
      const context1: InstanceContext = {
        n8nApiUrl: 'https://api1.n8n.cloud',
        n8nApiKey: 'key1',
        instanceId: 'instance1'
      };

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

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

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

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

      // Create clients to verify different cache entries
      const client1 = getN8nApiClient(context1);
      const client2 = getN8nApiClient(context2);

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

    it('should generate same cache key for identical contexts', () => {
      const context: InstanceContext = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'same-key',
        instanceId: 'same-instance'
      };

      const client1 = getN8nApiClient(context);
      const client2 = getN8nApiClient(context);

      // Should only create one client (cache hit)
      expect(mockN8nApiClient).toHaveBeenCalledTimes(1);
      expect(client1).toBe(client2);
    });

    it('should handle potential cache key collisions gracefully', () => {
      // Create contexts that might produce similar hashes but are valid
      const contexts = [
        {
          n8nApiUrl: 'https://a.com',
          n8nApiKey: 'keyb',
          instanceId: 'c'
        },
        {
          n8nApiUrl: 'https://ab.com',
          n8nApiKey: 'key',
          instanceId: 'bc'
        },
        {
          n8nApiUrl: 'https://abc.com',
          n8nApiKey: 'differentkey',  // Fixed: empty string causes config creation to fail
          instanceId: 'key'
        }
      ];

      contexts.forEach((context, index) => {
        const client = getN8nApiClient(context);
        expect(client).toBeDefined();
      });

      // Each should create a separate client due to different hashes
      expect(mockN8nApiClient).toHaveBeenCalledTimes(3);
    });
  });

  describe('LRU Eviction Behavior', () => {
    it('should evict oldest entries when cache is full', async () => {
      const loggerDebugSpy = vi.spyOn(logger, 'debug');

      // Create 101 different contexts to exceed max cache size of 100
      const contexts: InstanceContext[] = [];
      for (let i = 0; i < 101; i++) {
        contexts.push({
          n8nApiUrl: 'https://api.n8n.cloud',
          n8nApiKey: `key-${i}`,
          instanceId: `instance-${i}`
        });
      }

      // Create clients for all contexts
      contexts.forEach(context => {
        getN8nApiClient(context);
      });

      // Should have called dispose callback for evicted entries
      expect(loggerDebugSpy).toHaveBeenCalledWith(
        'Evicting API client from cache',
        expect.objectContaining({
          cacheKey: expect.stringMatching(/^[a-f0-9]{8}\.\.\.$/i)
        })
      );

      // Verify dispose was called at least once
      expect(loggerDebugSpy).toHaveBeenCalled();
    });

    it('should maintain LRU order during access', () => {
      const contexts: InstanceContext[] = [];
      for (let i = 0; i < 5; i++) {
        contexts.push({
          n8nApiUrl: 'https://api.n8n.cloud',
          n8nApiKey: `key-${i}`,
          instanceId: `instance-${i}`
        });
      }

      // Create initial clients
      contexts.forEach(context => {
        getN8nApiClient(context);
      });

      expect(mockN8nApiClient).toHaveBeenCalledTimes(5);

      // Access first context again (should move to most recent)
      getN8nApiClient(contexts[0]);

      // Should not create new client (cache hit)
      expect(mockN8nApiClient).toHaveBeenCalledTimes(5);
    });

    it('should handle rapid successive access patterns', () => {
      const context: InstanceContext = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'rapid-access-key',
        instanceId: 'rapid-instance'
      };

      // Rapidly access same context multiple times
      for (let i = 0; i < 10; i++) {
        getN8nApiClient(context);
      }

      // Should only create one client despite multiple accesses
      expect(mockN8nApiClient).toHaveBeenCalledTimes(1);
    });
  });

  describe('TTL (Time To Live) Behavior', () => {
    it('should respect TTL settings', async () => {
      const context: InstanceContext = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'ttl-test-key',
        instanceId: 'ttl-instance'
      };

      // Create initial client
      const client1 = getN8nApiClient(context);
      expect(mockN8nApiClient).toHaveBeenCalledTimes(1);

      // Access again immediately (should hit cache)
      const client2 = getN8nApiClient(context);
      expect(mockN8nApiClient).toHaveBeenCalledTimes(1);
      expect(client1).toBe(client2);

      // Note: We can't easily test TTL expiration in unit tests
      // as it requires actual time passage, but we can verify
      // the updateAgeOnGet behavior
    });

    it('should update age on cache access (updateAgeOnGet)', () => {
      const context: InstanceContext = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'age-update-key',
        instanceId: 'age-instance'
      };

      // Create and access multiple times
      getN8nApiClient(context);
      getN8nApiClient(context);
      getN8nApiClient(context);

      // Should only create one client due to cache hits
      expect(mockN8nApiClient).toHaveBeenCalledTimes(1);
    });
  });

  describe('Dispose Callback Security and Logging', () => {
    it('should sanitize cache keys in dispose callback logs', () => {
      const loggerDebugSpy = vi.spyOn(logger, 'debug');

      // Create enough contexts to trigger eviction
      const contexts: InstanceContext[] = [];
      for (let i = 0; i < 102; i++) {
        contexts.push({
          n8nApiUrl: 'https://sensitive-api.n8n.cloud',
          n8nApiKey: `super-secret-key-${i}`,
          instanceId: `sensitive-instance-${i}`
        });
      }

      // Create clients to trigger eviction
      contexts.forEach(context => {
        getN8nApiClient(context);
      });

      // Verify dispose callback logs don't contain sensitive data
      const logCalls = loggerDebugSpy.mock.calls.filter(call =>
        call[0] === 'Evicting API client from cache'
      );

      logCalls.forEach(call => {
        const logData = call[1] as any;

        // Should only log partial cache key (first 8 chars + ...)
        expect(logData.cacheKey).toMatch(/^[a-f0-9]{8}\.\.\.$/i);

        // Should not contain any sensitive information
        const logString = JSON.stringify(call);
        expect(logString).not.toContain('super-secret-key');
        expect(logString).not.toContain('sensitive-api');
        expect(logString).not.toContain('sensitive-instance');
      });
    });

    it('should handle dispose callback with undefined client', () => {
      const loggerDebugSpy = vi.spyOn(logger, 'debug');

      // Create many contexts to trigger disposal
      for (let i = 0; i < 105; i++) {
        const context: InstanceContext = {
          n8nApiUrl: 'https://api.n8n.cloud',
          n8nApiKey: `disposal-key-${i}`,
          instanceId: `disposal-${i}`
        };
        getN8nApiClient(context);
      }

      // Should handle disposal gracefully
      expect(() => {
        // The dispose callback should have been called
        expect(loggerDebugSpy).toHaveBeenCalled();
      }).not.toThrow();
    });
  });

  describe('Cache Memory Management', () => {
    it('should maintain consistent cache size limits', () => {
      // Create exactly 100 contexts (max cache size)
      const contexts: InstanceContext[] = [];
      for (let i = 0; i < 100; i++) {
        contexts.push({
          n8nApiUrl: 'https://api.n8n.cloud',
          n8nApiKey: `memory-key-${i}`,
          instanceId: `memory-${i}`
        });
      }

      // Create all clients
      contexts.forEach(context => {
        getN8nApiClient(context);
      });

      // All should be cached
      expect(mockN8nApiClient).toHaveBeenCalledTimes(100);

      // Access all again - should hit cache
      contexts.forEach(context => {
        getN8nApiClient(context);
      });

      // Should not create additional clients
      expect(mockN8nApiClient).toHaveBeenCalledTimes(100);
    });

    it('should handle edge case of single cache entry', () => {
      const context: InstanceContext = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'single-key',
        instanceId: 'single-instance'
      };

      // Create and access multiple times
      for (let i = 0; i < 5; i++) {
        getN8nApiClient(context);
      }

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

  describe('Cache Configuration Validation', () => {
    it('should use reasonable cache limits', () => {
      // These values should match the actual cache configuration
      const MAX_CACHE_SIZE = 100;
      const TTL_MINUTES = 30;
      const TTL_MS = TTL_MINUTES * 60 * 1000;

      // Verify limits are reasonable
      expect(MAX_CACHE_SIZE).toBeGreaterThan(0);
      expect(MAX_CACHE_SIZE).toBeLessThanOrEqual(1000);
      expect(TTL_MS).toBeGreaterThan(0);
      expect(TTL_MS).toBeLessThanOrEqual(60 * 60 * 1000); // Max 1 hour
    });
  });

  describe('Cache Interaction with Validation', () => {
    it('should not cache when context validation fails', () => {
      // Reset mocks to ensure clean state for this test
      vi.clearAllMocks();
      mockValidateInstanceContext.mockClear();

      const invalidContext: InstanceContext = {
        n8nApiUrl: 'invalid-url',
        n8nApiKey: 'test-key',
        instanceId: 'invalid-instance'
      };

      // Mock validation failure
      mockValidateInstanceContext.mockReturnValue({
        valid: false,
        errors: ['Invalid n8nApiUrl format']
      });

      const client = getN8nApiClient(invalidContext);

      // Should not create client or cache anything
      expect(client).toBeNull();
      expect(mockN8nApiClient).not.toHaveBeenCalled();
    });

    it('should handle cache when config creation fails', () => {
      const context: InstanceContext = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'test-key',
        instanceId: 'config-fail'
      };

      // Mock config creation failure
      mockGetN8nApiConfigFromContext.mockReturnValue(null);

      const client = getN8nApiClient(context);

      expect(client).toBeNull();
    });
  });

  describe('Complex Cache Scenarios', () => {
    it('should handle mixed valid and invalid contexts', () => {
      // Reset mocks to ensure clean state for this test
      vi.clearAllMocks();
      mockValidateInstanceContext.mockClear();

      // First, set up default valid behavior
      mockValidateInstanceContext.mockReturnValue({
        valid: true,
        errors: undefined
      });

      const validContext: InstanceContext = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'valid-key',
        instanceId: 'valid'
      };

      const invalidContext: InstanceContext = {
        n8nApiUrl: 'invalid-url',
        n8nApiKey: 'key',
        instanceId: 'invalid'
      };

      // Valid context should work
      const validClient = getN8nApiClient(validContext);
      expect(validClient).toBeDefined();

      // Change mock for invalid context
      mockValidateInstanceContext.mockReturnValueOnce({
        valid: false,
        errors: ['Invalid URL']
      });

      const invalidClient = getN8nApiClient(invalidContext);
      expect(invalidClient).toBeNull();

      // Reset mock back to valid for subsequent calls
      mockValidateInstanceContext.mockReturnValue({
        valid: true,
        errors: undefined
      });

      // Valid context should still work (cache hit)
      const validClient2 = getN8nApiClient(validContext);
      expect(validClient2).toBe(validClient);
    });

    it('should handle concurrent access to same cache key', () => {
      const context: InstanceContext = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'concurrent-key',
        instanceId: 'concurrent'
      };

      // Simulate concurrent access
      const promises = Array(10).fill(null).map(() =>
        Promise.resolve(getN8nApiClient(context))
      );

      return Promise.all(promises).then(clients => {
        // All should return the same cached client
        const firstClient = clients[0];
        clients.forEach(client => {
          expect(client).toBe(firstClient);
        });

        // Should only create one client
        expect(mockN8nApiClient).toHaveBeenCalledTimes(1);
      });
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/unit/templates/metadata-generator.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { MetadataGenerator, TemplateMetadataSchema, MetadataRequest } from '../../../src/templates/metadata-generator';

// Mock OpenAI
vi.mock('openai', () => {
  return {
    default: vi.fn().mockImplementation(() => ({
      chat: {
        completions: {
          create: vi.fn()
        }
      }
    }))
  };
});

describe('MetadataGenerator', () => {
  let generator: MetadataGenerator;
  
  beforeEach(() => {
    generator = new MetadataGenerator('test-api-key', 'gpt-5-mini-2025-08-07');
  });
  
  describe('createBatchRequest', () => {
    it('should create a valid batch request', () => {
      const template: MetadataRequest = {
        templateId: 123,
        name: 'Test Workflow',
        description: 'A test workflow',
        nodes: ['n8n-nodes-base.webhook', 'n8n-nodes-base.httpRequest', 'n8n-nodes-base.slack']
      };
      
      const request = generator.createBatchRequest(template);
      
      expect(request.custom_id).toBe('template-123');
      expect(request.method).toBe('POST');
      expect(request.url).toBe('/v1/chat/completions');
      expect(request.body.model).toBe('gpt-5-mini-2025-08-07');
      expect(request.body.response_format.type).toBe('json_schema');
      expect(request.body.response_format.json_schema.strict).toBe(true);
      expect(request.body.messages).toHaveLength(2);
    });
    
    it('should summarize nodes effectively', () => {
      const template: MetadataRequest = {
        templateId: 456,
        name: 'Complex Workflow',
        nodes: [
          'n8n-nodes-base.webhook',
          'n8n-nodes-base.httpRequest',
          'n8n-nodes-base.httpRequest',
          'n8n-nodes-base.postgres',
          'n8n-nodes-base.slack',
          '@n8n/n8n-nodes-langchain.agent'
        ]
      };
      
      const request = generator.createBatchRequest(template);
      const userMessage = request.body.messages[1].content;
      
      expect(userMessage).toContain('Complex Workflow');
      expect(userMessage).toContain('Nodes Used (6)');
      expect(userMessage).toContain('HTTP/Webhooks');
    });
  });
  
  describe('parseResult', () => {
    it('should parse a successful result', () => {
      const mockResult = {
        custom_id: 'template-789',
        response: {
          body: {
            choices: [{
              message: {
                content: JSON.stringify({
                  categories: ['automation', 'integration'],
                  complexity: 'medium',
                  use_cases: ['API integration', 'Data sync'],
                  estimated_setup_minutes: 30,
                  required_services: ['Slack API'],
                  key_features: ['Webhook triggers', 'API calls'],
                  target_audience: ['developers']
                })
              },
              finish_reason: 'stop'
            }]
          }
        }
      };
      
      const result = generator.parseResult(mockResult);
      
      expect(result.templateId).toBe(789);
      expect(result.metadata.categories).toEqual(['automation', 'integration']);
      expect(result.metadata.complexity).toBe('medium');
      expect(result.error).toBeUndefined();
    });
    
    it('should handle error results', () => {
      const mockResult = {
        custom_id: 'template-999',
        error: {
          message: 'API error'
        }
      };
      
      const result = generator.parseResult(mockResult);
      
      expect(result.templateId).toBe(999);
      expect(result.error).toBe('API error');
      expect(result.metadata).toBeDefined();
      expect(result.metadata.complexity).toBe('medium'); // Default metadata
    });
    
    it('should handle malformed responses', () => {
      const mockResult = {
        custom_id: 'template-111',
        response: {
          body: {
            choices: [{
              message: {
                content: 'not valid json'
              },
              finish_reason: 'stop'
            }]
          }
        }
      };
      
      const result = generator.parseResult(mockResult);
      
      expect(result.templateId).toBe(111);
      expect(result.error).toContain('Unexpected token');
      expect(result.metadata).toBeDefined();
    });
  });
  
  describe('TemplateMetadataSchema', () => {
    it('should validate correct metadata', () => {
      const validMetadata = {
        categories: ['automation', 'integration'],
        complexity: 'simple' as const,
        use_cases: ['API calls', 'Data processing'],
        estimated_setup_minutes: 15,
        required_services: [],
        key_features: ['Fast processing'],
        target_audience: ['developers']
      };
      
      const result = TemplateMetadataSchema.safeParse(validMetadata);
      
      expect(result.success).toBe(true);
    });
    
    it('should reject invalid complexity', () => {
      const invalidMetadata = {
        categories: ['automation'],
        complexity: 'very-hard', // Invalid
        use_cases: ['API calls'],
        estimated_setup_minutes: 15,
        required_services: [],
        key_features: ['Fast'],
        target_audience: ['developers']
      };
      
      const result = TemplateMetadataSchema.safeParse(invalidMetadata);
      
      expect(result.success).toBe(false);
    });
    
    it('should enforce array limits', () => {
      const tooManyCategories = {
        categories: ['a', 'b', 'c', 'd', 'e', 'f'], // Max 5
        complexity: 'simple' as const,
        use_cases: ['API calls'],
        estimated_setup_minutes: 15,
        required_services: [],
        key_features: ['Fast'],
        target_audience: ['developers']
      };
      
      const result = TemplateMetadataSchema.safeParse(tooManyCategories);
      
      expect(result.success).toBe(false);
    });
    
    it('should enforce time limits', () => {
      const tooLongSetup = {
        categories: ['automation'],
        complexity: 'complex' as const,
        use_cases: ['API calls'],
        estimated_setup_minutes: 500, // Max 480
        required_services: [],
        key_features: ['Fast'],
        target_audience: ['developers']
      };
      
      const result = TemplateMetadataSchema.safeParse(tooLongSetup);
      
      expect(result.success).toBe(false);
    });
  });

  describe('Input Sanitization and Security', () => {
    it('should handle malicious template names safely', () => {
      const maliciousTemplate: MetadataRequest = {
        templateId: 123,
        name: '<script>alert("xss")</script>',
        description: 'javascript:alert(1)',
        nodes: ['n8n-nodes-base.webhook']
      };
      
      const request = generator.createBatchRequest(maliciousTemplate);
      const userMessage = request.body.messages[1].content;
      
      // Should contain the malicious content as-is (OpenAI will handle it)
      // but should not cause any injection in our code
      expect(userMessage).toContain('<script>alert("xss")</script>');
      expect(userMessage).toContain('javascript:alert(1)');
      expect(request.body.model).toBe('gpt-5-mini-2025-08-07');
    });

    it('should handle extremely long template names', () => {
      const longName = 'A'.repeat(10000); // Very long name
      const template: MetadataRequest = {
        templateId: 456,
        name: longName,
        nodes: ['n8n-nodes-base.webhook']
      };
      
      const request = generator.createBatchRequest(template);
      
      expect(request.custom_id).toBe('template-456');
      expect(request.body.messages[1].content).toContain(longName);
    });

    it('should handle special characters in node names', () => {
      const template: MetadataRequest = {
        templateId: 789,
        name: 'Test Workflow',
        nodes: [
          'n8n-nodes-base.webhook',
          '@n8n/custom-node.with.dots',
          'custom-package/node-with-slashes',
          'node_with_underscore',
          'node-with-unicode-名前'
        ]
      };
      
      const request = generator.createBatchRequest(template);
      const userMessage = request.body.messages[1].content;
      
      expect(userMessage).toContain('HTTP/Webhooks');
      expect(userMessage).toContain('custom-node.with.dots');
    });

    it('should handle empty or undefined descriptions safely', () => {
      const template: MetadataRequest = {
        templateId: 100,
        name: 'Test',
        description: undefined,
        nodes: ['n8n-nodes-base.webhook']
      };
      
      const request = generator.createBatchRequest(template);
      const userMessage = request.body.messages[1].content;
      
      // Should not include undefined or null in the message
      expect(userMessage).not.toContain('undefined');
      expect(userMessage).not.toContain('null');
      expect(userMessage).toContain('Test');
    });

    it('should limit context size for very large workflows', () => {
      const manyNodes = Array.from({ length: 1000 }, (_, i) => `n8n-nodes-base.node${i}`);
      const template: MetadataRequest = {
        templateId: 200,
        name: 'Huge Workflow',
        nodes: manyNodes,
        workflow: {
          nodes: Array.from({ length: 500 }, (_, i) => ({ id: `node${i}` })),
          connections: {}
        }
      };
      
      const request = generator.createBatchRequest(template);
      const userMessage = request.body.messages[1].content;
      
      // Should handle large amounts of data gracefully
      expect(userMessage.length).toBeLessThan(50000); // Reasonable limit
      expect(userMessage).toContain('Huge Workflow');
    });
  });

  describe('Error Handling and Edge Cases', () => {
    it('should handle malformed OpenAI responses', () => {
      const malformedResults = [
        {
          custom_id: 'template-111',
          response: {
            body: {
              choices: [{
                message: {
                  content: '{"invalid": json syntax}'
                },
                finish_reason: 'stop'
              }]
            }
          }
        },
        {
          custom_id: 'template-222', 
          response: {
            body: {
              choices: [{
                message: {
                  content: null
                },
                finish_reason: 'stop'
              }]
            }
          }
        },
        {
          custom_id: 'template-333',
          response: {
            body: {
              choices: []
            }
          }
        }
      ];
      
      malformedResults.forEach(result => {
        const parsed = generator.parseResult(result);
        expect(parsed.error).toBeDefined();
        expect(parsed.metadata).toBeDefined();
        expect(parsed.metadata.complexity).toBe('medium'); // Default metadata
      });
    });

    it('should handle Zod validation failures', () => {
      const invalidResponse = {
        custom_id: 'template-444',
        response: {
          body: {
            choices: [{
              message: {
                content: JSON.stringify({
                  categories: ['too', 'many', 'categories', 'here', 'way', 'too', 'many'],
                  complexity: 'invalid-complexity',
                  use_cases: [],
                  estimated_setup_minutes: -5, // Invalid negative time
                  required_services: 'not-an-array',
                  key_features: null,
                  target_audience: ['too', 'many', 'audiences', 'here']
                })
              },
              finish_reason: 'stop'
            }]
          }
        }
      };
      
      const result = generator.parseResult(invalidResponse);
      
      expect(result.templateId).toBe(444);
      expect(result.error).toBeDefined();
      expect(result.metadata).toEqual(generator['getDefaultMetadata']());
    });

    it('should handle network timeouts gracefully in generateSingle', async () => {
      // Create a new generator with mocked OpenAI client
      const mockClient = {
        chat: {
          completions: {
            create: vi.fn().mockRejectedValue(new Error('Request timed out'))
          }
        }
      };
      
      // Override the client property using Object.defineProperty
      Object.defineProperty(generator, 'client', {
        value: mockClient,
        writable: true
      });
      
      const template: MetadataRequest = {
        templateId: 555,
        name: 'Timeout Test',
        nodes: ['n8n-nodes-base.webhook']
      };
      
      const result = await generator.generateSingle(template);
      
      // Should return default metadata instead of throwing
      expect(result).toEqual(generator['getDefaultMetadata']());
    });
  });

  describe('Node Summarization Logic', () => {
    it('should group similar nodes correctly', () => {
      const template: MetadataRequest = {
        templateId: 666,
        name: 'Complex Workflow',
        nodes: [
          'n8n-nodes-base.webhook',
          'n8n-nodes-base.httpRequest',
          'n8n-nodes-base.postgres',
          'n8n-nodes-base.mysql',
          'n8n-nodes-base.slack',
          'n8n-nodes-base.gmail',
          '@n8n/n8n-nodes-langchain.openAi',
          '@n8n/n8n-nodes-langchain.agent',
          'n8n-nodes-base.googleSheets',
          'n8n-nodes-base.excel'
        ]
      };
      
      const request = generator.createBatchRequest(template);
      const userMessage = request.body.messages[1].content;
      
      expect(userMessage).toContain('HTTP/Webhooks (2)');
      expect(userMessage).toContain('Database (2)');
      expect(userMessage).toContain('Communication (2)');
      expect(userMessage).toContain('AI/ML (2)');
      expect(userMessage).toContain('Spreadsheets (2)');
    });

    it('should handle unknown node types gracefully', () => {
      const template: MetadataRequest = {
        templateId: 777,
        name: 'Unknown Nodes',
        nodes: [
          'custom-package.unknownNode',
          'another-package.weirdNodeType',
          'someNodeTrigger',
          'anotherNode'
        ]
      };
      
      const request = generator.createBatchRequest(template);
      const userMessage = request.body.messages[1].content;
      
      // Should handle unknown nodes without crashing
      expect(userMessage).toContain('unknownNode');
      expect(userMessage).toContain('weirdNodeType');
      expect(userMessage).toContain('someNode'); // Trigger suffix removed
    });

    it('should limit node summary length', () => {
      const manyNodes = Array.from({ length: 50 }, (_, i) => 
        `n8n-nodes-base.customNode${i}`
      );
      
      const template: MetadataRequest = {
        templateId: 888,
        name: 'Many Nodes',
        nodes: manyNodes
      };
      
      const request = generator.createBatchRequest(template);
      const userMessage = request.body.messages[1].content;
      
      // Should limit to top 10 groups
      const summaryLine = userMessage.split('\n').find((line: string) => 
        line.includes('Nodes Used (50)')
      );
      
      expect(summaryLine).toBeDefined();
      const nodeGroups = summaryLine!.split(': ')[1].split(', ');
      expect(nodeGroups.length).toBeLessThanOrEqual(10);
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/integration/docker/docker-config.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
import { execSync, spawn } from 'child_process';
import path from 'path';
import fs from 'fs';
import os from 'os';
import { exec, waitForHealthy, isRunningInHttpMode, getProcessEnv } from './test-helpers';

// Skip tests if not in CI or if Docker is not available
const SKIP_DOCKER_TESTS = process.env.CI !== 'true' && !process.env.RUN_DOCKER_TESTS;
const describeDocker = SKIP_DOCKER_TESTS ? describe.skip : describe;

// Helper to check if Docker is available
async function isDockerAvailable(): Promise<boolean> {
  try {
    await exec('docker --version');
    return true;
  } catch {
    return false;
  }
}

// Helper to generate unique container names
function generateContainerName(suffix: string): string {
  return `n8n-mcp-test-${Date.now()}-${suffix}`;
}

// Helper to clean up containers
async function cleanupContainer(containerName: string) {
  try {
    await exec(`docker stop ${containerName}`);
    await exec(`docker rm ${containerName}`);
  } catch {
    // Ignore errors - container might not exist
  }
}

describeDocker('Docker Config File Integration', () => {
  let tempDir: string;
  let dockerAvailable: boolean;
  const imageName = 'n8n-mcp-test:latest';
  const containers: string[] = [];

  beforeAll(async () => {
    dockerAvailable = await isDockerAvailable();
    if (!dockerAvailable) {
      console.warn('Docker not available, skipping Docker integration tests');
      return;
    }

    // Check if image exists
    let imageExists = false;
    try {
      await exec(`docker image inspect ${imageName}`);
      imageExists = true;
    } catch {
      imageExists = false;
    }

    // Build test image if in CI or if explicitly requested or if image doesn't exist
    if (!imageExists || process.env.CI === 'true' || process.env.BUILD_DOCKER_TEST_IMAGE === 'true') {
      const projectRoot = path.resolve(__dirname, '../../../');
      console.log('Building Docker image for tests...');
      try {
        execSync(`docker build -t ${imageName} .`, {
          cwd: projectRoot,
          stdio: 'inherit'
        });
        console.log('Docker image built successfully');
      } catch (error) {
        console.error('Failed to build Docker image:', error);
        throw new Error('Docker image build failed - tests cannot continue');
      }
    } else {
      console.log(`Using existing Docker image: ${imageName}`);
    }
  }, 60000); // Increase timeout to 60s for Docker build

  beforeEach(() => {
    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'docker-config-test-'));
  });

  afterEach(async () => {
    // Clean up containers
    for (const container of containers) {
      await cleanupContainer(container);
    }
    containers.length = 0;

    // Clean up temp directory
    if (fs.existsSync(tempDir)) {
      fs.rmSync(tempDir, { recursive: true });
    }
  });

  describe('Config file loading', () => {
    it('should load config.json and set environment variables', async () => {
      if (!dockerAvailable) return;

      const containerName = generateContainerName('config-load');
      containers.push(containerName);

      // Create config file
      const configPath = path.join(tempDir, 'config.json');
      const config = {
        mcp_mode: 'http',
        auth_token: 'test-token-from-config',
        port: 3456,
        database: {
          path: '/data/custom.db'
        }
      };
      fs.writeFileSync(configPath, JSON.stringify(config));

      // Run container with config file mounted
      const { stdout } = await exec(
        `docker run --name ${containerName} -v "${configPath}:/app/config.json:ro" ${imageName} sh -c "env | grep -E '^(MCP_MODE|AUTH_TOKEN|PORT|DATABASE_PATH)=' | sort"`
      );

      const envVars = stdout.trim().split('\n').reduce((acc, line) => {
        const [key, value] = line.split('=');
        acc[key] = value;
        return acc;
      }, {} as Record<string, string>);

      expect(envVars.MCP_MODE).toBe('http');
      expect(envVars.AUTH_TOKEN).toBe('test-token-from-config');
      expect(envVars.PORT).toBe('3456');
      expect(envVars.DATABASE_PATH).toBe('/data/custom.db');
    });

    it('should give precedence to environment variables over config file', async () => {
      if (!dockerAvailable) return;

      const containerName = generateContainerName('env-precedence');
      containers.push(containerName);

      // Create config file
      const configPath = path.join(tempDir, 'config.json');
      const config = {
        mcp_mode: 'stdio',
        auth_token: 'config-token',
        custom_var: 'from-config'
      };
      fs.writeFileSync(configPath, JSON.stringify(config));

      // Run container with both env vars and config file
      const { stdout } = await exec(
        `docker run --name ${containerName} ` +
        `-e MCP_MODE=http ` +
        `-e AUTH_TOKEN=env-token ` +
        `-v "${configPath}:/app/config.json:ro" ` +
        `${imageName} sh -c "env | grep -E '^(MCP_MODE|AUTH_TOKEN|CUSTOM_VAR)=' | sort"`
      );

      const envVars = stdout.trim().split('\n').reduce((acc, line) => {
        const [key, value] = line.split('=');
        acc[key] = value;
        return acc;
      }, {} as Record<string, string>);

      expect(envVars.MCP_MODE).toBe('http'); // From env var
      expect(envVars.AUTH_TOKEN).toBe('env-token'); // From env var
      expect(envVars.CUSTOM_VAR).toBe('from-config'); // From config file
    });

    it('should handle missing config file gracefully', async () => {
      if (!dockerAvailable) return;

      const containerName = generateContainerName('no-config');
      containers.push(containerName);

      // Run container without config file
      const { stdout, stderr } = await exec(
        `docker run --name ${containerName} ${imageName} echo "Container started successfully"`
      );

      expect(stdout.trim()).toBe('Container started successfully');
      expect(stderr).toBe('');
    });

    it('should handle invalid JSON in config file gracefully', async () => {
      if (!dockerAvailable) return;

      const containerName = generateContainerName('invalid-json');
      containers.push(containerName);

      // Create invalid config file
      const configPath = path.join(tempDir, 'config.json');
      fs.writeFileSync(configPath, '{ invalid json }');

      // Container should still start despite invalid config
      const { stdout } = await exec(
        `docker run --name ${containerName} -v "${configPath}:/app/config.json:ro" ${imageName} echo "Started despite invalid config"`
      );

      expect(stdout.trim()).toBe('Started despite invalid config');
    });
  });

  describe('n8n-mcp serve command', () => {
    it('should automatically set MCP_MODE=http for "n8n-mcp serve" command', async () => {
      if (!dockerAvailable) return;

      const containerName = generateContainerName('serve-command');
      containers.push(containerName);

      // Run container with n8n-mcp serve command
      // Start the container in detached mode
      await exec(
        `docker run -d --name ${containerName} -e AUTH_TOKEN=test-token -p 13001:3000 ${imageName} n8n-mcp serve`
      );
      
      // Give it time to start
      await new Promise(resolve => setTimeout(resolve, 3000));
      
      // Verify it's running in HTTP mode by checking the health endpoint
      const { stdout } = await exec(
        `docker exec ${containerName} curl -s http://localhost:3000/health || echo 'Server not responding'`
      );

      // If HTTP mode is active, health endpoint should respond
      expect(stdout).toContain('ok');
    });

    it('should preserve additional arguments when using "n8n-mcp serve"', async () => {
      if (!dockerAvailable) return;

      const containerName = generateContainerName('serve-args');
      containers.push(containerName);

      // Test that additional arguments are passed through
      // Note: This test is checking the command construction, not actual execution
      const result = await exec(
        `docker run --name ${containerName} ${imageName} sh -c "set -x; n8n-mcp serve --port 8080 2>&1 | grep -E 'node.*index.js.*--port.*8080' || echo 'Pattern not found'"`
      );

      // The serve command should transform to node command with arguments preserved
      expect(result.stdout).toBeTruthy();
    });
  });

  describe('Database initialization', () => {
    it('should initialize database when not present', async () => {
      if (!dockerAvailable) return;

      const containerName = generateContainerName('db-init');
      containers.push(containerName);

      // Run container and check database initialization
      const { stdout } = await exec(
        `docker run --name ${containerName} ${imageName} sh -c "ls -la /app/data/nodes.db && echo 'Database initialized'"`
      );

      expect(stdout).toContain('nodes.db');
      expect(stdout).toContain('Database initialized');
    });

    it('should respect NODE_DB_PATH from config file', async () => {
      if (!dockerAvailable) return;

      const containerName = generateContainerName('custom-db-path');
      containers.push(containerName);

      // Create config with custom database path
      const configPath = path.join(tempDir, 'config.json');
      const config = {
        NODE_DB_PATH: '/app/data/custom/custom.db'  // Use uppercase and a writable path
      };
      fs.writeFileSync(configPath, JSON.stringify(config));

      // Run container in detached mode to check environment after initialization
      // Set MCP_MODE=http so the server keeps running (stdio mode exits when stdin is closed in detached mode)
      await exec(
        `docker run -d --name ${containerName} -e MCP_MODE=http -e AUTH_TOKEN=test -v "${configPath}:/app/config.json:ro" ${imageName}`
      );
      
      // Give it time to load config and start
      await new Promise(resolve => setTimeout(resolve, 2000));
      
      // Check the actual process environment
      const { stdout } = await exec(
        `docker exec ${containerName} sh -c "cat /proc/1/environ | tr '\\0' '\\n' | grep NODE_DB_PATH || echo 'NODE_DB_PATH not found'"`
      );

      expect(stdout.trim()).toBe('NODE_DB_PATH=/app/data/custom/custom.db');
    });
  });

  describe('Authentication configuration', () => {
    it('should enforce AUTH_TOKEN requirement in HTTP mode', async () => {
      if (!dockerAvailable) return;

      const containerName = generateContainerName('auth-required');
      containers.push(containerName);

      // Try to run in HTTP mode without auth token
      try {
        await exec(
          `docker run --name ${containerName} -e MCP_MODE=http ${imageName} echo "Should not reach here"`
        );
        expect.fail('Container should have exited with error');
      } catch (error: any) {
        expect(error.stderr).toContain('AUTH_TOKEN or AUTH_TOKEN_FILE is required for HTTP mode');
      }
    });

    it('should accept AUTH_TOKEN from config file', async () => {
      if (!dockerAvailable) return;

      const containerName = generateContainerName('auth-config');
      containers.push(containerName);

      // Create config with auth token
      const configPath = path.join(tempDir, 'config.json');
      const config = {
        mcp_mode: 'http',
        auth_token: 'config-auth-token'
      };
      fs.writeFileSync(configPath, JSON.stringify(config));

      // Run container with config file
      const { stdout } = await exec(
        `docker run --name ${containerName} -v "${configPath}:/app/config.json:ro" ${imageName} sh -c "env | grep AUTH_TOKEN"`
      );

      expect(stdout.trim()).toBe('AUTH_TOKEN=config-auth-token');
    });
  });

  describe('Security and permissions', () => {
    it('should handle malicious config values safely', async () => {
      if (!dockerAvailable) return;

      const containerName = generateContainerName('security-test');
      containers.push(containerName);

      // Create config with potentially malicious values
      const configPath = path.join(tempDir, 'config.json');
      const config = {
        malicious1: "'; echo 'hacked' > /tmp/hacked.txt; '",
        malicious2: "$( touch /tmp/command-injection.txt )",
        malicious3: "`touch /tmp/backtick-injection.txt`"
      };
      fs.writeFileSync(configPath, JSON.stringify(config));

      // Run container and check that no files were created
      const { stdout } = await exec(
        `docker run --name ${containerName} -v "${configPath}:/app/config.json:ro" ${imageName} sh -c "ls -la /tmp/ | grep -E '(hacked|injection)' || echo 'No malicious files created'"`
      );

      expect(stdout.trim()).toBe('No malicious files created');
    });

    it('should run as non-root user by default', async () => {
      if (!dockerAvailable) return;

      const containerName = generateContainerName('non-root');
      containers.push(containerName);

      // Check user inside container
      const { stdout } = await exec(
        `docker run --name ${containerName} ${imageName} whoami`
      );

      expect(stdout.trim()).toBe('nodejs');
    });
  });

  describe('Complex configuration scenarios', () => {
    it('should handle nested configuration with all supported types', async () => {
      if (!dockerAvailable) return;

      const containerName = generateContainerName('complex-config');
      containers.push(containerName);

      // Create complex config
      const configPath = path.join(tempDir, 'config.json');
      const config = {
        server: {
          http: {
            port: 8080,
            host: '0.0.0.0',
            ssl: {
              enabled: true,
              cert_path: '/certs/server.crt'
            }
          }
        },
        features: {
          debug: false,
          metrics: true,
          logging: {
            level: 'info',
            format: 'json'
          }
        },
        limits: {
          max_connections: 100,
          timeout_seconds: 30
        }
      };
      fs.writeFileSync(configPath, JSON.stringify(config));

      // Run container and verify all variables
      const { stdout } = await exec(
        `docker run --name ${containerName} -v "${configPath}:/app/config.json:ro" ${imageName} sh -c "env | grep -E '^(SERVER_|FEATURES_|LIMITS_)' | sort"`
      );

      const lines = stdout.trim().split('\n');
      const envVars = lines.reduce((acc, line) => {
        const [key, value] = line.split('=');
        acc[key] = value;
        return acc;
      }, {} as Record<string, string>);

      // Verify nested values are correctly flattened
      expect(envVars.SERVER_HTTP_PORT).toBe('8080');
      expect(envVars.SERVER_HTTP_HOST).toBe('0.0.0.0');
      expect(envVars.SERVER_HTTP_SSL_ENABLED).toBe('true');
      expect(envVars.SERVER_HTTP_SSL_CERT_PATH).toBe('/certs/server.crt');
      expect(envVars.FEATURES_DEBUG).toBe('false');
      expect(envVars.FEATURES_METRICS).toBe('true');
      expect(envVars.FEATURES_LOGGING_LEVEL).toBe('info');
      expect(envVars.FEATURES_LOGGING_FORMAT).toBe('json');
      expect(envVars.LIMITS_MAX_CONNECTIONS).toBe('100');
      expect(envVars.LIMITS_TIMEOUT_SECONDS).toBe('30');
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/unit/docker/config-security.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import os from 'os';

describe('Config File Security Tests', () => {
  let tempDir: string;
  let configPath: string;
  const parseConfigPath = path.resolve(__dirname, '../../../docker/parse-config.js');
  
  // Clean environment for tests - only include essential variables
  const cleanEnv = { 
    PATH: process.env.PATH, 
    HOME: process.env.HOME,
    NODE_ENV: process.env.NODE_ENV 
  };

  beforeEach(() => {
    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'config-security-test-'));
    configPath = path.join(tempDir, 'config.json');
  });

  afterEach(() => {
    if (fs.existsSync(tempDir)) {
      fs.rmSync(tempDir, { recursive: true });
    }
  });

  describe('Command injection prevention', () => {
    it('should prevent basic command injection attempts', () => {
      const maliciousConfigs = [
        { cmd: "'; echo 'hacked' > /tmp/hacked.txt; '" },
        { cmd: '"; echo "hacked" > /tmp/hacked.txt; "' },
        { cmd: '`echo hacked > /tmp/hacked.txt`' },
        { cmd: '$(echo hacked > /tmp/hacked.txt)' },
        { cmd: '| echo hacked > /tmp/hacked.txt' },
        { cmd: '|| echo hacked > /tmp/hacked.txt' },
        { cmd: '& echo hacked > /tmp/hacked.txt' },
        { cmd: '&& echo hacked > /tmp/hacked.txt' },
        { cmd: '; echo hacked > /tmp/hacked.txt' },
        { cmd: '\n echo hacked > /tmp/hacked.txt \n' },
        { cmd: '\r\n echo hacked > /tmp/hacked.txt \r\n' }
      ];

      maliciousConfigs.forEach((config, index) => {
        fs.writeFileSync(configPath, JSON.stringify(config));
        const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
          encoding: 'utf8',
          env: cleanEnv
        });
        
        // The output should safely quote the malicious content
        expect(output).toContain("export CMD='");
        
        // Verify that the output contains a properly quoted export
        expect(output).toContain("export CMD='");
        
        // Create a test script to verify safety
        const testScript = `#!/bin/sh
set -e
${output}
# If command injection worked, this would fail
test -f /tmp/hacked.txt && exit 1
echo "SUCCESS: No injection occurred"
`;
        
        const tempScript = path.join(tempDir, `test-injection-${index}.sh`);
        fs.writeFileSync(tempScript, testScript);
        fs.chmodSync(tempScript, '755');
        
        const result = execSync(tempScript, { encoding: 'utf8', env: cleanEnv });
        expect(result.trim()).toBe('SUCCESS: No injection occurred');
        
        // Double-check no files were created
        expect(fs.existsSync('/tmp/hacked.txt')).toBe(false);
      });
    });

    it('should handle complex nested injection attempts', () => {
      const config = {
        database: {
          host: "localhost'; DROP TABLE users; --",
          port: 5432,
          credentials: {
            password: "$( cat /etc/passwd )",
            backup_cmd: "`rm -rf /`"
          }
        },
        scripts: {
          init: "#!/bin/bash\nrm -rf /\nexit 0"
        }
      };
      fs.writeFileSync(configPath, JSON.stringify(config));

      const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
        encoding: 'utf8',
        env: cleanEnv
      });
      
      // All values should be safely quoted
      expect(output).toContain("DATABASE_HOST='localhost'\"'\"'; DROP TABLE users; --'");
      expect(output).toContain("DATABASE_CREDENTIALS_PASSWORD='$( cat /etc/passwd )'");
      expect(output).toContain("DATABASE_CREDENTIALS_BACKUP_CMD='`rm -rf /`'");
      expect(output).toContain("SCRIPTS_INIT='#!/bin/bash\nrm -rf /\nexit 0'");
    });

    it('should handle Unicode and special characters safely', () => {
      const config = {
        unicode: "Hello 世界 🌍",
        emoji: "🚀 Deploy! 🎉",
        special: "Line1\nLine2\tTab\rCarriage",
        quotes_mix: `It's a "test" with 'various' quotes`,
        backslash: "C:\\Users\\test\\path",
        regex: "^[a-zA-Z0-9]+$",
        json_string: '{"key": "value"}',
        xml_string: '<tag attr="value">content</tag>',
        sql_injection: "1' OR '1'='1",
        null_byte: "test\x00null",
        escape_sequences: "test\\n\\r\\t\\b\\f"
      };
      fs.writeFileSync(configPath, JSON.stringify(config));

      const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
        encoding: 'utf8',
        env: cleanEnv
      });
      
      // All special characters should be preserved within quotes
      expect(output).toContain("UNICODE='Hello 世界 🌍'");
      expect(output).toContain("EMOJI='🚀 Deploy! 🎉'");
      expect(output).toContain("SPECIAL='Line1\nLine2\tTab\rCarriage'");
      expect(output).toContain("BACKSLASH='C:\\Users\\test\\path'");
      expect(output).toContain("REGEX='^[a-zA-Z0-9]+$'");
      expect(output).toContain("SQL_INJECTION='1'\"'\"' OR '\"'\"'1'\"'\"'='\"'\"'1'");
    });
  });

  describe('Shell metacharacter handling', () => {
    it('should safely handle all shell metacharacters', () => {
      const config = {
        dollar: "$HOME $USER ${PATH}",
        backtick: "`date` `whoami`",
        parentheses: "$(date) $(whoami)",
        semicolon: "cmd1; cmd2; cmd3",
        ampersand: "cmd1 & cmd2 && cmd3",
        pipe: "cmd1 | cmd2 || cmd3",
        redirect: "cmd > file < input >> append",
        glob: "*.txt ?.log [a-z]*",
        tilde: "~/home ~/.config",
        exclamation: "!history !!",
        question: "file? test?",
        asterisk: "*.* *",
        brackets: "[abc] [0-9]",
        braces: "{a,b,c} ${var}",
        caret: "^pattern^replacement^",
        hash: "#comment # another",
        at: "@variable @{array}"
      };
      fs.writeFileSync(configPath, JSON.stringify(config));

      const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
        encoding: 'utf8',
        env: cleanEnv
      });
      
      // Verify all metacharacters are safely quoted
      const lines = output.trim().split('\n');
      lines.forEach(line => {
        // Each line should be in the format: export KEY='value'
        expect(line).toMatch(/^export [A-Z_]+='.*'$/);
      });
      
      // Test that the values are safe when evaluated
      const testScript = `
#!/bin/sh
set -e
${output}
# If any metacharacters were unescaped, these would fail
test "\$DOLLAR" = '\$HOME \$USER \${PATH}'
test "\$BACKTICK" = '\`date\` \`whoami\`'
test "\$PARENTHESES" = '\$(date) \$(whoami)'
test "\$SEMICOLON" = 'cmd1; cmd2; cmd3'
test "\$PIPE" = 'cmd1 | cmd2 || cmd3'
echo "SUCCESS: All metacharacters safely contained"
`;
      
      const tempScript = path.join(tempDir, 'test-metachar.sh');
      fs.writeFileSync(tempScript, testScript);
      fs.chmodSync(tempScript, '755');
      
      const result = execSync(tempScript, { encoding: 'utf8', env: cleanEnv });
      expect(result.trim()).toBe('SUCCESS: All metacharacters safely contained');
    });
  });

  describe('Escaping edge cases', () => {
    it('should handle consecutive single quotes', () => {
      const config = {
        test1: "'''",
        test2: "It'''s",
        test3: "start'''middle'''end",
        test4: "''''''''",
      };
      fs.writeFileSync(configPath, JSON.stringify(config));

      const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
        encoding: 'utf8',
        env: cleanEnv
      });
      
      // Verify the escaping is correct
      expect(output).toContain(`TEST1=''"'"''"'"''"'"'`);
      expect(output).toContain(`TEST2='It'"'"''"'"''"'"'s'`);
    });

    it('should handle empty and whitespace-only values', () => {
      const config = {
        empty: "",
        space: " ",
        spaces: "   ",
        tab: "\t",
        newline: "\n",
        mixed_whitespace: " \t\n\r "
      };
      fs.writeFileSync(configPath, JSON.stringify(config));

      const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
        encoding: 'utf8',
        env: cleanEnv
      });
      
      expect(output).toContain("EMPTY=''");
      expect(output).toContain("SPACE=' '");
      expect(output).toContain("SPACES='   '");
      expect(output).toContain("TAB='\t'");
      expect(output).toContain("NEWLINE='\n'");
      expect(output).toContain("MIXED_WHITESPACE=' \t\n\r '");
    });

    it('should handle very long values', () => {
      const longString = 'a'.repeat(10000) + "'; echo 'injection'; '" + 'b'.repeat(10000);
      const config = {
        long_value: longString
      };
      fs.writeFileSync(configPath, JSON.stringify(config));

      const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
        encoding: 'utf8',
        env: cleanEnv
      });
      
      expect(output).toContain('LONG_VALUE=');
      expect(output.length).toBeGreaterThan(20000);
      // The injection attempt should be safely quoted
      expect(output).toContain("'\"'\"'; echo '\"'\"'injection'\"'\"'; '\"'\"'");
    });
  });

  describe('Environment variable name security', () => {
    it('should handle potentially dangerous key names', () => {
      const config = {
        "PATH": "should-not-override",
        "LD_PRELOAD": "dangerous",
        "valid_key": "safe_value",
        "123invalid": "should-be-skipped",
        "key-with-dash": "should-work",
        "key.with.dots": "should-work",
        "KEY WITH SPACES": "should-work"
      };
      fs.writeFileSync(configPath, JSON.stringify(config));

      const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
        encoding: 'utf8',
        env: cleanEnv
      });
      
      // Dangerous variables should be blocked
      expect(output).not.toContain("export PATH=");
      expect(output).not.toContain("export LD_PRELOAD=");
      
      // Valid keys should be converted to safe names
      expect(output).toContain("export VALID_KEY='safe_value'");
      expect(output).toContain("export KEY_WITH_DASH='should-work'");
      expect(output).toContain("export KEY_WITH_DOTS='should-work'");
      expect(output).toContain("export KEY_WITH_SPACES='should-work'");
      
      // Invalid starting with number should be prefixed with _
      expect(output).toContain("export _123INVALID='should-be-skipped'");
    });
  });

  describe('Real-world attack scenarios', () => {
    it('should prevent path traversal attempts', () => {
      const config = {
        file_path: "../../../etc/passwd",
        backup_location: "../../../../../../tmp/evil",
        template: "${../../secret.key}",
        include: "<?php include('/etc/passwd'); ?>"
      };
      fs.writeFileSync(configPath, JSON.stringify(config));

      const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
        encoding: 'utf8',
        env: cleanEnv
      });
      
      // Path traversal attempts should be preserved as strings, not resolved
      expect(output).toContain("FILE_PATH='../../../etc/passwd'");
      expect(output).toContain("BACKUP_LOCATION='../../../../../../tmp/evil'");
      expect(output).toContain("TEMPLATE='${../../secret.key}'");
      expect(output).toContain("INCLUDE='<?php include('\"'\"'/etc/passwd'\"'\"'); ?>'");
    });

    it('should handle polyglot payloads safely', () => {
      const config = {
        // JavaScript/Shell polyglot
        polyglot1: "';alert(String.fromCharCode(88,83,83))//';alert(String.fromCharCode(88,83,83))//\";alert(String.fromCharCode(88,83,83))//\";alert(String.fromCharCode(88,83,83))//--></SCRIPT>\">'><SCRIPT>alert(String.fromCharCode(88,83,83))</SCRIPT>",
        // SQL/Shell polyglot
        polyglot2: "1' OR '1'='1' /*' or 1=1 # ' or 1=1-- ' or 1=1;--",
        // XML/Shell polyglot
        polyglot3: "<?xml version=\"1.0\"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM \"file:///etc/passwd\">]><foo>&xxe;</foo>"
      };
      fs.writeFileSync(configPath, JSON.stringify(config));

      const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
        encoding: 'utf8',
        env: cleanEnv
      });
      
      // All polyglot payloads should be safely quoted
      const lines = output.trim().split('\n');
      lines.forEach(line => {
        if (line.startsWith('export POLYGLOT')) {
          // Should be safely wrapped in single quotes with proper escaping
          expect(line).toMatch(/^export POLYGLOT[0-9]='.*'$/);
          // The dangerous content is there but safely quoted
          // What matters is that when evaluated, it's just a string
        }
      });
    });
  });

  describe('Stress testing', () => {
    it('should handle deeply nested malicious structures', () => {
      const createNestedMalicious = (depth: number): any => {
        if (depth === 0) {
          return "'; rm -rf /; '";
        }
        return {
          [`level${depth}`]: createNestedMalicious(depth - 1),
          [`inject${depth}`]: "$( echo 'level " + depth + "' )"
        };
      };

      const config = createNestedMalicious(10);
      fs.writeFileSync(configPath, JSON.stringify(config));

      const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
        encoding: 'utf8',
        env: cleanEnv
      });
      
      // Should handle deep nesting without issues
      expect(output).toContain("LEVEL10_LEVEL9_LEVEL8");
      expect(output).toContain("'\"'\"'; rm -rf /; '\"'\"'");
      
      // All injection attempts should be quoted
      const lines = output.trim().split('\n');
      lines.forEach(line => {
        if (line.includes('INJECT')) {
          expect(line).toContain("$( echo '\"'\"'level");
        }
      });
    });

    it('should handle mixed attack vectors in single config', () => {
      const config = {
        normal_value: "This is safe",
        sql_injection: "1' OR '1'='1",
        cmd_injection: "; cat /etc/passwd",
        xxe_attempt: '<!ENTITY xxe SYSTEM "file:///etc/passwd">',
        code_injection: "${constructor.constructor('return process')().exit()}",
        format_string: "%s%s%s%s%s%s%s%s%s%s",
        buffer_overflow: "A".repeat(10000),
        null_injection: "test\x00admin",
        ldap_injection: "*)(&(1=1",
        xpath_injection: "' or '1'='1",
        template_injection: "{{7*7}}",
        ssti: "${7*7}",
        crlf_injection: "test\r\nSet-Cookie: admin=true",
        host_header: "evil.com\r\nX-Forwarded-Host: evil.com",
        cache_poisoning: "index.html%0d%0aContent-Length:%200%0d%0a%0d%0aHTTP/1.1%20200%20OK"
      };
      fs.writeFileSync(configPath, JSON.stringify(config));

      const output = execSync(`node "${parseConfigPath}" "${configPath}"`, { 
        encoding: 'utf8',
        env: cleanEnv
      });
      
      // Verify each attack vector is safely handled
      expect(output).toContain("NORMAL_VALUE='This is safe'");
      expect(output).toContain("SQL_INJECTION='1'\"'\"' OR '\"'\"'1'\"'\"'='\"'\"'1'");
      expect(output).toContain("CMD_INJECTION='; cat /etc/passwd'");
      expect(output).toContain("XXE_ATTEMPT='<!ENTITY xxe SYSTEM \"file:///etc/passwd\">'");
      expect(output).toContain("CODE_INJECTION='${constructor.constructor('\"'\"'return process'\"'\"')().exit()}'");
      
      // Verify no actual code execution occurs
      const evalTest = `${output}\necho "Test completed successfully"`;
      const result = execSync(evalTest, { shell: '/bin/sh', encoding: 'utf8' });
      expect(result).toContain("Test completed successfully");
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/unit/mcp/get-node-essentials-examples.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { N8NDocumentationMCPServer } from '../../../src/mcp/server';

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

describe('get_node_essentials 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
    // NOTE: Database stores nodes in SHORT form (nodes-base.xxx, not n8n-nodes-base.xxx)
    const testNodes = [
      {
        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([])
      },
      {
        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.test',
        package_name: 'n8n-nodes-base',
        display_name: 'Test Node',
        description: 'Test node for examples',
        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
        );
      }
    }
  });

  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).getNodeEssentials('nodes-base.httpRequest', false);

      expect(result).toBeDefined();
      expect(result.examples).toBeUndefined();
    });

    it('should not include examples when includeExamples is undefined', async () => {
      const result = await (server as any).getNodeEssentials('nodes-base.httpRequest', undefined);

      expect(result).toBeDefined();
      expect(result.examples).toBeUndefined();
    });

    it('should include examples when includeExamples is true', async () => {
      const result = await (server as any).getNodeEssentials('nodes-base.httpRequest', true);

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

    it('should limit examples to top 3 per node', async () => {
      const result = await (server as any).getNodeEssentials('nodes-base.webhook', true);

      expect(result).toBeDefined();
      if (result.examples) {
        expect(result.examples.length).toBeLessThanOrEqual(3);
      }
    });
  });

  describe('example data structure with metadata', () => {
    it('should return examples with full metadata structure', async () => {
      // Mock database to return example data with metadata
      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',
                    responseMode: 'lastNode'
                  }),
                  template_name: 'Webhook Template',
                  template_views: 2000,
                  complexity: 'simple',
                  use_cases: JSON.stringify(['webhook processing', 'API integration']),
                  has_credentials: 0,
                  has_expressions: 1
                }
              ])
            };
          }
          return originalPrepare(query);
        });

        const result = await (server as any).getNodeEssentials('nodes-base.webhook', true);

        if (result.examples && result.examples.length > 0) {
          const example = result.examples[0];

          // Verify structure
          expect(example).toHaveProperty('configuration');
          expect(example).toHaveProperty('source');
          expect(example).toHaveProperty('useCases');
          expect(example).toHaveProperty('metadata');

          // Verify source structure
          expect(example.source).toHaveProperty('template');
          expect(example.source).toHaveProperty('views');
          expect(example.source).toHaveProperty('complexity');

          // Verify metadata structure
          expect(example.metadata).toHaveProperty('hasCredentials');
          expect(example.metadata).toHaveProperty('hasExpressions');

          // Verify types
          expect(typeof example.configuration).toBe('object');
          expect(typeof example.source.template).toBe('string');
          expect(typeof example.source.views).toBe('number');
          expect(typeof example.source.complexity).toBe('string');
          expect(Array.isArray(example.useCases)).toBe(true);
          expect(typeof example.metadata.hasCredentials).toBe('boolean');
          expect(typeof example.metadata.hasExpressions).toBe('boolean');
        }
      }
    });

    it('should include complexity in source metadata', 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: JSON.stringify({ url: 'https://api.example.com' }),
                  template_name: 'Simple HTTP Request',
                  template_views: 500,
                  complexity: 'simple',
                  use_cases: JSON.stringify([]),
                  has_credentials: 0,
                  has_expressions: 0
                },
                {
                  parameters_json: JSON.stringify({
                    url: '={{ $json.url }}',
                    options: { timeout: 30000 }
                  }),
                  template_name: 'Complex HTTP Request',
                  template_views: 300,
                  complexity: 'complex',
                  use_cases: JSON.stringify(['advanced API calls']),
                  has_credentials: 1,
                  has_expressions: 1
                }
              ])
            };
          }
          return originalPrepare(query);
        });

        const result = await (server as any).getNodeEssentials('nodes-base.httpRequest', true);

        if (result.examples && result.examples.length >= 2) {
          expect(result.examples[0].source.complexity).toBe('simple');
          expect(result.examples[1].source.complexity).toBe('complex');
        }
      }
    });

    it('should limit use cases to 2 items', 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: JSON.stringify({}),
                  template_name: 'Test Template',
                  template_views: 100,
                  complexity: 'medium',
                  use_cases: JSON.stringify([
                    'use case 1',
                    'use case 2',
                    'use case 3',
                    'use case 4'
                  ]),
                  has_credentials: 0,
                  has_expressions: 0
                }
              ])
            };
          }
          return originalPrepare(query);
        });

        const result = await (server as any).getNodeEssentials('nodes-base.test', true);

        if (result.examples && result.examples.length > 0) {
          expect(result.examples[0].useCases.length).toBeLessThanOrEqual(2);
        }
      }
    });

    it('should handle empty use_cases 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: JSON.stringify({}),
                  template_name: 'Test Template',
                  template_views: 100,
                  complexity: 'medium',
                  use_cases: null,
                  has_credentials: 0,
                  has_expressions: 0
                }
              ])
            };
          }
          return originalPrepare(query);
        });

        const result = await (server as any).getNodeEssentials('nodes-base.test', true);

        if (result.examples && result.examples.length > 0) {
          expect(result.examples[0].useCases).toEqual([]);
        }
      }
    });
  });

  describe('caching behavior with includeExamples', () => {
    it('should use different cache keys for with/without examples', async () => {
      const cache = (server as any).cache;
      const cacheGetSpy = vi.spyOn(cache, 'get');

      // First call without examples
      await (server as any).getNodeEssentials('nodes-base.httpRequest', false);
      expect(cacheGetSpy).toHaveBeenCalledWith(expect.stringContaining('basic'));

      // Second call with examples
      await (server as any).getNodeEssentials('nodes-base.httpRequest', true);
      expect(cacheGetSpy).toHaveBeenCalledWith(expect.stringContaining('withExamples'));
    });

    it('should cache results separately for different includeExamples values', async () => {
      // Call with examples
      const resultWithExamples1 = await (server as any).getNodeEssentials('nodes-base.httpRequest', true);

      // Call without examples
      const resultWithoutExamples = await (server as any).getNodeEssentials('nodes-base.httpRequest', false);

      // Call with examples again (should be cached)
      const resultWithExamples2 = await (server as any).getNodeEssentials('nodes-base.httpRequest', true);

      // Results with examples should match
      expect(resultWithExamples1).toEqual(resultWithExamples2);

      // Result without examples should not have examples
      expect(resultWithoutExamples.examples).toBeUndefined();
    });
  });

  describe('backward compatibility', () => {
    it('should maintain backward compatibility when includeExamples not specified', async () => {
      const result = await (server as any).getNodeEssentials('nodes-base.httpRequest');

      expect(result).toBeDefined();
      expect(result.nodeType).toBeDefined();
      expect(result.displayName).toBeDefined();
      expect(result.examples).toBeUndefined();
    });

    it('should return same core data regardless of includeExamples value', async () => {
      const resultWithout = await (server as any).getNodeEssentials('nodes-base.httpRequest', false);
      const resultWith = await (server as any).getNodeEssentials('nodes-base.httpRequest', true);

      // Core fields should be identical
      expect(resultWithout.nodeType).toBe(resultWith.nodeType);
      expect(resultWithout.displayName).toBe(resultWith.displayName);
      expect(resultWithout.description).toBe(resultWith.description);
    });
  });

  describe('error handling', () => {
    it('should continue to work even if example fetch fails', 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')) {
            throw new Error('Database error');
          }
          return originalPrepare(query);
        });

        // Should not throw
        const result = await (server as any).getNodeEssentials('nodes-base.webhook', true);

        expect(result).toBeDefined();
        expect(result.nodeType).toBeDefined();
        // Examples should be empty array due to error (fallback behavior)
        expect(result.examples).toEqual([]);
        expect(result.examplesCount).toBe(0);
      }
    });

    it('should handle malformed JSON in template configs 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_views: 100,
                  complexity: 'medium',
                  use_cases: 'also invalid',
                  has_credentials: 0,
                  has_expressions: 0
                }
              ])
            };
          }
          return originalPrepare(query);
        });

        // Should not throw
        const result = await (server as any).getNodeEssentials('nodes-base.test', true);
        expect(result).toBeDefined();
      }
    });
  });

  describe('performance', () => {
    it('should complete in reasonable time with examples', async () => {
      const start = Date.now();
      await (server as any).getNodeEssentials('nodes-base.httpRequest', true);
      const duration = Date.now() - start;

      // Should complete under 100ms
      expect(duration).toBeLessThan(100);
    });

    it('should not add significant overhead when includeExamples is false', async () => {
      const startWithout = Date.now();
      await (server as any).getNodeEssentials('nodes-base.httpRequest', false);
      const durationWithout = Date.now() - startWithout;

      const startWith = Date.now();
      await (server as any).getNodeEssentials('nodes-base.httpRequest', true);
      const durationWith = Date.now() - startWith;

      // Both should be fast
      expect(durationWithout).toBeLessThan(50);
      expect(durationWith).toBeLessThan(100);
    });
  });
});

```

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

```typescript
/**
 * Integration tests for multi-tenant support across the entire codebase
 *
 * This test file provides comprehensive coverage for the multi-tenant implementation
 * by testing the actual behavior and integration points rather than implementation details.
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { InstanceContext, isInstanceContext, validateInstanceContext } from '../../src/types/instance-context';

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

describe('Multi-Tenant Support Integration', () => {
  let originalEnv: NodeJS.ProcessEnv;

  beforeEach(() => {
    originalEnv = { ...process.env };
    vi.clearAllMocks();
  });

  afterEach(() => {
    process.env = originalEnv;
  });

  describe('InstanceContext Validation', () => {
    describe('Real-world URL patterns', () => {
      const validUrls = [
        'https://app.n8n.cloud',
        'https://tenant1.n8n.cloud',
        'https://my-company.n8n.cloud',
        'https://n8n.example.com',
        'https://automation.company.com',
        'http://localhost:5678',
        'https://localhost:8443',
        'http://127.0.0.1:5678',
        'https://192.168.1.100:8080',
        'https://10.0.0.1:3000',
        'http://n8n.internal.company.com',
        'https://workflow.enterprise.local'
      ];

      validUrls.forEach(url => {
        it(`should accept realistic n8n URL: ${url}`, () => {
          const context: InstanceContext = {
            n8nApiUrl: url,
            n8nApiKey: 'valid-api-key-123'
          };

          expect(isInstanceContext(context)).toBe(true);

          const validation = validateInstanceContext(context);
          expect(validation.valid).toBe(true);
          expect(validation.errors).toBeUndefined();
        });
      });
    });

    describe('Security validation', () => {
      const maliciousUrls = [
        'javascript:alert("xss")',
        'vbscript:msgbox("xss")',
        'data:text/html,<script>alert("xss")</script>',
        'file:///etc/passwd',
        'ldap://attacker.com/cn=admin',
        'ftp://malicious.com'
      ];

      maliciousUrls.forEach(url => {
        it(`should reject potentially malicious URL: ${url}`, () => {
          const context: InstanceContext = {
            n8nApiUrl: url,
            n8nApiKey: 'valid-key'
          };

          expect(isInstanceContext(context)).toBe(false);

          const validation = validateInstanceContext(context);
          expect(validation.valid).toBe(false);
          expect(validation.errors).toBeDefined();
        });
      });
    });

    describe('API key validation', () => {
      const invalidApiKeys = [
        '',
        'placeholder',
        'YOUR_API_KEY',
        'example',
        'your_api_key_here'
      ];

      invalidApiKeys.forEach(key => {
        it(`should reject invalid API key: "${key}"`, () => {
          const context: InstanceContext = {
            n8nApiUrl: 'https://valid.n8n.cloud',
            n8nApiKey: key
          };

          if (key === '') {
            // Empty string validation
            const validation = validateInstanceContext(context);
            expect(validation.valid).toBe(false);
            expect(validation.errors?.[0]).toContain('empty string');
          } else {
            // Placeholder validation
            expect(isInstanceContext(context)).toBe(false);
          }
        });
      });

      it('should accept valid API keys', () => {
        const validKeys = [
          'sk_live_AbCdEf123456789',
          'api-key-12345-abcdef',
          'n8n_api_key_production_v1_xyz',
          'Bearer-token-abc123',
          'jwt.eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9'
        ];

        validKeys.forEach(key => {
          const context: InstanceContext = {
            n8nApiUrl: 'https://valid.n8n.cloud',
            n8nApiKey: key
          };

          expect(isInstanceContext(context)).toBe(true);
          const validation = validateInstanceContext(context);
          expect(validation.valid).toBe(true);
        });
      });
    });

    describe('Edge cases and error handling', () => {
      it('should handle partial instance context', () => {
        const partialContext: InstanceContext = {
          n8nApiUrl: 'https://tenant1.n8n.cloud'
          // n8nApiKey intentionally missing
        };

        expect(isInstanceContext(partialContext)).toBe(true);
        const validation = validateInstanceContext(partialContext);
        expect(validation.valid).toBe(true);
      });

      it('should handle completely empty context', () => {
        const emptyContext: InstanceContext = {};

        expect(isInstanceContext(emptyContext)).toBe(true);
        const validation = validateInstanceContext(emptyContext);
        expect(validation.valid).toBe(true);
      });

      it('should handle numerical values gracefully', () => {
        const contextWithNumbers: InstanceContext = {
          n8nApiUrl: 'https://tenant1.n8n.cloud',
          n8nApiKey: 'valid-key',
          n8nApiTimeout: 30000,
          n8nApiMaxRetries: 3
        };

        expect(isInstanceContext(contextWithNumbers)).toBe(true);
        const validation = validateInstanceContext(contextWithNumbers);
        expect(validation.valid).toBe(true);
      });

      it('should reject invalid numerical values', () => {
        const invalidTimeout: InstanceContext = {
          n8nApiUrl: 'https://tenant1.n8n.cloud',
          n8nApiKey: 'valid-key',
          n8nApiTimeout: -1
        };

        expect(isInstanceContext(invalidTimeout)).toBe(false);
        const validation = validateInstanceContext(invalidTimeout);
        expect(validation.valid).toBe(false);
        expect(validation.errors?.[0]).toContain('Must be positive');
      });

      it('should reject invalid retry values', () => {
        const invalidRetries: InstanceContext = {
          n8nApiUrl: 'https://tenant1.n8n.cloud',
          n8nApiKey: 'valid-key',
          n8nApiMaxRetries: -5
        };

        expect(isInstanceContext(invalidRetries)).toBe(false);
        const validation = validateInstanceContext(invalidRetries);
        expect(validation.valid).toBe(false);
        expect(validation.errors?.[0]).toContain('Must be non-negative');
      });
    });
  });

  describe('Environment Variable Handling', () => {
    it('should handle ENABLE_MULTI_TENANT flag correctly', () => {
      // Test various flag values
      const flagValues = [
        { value: 'true', expected: true },
        { value: 'false', expected: false },
        { value: 'TRUE', expected: false },  // Case sensitive
        { value: 'yes', expected: false },
        { value: '1', expected: false },
        { value: '', expected: false },
        { value: undefined, expected: false }
      ];

      flagValues.forEach(({ value, expected }) => {
        if (value === undefined) {
          delete process.env.ENABLE_MULTI_TENANT;
        } else {
          process.env.ENABLE_MULTI_TENANT = value;
        }

        const isEnabled = process.env.ENABLE_MULTI_TENANT === 'true';
        expect(isEnabled).toBe(expected);
      });
    });

    it('should handle N8N_API_URL and N8N_API_KEY environment variables', () => {
      // Test backward compatibility
      process.env.N8N_API_URL = 'https://env.n8n.cloud';
      process.env.N8N_API_KEY = 'env-api-key';

      const hasEnvConfig = !!(process.env.N8N_API_URL || process.env.N8N_API_KEY);
      expect(hasEnvConfig).toBe(true);

      // Test when not set
      delete process.env.N8N_API_URL;
      delete process.env.N8N_API_KEY;

      const hasNoEnvConfig = !!(process.env.N8N_API_URL || process.env.N8N_API_KEY);
      expect(hasNoEnvConfig).toBe(false);
    });
  });

  describe('Header Processing Simulation', () => {
    it('should process multi-tenant headers correctly', () => {
      // Simulate Express request headers
      const mockHeaders = {
        'x-n8n-url': 'https://tenant1.n8n.cloud',
        'x-n8n-key': 'tenant1-api-key',
        'x-instance-id': 'tenant1-instance',
        'x-session-id': 'tenant1-session-123'
      };

      // Simulate header extraction
      const extractedContext: InstanceContext = {
        n8nApiUrl: mockHeaders['x-n8n-url'],
        n8nApiKey: mockHeaders['x-n8n-key'],
        instanceId: mockHeaders['x-instance-id'],
        sessionId: mockHeaders['x-session-id']
      };

      expect(isInstanceContext(extractedContext)).toBe(true);
      const validation = validateInstanceContext(extractedContext);
      expect(validation.valid).toBe(true);
    });

    it('should handle missing headers gracefully', () => {
      const mockHeaders: any = {
        'authorization': 'Bearer token',
        'content-type': 'application/json'
        // No x-n8n-* headers
      };

      const extractedContext = {
        n8nApiUrl: mockHeaders['x-n8n-url'], // undefined
        n8nApiKey: mockHeaders['x-n8n-key']  // undefined
      };

      // When no relevant headers exist, context should be undefined
      const shouldCreateContext = !!(extractedContext.n8nApiUrl || extractedContext.n8nApiKey);
      expect(shouldCreateContext).toBe(false);
    });

    it('should handle malformed headers', () => {
      const mockHeaders = {
        'x-n8n-url': 'not-a-url',
        'x-n8n-key': 'placeholder'
      };

      const extractedContext: InstanceContext = {
        n8nApiUrl: mockHeaders['x-n8n-url'],
        n8nApiKey: mockHeaders['x-n8n-key']
      };

      expect(isInstanceContext(extractedContext)).toBe(false);
      const validation = validateInstanceContext(extractedContext);
      expect(validation.valid).toBe(false);
    });
  });

  describe('Configuration Priority Logic', () => {
    it('should implement correct priority logic for tool inclusion', () => {
      // Test the shouldIncludeManagementTools logic
      const scenarios = [
        {
          name: 'env config only',
          envUrl: 'https://env.example.com',
          envKey: 'env-key',
          instanceContext: undefined,
          multiTenant: false,
          expected: true
        },
        {
          name: 'instance config only',
          envUrl: undefined,
          envKey: undefined,
          instanceContext: { n8nApiUrl: 'https://tenant.example.com', n8nApiKey: 'tenant-key' },
          multiTenant: false,
          expected: true
        },
        {
          name: 'multi-tenant flag only',
          envUrl: undefined,
          envKey: undefined,
          instanceContext: undefined,
          multiTenant: true,
          expected: true
        },
        {
          name: 'no configuration',
          envUrl: undefined,
          envKey: undefined,
          instanceContext: undefined,
          multiTenant: false,
          expected: false
        }
      ];

      scenarios.forEach(({ name, envUrl, envKey, instanceContext, multiTenant, expected }) => {
        // Setup environment
        if (envUrl) process.env.N8N_API_URL = envUrl;
        else delete process.env.N8N_API_URL;

        if (envKey) process.env.N8N_API_KEY = envKey;
        else delete process.env.N8N_API_KEY;

        if (multiTenant) process.env.ENABLE_MULTI_TENANT = 'true';
        else delete process.env.ENABLE_MULTI_TENANT;

        // Test logic
        const hasEnvConfig = !!(process.env.N8N_API_URL || process.env.N8N_API_KEY);
        const hasInstanceConfig = !!(instanceContext?.n8nApiUrl || instanceContext?.n8nApiKey);
        const isMultiTenantEnabled = process.env.ENABLE_MULTI_TENANT === 'true';

        const shouldIncludeManagementTools = hasEnvConfig || hasInstanceConfig || isMultiTenantEnabled;

        expect(shouldIncludeManagementTools).toBe(expected);
      });
    });
  });

  describe('Session Management Concepts', () => {
    it('should generate consistent identifiers for same configuration', () => {
      const config1 = {
        n8nApiUrl: 'https://tenant1.n8n.cloud',
        n8nApiKey: 'api-key-123'
      };

      const config2 = {
        n8nApiUrl: 'https://tenant1.n8n.cloud',
        n8nApiKey: 'api-key-123'
      };

      // Same configuration should produce same hash
      const hash1 = JSON.stringify(config1);
      const hash2 = JSON.stringify(config2);
      expect(hash1).toBe(hash2);
    });

    it('should generate different identifiers for different configurations', () => {
      const config1 = {
        n8nApiUrl: 'https://tenant1.n8n.cloud',
        n8nApiKey: 'api-key-123'
      };

      const config2 = {
        n8nApiUrl: 'https://tenant2.n8n.cloud',
        n8nApiKey: 'different-api-key'
      };

      // Different configuration should produce different hash
      const hash1 = JSON.stringify(config1);
      const hash2 = JSON.stringify(config2);
      expect(hash1).not.toBe(hash2);
    });

    it('should handle session isolation concepts', () => {
      const sessions = new Map();

      // Simulate creating sessions for different tenants
      const tenant1Context = {
        n8nApiUrl: 'https://tenant1.n8n.cloud',
        n8nApiKey: 'tenant1-key',
        instanceId: 'tenant1'
      };

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

      sessions.set('session-1', { context: tenant1Context, lastAccess: new Date() });
      sessions.set('session-2', { context: tenant2Context, lastAccess: new Date() });

      // Verify isolation
      expect(sessions.get('session-1').context.instanceId).toBe('tenant1');
      expect(sessions.get('session-2').context.instanceId).toBe('tenant2');
      expect(sessions.size).toBe(2);
    });
  });

  describe('Error Scenarios and Recovery', () => {
    it('should handle validation errors gracefully', () => {
      const invalidContext: InstanceContext = {
        n8nApiUrl: '',  // Empty URL
        n8nApiKey: '',  // Empty key
        n8nApiTimeout: -1,  // Invalid timeout
        n8nApiMaxRetries: -1  // Invalid retries
      };

      // Should not throw
      expect(() => isInstanceContext(invalidContext)).not.toThrow();
      expect(() => validateInstanceContext(invalidContext)).not.toThrow();

      const validation = validateInstanceContext(invalidContext);
      expect(validation.valid).toBe(false);
      expect(validation.errors?.length).toBeGreaterThan(0);

      // Each error should be descriptive
      validation.errors?.forEach(error => {
        expect(error).toContain('Invalid');
        expect(typeof error).toBe('string');
        expect(error.length).toBeGreaterThan(10);
      });
    });

    it('should provide specific error messages', () => {
      const testCases = [
        {
          context: { n8nApiUrl: '', n8nApiKey: 'valid' },
          expectedError: 'empty string'
        },
        {
          context: { n8nApiUrl: 'https://example.com', n8nApiKey: 'placeholder' },
          expectedError: 'placeholder'
        },
        {
          context: { n8nApiUrl: 'https://example.com', n8nApiKey: 'valid', n8nApiTimeout: -1 },
          expectedError: 'Must be positive'
        },
        {
          context: { n8nApiUrl: 'https://example.com', n8nApiKey: 'valid', n8nApiMaxRetries: -1 },
          expectedError: 'Must be non-negative'
        }
      ];

      testCases.forEach(({ context, expectedError }) => {
        const validation = validateInstanceContext(context);
        expect(validation.valid).toBe(false);
        expect(validation.errors?.some(err => err.includes(expectedError))).toBe(true);
      });
    });
  });
});
```
Page 17/46FirstPrevNextLast