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

# Directory Structure

```
├── _config.yml
├── .claude
│   └── agents
│       ├── code-reviewer.md
│       ├── context-manager.md
│       ├── debugger.md
│       ├── deployment-engineer.md
│       ├── mcp-backend-engineer.md
│       ├── n8n-mcp-tester.md
│       ├── technical-researcher.md
│       └── test-automator.md
├── .dockerignore
├── .env.docker
├── .env.example
├── .env.n8n.example
├── .env.test
├── .env.test.example
├── .github
│   ├── ABOUT.md
│   ├── BENCHMARK_THRESHOLDS.md
│   ├── FUNDING.yml
│   ├── gh-pages.yml
│   ├── secret_scanning.yml
│   └── workflows
│       ├── benchmark-pr.yml
│       ├── benchmark.yml
│       ├── docker-build-fast.yml
│       ├── docker-build-n8n.yml
│       ├── docker-build.yml
│       ├── release.yml
│       ├── test.yml
│       └── update-n8n-deps.yml
├── .gitignore
├── .npmignore
├── ATTRIBUTION.md
├── CHANGELOG.md
├── CLAUDE.md
├── codecov.yml
├── coverage.json
├── data
│   ├── .gitkeep
│   ├── nodes.db
│   ├── nodes.db-shm
│   ├── nodes.db-wal
│   └── templates.db
├── deploy
│   └── quick-deploy-n8n.sh
├── docker
│   ├── docker-entrypoint.sh
│   ├── n8n-mcp
│   ├── parse-config.js
│   └── README.md
├── docker-compose.buildkit.yml
├── docker-compose.extract.yml
├── docker-compose.n8n.yml
├── docker-compose.override.yml.example
├── docker-compose.test-n8n.yml
├── docker-compose.yml
├── Dockerfile
├── Dockerfile.railway
├── Dockerfile.test
├── docs
│   ├── AUTOMATED_RELEASES.md
│   ├── BENCHMARKS.md
│   ├── CHANGELOG.md
│   ├── CLAUDE_CODE_SETUP.md
│   ├── CLAUDE_INTERVIEW.md
│   ├── CODECOV_SETUP.md
│   ├── CODEX_SETUP.md
│   ├── CURSOR_SETUP.md
│   ├── DEPENDENCY_UPDATES.md
│   ├── DOCKER_README.md
│   ├── DOCKER_TROUBLESHOOTING.md
│   ├── FINAL_AI_VALIDATION_SPEC.md
│   ├── FLEXIBLE_INSTANCE_CONFIGURATION.md
│   ├── HTTP_DEPLOYMENT.md
│   ├── img
│   │   ├── cc_command.png
│   │   ├── cc_connected.png
│   │   ├── codex_connected.png
│   │   ├── cursor_tut.png
│   │   ├── Railway_api.png
│   │   ├── Railway_server_address.png
│   │   ├── vsc_ghcp_chat_agent_mode.png
│   │   ├── vsc_ghcp_chat_instruction_files.png
│   │   ├── vsc_ghcp_chat_thinking_tool.png
│   │   └── windsurf_tut.png
│   ├── INSTALLATION.md
│   ├── LIBRARY_USAGE.md
│   ├── local
│   │   ├── DEEP_DIVE_ANALYSIS_2025-10-02.md
│   │   ├── DEEP_DIVE_ANALYSIS_README.md
│   │   ├── Deep_dive_p1_p2.md
│   │   ├── integration-testing-plan.md
│   │   ├── integration-tests-phase1-summary.md
│   │   ├── N8N_AI_WORKFLOW_BUILDER_ANALYSIS.md
│   │   ├── P0_IMPLEMENTATION_PLAN.md
│   │   └── TEMPLATE_MINING_ANALYSIS.md
│   ├── MCP_ESSENTIALS_README.md
│   ├── MCP_QUICK_START_GUIDE.md
│   ├── N8N_DEPLOYMENT.md
│   ├── RAILWAY_DEPLOYMENT.md
│   ├── README_CLAUDE_SETUP.md
│   ├── README.md
│   ├── tools-documentation-usage.md
│   ├── VS_CODE_PROJECT_SETUP.md
│   ├── WINDSURF_SETUP.md
│   └── workflow-diff-examples.md
├── examples
│   └── enhanced-documentation-demo.js
├── fetch_log.txt
├── LICENSE
├── MEMORY_N8N_UPDATE.md
├── MEMORY_TEMPLATE_UPDATE.md
├── monitor_fetch.sh
├── N8N_HTTP_STREAMABLE_SETUP.md
├── n8n-nodes.db
├── P0-R3-TEST-PLAN.md
├── package-lock.json
├── package.json
├── package.runtime.json
├── PRIVACY.md
├── railway.json
├── README.md
├── renovate.json
├── scripts
│   ├── analyze-optimization.sh
│   ├── audit-schema-coverage.ts
│   ├── build-optimized.sh
│   ├── compare-benchmarks.js
│   ├── demo-optimization.sh
│   ├── deploy-http.sh
│   ├── deploy-to-vm.sh
│   ├── export-webhook-workflows.ts
│   ├── extract-changelog.js
│   ├── extract-from-docker.js
│   ├── extract-nodes-docker.sh
│   ├── extract-nodes-simple.sh
│   ├── format-benchmark-results.js
│   ├── generate-benchmark-stub.js
│   ├── generate-detailed-reports.js
│   ├── generate-test-summary.js
│   ├── http-bridge.js
│   ├── mcp-http-client.js
│   ├── migrate-nodes-fts.ts
│   ├── migrate-tool-docs.ts
│   ├── n8n-docs-mcp.service
│   ├── nginx-n8n-mcp.conf
│   ├── prebuild-fts5.ts
│   ├── prepare-release.js
│   ├── publish-npm-quick.sh
│   ├── publish-npm.sh
│   ├── quick-test.ts
│   ├── run-benchmarks-ci.js
│   ├── sync-runtime-version.js
│   ├── test-ai-validation-debug.ts
│   ├── test-code-node-enhancements.ts
│   ├── test-code-node-fixes.ts
│   ├── test-docker-config.sh
│   ├── test-docker-fingerprint.ts
│   ├── test-docker-optimization.sh
│   ├── test-docker.sh
│   ├── test-empty-connection-validation.ts
│   ├── test-error-message-tracking.ts
│   ├── test-error-output-validation.ts
│   ├── test-error-validation.js
│   ├── test-essentials.ts
│   ├── test-expression-code-validation.ts
│   ├── test-expression-format-validation.js
│   ├── test-fts5-search.ts
│   ├── test-fuzzy-fix.ts
│   ├── test-fuzzy-simple.ts
│   ├── test-helpers-validation.ts
│   ├── test-http-search.ts
│   ├── test-http.sh
│   ├── test-jmespath-validation.ts
│   ├── test-multi-tenant-simple.ts
│   ├── test-multi-tenant.ts
│   ├── test-n8n-integration.sh
│   ├── test-node-info.js
│   ├── test-node-type-validation.ts
│   ├── test-nodes-base-prefix.ts
│   ├── test-operation-validation.ts
│   ├── test-optimized-docker.sh
│   ├── test-release-automation.js
│   ├── test-search-improvements.ts
│   ├── test-security.ts
│   ├── test-single-session.sh
│   ├── test-sqljs-triggers.ts
│   ├── test-telemetry-debug.ts
│   ├── test-telemetry-direct.ts
│   ├── test-telemetry-env.ts
│   ├── test-telemetry-integration.ts
│   ├── test-telemetry-no-select.ts
│   ├── test-telemetry-security.ts
│   ├── test-telemetry-simple.ts
│   ├── test-typeversion-validation.ts
│   ├── test-url-configuration.ts
│   ├── test-user-id-persistence.ts
│   ├── test-webhook-validation.ts
│   ├── test-workflow-insert.ts
│   ├── test-workflow-sanitizer.ts
│   ├── test-workflow-tracking-debug.ts
│   ├── update-and-publish-prep.sh
│   ├── update-n8n-deps.js
│   ├── update-readme-version.js
│   ├── vitest-benchmark-json-reporter.js
│   └── vitest-benchmark-reporter.ts
├── SECURITY.md
├── src
│   ├── config
│   │   └── n8n-api.ts
│   ├── data
│   │   └── canonical-ai-tool-examples.json
│   ├── database
│   │   ├── database-adapter.ts
│   │   ├── migrations
│   │   │   └── add-template-node-configs.sql
│   │   ├── node-repository.ts
│   │   ├── nodes.db
│   │   ├── schema-optimized.sql
│   │   └── schema.sql
│   ├── errors
│   │   └── validation-service-error.ts
│   ├── http-server-single-session.ts
│   ├── http-server.ts
│   ├── index.ts
│   ├── loaders
│   │   └── node-loader.ts
│   ├── mappers
│   │   └── docs-mapper.ts
│   ├── mcp
│   │   ├── handlers-n8n-manager.ts
│   │   ├── handlers-workflow-diff.ts
│   │   ├── index.ts
│   │   ├── server.ts
│   │   ├── stdio-wrapper.ts
│   │   ├── tool-docs
│   │   │   ├── configuration
│   │   │   │   ├── get-node-as-tool-info.ts
│   │   │   │   ├── get-node-documentation.ts
│   │   │   │   ├── get-node-essentials.ts
│   │   │   │   ├── get-node-info.ts
│   │   │   │   ├── get-property-dependencies.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── search-node-properties.ts
│   │   │   ├── discovery
│   │   │   │   ├── get-database-statistics.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── list-ai-tools.ts
│   │   │   │   ├── list-nodes.ts
│   │   │   │   └── search-nodes.ts
│   │   │   ├── guides
│   │   │   │   ├── ai-agents-guide.ts
│   │   │   │   └── index.ts
│   │   │   ├── index.ts
│   │   │   ├── system
│   │   │   │   ├── index.ts
│   │   │   │   ├── n8n-diagnostic.ts
│   │   │   │   ├── n8n-health-check.ts
│   │   │   │   ├── n8n-list-available-tools.ts
│   │   │   │   └── tools-documentation.ts
│   │   │   ├── templates
│   │   │   │   ├── get-template.ts
│   │   │   │   ├── get-templates-for-task.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── list-node-templates.ts
│   │   │   │   ├── list-tasks.ts
│   │   │   │   ├── search-templates-by-metadata.ts
│   │   │   │   └── search-templates.ts
│   │   │   ├── types.ts
│   │   │   ├── validation
│   │   │   │   ├── index.ts
│   │   │   │   ├── validate-node-minimal.ts
│   │   │   │   ├── validate-node-operation.ts
│   │   │   │   ├── validate-workflow-connections.ts
│   │   │   │   ├── validate-workflow-expressions.ts
│   │   │   │   └── validate-workflow.ts
│   │   │   └── workflow_management
│   │   │       ├── index.ts
│   │   │       ├── n8n-autofix-workflow.ts
│   │   │       ├── n8n-create-workflow.ts
│   │   │       ├── n8n-delete-execution.ts
│   │   │       ├── n8n-delete-workflow.ts
│   │   │       ├── n8n-get-execution.ts
│   │   │       ├── n8n-get-workflow-details.ts
│   │   │       ├── n8n-get-workflow-minimal.ts
│   │   │       ├── n8n-get-workflow-structure.ts
│   │   │       ├── n8n-get-workflow.ts
│   │   │       ├── n8n-list-executions.ts
│   │   │       ├── n8n-list-workflows.ts
│   │   │       ├── n8n-trigger-webhook-workflow.ts
│   │   │       ├── n8n-update-full-workflow.ts
│   │   │       ├── n8n-update-partial-workflow.ts
│   │   │       └── n8n-validate-workflow.ts
│   │   ├── tools-documentation.ts
│   │   ├── tools-n8n-friendly.ts
│   │   ├── tools-n8n-manager.ts
│   │   ├── tools.ts
│   │   └── workflow-examples.ts
│   ├── mcp-engine.ts
│   ├── mcp-tools-engine.ts
│   ├── n8n
│   │   ├── MCPApi.credentials.ts
│   │   └── MCPNode.node.ts
│   ├── parsers
│   │   ├── node-parser.ts
│   │   ├── property-extractor.ts
│   │   └── simple-parser.ts
│   ├── scripts
│   │   ├── debug-http-search.ts
│   │   ├── extract-from-docker.ts
│   │   ├── fetch-templates-robust.ts
│   │   ├── fetch-templates.ts
│   │   ├── rebuild-database.ts
│   │   ├── rebuild-optimized.ts
│   │   ├── rebuild.ts
│   │   ├── sanitize-templates.ts
│   │   ├── seed-canonical-ai-examples.ts
│   │   ├── test-autofix-documentation.ts
│   │   ├── test-autofix-workflow.ts
│   │   ├── test-execution-filtering.ts
│   │   ├── test-node-suggestions.ts
│   │   ├── test-protocol-negotiation.ts
│   │   ├── test-summary.ts
│   │   ├── test-webhook-autofix.ts
│   │   ├── validate.ts
│   │   └── validation-summary.ts
│   ├── services
│   │   ├── ai-node-validator.ts
│   │   ├── ai-tool-validators.ts
│   │   ├── confidence-scorer.ts
│   │   ├── config-validator.ts
│   │   ├── enhanced-config-validator.ts
│   │   ├── example-generator.ts
│   │   ├── execution-processor.ts
│   │   ├── expression-format-validator.ts
│   │   ├── expression-validator.ts
│   │   ├── n8n-api-client.ts
│   │   ├── n8n-validation.ts
│   │   ├── node-documentation-service.ts
│   │   ├── node-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/http-server.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | /**
  3 |  * Fixed HTTP server for n8n-MCP that properly handles StreamableHTTPServerTransport initialization
  4 |  * This implementation ensures the transport is properly initialized before handling requests
  5 |  */
  6 | import express from 'express';
  7 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
  8 | import { n8nDocumentationToolsFinal } from './mcp/tools';
  9 | import { n8nManagementTools } from './mcp/tools-n8n-manager';
 10 | import { N8NDocumentationMCPServer } from './mcp/server';
 11 | import { logger } from './utils/logger';
 12 | import { AuthManager } from './utils/auth';
 13 | import { PROJECT_VERSION } from './utils/version';
 14 | import { isN8nApiConfigured } from './config/n8n-api';
 15 | import dotenv from 'dotenv';
 16 | import { readFileSync } from 'fs';
 17 | import { getStartupBaseUrl, formatEndpointUrls, detectBaseUrl } from './utils/url-detector';
 18 | import { 
 19 |   negotiateProtocolVersion, 
 20 |   logProtocolNegotiation,
 21 |   N8N_PROTOCOL_VERSION 
 22 | } from './utils/protocol-version';
 23 | 
 24 | dotenv.config();
 25 | 
 26 | let expressServer: any;
 27 | let authToken: string | null = null;
 28 | 
 29 | /**
 30 |  * Load auth token from environment variable or file
 31 |  */
 32 | export function loadAuthToken(): string | null {
 33 |   // First, try AUTH_TOKEN environment variable
 34 |   if (process.env.AUTH_TOKEN) {
 35 |     logger.info('Using AUTH_TOKEN from environment variable');
 36 |     return process.env.AUTH_TOKEN;
 37 |   }
 38 |   
 39 |   // Then, try AUTH_TOKEN_FILE
 40 |   if (process.env.AUTH_TOKEN_FILE) {
 41 |     try {
 42 |       const token = readFileSync(process.env.AUTH_TOKEN_FILE, 'utf-8').trim();
 43 |       logger.info(`Loaded AUTH_TOKEN from file: ${process.env.AUTH_TOKEN_FILE}`);
 44 |       return token;
 45 |     } catch (error) {
 46 |       logger.error(`Failed to read AUTH_TOKEN_FILE: ${process.env.AUTH_TOKEN_FILE}`, error);
 47 |       console.error(`ERROR: Failed to read AUTH_TOKEN_FILE: ${process.env.AUTH_TOKEN_FILE}`);
 48 |       console.error(error instanceof Error ? error.message : 'Unknown error');
 49 |       return null;
 50 |     }
 51 |   }
 52 |   
 53 |   return null;
 54 | }
 55 | 
 56 | /**
 57 |  * Validate required environment variables
 58 |  */
 59 | function validateEnvironment() {
 60 |   // Load auth token from env var or file
 61 |   authToken = loadAuthToken();
 62 |   
 63 |   if (!authToken || authToken.trim() === '') {
 64 |     logger.error('No authentication token found or token is empty');
 65 |     console.error('ERROR: AUTH_TOKEN is required for HTTP mode and cannot be empty');
 66 |     console.error('Set AUTH_TOKEN environment variable or AUTH_TOKEN_FILE pointing to a file containing the token');
 67 |     console.error('Generate AUTH_TOKEN with: openssl rand -base64 32');
 68 |     process.exit(1);
 69 |   }
 70 |   
 71 |   // Update authToken to trimmed version
 72 |   authToken = authToken.trim();
 73 |   
 74 |   if (authToken.length < 32) {
 75 |     logger.warn('AUTH_TOKEN should be at least 32 characters for security');
 76 |     console.warn('WARNING: AUTH_TOKEN should be at least 32 characters for security');
 77 |   }
 78 |   
 79 |   // Check for default token and show prominent warnings
 80 |   if (authToken === 'REPLACE_THIS_AUTH_TOKEN_32_CHARS_MIN_abcdefgh') {
 81 |     logger.warn('⚠️ SECURITY WARNING: Using default AUTH_TOKEN - CHANGE IMMEDIATELY!');
 82 |     logger.warn('Generate secure token with: openssl rand -base64 32');
 83 |     
 84 |     // Only show console warnings in HTTP mode
 85 |     if (process.env.MCP_MODE === 'http') {
 86 |       console.warn('\n⚠️  SECURITY WARNING ⚠️');
 87 |       console.warn('Using default AUTH_TOKEN - CHANGE IMMEDIATELY!');
 88 |       console.warn('Generate secure token: openssl rand -base64 32');
 89 |       console.warn('Update via Railway dashboard environment variables\n');
 90 |     }
 91 |   }
 92 | }
 93 | 
 94 | /**
 95 |  * Graceful shutdown handler
 96 |  */
 97 | async function shutdown() {
 98 |   logger.info('Shutting down HTTP server...');
 99 |   console.log('Shutting down HTTP server...');
100 |   
101 |   if (expressServer) {
102 |     expressServer.close(() => {
103 |       logger.info('HTTP server closed');
104 |       console.log('HTTP server closed');
105 |       process.exit(0);
106 |     });
107 |     
108 |     setTimeout(() => {
109 |       logger.error('Forced shutdown after timeout');
110 |       process.exit(1);
111 |     }, 10000);
112 |   } else {
113 |     process.exit(0);
114 |   }
115 | }
116 | 
117 | export async function startFixedHTTPServer() {
118 |   validateEnvironment();
119 |   
120 |   const app = express();
121 |   
122 |   // Configure trust proxy for correct IP logging behind reverse proxies
123 |   const trustProxy = process.env.TRUST_PROXY ? Number(process.env.TRUST_PROXY) : 0;
124 |   if (trustProxy > 0) {
125 |     app.set('trust proxy', trustProxy);
126 |     logger.info(`Trust proxy enabled with ${trustProxy} hop(s)`);
127 |   }
128 |   
129 |   // CRITICAL: Don't use any body parser - StreamableHTTPServerTransport needs raw stream
130 |   
131 |   // Security headers
132 |   app.use((req, res, next) => {
133 |     res.setHeader('X-Content-Type-Options', 'nosniff');
134 |     res.setHeader('X-Frame-Options', 'DENY');
135 |     res.setHeader('X-XSS-Protection', '1; mode=block');
136 |     res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
137 |     next();
138 |   });
139 |   
140 |   // CORS configuration
141 |   app.use((req, res, next) => {
142 |     const allowedOrigin = process.env.CORS_ORIGIN || '*';
143 |     res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
144 |     res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
145 |     res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept');
146 |     res.setHeader('Access-Control-Max-Age', '86400');
147 |     
148 |     if (req.method === 'OPTIONS') {
149 |       res.sendStatus(204);
150 |       return;
151 |     }
152 |     next();
153 |   });
154 |   
155 |   // Request logging
156 |   app.use((req, res, next) => {
157 |     logger.info(`${req.method} ${req.path}`, {
158 |       ip: req.ip,
159 |       userAgent: req.get('user-agent'),
160 |       contentLength: req.get('content-length')
161 |     });
162 |     next();
163 |   });
164 |   
165 |   // Create a single persistent MCP server instance
166 |   const mcpServer = new N8NDocumentationMCPServer();
167 |   logger.info('Created persistent MCP server instance');
168 | 
169 |   // Root endpoint with API information
170 |   app.get('/', (req, res) => {
171 |     const port = parseInt(process.env.PORT || '3000');
172 |     const host = process.env.HOST || '0.0.0.0';
173 |     const baseUrl = detectBaseUrl(req, host, port);
174 |     const endpoints = formatEndpointUrls(baseUrl);
175 |     
176 |     res.json({
177 |       name: 'n8n Documentation MCP Server',
178 |       version: PROJECT_VERSION,
179 |       description: 'Model Context Protocol server providing comprehensive n8n node documentation and workflow management',
180 |       endpoints: {
181 |         health: {
182 |           url: endpoints.health,
183 |           method: 'GET',
184 |           description: 'Health check and status information'
185 |         },
186 |         mcp: {
187 |           url: endpoints.mcp,
188 |           method: 'GET/POST',
189 |           description: 'MCP endpoint - GET for info, POST for JSON-RPC'
190 |         }
191 |       },
192 |       authentication: {
193 |         type: 'Bearer Token',
194 |         header: 'Authorization: Bearer <token>',
195 |         required_for: ['POST /mcp']
196 |       },
197 |       documentation: 'https://github.com/czlonkowski/n8n-mcp'
198 |     });
199 |   });
200 | 
201 |   // Health check endpoint
202 |   app.get('/health', (req, res) => {
203 |     res.json({ 
204 |       status: 'ok', 
205 |       mode: 'http-fixed',
206 |       version: PROJECT_VERSION,
207 |       uptime: Math.floor(process.uptime()),
208 |       memory: {
209 |         used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
210 |         total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024),
211 |         unit: 'MB'
212 |       },
213 |       timestamp: new Date().toISOString()
214 |     });
215 |   });
216 | 
217 |   // Version endpoint
218 |   app.get('/version', (req, res) => {
219 |     res.json({ 
220 |       version: PROJECT_VERSION,
221 |       buildTime: new Date().toISOString(),
222 |       tools: n8nDocumentationToolsFinal.map(t => t.name),
223 |       commit: process.env.GIT_COMMIT || 'unknown'
224 |     });
225 |   });
226 | 
227 |   // Test tools endpoint
228 |   app.get('/test-tools', async (req, res) => {
229 |     try {
230 |       const result = await mcpServer.executeTool('get_node_essentials', { nodeType: 'nodes-base.httpRequest' });
231 |       res.json({ status: 'ok', hasData: !!result, toolCount: n8nDocumentationToolsFinal.length });
232 |     } catch (error) {
233 |       res.json({ status: 'error', message: error instanceof Error ? error.message : 'Unknown error' });
234 |     }
235 |   });
236 |   
237 |   // MCP information endpoint (no auth required for discovery)
238 |   app.get('/mcp', (req, res) => {
239 |     res.json({
240 |       description: 'n8n Documentation MCP Server',
241 |       version: PROJECT_VERSION,
242 |       endpoints: {
243 |         mcp: {
244 |           method: 'POST',
245 |           path: '/mcp',
246 |           description: 'Main MCP JSON-RPC endpoint',
247 |           authentication: 'Bearer token required'
248 |         },
249 |         health: {
250 |           method: 'GET',
251 |           path: '/health',
252 |           description: 'Health check endpoint',
253 |           authentication: 'None'
254 |         },
255 |         root: {
256 |           method: 'GET',
257 |           path: '/',
258 |           description: 'API information',
259 |           authentication: 'None'
260 |         }
261 |       },
262 |       documentation: 'https://github.com/czlonkowski/n8n-mcp'
263 |     });
264 |   });
265 | 
266 |   // Main MCP endpoint - handle each request with custom transport handling
267 |   app.post('/mcp', async (req: express.Request, res: express.Response): Promise<void> => {
268 |     const startTime = Date.now();
269 |     
270 |     // Enhanced authentication check with specific logging
271 |     const authHeader = req.headers.authorization;
272 |     
273 |     // Check if Authorization header is missing
274 |     if (!authHeader) {
275 |       logger.warn('Authentication failed: Missing Authorization header', { 
276 |         ip: req.ip,
277 |         userAgent: req.get('user-agent'),
278 |         reason: 'no_auth_header'
279 |       });
280 |       res.status(401).json({ 
281 |         jsonrpc: '2.0',
282 |         error: {
283 |           code: -32001,
284 |           message: 'Unauthorized'
285 |         },
286 |         id: null
287 |       });
288 |       return;
289 |     }
290 |     
291 |     // Check if Authorization header has Bearer prefix
292 |     if (!authHeader.startsWith('Bearer ')) {
293 |       logger.warn('Authentication failed: Invalid Authorization header format (expected Bearer token)', { 
294 |         ip: req.ip,
295 |         userAgent: req.get('user-agent'),
296 |         reason: 'invalid_auth_format',
297 |         headerPrefix: authHeader.substring(0, Math.min(authHeader.length, 10)) + '...'  // Log first 10 chars for debugging
298 |       });
299 |       res.status(401).json({ 
300 |         jsonrpc: '2.0',
301 |         error: {
302 |           code: -32001,
303 |           message: 'Unauthorized'
304 |         },
305 |         id: null
306 |       });
307 |       return;
308 |     }
309 |     
310 |     // Extract token and trim whitespace
311 |     const token = authHeader.slice(7).trim();
312 | 
313 |     // SECURITY: Use timing-safe comparison to prevent timing attacks
314 |     // See: https://github.com/czlonkowski/n8n-mcp/issues/265 (CRITICAL-02)
315 |     const isValidToken = authToken &&
316 |       AuthManager.timingSafeCompare(token, authToken);
317 | 
318 |     if (!isValidToken) {
319 |       logger.warn('Authentication failed: Invalid token', {
320 |         ip: req.ip,
321 |         userAgent: req.get('user-agent'),
322 |         reason: 'invalid_token'
323 |       });
324 |       res.status(401).json({
325 |         jsonrpc: '2.0',
326 |         error: {
327 |           code: -32001,
328 |           message: 'Unauthorized'
329 |         },
330 |         id: null
331 |       });
332 |       return;
333 |     }
334 |     
335 |     try {
336 |       // Instead of using StreamableHTTPServerTransport, we'll handle the request directly
337 |       // This avoids the initialization issues with the transport
338 |       
339 |       // Collect the raw body
340 |       let body = '';
341 |       req.on('data', chunk => {
342 |         body += chunk.toString();
343 |       });
344 |       
345 |       req.on('end', async () => {
346 |         try {
347 |           const jsonRpcRequest = JSON.parse(body);
348 |           logger.debug('Received JSON-RPC request:', { method: jsonRpcRequest.method });
349 |           
350 |           // Handle the request based on method
351 |           let response;
352 |           
353 |           switch (jsonRpcRequest.method) {
354 |             case 'initialize':
355 |               // Negotiate protocol version for this client/request
356 |               const negotiationResult = negotiateProtocolVersion(
357 |                 jsonRpcRequest.params?.protocolVersion,
358 |                 jsonRpcRequest.params?.clientInfo,
359 |                 req.get('user-agent'),
360 |                 req.headers
361 |               );
362 |               
363 |               logProtocolNegotiation(negotiationResult, logger, 'HTTP_SERVER_INITIALIZE');
364 |               
365 |               response = {
366 |                 jsonrpc: '2.0',
367 |                 result: {
368 |                   protocolVersion: negotiationResult.version,
369 |                   capabilities: {
370 |                     tools: {},
371 |                     resources: {}
372 |                   },
373 |                   serverInfo: {
374 |                     name: 'n8n-documentation-mcp',
375 |                     version: PROJECT_VERSION
376 |                   }
377 |                 },
378 |                 id: jsonRpcRequest.id
379 |               };
380 |               break;
381 |               
382 |             case 'tools/list':
383 |               // Use the proper tool list that includes management tools when configured
384 |               const tools = [...n8nDocumentationToolsFinal];
385 |               
386 |               // Add management tools if n8n API is configured
387 |               if (isN8nApiConfigured()) {
388 |                 tools.push(...n8nManagementTools);
389 |               }
390 |               
391 |               response = {
392 |                 jsonrpc: '2.0',
393 |                 result: {
394 |                   tools
395 |                 },
396 |                 id: jsonRpcRequest.id
397 |               };
398 |               break;
399 |               
400 |             case 'tools/call':
401 |               // Delegate to the MCP server
402 |               const toolName = jsonRpcRequest.params?.name;
403 |               const toolArgs = jsonRpcRequest.params?.arguments || {};
404 |               
405 |               try {
406 |                 const result = await mcpServer.executeTool(toolName, toolArgs);
407 |                 response = {
408 |                   jsonrpc: '2.0',
409 |                   result: {
410 |                     content: [
411 |                       {
412 |                         type: 'text',
413 |                         text: JSON.stringify(result, null, 2)
414 |                       }
415 |                     ]
416 |                   },
417 |                   id: jsonRpcRequest.id
418 |                 };
419 |               } catch (error) {
420 |                 response = {
421 |                   jsonrpc: '2.0',
422 |                   error: {
423 |                     code: -32603,
424 |                     message: `Error executing tool ${toolName}: ${error instanceof Error ? error.message : 'Unknown error'}`
425 |                   },
426 |                   id: jsonRpcRequest.id
427 |                 };
428 |               }
429 |               break;
430 |               
431 |             default:
432 |               response = {
433 |                 jsonrpc: '2.0',
434 |                 error: {
435 |                   code: -32601,
436 |                   message: `Method not found: ${jsonRpcRequest.method}`
437 |                 },
438 |                 id: jsonRpcRequest.id
439 |               };
440 |           }
441 |           
442 |           // Send response
443 |           res.setHeader('Content-Type', 'application/json');
444 |           res.json(response);
445 |           
446 |           const duration = Date.now() - startTime;
447 |           logger.info('MCP request completed', { 
448 |             duration,
449 |             method: jsonRpcRequest.method 
450 |           });
451 |         } catch (error) {
452 |           logger.error('Error processing request:', error);
453 |           res.status(400).json({
454 |             jsonrpc: '2.0',
455 |             error: {
456 |               code: -32700,
457 |               message: 'Parse error',
458 |               data: error instanceof Error ? error.message : 'Unknown error'
459 |             },
460 |             id: null
461 |           });
462 |         }
463 |       });
464 |     } catch (error) {
465 |       logger.error('MCP request error:', error);
466 |       
467 |       if (!res.headersSent) {
468 |         res.status(500).json({ 
469 |           jsonrpc: '2.0',
470 |           error: {
471 |             code: -32603,
472 |             message: 'Internal server error',
473 |             data: process.env.NODE_ENV === 'development' 
474 |               ? (error as Error).message 
475 |               : undefined
476 |           },
477 |           id: null
478 |         });
479 |       }
480 |     }
481 |   });
482 |   
483 |   // 404 handler
484 |   app.use((req, res) => {
485 |     res.status(404).json({ 
486 |       error: 'Not found',
487 |       message: `Cannot ${req.method} ${req.path}`
488 |     });
489 |   });
490 |   
491 |   // Error handler
492 |   app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
493 |     logger.error('Express error handler:', err);
494 |     
495 |     if (!res.headersSent) {
496 |       res.status(500).json({ 
497 |         jsonrpc: '2.0',
498 |         error: {
499 |           code: -32603,
500 |           message: 'Internal server error',
501 |           data: process.env.NODE_ENV === 'development' ? err.message : undefined
502 |         },
503 |         id: null
504 |       });
505 |     }
506 |   });
507 |   
508 |   const port = parseInt(process.env.PORT || '3000');
509 |   const host = process.env.HOST || '0.0.0.0';
510 |   
511 |   expressServer = app.listen(port, host, () => {
512 |     logger.info(`n8n MCP Fixed HTTP Server started`, { port, host });
513 |     
514 |     // Detect the base URL using our utility
515 |     const baseUrl = getStartupBaseUrl(host, port);
516 |     const endpoints = formatEndpointUrls(baseUrl);
517 |     
518 |     console.log(`n8n MCP Fixed HTTP Server running on ${host}:${port}`);
519 |     console.log(`Health check: ${endpoints.health}`);
520 |     console.log(`MCP endpoint: ${endpoints.mcp}`);
521 |     console.log('\nPress Ctrl+C to stop the server');
522 |     
523 |     // Start periodic warning timer if using default token
524 |     if (authToken === 'REPLACE_THIS_AUTH_TOKEN_32_CHARS_MIN_abcdefgh') {
525 |       setInterval(() => {
526 |         logger.warn('⚠️ Still using default AUTH_TOKEN - security risk!');
527 |         if (process.env.MCP_MODE === 'http') {
528 |           console.warn('⚠️ REMINDER: Still using default AUTH_TOKEN - please change it!');
529 |         }
530 |       }, 300000); // Every 5 minutes
531 |     }
532 |     
533 |     if (process.env.BASE_URL || process.env.PUBLIC_URL) {
534 |       console.log(`\nPublic URL configured: ${baseUrl}`);
535 |     } else if (process.env.TRUST_PROXY && Number(process.env.TRUST_PROXY) > 0) {
536 |       console.log(`\nNote: TRUST_PROXY is enabled. URLs will be auto-detected from proxy headers.`);
537 |     }
538 |   });
539 |   
540 |   // Handle errors
541 |   expressServer.on('error', (error: any) => {
542 |     if (error.code === 'EADDRINUSE') {
543 |       logger.error(`Port ${port} is already in use`);
544 |       console.error(`ERROR: Port ${port} is already in use`);
545 |       process.exit(1);
546 |     } else {
547 |       logger.error('Server error:', error);
548 |       console.error('Server error:', error);
549 |       process.exit(1);
550 |     }
551 |   });
552 |   
553 |   // Graceful shutdown handlers
554 |   process.on('SIGTERM', shutdown);
555 |   process.on('SIGINT', shutdown);
556 |   
557 |   // Handle uncaught errors
558 |   process.on('uncaughtException', (error) => {
559 |     logger.error('Uncaught exception:', error);
560 |     console.error('Uncaught exception:', error);
561 |     shutdown();
562 |   });
563 |   
564 |   process.on('unhandledRejection', (reason, promise) => {
565 |     logger.error('Unhandled rejection:', reason);
566 |     console.error('Unhandled rejection at:', promise, 'reason:', reason);
567 |     shutdown();
568 |   });
569 | }
570 | 
571 | // Make executeTool public on the server
572 | declare module './mcp/server' {
573 |   interface N8NDocumentationMCPServer {
574 |     executeTool(name: string, args: any): Promise<any>;
575 |   }
576 | }
577 | 
578 | // Start if called directly
579 | // Check if this file is being run directly (not imported)
580 | // In ES modules, we check import.meta.url against process.argv[1]
581 | // But since we're transpiling to CommonJS, we use the require.main check
582 | if (typeof require !== 'undefined' && require.main === module) {
583 |   startFixedHTTPServer().catch(error => {
584 |     logger.error('Failed to start Fixed HTTP server:', error);
585 |     console.error('Failed to start Fixed HTTP server:', error);
586 |     process.exit(1);
587 |   });
588 | }
```

--------------------------------------------------------------------------------
/tests/unit/telemetry/telemetry-error.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
  2 | import { TelemetryError, TelemetryCircuitBreaker, TelemetryErrorAggregator } from '../../../src/telemetry/telemetry-error';
  3 | import { TelemetryErrorType } from '../../../src/telemetry/telemetry-types';
  4 | import { logger } from '../../../src/utils/logger';
  5 | 
  6 | // Mock logger to avoid console output in tests
  7 | vi.mock('../../../src/utils/logger', () => ({
  8 |   logger: {
  9 |     debug: vi.fn(),
 10 |     info: vi.fn(),
 11 |     warn: vi.fn(),
 12 |     error: vi.fn(),
 13 |   }
 14 | }));
 15 | 
 16 | describe('TelemetryError', () => {
 17 |   beforeEach(() => {
 18 |     vi.clearAllMocks();
 19 |     vi.useFakeTimers();
 20 |   });
 21 | 
 22 |   afterEach(() => {
 23 |     vi.useRealTimers();
 24 |   });
 25 | 
 26 |   describe('constructor', () => {
 27 |     it('should create error with all properties', () => {
 28 |       const context = { operation: 'test', detail: 'info' };
 29 |       const error = new TelemetryError(
 30 |         TelemetryErrorType.NETWORK_ERROR,
 31 |         'Test error',
 32 |         context,
 33 |         true
 34 |       );
 35 | 
 36 |       expect(error.name).toBe('TelemetryError');
 37 |       expect(error.message).toBe('Test error');
 38 |       expect(error.type).toBe(TelemetryErrorType.NETWORK_ERROR);
 39 |       expect(error.context).toEqual(context);
 40 |       expect(error.retryable).toBe(true);
 41 |       expect(error.timestamp).toBeTypeOf('number');
 42 |     });
 43 | 
 44 |     it('should default retryable to false', () => {
 45 |       const error = new TelemetryError(
 46 |         TelemetryErrorType.VALIDATION_ERROR,
 47 |         'Test error'
 48 |       );
 49 | 
 50 |       expect(error.retryable).toBe(false);
 51 |     });
 52 | 
 53 |     it('should handle undefined context', () => {
 54 |       const error = new TelemetryError(
 55 |         TelemetryErrorType.UNKNOWN_ERROR,
 56 |         'Test error'
 57 |       );
 58 | 
 59 |       expect(error.context).toBeUndefined();
 60 |     });
 61 | 
 62 |     it('should maintain proper prototype chain', () => {
 63 |       const error = new TelemetryError(
 64 |         TelemetryErrorType.NETWORK_ERROR,
 65 |         'Test error'
 66 |       );
 67 | 
 68 |       expect(error instanceof TelemetryError).toBe(true);
 69 |       expect(error instanceof Error).toBe(true);
 70 |     });
 71 |   });
 72 | 
 73 |   describe('toContext()', () => {
 74 |     it('should convert error to context object', () => {
 75 |       const context = { operation: 'flush', batch: 'events' };
 76 |       const error = new TelemetryError(
 77 |         TelemetryErrorType.NETWORK_ERROR,
 78 |         'Failed to flush',
 79 |         context,
 80 |         true
 81 |       );
 82 | 
 83 |       const contextObj = error.toContext();
 84 |       expect(contextObj).toEqual({
 85 |         type: TelemetryErrorType.NETWORK_ERROR,
 86 |         message: 'Failed to flush',
 87 |         context,
 88 |         timestamp: error.timestamp,
 89 |         retryable: true
 90 |       });
 91 |     });
 92 |   });
 93 | 
 94 |   describe('log()', () => {
 95 |     it('should log retryable errors as debug', () => {
 96 |       const error = new TelemetryError(
 97 |         TelemetryErrorType.NETWORK_ERROR,
 98 |         'Retryable error',
 99 |         { attempt: 1 },
100 |         true
101 |       );
102 | 
103 |       error.log();
104 | 
105 |       expect(logger.debug).toHaveBeenCalledWith(
106 |         'Retryable telemetry error:',
107 |         expect.objectContaining({
108 |           type: TelemetryErrorType.NETWORK_ERROR,
109 |           message: 'Retryable error',
110 |           attempt: 1
111 |         })
112 |       );
113 |     });
114 | 
115 |     it('should log non-retryable errors as debug', () => {
116 |       const error = new TelemetryError(
117 |         TelemetryErrorType.VALIDATION_ERROR,
118 |         'Non-retryable error',
119 |         { field: 'user_id' },
120 |         false
121 |       );
122 | 
123 |       error.log();
124 | 
125 |       expect(logger.debug).toHaveBeenCalledWith(
126 |         'Non-retryable telemetry error:',
127 |         expect.objectContaining({
128 |           type: TelemetryErrorType.VALIDATION_ERROR,
129 |           message: 'Non-retryable error',
130 |           field: 'user_id'
131 |         })
132 |       );
133 |     });
134 | 
135 |     it('should handle errors without context', () => {
136 |       const error = new TelemetryError(
137 |         TelemetryErrorType.UNKNOWN_ERROR,
138 |         'Simple error'
139 |       );
140 | 
141 |       error.log();
142 | 
143 |       expect(logger.debug).toHaveBeenCalledWith(
144 |         'Non-retryable telemetry error:',
145 |         expect.objectContaining({
146 |           type: TelemetryErrorType.UNKNOWN_ERROR,
147 |           message: 'Simple error'
148 |         })
149 |       );
150 |     });
151 |   });
152 | });
153 | 
154 | describe('TelemetryCircuitBreaker', () => {
155 |   let circuitBreaker: TelemetryCircuitBreaker;
156 | 
157 |   beforeEach(() => {
158 |     vi.clearAllMocks();
159 |     vi.useFakeTimers();
160 |     circuitBreaker = new TelemetryCircuitBreaker(3, 10000, 2); // 3 failures, 10s reset, 2 half-open requests
161 |   });
162 | 
163 |   afterEach(() => {
164 |     vi.useRealTimers();
165 |   });
166 | 
167 |   describe('shouldAllow()', () => {
168 |     it('should allow requests in closed state', () => {
169 |       expect(circuitBreaker.shouldAllow()).toBe(true);
170 |     });
171 | 
172 |     it('should open circuit after failure threshold', () => {
173 |       // Record 3 failures to reach threshold
174 |       for (let i = 0; i < 3; i++) {
175 |         circuitBreaker.recordFailure();
176 |       }
177 | 
178 |       expect(circuitBreaker.shouldAllow()).toBe(false);
179 |       expect(circuitBreaker.getState().state).toBe('open');
180 |     });
181 | 
182 |     it('should transition to half-open after reset timeout', () => {
183 |       // Open the circuit
184 |       for (let i = 0; i < 3; i++) {
185 |         circuitBreaker.recordFailure();
186 |       }
187 |       expect(circuitBreaker.shouldAllow()).toBe(false);
188 | 
189 |       // Advance time past reset timeout
190 |       vi.advanceTimersByTime(11000);
191 | 
192 |       // Should transition to half-open and allow request
193 |       expect(circuitBreaker.shouldAllow()).toBe(true);
194 |       expect(circuitBreaker.getState().state).toBe('half-open');
195 |     });
196 | 
197 |     it('should limit requests in half-open state', () => {
198 |       // Open the circuit
199 |       for (let i = 0; i < 3; i++) {
200 |         circuitBreaker.recordFailure();
201 |       }
202 | 
203 |       // Advance to half-open
204 |       vi.advanceTimersByTime(11000);
205 | 
206 |       // Should allow limited number of requests (2 in our config)
207 |       expect(circuitBreaker.shouldAllow()).toBe(true);
208 |       expect(circuitBreaker.shouldAllow()).toBe(true);
209 |       expect(circuitBreaker.shouldAllow()).toBe(true); // Note: simplified implementation allows all
210 |     });
211 | 
212 |     it('should not allow requests before reset timeout in open state', () => {
213 |       // Open the circuit
214 |       for (let i = 0; i < 3; i++) {
215 |         circuitBreaker.recordFailure();
216 |       }
217 | 
218 |       // Advance time but not enough to reset
219 |       vi.advanceTimersByTime(5000);
220 | 
221 |       expect(circuitBreaker.shouldAllow()).toBe(false);
222 |     });
223 |   });
224 | 
225 |   describe('recordSuccess()', () => {
226 |     it('should reset failure count in closed state', () => {
227 |       // Record some failures but not enough to open
228 |       circuitBreaker.recordFailure();
229 |       circuitBreaker.recordFailure();
230 |       expect(circuitBreaker.getState().failureCount).toBe(2);
231 | 
232 |       // Success should reset count
233 |       circuitBreaker.recordSuccess();
234 |       expect(circuitBreaker.getState().failureCount).toBe(0);
235 |     });
236 | 
237 |     it('should close circuit after successful half-open requests', () => {
238 |       // Open the circuit
239 |       for (let i = 0; i < 3; i++) {
240 |         circuitBreaker.recordFailure();
241 |       }
242 | 
243 |       // Go to half-open
244 |       vi.advanceTimersByTime(11000);
245 |       circuitBreaker.shouldAllow(); // First half-open request
246 |       circuitBreaker.shouldAllow(); // Second half-open request
247 | 
248 |       // The circuit breaker implementation requires success calls
249 |       // to match the number of half-open requests configured
250 |       circuitBreaker.recordSuccess();
251 |       // In current implementation, state remains half-open
252 |       // This is a known behavior of the simplified circuit breaker
253 |       expect(circuitBreaker.getState().state).toBe('half-open');
254 | 
255 |       // After another success, it should close
256 |       circuitBreaker.recordSuccess();
257 |       expect(circuitBreaker.getState().state).toBe('closed');
258 |       expect(circuitBreaker.getState().failureCount).toBe(0);
259 |       expect(logger.debug).toHaveBeenCalledWith('Circuit breaker closed after successful recovery');
260 |     });
261 | 
262 |     it('should not affect state when not in half-open after sufficient requests', () => {
263 |       // Open circuit, go to half-open, make one request
264 |       for (let i = 0; i < 3; i++) {
265 |         circuitBreaker.recordFailure();
266 |       }
267 |       vi.advanceTimersByTime(11000);
268 |       circuitBreaker.shouldAllow(); // One half-open request
269 | 
270 |       // Record success but should not close yet (need 2 successful requests)
271 |       circuitBreaker.recordSuccess();
272 |       expect(circuitBreaker.getState().state).toBe('half-open');
273 |     });
274 |   });
275 | 
276 |   describe('recordFailure()', () => {
277 |     it('should increment failure count in closed state', () => {
278 |       circuitBreaker.recordFailure();
279 |       expect(circuitBreaker.getState().failureCount).toBe(1);
280 | 
281 |       circuitBreaker.recordFailure();
282 |       expect(circuitBreaker.getState().failureCount).toBe(2);
283 |     });
284 | 
285 |     it('should open circuit when threshold reached', () => {
286 |       const error = new Error('Test error');
287 | 
288 |       // Record failures to reach threshold
289 |       circuitBreaker.recordFailure(error);
290 |       circuitBreaker.recordFailure(error);
291 |       expect(circuitBreaker.getState().state).toBe('closed');
292 | 
293 |       circuitBreaker.recordFailure(error);
294 |       expect(circuitBreaker.getState().state).toBe('open');
295 |       expect(logger.debug).toHaveBeenCalledWith(
296 |         'Circuit breaker opened after 3 failures',
297 |         { error: 'Test error' }
298 |       );
299 |     });
300 | 
301 |     it('should immediately open from half-open on failure', () => {
302 |       // Open circuit, go to half-open
303 |       for (let i = 0; i < 3; i++) {
304 |         circuitBreaker.recordFailure();
305 |       }
306 |       vi.advanceTimersByTime(11000);
307 |       circuitBreaker.shouldAllow();
308 | 
309 |       // Failure in half-open should immediately open
310 |       const error = new Error('Half-open failure');
311 |       circuitBreaker.recordFailure(error);
312 |       expect(circuitBreaker.getState().state).toBe('open');
313 |       expect(logger.debug).toHaveBeenCalledWith(
314 |         'Circuit breaker opened from half-open state',
315 |         { error: 'Half-open failure' }
316 |       );
317 |     });
318 | 
319 |     it('should handle failure without error object', () => {
320 |       for (let i = 0; i < 3; i++) {
321 |         circuitBreaker.recordFailure();
322 |       }
323 | 
324 |       expect(circuitBreaker.getState().state).toBe('open');
325 |       expect(logger.debug).toHaveBeenCalledWith(
326 |         'Circuit breaker opened after 3 failures',
327 |         { error: undefined }
328 |       );
329 |     });
330 |   });
331 | 
332 |   describe('getState()', () => {
333 |     it('should return current state information', () => {
334 |       const state = circuitBreaker.getState();
335 |       expect(state).toEqual({
336 |         state: 'closed',
337 |         failureCount: 0,
338 |         canRetry: true
339 |       });
340 |     });
341 | 
342 |     it('should reflect state changes', () => {
343 |       circuitBreaker.recordFailure();
344 |       circuitBreaker.recordFailure();
345 | 
346 |       const state = circuitBreaker.getState();
347 |       expect(state).toEqual({
348 |         state: 'closed',
349 |         failureCount: 2,
350 |         canRetry: true
351 |       });
352 | 
353 |       // Open circuit
354 |       circuitBreaker.recordFailure();
355 |       const openState = circuitBreaker.getState();
356 |       expect(openState).toEqual({
357 |         state: 'open',
358 |         failureCount: 3,
359 |         canRetry: false
360 |       });
361 |     });
362 |   });
363 | 
364 |   describe('reset()', () => {
365 |     it('should reset circuit breaker to initial state', () => {
366 |       // Open the circuit and advance time
367 |       for (let i = 0; i < 3; i++) {
368 |         circuitBreaker.recordFailure();
369 |       }
370 |       vi.advanceTimersByTime(11000);
371 |       circuitBreaker.shouldAllow(); // Go to half-open
372 | 
373 |       // Reset
374 |       circuitBreaker.reset();
375 | 
376 |       const state = circuitBreaker.getState();
377 |       expect(state).toEqual({
378 |         state: 'closed',
379 |         failureCount: 0,
380 |         canRetry: true
381 |       });
382 |     });
383 |   });
384 | 
385 |   describe('different configurations', () => {
386 |     it('should work with custom failure threshold', () => {
387 |       const customBreaker = new TelemetryCircuitBreaker(1, 5000, 1); // 1 failure threshold
388 | 
389 |       expect(customBreaker.getState().state).toBe('closed');
390 |       customBreaker.recordFailure();
391 |       expect(customBreaker.getState().state).toBe('open');
392 |     });
393 | 
394 |     it('should work with custom half-open request count', () => {
395 |       const customBreaker = new TelemetryCircuitBreaker(1, 5000, 3); // 3 half-open requests
396 | 
397 |       // Open and go to half-open
398 |       customBreaker.recordFailure();
399 |       vi.advanceTimersByTime(6000);
400 | 
401 |       // Should allow 3 requests in half-open
402 |       expect(customBreaker.shouldAllow()).toBe(true);
403 |       expect(customBreaker.shouldAllow()).toBe(true);
404 |       expect(customBreaker.shouldAllow()).toBe(true);
405 |       expect(customBreaker.shouldAllow()).toBe(true); // Fourth also allowed in simplified implementation
406 |     });
407 |   });
408 | });
409 | 
410 | describe('TelemetryErrorAggregator', () => {
411 |   let aggregator: TelemetryErrorAggregator;
412 | 
413 |   beforeEach(() => {
414 |     aggregator = new TelemetryErrorAggregator();
415 |     vi.clearAllMocks();
416 |   });
417 | 
418 |   describe('record()', () => {
419 |     it('should record error and increment counter', () => {
420 |       const error = new TelemetryError(
421 |         TelemetryErrorType.NETWORK_ERROR,
422 |         'Network failure'
423 |       );
424 | 
425 |       aggregator.record(error);
426 | 
427 |       const stats = aggregator.getStats();
428 |       expect(stats.totalErrors).toBe(1);
429 |       expect(stats.errorsByType[TelemetryErrorType.NETWORK_ERROR]).toBe(1);
430 |     });
431 | 
432 |     it('should increment counter for repeated error types', () => {
433 |       const error1 = new TelemetryError(
434 |         TelemetryErrorType.NETWORK_ERROR,
435 |         'First failure'
436 |       );
437 |       const error2 = new TelemetryError(
438 |         TelemetryErrorType.NETWORK_ERROR,
439 |         'Second failure'
440 |       );
441 | 
442 |       aggregator.record(error1);
443 |       aggregator.record(error2);
444 | 
445 |       const stats = aggregator.getStats();
446 |       expect(stats.totalErrors).toBe(2);
447 |       expect(stats.errorsByType[TelemetryErrorType.NETWORK_ERROR]).toBe(2);
448 |     });
449 | 
450 |     it('should maintain limited error detail history', () => {
451 |       // Record more than max details (100) to test limiting
452 |       for (let i = 0; i < 105; i++) {
453 |         const error = new TelemetryError(
454 |           TelemetryErrorType.VALIDATION_ERROR,
455 |           `Error ${i}`
456 |         );
457 |         aggregator.record(error);
458 |       }
459 | 
460 |       const stats = aggregator.getStats();
461 |       expect(stats.totalErrors).toBe(105);
462 |       expect(stats.recentErrors).toHaveLength(10); // Only last 10
463 |     });
464 | 
465 |     it('should track different error types separately', () => {
466 |       const networkError = new TelemetryError(
467 |         TelemetryErrorType.NETWORK_ERROR,
468 |         'Network issue'
469 |       );
470 |       const validationError = new TelemetryError(
471 |         TelemetryErrorType.VALIDATION_ERROR,
472 |         'Validation issue'
473 |       );
474 |       const rateLimitError = new TelemetryError(
475 |         TelemetryErrorType.RATE_LIMIT_ERROR,
476 |         'Rate limit hit'
477 |       );
478 | 
479 |       aggregator.record(networkError);
480 |       aggregator.record(networkError);
481 |       aggregator.record(validationError);
482 |       aggregator.record(rateLimitError);
483 | 
484 |       const stats = aggregator.getStats();
485 |       expect(stats.totalErrors).toBe(4);
486 |       expect(stats.errorsByType[TelemetryErrorType.NETWORK_ERROR]).toBe(2);
487 |       expect(stats.errorsByType[TelemetryErrorType.VALIDATION_ERROR]).toBe(1);
488 |       expect(stats.errorsByType[TelemetryErrorType.RATE_LIMIT_ERROR]).toBe(1);
489 |     });
490 |   });
491 | 
492 |   describe('getStats()', () => {
493 |     it('should return empty stats when no errors recorded', () => {
494 |       const stats = aggregator.getStats();
495 |       expect(stats).toEqual({
496 |         totalErrors: 0,
497 |         errorsByType: {},
498 |         mostCommonError: undefined,
499 |         recentErrors: []
500 |       });
501 |     });
502 | 
503 |     it('should identify most common error type', () => {
504 |       const networkError = new TelemetryError(
505 |         TelemetryErrorType.NETWORK_ERROR,
506 |         'Network issue'
507 |       );
508 |       const validationError = new TelemetryError(
509 |         TelemetryErrorType.VALIDATION_ERROR,
510 |         'Validation issue'
511 |       );
512 | 
513 |       // Network errors more frequent
514 |       aggregator.record(networkError);
515 |       aggregator.record(networkError);
516 |       aggregator.record(networkError);
517 |       aggregator.record(validationError);
518 | 
519 |       const stats = aggregator.getStats();
520 |       expect(stats.mostCommonError).toBe(TelemetryErrorType.NETWORK_ERROR);
521 |     });
522 | 
523 |     it('should return recent errors in order', () => {
524 |       const error1 = new TelemetryError(
525 |         TelemetryErrorType.NETWORK_ERROR,
526 |         'First error'
527 |       );
528 |       const error2 = new TelemetryError(
529 |         TelemetryErrorType.VALIDATION_ERROR,
530 |         'Second error'
531 |       );
532 |       const error3 = new TelemetryError(
533 |         TelemetryErrorType.RATE_LIMIT_ERROR,
534 |         'Third error'
535 |       );
536 | 
537 |       aggregator.record(error1);
538 |       aggregator.record(error2);
539 |       aggregator.record(error3);
540 | 
541 |       const stats = aggregator.getStats();
542 |       expect(stats.recentErrors).toHaveLength(3);
543 |       expect(stats.recentErrors[0].message).toBe('First error');
544 |       expect(stats.recentErrors[1].message).toBe('Second error');
545 |       expect(stats.recentErrors[2].message).toBe('Third error');
546 |     });
547 | 
548 |     it('should handle tie in most common error', () => {
549 |       const networkError = new TelemetryError(
550 |         TelemetryErrorType.NETWORK_ERROR,
551 |         'Network issue'
552 |       );
553 |       const validationError = new TelemetryError(
554 |         TelemetryErrorType.VALIDATION_ERROR,
555 |         'Validation issue'
556 |       );
557 | 
558 |       // Equal counts
559 |       aggregator.record(networkError);
560 |       aggregator.record(validationError);
561 | 
562 |       const stats = aggregator.getStats();
563 |       // Should return one of them (implementation dependent)
564 |       expect(stats.mostCommonError).toBeDefined();
565 |       expect([TelemetryErrorType.NETWORK_ERROR, TelemetryErrorType.VALIDATION_ERROR])
566 |         .toContain(stats.mostCommonError);
567 |     });
568 |   });
569 | 
570 |   describe('reset()', () => {
571 |     it('should clear all error data', () => {
572 |       const error = new TelemetryError(
573 |         TelemetryErrorType.NETWORK_ERROR,
574 |         'Test error'
575 |       );
576 |       aggregator.record(error);
577 | 
578 |       // Verify data exists
579 |       expect(aggregator.getStats().totalErrors).toBe(1);
580 | 
581 |       // Reset
582 |       aggregator.reset();
583 | 
584 |       // Verify cleared
585 |       const stats = aggregator.getStats();
586 |       expect(stats).toEqual({
587 |         totalErrors: 0,
588 |         errorsByType: {},
589 |         mostCommonError: undefined,
590 |         recentErrors: []
591 |       });
592 |     });
593 |   });
594 | 
595 |   describe('error detail management', () => {
596 |     it('should preserve error context in details', () => {
597 |       const context = { operation: 'flush', batchSize: 50 };
598 |       const error = new TelemetryError(
599 |         TelemetryErrorType.NETWORK_ERROR,
600 |         'Network failure',
601 |         context,
602 |         true
603 |       );
604 | 
605 |       aggregator.record(error);
606 | 
607 |       const stats = aggregator.getStats();
608 |       expect(stats.recentErrors[0]).toEqual({
609 |         type: TelemetryErrorType.NETWORK_ERROR,
610 |         message: 'Network failure',
611 |         context,
612 |         timestamp: error.timestamp,
613 |         retryable: true
614 |       });
615 |     });
616 | 
617 |     it('should maintain error details queue with FIFO behavior', () => {
618 |       // Add more than max to test queue behavior
619 |       const errors = [];
620 |       for (let i = 0; i < 15; i++) {
621 |         const error = new TelemetryError(
622 |           TelemetryErrorType.VALIDATION_ERROR,
623 |           `Error ${i}`
624 |         );
625 |         errors.push(error);
626 |         aggregator.record(error);
627 |       }
628 | 
629 |       const stats = aggregator.getStats();
630 |       // Should have last 10 errors (5-14)
631 |       expect(stats.recentErrors).toHaveLength(10);
632 |       expect(stats.recentErrors[0].message).toBe('Error 5');
633 |       expect(stats.recentErrors[9].message).toBe('Error 14');
634 |     });
635 |   });
636 | });
```

--------------------------------------------------------------------------------
/src/services/ai-tool-validators.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * AI Tool Sub-Node Validators
  3 |  *
  4 |  * Implements validation logic for all 13 AI tool sub-nodes from
  5 |  * docs/FINAL_AI_VALIDATION_SPEC.md
  6 |  *
  7 |  * Each validator checks configuration requirements, connections, and
  8 |  * parameters specific to that tool type.
  9 |  */
 10 | 
 11 | import { NodeTypeNormalizer } from '../utils/node-type-normalizer';
 12 | 
 13 | // Validation constants
 14 | const MIN_DESCRIPTION_LENGTH_SHORT = 10;
 15 | const MIN_DESCRIPTION_LENGTH_MEDIUM = 15;
 16 | const MIN_DESCRIPTION_LENGTH_LONG = 20;
 17 | const MAX_ITERATIONS_WARNING_THRESHOLD = 50;
 18 | const MAX_TOPK_WARNING_THRESHOLD = 20;
 19 | 
 20 | export interface WorkflowNode {
 21 |   id: string;
 22 |   name: string;
 23 |   type: string;
 24 |   position: [number, number];
 25 |   parameters: any;
 26 |   credentials?: any;
 27 |   disabled?: boolean;
 28 |   typeVersion?: number;
 29 | }
 30 | 
 31 | export interface WorkflowJson {
 32 |   name?: string;
 33 |   nodes: WorkflowNode[];
 34 |   connections: Record<string, any>;
 35 |   settings?: any;
 36 | }
 37 | 
 38 | export interface ReverseConnection {
 39 |   sourceName: string;
 40 |   sourceType: string;
 41 |   type: string;  // main, ai_tool, ai_languageModel, etc.
 42 |   index: number;
 43 | }
 44 | 
 45 | export interface ValidationIssue {
 46 |   severity: 'error' | 'warning' | 'info';
 47 |   nodeId?: string;
 48 |   nodeName?: string;
 49 |   message: string;
 50 |   code?: string;
 51 | }
 52 | 
 53 | /**
 54 |  * 1. HTTP Request Tool Validator
 55 |  * From spec lines 883-1123
 56 |  */
 57 | export function validateHTTPRequestTool(node: WorkflowNode): ValidationIssue[] {
 58 |   const issues: ValidationIssue[] = [];
 59 | 
 60 |   // 1. Check toolDescription (REQUIRED)
 61 |   if (!node.parameters.toolDescription) {
 62 |     issues.push({
 63 |       severity: 'error',
 64 |       nodeId: node.id,
 65 |       nodeName: node.name,
 66 |       message: `HTTP Request Tool "${node.name}" has no toolDescription. Add a clear description to help the LLM know when to use this API.`,
 67 |       code: 'MISSING_TOOL_DESCRIPTION'
 68 |     });
 69 |   } else if (node.parameters.toolDescription.trim().length < MIN_DESCRIPTION_LENGTH_MEDIUM) {
 70 |     issues.push({
 71 |       severity: 'warning',
 72 |       nodeId: node.id,
 73 |       nodeName: node.name,
 74 |       message: `HTTP Request Tool "${node.name}" toolDescription is too short (minimum ${MIN_DESCRIPTION_LENGTH_MEDIUM} characters). Explain what API this calls and when to use it.`
 75 |     });
 76 |   }
 77 | 
 78 |   // 2. Check URL (REQUIRED)
 79 |   if (!node.parameters.url) {
 80 |     issues.push({
 81 |       severity: 'error',
 82 |       nodeId: node.id,
 83 |       nodeName: node.name,
 84 |       message: `HTTP Request Tool "${node.name}" has no URL. Add the API endpoint URL.`,
 85 |       code: 'MISSING_URL'
 86 |     });
 87 |   } else {
 88 |     // Validate URL protocol (must be http or https)
 89 |     try {
 90 |       const urlObj = new URL(node.parameters.url);
 91 |       if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') {
 92 |         issues.push({
 93 |           severity: 'error',
 94 |           nodeId: node.id,
 95 |           nodeName: node.name,
 96 |           message: `HTTP Request Tool "${node.name}" has invalid URL protocol "${urlObj.protocol}". Use http:// or https:// only.`,
 97 |           code: 'INVALID_URL_PROTOCOL'
 98 |         });
 99 |       }
100 |     } catch (e) {
101 |       // URL parsing failed - invalid format
102 |       // Only warn if it's not an n8n expression
103 |       if (!node.parameters.url.includes('{{')) {
104 |         issues.push({
105 |           severity: 'warning',
106 |           nodeId: node.id,
107 |           nodeName: node.name,
108 |           message: `HTTP Request Tool "${node.name}" has potentially invalid URL format. Ensure it's a valid URL or n8n expression.`
109 |         });
110 |       }
111 |     }
112 |   }
113 | 
114 |   // 3. Validate placeholders match definitions
115 |   if (node.parameters.url || node.parameters.body || node.parameters.headers) {
116 |     const placeholderRegex = /\{([^}]+)\}/g;
117 |     const placeholders = new Set<string>();
118 | 
119 |     // Extract placeholders from URL, body, headers
120 |     [node.parameters.url, node.parameters.body, JSON.stringify(node.parameters.headers || {})].forEach(text => {
121 |       if (text) {
122 |         let match;
123 |         while ((match = placeholderRegex.exec(text)) !== null) {
124 |           placeholders.add(match[1]);
125 |         }
126 |       }
127 |     });
128 | 
129 |     // If placeholders exist in URL/body/headers
130 |     if (placeholders.size > 0) {
131 |       const definitions = node.parameters.placeholderDefinitions?.values || [];
132 |       const definedNames = new Set(definitions.map((d: any) => d.name));
133 | 
134 |       // If no placeholderDefinitions at all, warn
135 |       if (!node.parameters.placeholderDefinitions) {
136 |         issues.push({
137 |           severity: 'warning',
138 |           nodeId: node.id,
139 |           nodeName: node.name,
140 |           message: `HTTP Request Tool "${node.name}" uses placeholders but has no placeholderDefinitions. Add definitions to describe the expected inputs.`
141 |         });
142 |       } else {
143 |         // Has placeholderDefinitions, check each placeholder
144 |         for (const placeholder of placeholders) {
145 |           if (!definedNames.has(placeholder)) {
146 |             issues.push({
147 |               severity: 'error',
148 |               nodeId: node.id,
149 |               nodeName: node.name,
150 |               message: `HTTP Request Tool "${node.name}" Placeholder "${placeholder}" in URL but it's not defined in placeholderDefinitions.`,
151 |               code: 'UNDEFINED_PLACEHOLDER'
152 |             });
153 |           }
154 |         }
155 | 
156 |         // Check for defined but unused placeholders
157 |         for (const def of definitions) {
158 |           if (!placeholders.has(def.name)) {
159 |             issues.push({
160 |               severity: 'warning',
161 |               nodeId: node.id,
162 |               nodeName: node.name,
163 |               message: `HTTP Request Tool "${node.name}" defines placeholder "${def.name}" but doesn't use it.`
164 |             });
165 |           }
166 |         }
167 |       }
168 |     }
169 |   }
170 | 
171 |   // 4. Validate authentication
172 |   if (node.parameters.authentication === 'predefinedCredentialType' &&
173 |       (!node.credentials || Object.keys(node.credentials).length === 0)) {
174 |     issues.push({
175 |       severity: 'error',
176 |       nodeId: node.id,
177 |       nodeName: node.name,
178 |       message: `HTTP Request Tool "${node.name}" requires credentials but none are configured.`,
179 |       code: 'MISSING_CREDENTIALS'
180 |     });
181 |   }
182 | 
183 |   // 5. Validate HTTP method
184 |   const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
185 |   if (node.parameters.method && !validMethods.includes(node.parameters.method.toUpperCase())) {
186 |     issues.push({
187 |       severity: 'error',
188 |       nodeId: node.id,
189 |       nodeName: node.name,
190 |       message: `HTTP Request Tool "${node.name}" has invalid HTTP method "${node.parameters.method}". Use one of: ${validMethods.join(', ')}.`,
191 |       code: 'INVALID_HTTP_METHOD'
192 |     });
193 |   }
194 | 
195 |   // 6. Validate body for POST/PUT/PATCH
196 |   if (['POST', 'PUT', 'PATCH'].includes(node.parameters.method?.toUpperCase())) {
197 |     if (!node.parameters.body && !node.parameters.jsonBody) {
198 |       issues.push({
199 |         severity: 'warning',
200 |         nodeId: node.id,
201 |         nodeName: node.name,
202 |         message: `HTTP Request Tool "${node.name}" uses ${node.parameters.method} but has no body. Consider adding a body or using GET instead.`
203 |       });
204 |     }
205 |   }
206 | 
207 |   return issues;
208 | }
209 | 
210 | /**
211 |  * 2. Code Tool Validator
212 |  * From spec lines 1125-1393
213 |  */
214 | export function validateCodeTool(node: WorkflowNode): ValidationIssue[] {
215 |   const issues: ValidationIssue[] = [];
216 | 
217 |   // 1. Check toolDescription (REQUIRED)
218 |   if (!node.parameters.toolDescription) {
219 |     issues.push({
220 |       severity: 'error',
221 |       nodeId: node.id,
222 |       nodeName: node.name,
223 |       message: `Code Tool "${node.name}" has no toolDescription. Add one to help the LLM understand the tool's purpose.`,
224 |       code: 'MISSING_TOOL_DESCRIPTION'
225 |     });
226 |   }
227 | 
228 |   // 2. Check jsCode exists (REQUIRED)
229 |   if (!node.parameters.jsCode || node.parameters.jsCode.trim().length === 0) {
230 |     issues.push({
231 |       severity: 'error',
232 |       nodeId: node.id,
233 |       nodeName: node.name,
234 |       message: `Code Tool "${node.name}" code is empty. Add the JavaScript code to execute.`,
235 |       code: 'MISSING_CODE'
236 |     });
237 |   }
238 | 
239 |   // 3. Recommend input/output schema
240 |   if (!node.parameters.inputSchema && !node.parameters.specifyInputSchema) {
241 |     issues.push({
242 |       severity: 'warning',
243 |       nodeId: node.id,
244 |       nodeName: node.name,
245 |       message: `Code Tool "${node.name}" has no input schema. Consider adding one to validate LLM inputs.`
246 |     });
247 |   }
248 | 
249 |   return issues;
250 | }
251 | 
252 | /**
253 |  * 3. Vector Store Tool Validator
254 |  * From spec lines 1395-1620
255 |  */
256 | export function validateVectorStoreTool(
257 |   node: WorkflowNode,
258 |   reverseConnections: Map<string, ReverseConnection[]>,
259 |   workflow: WorkflowJson
260 | ): ValidationIssue[] {
261 |   const issues: ValidationIssue[] = [];
262 | 
263 |   // 1. Check toolDescription (REQUIRED)
264 |   if (!node.parameters.toolDescription) {
265 |     issues.push({
266 |       severity: 'error',
267 |       nodeId: node.id,
268 |       nodeName: node.name,
269 |       message: `Vector Store Tool "${node.name}" has no toolDescription. Add one to explain what data it searches.`,
270 |       code: 'MISSING_TOOL_DESCRIPTION'
271 |     });
272 |   }
273 | 
274 |   // 2. Validate topK parameter if specified
275 |   if (node.parameters.topK !== undefined) {
276 |     if (typeof node.parameters.topK !== 'number' || node.parameters.topK < 1) {
277 |       issues.push({
278 |         severity: 'error',
279 |         nodeId: node.id,
280 |         nodeName: node.name,
281 |         message: `Vector Store Tool "${node.name}" has invalid topK value. Must be a positive number.`,
282 |         code: 'INVALID_TOPK'
283 |       });
284 |     } else if (node.parameters.topK > MAX_TOPK_WARNING_THRESHOLD) {
285 |       issues.push({
286 |         severity: 'warning',
287 |         nodeId: node.id,
288 |         nodeName: node.name,
289 |         message: `Vector Store Tool "${node.name}" has topK=${node.parameters.topK}. Large values (>${MAX_TOPK_WARNING_THRESHOLD}) may overwhelm the LLM context. Consider reducing to 10 or less.`
290 |       });
291 |     }
292 |   }
293 | 
294 |   return issues;
295 | }
296 | 
297 | /**
298 |  * 4. Workflow Tool Validator
299 |  * From spec lines 1622-1831 (already complete in spec)
300 |  */
301 | export function validateWorkflowTool(node: WorkflowNode, reverseConnections?: Map<string, ReverseConnection[]>): ValidationIssue[] {
302 |   const issues: ValidationIssue[] = [];
303 | 
304 |   // 1. Check toolDescription (REQUIRED)
305 |   if (!node.parameters.toolDescription) {
306 |     issues.push({
307 |       severity: 'error',
308 |       nodeId: node.id,
309 |       nodeName: node.name,
310 |       message: `Workflow Tool "${node.name}" has no toolDescription. Add one to help the LLM know when to use this tool.`,
311 |       code: 'MISSING_TOOL_DESCRIPTION'
312 |     });
313 |   }
314 | 
315 |   // 2. Check workflowId (REQUIRED)
316 |   if (!node.parameters.workflowId) {
317 |     issues.push({
318 |       severity: 'error',
319 |       nodeId: node.id,
320 |       nodeName: node.name,
321 |       message: `Workflow Tool "${node.name}" has no workflowId. Select a workflow to execute.`,
322 |       code: 'MISSING_WORKFLOW_ID'
323 |     });
324 |   }
325 | 
326 |   return issues;
327 | }
328 | 
329 | /**
330 |  * 5. AI Agent Tool Validator
331 |  * From spec lines 1882-2122
332 |  */
333 | export function validateAIAgentTool(
334 |   node: WorkflowNode,
335 |   reverseConnections: Map<string, ReverseConnection[]>
336 | ): ValidationIssue[] {
337 |   const issues: ValidationIssue[] = [];
338 | 
339 |   // 1. Check toolDescription (REQUIRED)
340 |   if (!node.parameters.toolDescription) {
341 |     issues.push({
342 |       severity: 'error',
343 |       nodeId: node.id,
344 |       nodeName: node.name,
345 |       message: `AI Agent Tool "${node.name}" has no toolDescription. Add one to help the LLM know when to use this tool.`,
346 |       code: 'MISSING_TOOL_DESCRIPTION'
347 |     });
348 |   }
349 | 
350 |   // 2. Validate maxIterations if specified
351 |   if (node.parameters.maxIterations !== undefined) {
352 |     if (typeof node.parameters.maxIterations !== 'number' || node.parameters.maxIterations < 1) {
353 |       issues.push({
354 |         severity: 'error',
355 |         nodeId: node.id,
356 |         nodeName: node.name,
357 |         message: `AI Agent Tool "${node.name}" has invalid maxIterations. Must be a positive number.`,
358 |         code: 'INVALID_MAX_ITERATIONS'
359 |       });
360 |     } else if (node.parameters.maxIterations > MAX_ITERATIONS_WARNING_THRESHOLD) {
361 |       issues.push({
362 |         severity: 'warning',
363 |         nodeId: node.id,
364 |         nodeName: node.name,
365 |         message: `AI Agent Tool "${node.name}" has maxIterations=${node.parameters.maxIterations}. Large values (>${MAX_ITERATIONS_WARNING_THRESHOLD}) may lead to long execution times.`
366 |       });
367 |     }
368 |   }
369 | 
370 |   return issues;
371 | }
372 | 
373 | /**
374 |  * 6. MCP Client Tool Validator
375 |  * From spec lines 2124-2534 (already complete in spec)
376 |  */
377 | export function validateMCPClientTool(node: WorkflowNode): ValidationIssue[] {
378 |   const issues: ValidationIssue[] = [];
379 | 
380 |   // 1. Check toolDescription (REQUIRED)
381 |   if (!node.parameters.toolDescription) {
382 |     issues.push({
383 |       severity: 'error',
384 |       nodeId: node.id,
385 |       nodeName: node.name,
386 |       message: `MCP Client Tool "${node.name}" has no toolDescription. Add one to help the LLM know when to use this tool.`,
387 |       code: 'MISSING_TOOL_DESCRIPTION'
388 |     });
389 |   }
390 | 
391 |   // 2. Check serverUrl (REQUIRED)
392 |   if (!node.parameters.serverUrl) {
393 |     issues.push({
394 |       severity: 'error',
395 |       nodeId: node.id,
396 |       nodeName: node.name,
397 |       message: `MCP Client Tool "${node.name}" has no serverUrl. Configure the MCP server URL.`,
398 |       code: 'MISSING_SERVER_URL'
399 |     });
400 |   }
401 | 
402 |   return issues;
403 | }
404 | 
405 | /**
406 |  * 7-8. Simple Tools (Calculator, Think) Validators
407 |  * From spec lines 1868-2009
408 |  */
409 | export function validateCalculatorTool(node: WorkflowNode): ValidationIssue[] {
410 |   const issues: ValidationIssue[] = [];
411 | 
412 |   // Calculator Tool has a built-in description and is self-explanatory
413 |   // toolDescription is optional - no validation needed
414 |   return issues;
415 | }
416 | 
417 | export function validateThinkTool(node: WorkflowNode): ValidationIssue[] {
418 |   const issues: ValidationIssue[] = [];
419 | 
420 |   // Think Tool has a built-in description and is self-explanatory
421 |   // toolDescription is optional - no validation needed
422 |   return issues;
423 | }
424 | 
425 | /**
426 |  * 9-12. Search Tools Validators
427 |  * From spec lines 1833-2139
428 |  */
429 | export function validateSerpApiTool(node: WorkflowNode): ValidationIssue[] {
430 |   const issues: ValidationIssue[] = [];
431 | 
432 |   // 1. Check toolDescription (REQUIRED)
433 |   if (!node.parameters.toolDescription) {
434 |     issues.push({
435 |       severity: 'error',
436 |       nodeId: node.id,
437 |       nodeName: node.name,
438 |       message: `SerpApi Tool "${node.name}" has no toolDescription. Add one to explain when to use Google search.`,
439 |       code: 'MISSING_TOOL_DESCRIPTION'
440 |     });
441 |   }
442 | 
443 |   // 2. Check credentials (RECOMMENDED)
444 |   if (!node.credentials || !node.credentials.serpApiApi) {
445 |     issues.push({
446 |       severity: 'warning',
447 |       nodeId: node.id,
448 |       nodeName: node.name,
449 |       message: `SerpApi Tool "${node.name}" requires SerpApi credentials. Configure your API key.`
450 |     });
451 |   }
452 | 
453 |   return issues;
454 | }
455 | 
456 | export function validateWikipediaTool(node: WorkflowNode): ValidationIssue[] {
457 |   const issues: ValidationIssue[] = [];
458 | 
459 |   // 1. Check toolDescription (REQUIRED)
460 |   if (!node.parameters.toolDescription) {
461 |     issues.push({
462 |       severity: 'error',
463 |       nodeId: node.id,
464 |       nodeName: node.name,
465 |       message: `Wikipedia Tool "${node.name}" has no toolDescription. Add one to explain when to use Wikipedia.`,
466 |       code: 'MISSING_TOOL_DESCRIPTION'
467 |     });
468 |   }
469 | 
470 |   // 2. Validate language if specified
471 |   if (node.parameters.language) {
472 |     const validLanguageCodes = /^[a-z]{2,3}$/;  // ISO 639 codes
473 |     if (!validLanguageCodes.test(node.parameters.language)) {
474 |       issues.push({
475 |         severity: 'warning',
476 |         nodeId: node.id,
477 |         nodeName: node.name,
478 |         message: `Wikipedia Tool "${node.name}" has potentially invalid language code "${node.parameters.language}". Use ISO 639 codes (e.g., "en", "es", "fr").`
479 |       });
480 |     }
481 |   }
482 | 
483 |   return issues;
484 | }
485 | 
486 | export function validateSearXngTool(node: WorkflowNode): ValidationIssue[] {
487 |   const issues: ValidationIssue[] = [];
488 | 
489 |   // 1. Check toolDescription (REQUIRED)
490 |   if (!node.parameters.toolDescription) {
491 |     issues.push({
492 |       severity: 'error',
493 |       nodeId: node.id,
494 |       nodeName: node.name,
495 |       message: `SearXNG Tool "${node.name}" has no toolDescription. Add one to explain when to use SearXNG.`,
496 |       code: 'MISSING_TOOL_DESCRIPTION'
497 |     });
498 |   }
499 | 
500 |   // 2. Check baseUrl (REQUIRED)
501 |   if (!node.parameters.baseUrl) {
502 |     issues.push({
503 |       severity: 'error',
504 |       nodeId: node.id,
505 |       nodeName: node.name,
506 |       message: `SearXNG Tool "${node.name}" has no baseUrl. Configure your SearXNG instance URL.`,
507 |       code: 'MISSING_BASE_URL'
508 |     });
509 |   }
510 | 
511 |   return issues;
512 | }
513 | 
514 | export function validateWolframAlphaTool(node: WorkflowNode): ValidationIssue[] {
515 |   const issues: ValidationIssue[] = [];
516 | 
517 |   // 1. Check credentials (REQUIRED)
518 |   if (!node.credentials || (!node.credentials.wolframAlpha && !node.credentials.wolframAlphaApi)) {
519 |     issues.push({
520 |       severity: 'error',
521 |       nodeId: node.id,
522 |       nodeName: node.name,
523 |       message: `WolframAlpha Tool "${node.name}" requires Wolfram|Alpha API credentials. Configure your App ID.`,
524 |       code: 'MISSING_CREDENTIALS'
525 |     });
526 |   }
527 | 
528 |   // 2. Check description (INFO)
529 |   if (!node.parameters.description && !node.parameters.toolDescription) {
530 |     issues.push({
531 |       severity: 'info',
532 |       nodeId: node.id,
533 |       nodeName: node.name,
534 |       message: `WolframAlpha Tool "${node.name}" has no custom description. Add one to explain when to use Wolfram|Alpha for computational queries.`
535 |     });
536 |   }
537 | 
538 |   return issues;
539 | }
540 | 
541 | /**
542 |  * Helper: Map node types to validator functions
543 |  */
544 | export const AI_TOOL_VALIDATORS = {
545 |   'nodes-langchain.toolHttpRequest': validateHTTPRequestTool,
546 |   'nodes-langchain.toolCode': validateCodeTool,
547 |   'nodes-langchain.toolVectorStore': validateVectorStoreTool,
548 |   'nodes-langchain.toolWorkflow': validateWorkflowTool,
549 |   'nodes-langchain.agentTool': validateAIAgentTool,
550 |   'nodes-langchain.mcpClientTool': validateMCPClientTool,
551 |   'nodes-langchain.toolCalculator': validateCalculatorTool,
552 |   'nodes-langchain.toolThink': validateThinkTool,
553 |   'nodes-langchain.toolSerpApi': validateSerpApiTool,
554 |   'nodes-langchain.toolWikipedia': validateWikipediaTool,
555 |   'nodes-langchain.toolSearXng': validateSearXngTool,
556 |   'nodes-langchain.toolWolframAlpha': validateWolframAlphaTool,
557 | } as const;
558 | 
559 | /**
560 |  * Check if a node type is an AI tool sub-node
561 |  */
562 | export function isAIToolSubNode(nodeType: string): boolean {
563 |   const normalized = NodeTypeNormalizer.normalizeToFullForm(nodeType);
564 |   return normalized in AI_TOOL_VALIDATORS;
565 | }
566 | 
567 | /**
568 |  * Validate an AI tool sub-node with the appropriate validator
569 |  */
570 | export function validateAIToolSubNode(
571 |   node: WorkflowNode,
572 |   nodeType: string,
573 |   reverseConnections: Map<string, ReverseConnection[]>,
574 |   workflow: WorkflowJson
575 | ): ValidationIssue[] {
576 |   const normalized = NodeTypeNormalizer.normalizeToFullForm(nodeType);
577 | 
578 |   // Route to appropriate validator based on node type
579 |   switch (normalized) {
580 |     case 'nodes-langchain.toolHttpRequest':
581 |       return validateHTTPRequestTool(node);
582 |     case 'nodes-langchain.toolCode':
583 |       return validateCodeTool(node);
584 |     case 'nodes-langchain.toolVectorStore':
585 |       return validateVectorStoreTool(node, reverseConnections, workflow);
586 |     case 'nodes-langchain.toolWorkflow':
587 |       return validateWorkflowTool(node);
588 |     case 'nodes-langchain.agentTool':
589 |       return validateAIAgentTool(node, reverseConnections);
590 |     case 'nodes-langchain.mcpClientTool':
591 |       return validateMCPClientTool(node);
592 |     case 'nodes-langchain.toolCalculator':
593 |       return validateCalculatorTool(node);
594 |     case 'nodes-langchain.toolThink':
595 |       return validateThinkTool(node);
596 |     case 'nodes-langchain.toolSerpApi':
597 |       return validateSerpApiTool(node);
598 |     case 'nodes-langchain.toolWikipedia':
599 |       return validateWikipediaTool(node);
600 |     case 'nodes-langchain.toolSearXng':
601 |       return validateSearXngTool(node);
602 |     case 'nodes-langchain.toolWolframAlpha':
603 |       return validateWolframAlphaTool(node);
604 |     default:
605 |       return [];
606 |   }
607 | }
608 | 
```

--------------------------------------------------------------------------------
/src/services/workflow-auto-fixer.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Workflow Auto-Fixer Service
  3 |  *
  4 |  * Automatically generates fix operations for common workflow validation errors.
  5 |  * Converts validation results into diff operations that can be applied to fix the workflow.
  6 |  */
  7 | 
  8 | import crypto from 'crypto';
  9 | import { WorkflowValidationResult } from './workflow-validator';
 10 | import { ExpressionFormatIssue } from './expression-format-validator';
 11 | import { NodeSimilarityService } from './node-similarity-service';
 12 | import { NodeRepository } from '../database/node-repository';
 13 | import {
 14 |   WorkflowDiffOperation,
 15 |   UpdateNodeOperation
 16 | } from '../types/workflow-diff';
 17 | import { WorkflowNode, Workflow } from '../types/n8n-api';
 18 | import { Logger } from '../utils/logger';
 19 | 
 20 | const logger = new Logger({ prefix: '[WorkflowAutoFixer]' });
 21 | 
 22 | export type FixConfidenceLevel = 'high' | 'medium' | 'low';
 23 | export type FixType =
 24 |   | 'expression-format'
 25 |   | 'typeversion-correction'
 26 |   | 'error-output-config'
 27 |   | 'node-type-correction'
 28 |   | 'webhook-missing-path';
 29 | 
 30 | export interface AutoFixConfig {
 31 |   applyFixes: boolean;
 32 |   fixTypes?: FixType[];
 33 |   confidenceThreshold?: FixConfidenceLevel;
 34 |   maxFixes?: number;
 35 | }
 36 | 
 37 | export interface FixOperation {
 38 |   node: string;
 39 |   field: string;
 40 |   type: FixType;
 41 |   before: any;
 42 |   after: any;
 43 |   confidence: FixConfidenceLevel;
 44 |   description: string;
 45 | }
 46 | 
 47 | export interface AutoFixResult {
 48 |   operations: WorkflowDiffOperation[];
 49 |   fixes: FixOperation[];
 50 |   summary: string;
 51 |   stats: {
 52 |     total: number;
 53 |     byType: Record<FixType, number>;
 54 |     byConfidence: Record<FixConfidenceLevel, number>;
 55 |   };
 56 | }
 57 | 
 58 | export interface NodeFormatIssue extends ExpressionFormatIssue {
 59 |   nodeName: string;
 60 |   nodeId: string;
 61 | }
 62 | 
 63 | /**
 64 |  * Type guard to check if an issue has node information
 65 |  */
 66 | export function isNodeFormatIssue(issue: ExpressionFormatIssue): issue is NodeFormatIssue {
 67 |   return 'nodeName' in issue && 'nodeId' in issue &&
 68 |          typeof (issue as any).nodeName === 'string' &&
 69 |          typeof (issue as any).nodeId === 'string';
 70 | }
 71 | 
 72 | /**
 73 |  * Error with suggestions for node type issues
 74 |  */
 75 | export interface NodeTypeError {
 76 |   type: 'error';
 77 |   nodeId?: string;
 78 |   nodeName?: string;
 79 |   message: string;
 80 |   suggestions?: Array<{
 81 |     nodeType: string;
 82 |     confidence: number;
 83 |     reason: string;
 84 |   }>;
 85 | }
 86 | 
 87 | export class WorkflowAutoFixer {
 88 |   private readonly defaultConfig: AutoFixConfig = {
 89 |     applyFixes: false,
 90 |     confidenceThreshold: 'medium',
 91 |     maxFixes: 50
 92 |   };
 93 |   private similarityService: NodeSimilarityService | null = null;
 94 | 
 95 |   constructor(repository?: NodeRepository) {
 96 |     if (repository) {
 97 |       this.similarityService = new NodeSimilarityService(repository);
 98 |     }
 99 |   }
100 | 
101 |   /**
102 |    * Generate fix operations from validation results
103 |    */
104 |   generateFixes(
105 |     workflow: Workflow,
106 |     validationResult: WorkflowValidationResult,
107 |     formatIssues: ExpressionFormatIssue[] = [],
108 |     config: Partial<AutoFixConfig> = {}
109 |   ): AutoFixResult {
110 |     const fullConfig = { ...this.defaultConfig, ...config };
111 |     const operations: WorkflowDiffOperation[] = [];
112 |     const fixes: FixOperation[] = [];
113 | 
114 |     // Create a map for quick node lookup
115 |     const nodeMap = new Map<string, WorkflowNode>();
116 |     workflow.nodes.forEach(node => {
117 |       nodeMap.set(node.name, node);
118 |       nodeMap.set(node.id, node);
119 |     });
120 | 
121 |     // Process expression format issues (HIGH confidence)
122 |     if (!fullConfig.fixTypes || fullConfig.fixTypes.includes('expression-format')) {
123 |       this.processExpressionFormatFixes(formatIssues, nodeMap, operations, fixes);
124 |     }
125 | 
126 |     // Process typeVersion errors (MEDIUM confidence)
127 |     if (!fullConfig.fixTypes || fullConfig.fixTypes.includes('typeversion-correction')) {
128 |       this.processTypeVersionFixes(validationResult, nodeMap, operations, fixes);
129 |     }
130 | 
131 |     // Process error output configuration issues (MEDIUM confidence)
132 |     if (!fullConfig.fixTypes || fullConfig.fixTypes.includes('error-output-config')) {
133 |       this.processErrorOutputFixes(validationResult, nodeMap, workflow, operations, fixes);
134 |     }
135 | 
136 |     // Process node type corrections (HIGH confidence only)
137 |     if (!fullConfig.fixTypes || fullConfig.fixTypes.includes('node-type-correction')) {
138 |       this.processNodeTypeFixes(validationResult, nodeMap, operations, fixes);
139 |     }
140 | 
141 |     // Process webhook path fixes (HIGH confidence)
142 |     if (!fullConfig.fixTypes || fullConfig.fixTypes.includes('webhook-missing-path')) {
143 |       this.processWebhookPathFixes(validationResult, nodeMap, operations, fixes);
144 |     }
145 | 
146 |     // Filter by confidence threshold
147 |     const filteredFixes = this.filterByConfidence(fixes, fullConfig.confidenceThreshold);
148 |     const filteredOperations = this.filterOperationsByFixes(operations, filteredFixes, fixes);
149 | 
150 |     // Apply max fixes limit
151 |     const limitedFixes = filteredFixes.slice(0, fullConfig.maxFixes);
152 |     const limitedOperations = this.filterOperationsByFixes(filteredOperations, limitedFixes, filteredFixes);
153 | 
154 |     // Generate summary
155 |     const stats = this.calculateStats(limitedFixes);
156 |     const summary = this.generateSummary(stats);
157 | 
158 |     return {
159 |       operations: limitedOperations,
160 |       fixes: limitedFixes,
161 |       summary,
162 |       stats
163 |     };
164 |   }
165 | 
166 |   /**
167 |    * Process expression format fixes (missing = prefix)
168 |    */
169 |   private processExpressionFormatFixes(
170 |     formatIssues: ExpressionFormatIssue[],
171 |     nodeMap: Map<string, WorkflowNode>,
172 |     operations: WorkflowDiffOperation[],
173 |     fixes: FixOperation[]
174 |   ): void {
175 |     // Group fixes by node to create single update operation per node
176 |     const fixesByNode = new Map<string, ExpressionFormatIssue[]>();
177 | 
178 |     for (const issue of formatIssues) {
179 |       // Process both errors and warnings for missing-prefix issues
180 |       if (issue.issueType === 'missing-prefix') {
181 |         // Use type guard to ensure we have node information
182 |         if (!isNodeFormatIssue(issue)) {
183 |           logger.warn('Expression format issue missing node information', {
184 |             fieldPath: issue.fieldPath,
185 |             issueType: issue.issueType
186 |           });
187 |           continue;
188 |         }
189 | 
190 |         const nodeName = issue.nodeName;
191 | 
192 |         if (!fixesByNode.has(nodeName)) {
193 |           fixesByNode.set(nodeName, []);
194 |         }
195 |         fixesByNode.get(nodeName)!.push(issue);
196 |       }
197 |     }
198 | 
199 |     // Create update operations for each node
200 |     for (const [nodeName, nodeIssues] of fixesByNode) {
201 |       const node = nodeMap.get(nodeName);
202 |       if (!node) continue;
203 | 
204 |       const updatedParameters = JSON.parse(JSON.stringify(node.parameters || {}));
205 | 
206 |       for (const issue of nodeIssues) {
207 |         // Apply the fix to parameters
208 |         // The fieldPath doesn't include node name, use as is
209 |         const fieldPath = issue.fieldPath.split('.');
210 |         this.setNestedValue(updatedParameters, fieldPath, issue.correctedValue);
211 | 
212 |         fixes.push({
213 |           node: nodeName,
214 |           field: issue.fieldPath,
215 |           type: 'expression-format',
216 |           before: issue.currentValue,
217 |           after: issue.correctedValue,
218 |           confidence: 'high',
219 |           description: issue.explanation
220 |         });
221 |       }
222 | 
223 |       // Create update operation
224 |       const operation: UpdateNodeOperation = {
225 |         type: 'updateNode',
226 |         nodeId: nodeName, // Can be name or ID
227 |         updates: {
228 |           parameters: updatedParameters
229 |         }
230 |       };
231 |       operations.push(operation);
232 |     }
233 |   }
234 | 
235 |   /**
236 |    * Process typeVersion fixes
237 |    */
238 |   private processTypeVersionFixes(
239 |     validationResult: WorkflowValidationResult,
240 |     nodeMap: Map<string, WorkflowNode>,
241 |     operations: WorkflowDiffOperation[],
242 |     fixes: FixOperation[]
243 |   ): void {
244 |     for (const error of validationResult.errors) {
245 |       if (error.message.includes('typeVersion') && error.message.includes('exceeds maximum')) {
246 |         // Extract version info from error message
247 |         const versionMatch = error.message.match(/typeVersion (\d+(?:\.\d+)?) exceeds maximum supported version (\d+(?:\.\d+)?)/);
248 |         if (versionMatch) {
249 |           const currentVersion = parseFloat(versionMatch[1]);
250 |           const maxVersion = parseFloat(versionMatch[2]);
251 |           const nodeName = error.nodeName || error.nodeId;
252 | 
253 |           if (!nodeName) continue;
254 | 
255 |           const node = nodeMap.get(nodeName);
256 |           if (!node) continue;
257 | 
258 |           fixes.push({
259 |             node: nodeName,
260 |             field: 'typeVersion',
261 |             type: 'typeversion-correction',
262 |             before: currentVersion,
263 |             after: maxVersion,
264 |             confidence: 'medium',
265 |             description: `Corrected typeVersion from ${currentVersion} to maximum supported ${maxVersion}`
266 |           });
267 | 
268 |           const operation: UpdateNodeOperation = {
269 |             type: 'updateNode',
270 |             nodeId: nodeName,
271 |             updates: {
272 |               typeVersion: maxVersion
273 |             }
274 |           };
275 |           operations.push(operation);
276 |         }
277 |       }
278 |     }
279 |   }
280 | 
281 |   /**
282 |    * Process error output configuration fixes
283 |    */
284 |   private processErrorOutputFixes(
285 |     validationResult: WorkflowValidationResult,
286 |     nodeMap: Map<string, WorkflowNode>,
287 |     workflow: Workflow,
288 |     operations: WorkflowDiffOperation[],
289 |     fixes: FixOperation[]
290 |   ): void {
291 |     for (const error of validationResult.errors) {
292 |       if (error.message.includes('onError: \'continueErrorOutput\'') &&
293 |           error.message.includes('no error output connections')) {
294 |         const nodeName = error.nodeName || error.nodeId;
295 |         if (!nodeName) continue;
296 | 
297 |         const node = nodeMap.get(nodeName);
298 |         if (!node) continue;
299 | 
300 |         // Remove the conflicting onError setting
301 |         fixes.push({
302 |           node: nodeName,
303 |           field: 'onError',
304 |           type: 'error-output-config',
305 |           before: 'continueErrorOutput',
306 |           after: undefined,
307 |           confidence: 'medium',
308 |           description: 'Removed onError setting due to missing error output connections'
309 |         });
310 | 
311 |         const operation: UpdateNodeOperation = {
312 |           type: 'updateNode',
313 |           nodeId: nodeName,
314 |           updates: {
315 |             onError: undefined // This will remove the property
316 |           }
317 |         };
318 |         operations.push(operation);
319 |       }
320 |     }
321 |   }
322 | 
323 |   /**
324 |    * Process node type corrections for unknown nodes
325 |    */
326 |   private processNodeTypeFixes(
327 |     validationResult: WorkflowValidationResult,
328 |     nodeMap: Map<string, WorkflowNode>,
329 |     operations: WorkflowDiffOperation[],
330 |     fixes: FixOperation[]
331 |   ): void {
332 |     // Only process if we have the similarity service
333 |     if (!this.similarityService) {
334 |       return;
335 |     }
336 | 
337 |     for (const error of validationResult.errors) {
338 |       // Type-safe check for unknown node type errors with suggestions
339 |       const nodeError = error as NodeTypeError;
340 | 
341 |       if (error.message?.includes('Unknown node type:') && nodeError.suggestions) {
342 |         // Only auto-fix if we have a high-confidence suggestion (>= 0.9)
343 |         const highConfidenceSuggestion = nodeError.suggestions.find(s => s.confidence >= 0.9);
344 | 
345 |         if (highConfidenceSuggestion && nodeError.nodeId) {
346 |           const node = nodeMap.get(nodeError.nodeId) || nodeMap.get(nodeError.nodeName || '');
347 | 
348 |           if (node) {
349 |             fixes.push({
350 |               node: node.name,
351 |               field: 'type',
352 |               type: 'node-type-correction',
353 |               before: node.type,
354 |               after: highConfidenceSuggestion.nodeType,
355 |               confidence: 'high',
356 |               description: `Fix node type: "${node.type}" → "${highConfidenceSuggestion.nodeType}" (${highConfidenceSuggestion.reason})`
357 |             });
358 | 
359 |             const operation: UpdateNodeOperation = {
360 |               type: 'updateNode',
361 |               nodeId: node.name,
362 |               updates: {
363 |                 type: highConfidenceSuggestion.nodeType
364 |               }
365 |             };
366 |             operations.push(operation);
367 |           }
368 |         }
369 |       }
370 |     }
371 |   }
372 | 
373 |   /**
374 |    * Process webhook path fixes for webhook nodes missing path parameter
375 |    */
376 |   private processWebhookPathFixes(
377 |     validationResult: WorkflowValidationResult,
378 |     nodeMap: Map<string, WorkflowNode>,
379 |     operations: WorkflowDiffOperation[],
380 |     fixes: FixOperation[]
381 |   ): void {
382 |     for (const error of validationResult.errors) {
383 |       // Check for webhook path required error
384 |       if (error.message === 'Webhook path is required') {
385 |         const nodeName = error.nodeName || error.nodeId;
386 |         if (!nodeName) continue;
387 | 
388 |         const node = nodeMap.get(nodeName);
389 |         if (!node) continue;
390 | 
391 |         // Only fix webhook nodes
392 |         if (!node.type?.includes('webhook')) continue;
393 | 
394 |         // Generate a unique UUID for both path and webhookId
395 |         const webhookId = crypto.randomUUID();
396 | 
397 |         // Check if we need to update typeVersion
398 |         const currentTypeVersion = node.typeVersion || 1;
399 |         const needsVersionUpdate = currentTypeVersion < 2.1;
400 | 
401 |         fixes.push({
402 |           node: nodeName,
403 |           field: 'path',
404 |           type: 'webhook-missing-path',
405 |           before: undefined,
406 |           after: webhookId,
407 |           confidence: 'high',
408 |           description: needsVersionUpdate
409 |             ? `Generated webhook path and ID: ${webhookId} (also updating typeVersion to 2.1)`
410 |             : `Generated webhook path and ID: ${webhookId}`
411 |         });
412 | 
413 |         // Create update operation with both path and webhookId
414 |         // The updates object uses dot notation for nested properties
415 |         const updates: Record<string, any> = {
416 |           'parameters.path': webhookId,
417 |           'webhookId': webhookId
418 |         };
419 | 
420 |         // Only update typeVersion if it's older than 2.1
421 |         if (needsVersionUpdate) {
422 |           updates['typeVersion'] = 2.1;
423 |         }
424 | 
425 |         const operation: UpdateNodeOperation = {
426 |           type: 'updateNode',
427 |           nodeId: nodeName,
428 |           updates
429 |         };
430 |         operations.push(operation);
431 |       }
432 |     }
433 |   }
434 | 
435 |   /**
436 |    * Set a nested value in an object using a path array
437 |    * Includes validation to prevent silent failures
438 |    */
439 |   private setNestedValue(obj: any, path: string[], value: any): void {
440 |     if (!obj || typeof obj !== 'object') {
441 |       throw new Error('Cannot set value on non-object');
442 |     }
443 | 
444 |     if (path.length === 0) {
445 |       throw new Error('Cannot set value with empty path');
446 |     }
447 | 
448 |     try {
449 |       let current = obj;
450 | 
451 |       for (let i = 0; i < path.length - 1; i++) {
452 |         const key = path[i];
453 | 
454 |         // Handle array indices
455 |         if (key.includes('[')) {
456 |           const matches = key.match(/^([^[]+)\[(\d+)\]$/);
457 |           if (!matches) {
458 |             throw new Error(`Invalid array notation: ${key}`);
459 |           }
460 | 
461 |           const [, arrayKey, indexStr] = matches;
462 |           const index = parseInt(indexStr, 10);
463 | 
464 |           if (isNaN(index) || index < 0) {
465 |             throw new Error(`Invalid array index: ${indexStr}`);
466 |           }
467 | 
468 |           if (!current[arrayKey]) {
469 |             current[arrayKey] = [];
470 |           }
471 | 
472 |           if (!Array.isArray(current[arrayKey])) {
473 |             throw new Error(`Expected array at ${arrayKey}, got ${typeof current[arrayKey]}`);
474 |           }
475 | 
476 |           while (current[arrayKey].length <= index) {
477 |             current[arrayKey].push({});
478 |           }
479 | 
480 |           current = current[arrayKey][index];
481 |         } else {
482 |           if (current[key] === null || current[key] === undefined) {
483 |             current[key] = {};
484 |           }
485 | 
486 |           if (typeof current[key] !== 'object' || Array.isArray(current[key])) {
487 |             throw new Error(`Cannot traverse through ${typeof current[key]} at ${key}`);
488 |           }
489 | 
490 |           current = current[key];
491 |         }
492 |       }
493 | 
494 |       // Set the final value
495 |       const lastKey = path[path.length - 1];
496 | 
497 |       if (lastKey.includes('[')) {
498 |         const matches = lastKey.match(/^([^[]+)\[(\d+)\]$/);
499 |         if (!matches) {
500 |           throw new Error(`Invalid array notation: ${lastKey}`);
501 |         }
502 | 
503 |         const [, arrayKey, indexStr] = matches;
504 |         const index = parseInt(indexStr, 10);
505 | 
506 |         if (isNaN(index) || index < 0) {
507 |           throw new Error(`Invalid array index: ${indexStr}`);
508 |         }
509 | 
510 |         if (!current[arrayKey]) {
511 |           current[arrayKey] = [];
512 |         }
513 | 
514 |         if (!Array.isArray(current[arrayKey])) {
515 |           throw new Error(`Expected array at ${arrayKey}, got ${typeof current[arrayKey]}`);
516 |         }
517 | 
518 |         while (current[arrayKey].length <= index) {
519 |           current[arrayKey].push(null);
520 |         }
521 | 
522 |         current[arrayKey][index] = value;
523 |       } else {
524 |         current[lastKey] = value;
525 |       }
526 |     } catch (error) {
527 |       logger.error('Failed to set nested value', {
528 |         path: path.join('.'),
529 |         error: error instanceof Error ? error.message : String(error)
530 |       });
531 |       throw error;
532 |     }
533 |   }
534 | 
535 |   /**
536 |    * Filter fixes by confidence level
537 |    */
538 |   private filterByConfidence(
539 |     fixes: FixOperation[],
540 |     threshold?: FixConfidenceLevel
541 |   ): FixOperation[] {
542 |     if (!threshold) return fixes;
543 | 
544 |     const levels: FixConfidenceLevel[] = ['high', 'medium', 'low'];
545 |     const thresholdIndex = levels.indexOf(threshold);
546 | 
547 |     return fixes.filter(fix => {
548 |       const fixIndex = levels.indexOf(fix.confidence);
549 |       return fixIndex <= thresholdIndex;
550 |     });
551 |   }
552 | 
553 |   /**
554 |    * Filter operations to match filtered fixes
555 |    */
556 |   private filterOperationsByFixes(
557 |     operations: WorkflowDiffOperation[],
558 |     filteredFixes: FixOperation[],
559 |     allFixes: FixOperation[]
560 |   ): WorkflowDiffOperation[] {
561 |     const fixedNodes = new Set(filteredFixes.map(f => f.node));
562 |     return operations.filter(op => {
563 |       if (op.type === 'updateNode') {
564 |         return fixedNodes.has(op.nodeId || '');
565 |       }
566 |       return true;
567 |     });
568 |   }
569 | 
570 |   /**
571 |    * Calculate statistics about fixes
572 |    */
573 |   private calculateStats(fixes: FixOperation[]): AutoFixResult['stats'] {
574 |     const stats: AutoFixResult['stats'] = {
575 |       total: fixes.length,
576 |       byType: {
577 |         'expression-format': 0,
578 |         'typeversion-correction': 0,
579 |         'error-output-config': 0,
580 |         'node-type-correction': 0,
581 |         'webhook-missing-path': 0
582 |       },
583 |       byConfidence: {
584 |         'high': 0,
585 |         'medium': 0,
586 |         'low': 0
587 |       }
588 |     };
589 | 
590 |     for (const fix of fixes) {
591 |       stats.byType[fix.type]++;
592 |       stats.byConfidence[fix.confidence]++;
593 |     }
594 | 
595 |     return stats;
596 |   }
597 | 
598 |   /**
599 |    * Generate a human-readable summary
600 |    */
601 |   private generateSummary(stats: AutoFixResult['stats']): string {
602 |     if (stats.total === 0) {
603 |       return 'No fixes available';
604 |     }
605 | 
606 |     const parts: string[] = [];
607 | 
608 |     if (stats.byType['expression-format'] > 0) {
609 |       parts.push(`${stats.byType['expression-format']} expression format ${stats.byType['expression-format'] === 1 ? 'error' : 'errors'}`);
610 |     }
611 |     if (stats.byType['typeversion-correction'] > 0) {
612 |       parts.push(`${stats.byType['typeversion-correction']} version ${stats.byType['typeversion-correction'] === 1 ? 'issue' : 'issues'}`);
613 |     }
614 |     if (stats.byType['error-output-config'] > 0) {
615 |       parts.push(`${stats.byType['error-output-config']} error output ${stats.byType['error-output-config'] === 1 ? 'configuration' : 'configurations'}`);
616 |     }
617 |     if (stats.byType['node-type-correction'] > 0) {
618 |       parts.push(`${stats.byType['node-type-correction']} node type ${stats.byType['node-type-correction'] === 1 ? 'correction' : 'corrections'}`);
619 |     }
620 |     if (stats.byType['webhook-missing-path'] > 0) {
621 |       parts.push(`${stats.byType['webhook-missing-path']} webhook ${stats.byType['webhook-missing-path'] === 1 ? 'path' : 'paths'}`);
622 |     }
623 | 
624 |     if (parts.length === 0) {
625 |       return `Fixed ${stats.total} ${stats.total === 1 ? 'issue' : 'issues'}`;
626 |     }
627 | 
628 |     return `Fixed ${parts.join(', ')}`;
629 |   }
630 | }
```

--------------------------------------------------------------------------------
/src/services/resource-similarity-service.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { NodeRepository } from '../database/node-repository';
  2 | import { logger } from '../utils/logger';
  3 | import { ValidationServiceError } from '../errors/validation-service-error';
  4 | 
  5 | export interface ResourceSuggestion {
  6 |   value: string;
  7 |   confidence: number;
  8 |   reason: string;
  9 |   availableOperations?: string[];
 10 | }
 11 | 
 12 | interface ResourcePattern {
 13 |   pattern: string;
 14 |   suggestion: string;
 15 |   confidence: number;
 16 |   reason: string;
 17 | }
 18 | 
 19 | export class ResourceSimilarityService {
 20 |   private static readonly CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes
 21 |   private static readonly MIN_CONFIDENCE = 0.3; // 30% minimum confidence to suggest
 22 |   private static readonly MAX_SUGGESTIONS = 5;
 23 | 
 24 |   // Confidence thresholds for better code clarity
 25 |   private static readonly CONFIDENCE_THRESHOLDS = {
 26 |     EXACT: 1.0,
 27 |     VERY_HIGH: 0.95,
 28 |     HIGH: 0.8,
 29 |     MEDIUM: 0.6,
 30 |     MIN_SUBSTRING: 0.7
 31 |   } as const;
 32 | 
 33 |   private repository: NodeRepository;
 34 |   private resourceCache: Map<string, { resources: any[], timestamp: number }> = new Map();
 35 |   private suggestionCache: Map<string, ResourceSuggestion[]> = new Map();
 36 |   private commonPatterns: Map<string, ResourcePattern[]>;
 37 | 
 38 |   constructor(repository: NodeRepository) {
 39 |     this.repository = repository;
 40 |     this.commonPatterns = this.initializeCommonPatterns();
 41 |   }
 42 | 
 43 |   /**
 44 |    * Clean up expired cache entries to prevent memory leaks
 45 |    */
 46 |   private cleanupExpiredEntries(): void {
 47 |     const now = Date.now();
 48 | 
 49 |     // Clean resource cache
 50 |     for (const [key, value] of this.resourceCache.entries()) {
 51 |       if (now - value.timestamp >= ResourceSimilarityService.CACHE_DURATION_MS) {
 52 |         this.resourceCache.delete(key);
 53 |       }
 54 |     }
 55 | 
 56 |     // Clean suggestion cache - these don't have timestamps, so clear if cache is too large
 57 |     if (this.suggestionCache.size > 100) {
 58 |       // Keep only the most recent 50 entries
 59 |       const entries = Array.from(this.suggestionCache.entries());
 60 |       this.suggestionCache.clear();
 61 |       entries.slice(-50).forEach(([key, value]) => {
 62 |         this.suggestionCache.set(key, value);
 63 |       });
 64 |     }
 65 |   }
 66 | 
 67 |   /**
 68 |    * Initialize common resource mistake patterns
 69 |    */
 70 |   private initializeCommonPatterns(): Map<string, ResourcePattern[]> {
 71 |     const patterns = new Map<string, ResourcePattern[]>();
 72 | 
 73 |     // Google Drive patterns
 74 |     patterns.set('googleDrive', [
 75 |       { pattern: 'files', suggestion: 'file', confidence: 0.95, reason: 'Use singular "file" not plural' },
 76 |       { pattern: 'folders', suggestion: 'folder', confidence: 0.95, reason: 'Use singular "folder" not plural' },
 77 |       { pattern: 'permissions', suggestion: 'permission', confidence: 0.9, reason: 'Use singular form' },
 78 |       { pattern: 'fileAndFolder', suggestion: 'fileFolder', confidence: 0.9, reason: 'Use "fileFolder" for combined operations' },
 79 |       { pattern: 'driveFiles', suggestion: 'file', confidence: 0.8, reason: 'Use "file" for file operations' },
 80 |       { pattern: 'sharedDrives', suggestion: 'drive', confidence: 0.85, reason: 'Use "drive" for shared drive operations' },
 81 |     ]);
 82 | 
 83 |     // Slack patterns
 84 |     patterns.set('slack', [
 85 |       { pattern: 'messages', suggestion: 'message', confidence: 0.95, reason: 'Use singular "message" not plural' },
 86 |       { pattern: 'channels', suggestion: 'channel', confidence: 0.95, reason: 'Use singular "channel" not plural' },
 87 |       { pattern: 'users', suggestion: 'user', confidence: 0.95, reason: 'Use singular "user" not plural' },
 88 |       { pattern: 'msg', suggestion: 'message', confidence: 0.85, reason: 'Use full "message" not abbreviation' },
 89 |       { pattern: 'dm', suggestion: 'message', confidence: 0.7, reason: 'Use "message" for direct messages' },
 90 |       { pattern: 'conversation', suggestion: 'channel', confidence: 0.7, reason: 'Use "channel" for conversations' },
 91 |     ]);
 92 | 
 93 |     // Database patterns (postgres, mysql, mongodb)
 94 |     patterns.set('database', [
 95 |       { pattern: 'tables', suggestion: 'table', confidence: 0.95, reason: 'Use singular "table" not plural' },
 96 |       { pattern: 'queries', suggestion: 'query', confidence: 0.95, reason: 'Use singular "query" not plural' },
 97 |       { pattern: 'collections', suggestion: 'collection', confidence: 0.95, reason: 'Use singular "collection" not plural' },
 98 |       { pattern: 'documents', suggestion: 'document', confidence: 0.95, reason: 'Use singular "document" not plural' },
 99 |       { pattern: 'records', suggestion: 'record', confidence: 0.85, reason: 'Use "record" or "document"' },
100 |       { pattern: 'rows', suggestion: 'row', confidence: 0.9, reason: 'Use singular "row"' },
101 |     ]);
102 | 
103 |     // Google Sheets patterns
104 |     patterns.set('googleSheets', [
105 |       { pattern: 'sheets', suggestion: 'sheet', confidence: 0.95, reason: 'Use singular "sheet" not plural' },
106 |       { pattern: 'spreadsheets', suggestion: 'spreadsheet', confidence: 0.95, reason: 'Use singular "spreadsheet"' },
107 |       { pattern: 'cells', suggestion: 'cell', confidence: 0.9, reason: 'Use singular "cell"' },
108 |       { pattern: 'ranges', suggestion: 'range', confidence: 0.9, reason: 'Use singular "range"' },
109 |       { pattern: 'worksheets', suggestion: 'sheet', confidence: 0.8, reason: 'Use "sheet" for worksheet operations' },
110 |     ]);
111 | 
112 |     // Email patterns
113 |     patterns.set('email', [
114 |       { pattern: 'emails', suggestion: 'email', confidence: 0.95, reason: 'Use singular "email" not plural' },
115 |       { pattern: 'messages', suggestion: 'message', confidence: 0.9, reason: 'Use "message" for email operations' },
116 |       { pattern: 'mails', suggestion: 'email', confidence: 0.9, reason: 'Use "email" not "mail"' },
117 |       { pattern: 'attachments', suggestion: 'attachment', confidence: 0.95, reason: 'Use singular "attachment"' },
118 |     ]);
119 | 
120 |     // Generic plural/singular patterns
121 |     patterns.set('generic', [
122 |       { pattern: 'items', suggestion: 'item', confidence: 0.9, reason: 'Use singular form' },
123 |       { pattern: 'objects', suggestion: 'object', confidence: 0.9, reason: 'Use singular form' },
124 |       { pattern: 'entities', suggestion: 'entity', confidence: 0.9, reason: 'Use singular form' },
125 |       { pattern: 'resources', suggestion: 'resource', confidence: 0.9, reason: 'Use singular form' },
126 |       { pattern: 'elements', suggestion: 'element', confidence: 0.9, reason: 'Use singular form' },
127 |     ]);
128 | 
129 |     return patterns;
130 |   }
131 | 
132 |   /**
133 |    * Find similar resources for an invalid resource using pattern matching
134 |    * and Levenshtein distance algorithms
135 |    *
136 |    * @param nodeType - The n8n node type (e.g., 'nodes-base.googleDrive')
137 |    * @param invalidResource - The invalid resource provided by the user
138 |    * @param maxSuggestions - Maximum number of suggestions to return (default: 5)
139 |    * @returns Array of resource suggestions sorted by confidence
140 |    *
141 |    * @example
142 |    * findSimilarResources('nodes-base.googleDrive', 'files', 3)
143 |    * // Returns: [{ value: 'file', confidence: 0.95, reason: 'Use singular "file" not plural' }]
144 |    */
145 |   findSimilarResources(
146 |     nodeType: string,
147 |     invalidResource: string,
148 |     maxSuggestions: number = ResourceSimilarityService.MAX_SUGGESTIONS
149 |   ): ResourceSuggestion[] {
150 |     // Clean up expired cache entries periodically
151 |     if (Math.random() < 0.1) { // 10% chance to cleanup on each call
152 |       this.cleanupExpiredEntries();
153 |     }
154 |     // Check cache first
155 |     const cacheKey = `${nodeType}:${invalidResource}`;
156 |     if (this.suggestionCache.has(cacheKey)) {
157 |       return this.suggestionCache.get(cacheKey)!;
158 |     }
159 | 
160 |     const suggestions: ResourceSuggestion[] = [];
161 | 
162 |     // Get valid resources for the node
163 |     const validResources = this.getNodeResources(nodeType);
164 | 
165 |     // Early termination for exact match - no suggestions needed
166 |     for (const resource of validResources) {
167 |       const resourceValue = this.getResourceValue(resource);
168 |       if (resourceValue.toLowerCase() === invalidResource.toLowerCase()) {
169 |         return []; // Valid resource, no suggestions needed
170 |       }
171 |     }
172 | 
173 |     // Check for exact pattern matches first
174 |     const nodePatterns = this.getNodePatterns(nodeType);
175 |     for (const pattern of nodePatterns) {
176 |       if (pattern.pattern.toLowerCase() === invalidResource.toLowerCase()) {
177 |         // Check if the suggested resource actually exists with type safety
178 |         const exists = validResources.some(r => {
179 |           const resourceValue = this.getResourceValue(r);
180 |           return resourceValue === pattern.suggestion;
181 |         });
182 |         if (exists) {
183 |           suggestions.push({
184 |             value: pattern.suggestion,
185 |             confidence: pattern.confidence,
186 |             reason: pattern.reason
187 |           });
188 |         }
189 |       }
190 |     }
191 | 
192 |     // Handle automatic plural/singular conversion
193 |     const singularForm = this.toSingular(invalidResource);
194 |     const pluralForm = this.toPlural(invalidResource);
195 | 
196 |     for (const resource of validResources) {
197 |       const resourceValue = this.getResourceValue(resource);
198 | 
199 |       // Check for plural/singular match
200 |       if (resourceValue === singularForm || resourceValue === pluralForm) {
201 |         if (!suggestions.some(s => s.value === resourceValue)) {
202 |           suggestions.push({
203 |             value: resourceValue,
204 |             confidence: 0.9,
205 |             reason: invalidResource.endsWith('s') ?
206 |               'Use singular form for resources' :
207 |               'Incorrect plural/singular form',
208 |             availableOperations: typeof resource === 'object' ? resource.operations : undefined
209 |           });
210 |         }
211 |       }
212 | 
213 |       // Calculate similarity
214 |       const similarity = this.calculateSimilarity(invalidResource, resourceValue);
215 |       if (similarity >= ResourceSimilarityService.MIN_CONFIDENCE) {
216 |         if (!suggestions.some(s => s.value === resourceValue)) {
217 |           suggestions.push({
218 |             value: resourceValue,
219 |             confidence: similarity,
220 |             reason: this.getSimilarityReason(similarity, invalidResource, resourceValue),
221 |             availableOperations: typeof resource === 'object' ? resource.operations : undefined
222 |           });
223 |         }
224 |       }
225 |     }
226 | 
227 |     // Sort by confidence and limit
228 |     suggestions.sort((a, b) => b.confidence - a.confidence);
229 |     const topSuggestions = suggestions.slice(0, maxSuggestions);
230 | 
231 |     // Cache the result
232 |     this.suggestionCache.set(cacheKey, topSuggestions);
233 | 
234 |     return topSuggestions;
235 |   }
236 | 
237 |   /**
238 |    * Type-safe extraction of resource value from various formats
239 |    * @param resource - Resource object or string
240 |    * @returns The resource value as a string
241 |    */
242 |   private getResourceValue(resource: any): string {
243 |     if (typeof resource === 'string') {
244 |       return resource;
245 |     }
246 |     if (typeof resource === 'object' && resource !== null) {
247 |       return resource.value || '';
248 |     }
249 |     return '';
250 |   }
251 | 
252 |   /**
253 |    * Get resources for a node with caching
254 |    */
255 |   private getNodeResources(nodeType: string): any[] {
256 |     // Cleanup cache periodically
257 |     if (Math.random() < 0.05) { // 5% chance
258 |       this.cleanupExpiredEntries();
259 |     }
260 | 
261 |     const cacheKey = nodeType;
262 |     const cached = this.resourceCache.get(cacheKey);
263 | 
264 |     if (cached && Date.now() - cached.timestamp < ResourceSimilarityService.CACHE_DURATION_MS) {
265 |       return cached.resources;
266 |     }
267 | 
268 |     const nodeInfo = this.repository.getNode(nodeType);
269 |     if (!nodeInfo) return [];
270 | 
271 |     const resources: any[] = [];
272 |     const resourceMap: Map<string, string[]> = new Map();
273 | 
274 |     // Parse properties for resource fields
275 |     try {
276 |       const properties = nodeInfo.properties || [];
277 |       for (const prop of properties) {
278 |         if (prop.name === 'resource' && prop.options) {
279 |           for (const option of prop.options) {
280 |             resources.push({
281 |               value: option.value,
282 |               name: option.name,
283 |               operations: []
284 |             });
285 |             resourceMap.set(option.value, []);
286 |           }
287 |         }
288 | 
289 |         // Find operations for each resource
290 |         if (prop.name === 'operation' && prop.displayOptions?.show?.resource) {
291 |           const resourceValues = Array.isArray(prop.displayOptions.show.resource)
292 |             ? prop.displayOptions.show.resource
293 |             : [prop.displayOptions.show.resource];
294 | 
295 |           for (const resourceValue of resourceValues) {
296 |             if (resourceMap.has(resourceValue) && prop.options) {
297 |               const ops = prop.options.map((op: any) => op.value);
298 |               resourceMap.get(resourceValue)!.push(...ops);
299 |             }
300 |           }
301 |         }
302 |       }
303 | 
304 |       // Update resources with their operations
305 |       for (const resource of resources) {
306 |         if (resourceMap.has(resource.value)) {
307 |           resource.operations = resourceMap.get(resource.value);
308 |         }
309 |       }
310 | 
311 |       // If no explicit resources, check for common patterns
312 |       if (resources.length === 0) {
313 |         // Some nodes don't have explicit resource fields
314 |         const implicitResources = this.extractImplicitResources(properties);
315 |         resources.push(...implicitResources);
316 |       }
317 |     } catch (error) {
318 |       logger.warn(`Failed to extract resources for ${nodeType}:`, error);
319 |     }
320 | 
321 |     // Cache and return
322 |     this.resourceCache.set(cacheKey, { resources, timestamp: Date.now() });
323 |     return resources;
324 |   }
325 | 
326 |   /**
327 |    * Extract implicit resources from node properties
328 |    */
329 |   private extractImplicitResources(properties: any[]): any[] {
330 |     const resources: any[] = [];
331 | 
332 |     // Look for properties that suggest resources
333 |     for (const prop of properties) {
334 |       if (prop.name === 'operation' && prop.options) {
335 |         // If there's no explicit resource field, operations might imply resources
336 |         const resourceFromOps = this.inferResourceFromOperations(prop.options);
337 |         if (resourceFromOps) {
338 |           resources.push({
339 |             value: resourceFromOps,
340 |             name: resourceFromOps.charAt(0).toUpperCase() + resourceFromOps.slice(1),
341 |             operations: prop.options.map((op: any) => op.value)
342 |           });
343 |         }
344 |       }
345 |     }
346 | 
347 |     return resources;
348 |   }
349 | 
350 |   /**
351 |    * Infer resource type from operations
352 |    */
353 |   private inferResourceFromOperations(operations: any[]): string | null {
354 |     // Common patterns in operation names that suggest resources
355 |     const patterns = [
356 |       { keywords: ['file', 'upload', 'download'], resource: 'file' },
357 |       { keywords: ['folder', 'directory'], resource: 'folder' },
358 |       { keywords: ['message', 'send', 'reply'], resource: 'message' },
359 |       { keywords: ['channel', 'broadcast'], resource: 'channel' },
360 |       { keywords: ['user', 'member'], resource: 'user' },
361 |       { keywords: ['table', 'row', 'column'], resource: 'table' },
362 |       { keywords: ['document', 'doc'], resource: 'document' },
363 |     ];
364 | 
365 |     for (const pattern of patterns) {
366 |       for (const op of operations) {
367 |         const opName = (op.value || op).toLowerCase();
368 |         if (pattern.keywords.some(keyword => opName.includes(keyword))) {
369 |           return pattern.resource;
370 |         }
371 |       }
372 |     }
373 | 
374 |     return null;
375 |   }
376 | 
377 |   /**
378 |    * Get patterns for a specific node type
379 |    */
380 |   private getNodePatterns(nodeType: string): ResourcePattern[] {
381 |     const patterns: ResourcePattern[] = [];
382 | 
383 |     // Add node-specific patterns
384 |     if (nodeType.includes('googleDrive')) {
385 |       patterns.push(...(this.commonPatterns.get('googleDrive') || []));
386 |     } else if (nodeType.includes('slack')) {
387 |       patterns.push(...(this.commonPatterns.get('slack') || []));
388 |     } else if (nodeType.includes('postgres') || nodeType.includes('mysql') || nodeType.includes('mongodb')) {
389 |       patterns.push(...(this.commonPatterns.get('database') || []));
390 |     } else if (nodeType.includes('googleSheets')) {
391 |       patterns.push(...(this.commonPatterns.get('googleSheets') || []));
392 |     } else if (nodeType.includes('gmail') || nodeType.includes('email')) {
393 |       patterns.push(...(this.commonPatterns.get('email') || []));
394 |     }
395 | 
396 |     // Always add generic patterns
397 |     patterns.push(...(this.commonPatterns.get('generic') || []));
398 | 
399 |     return patterns;
400 |   }
401 | 
402 |   /**
403 |    * Convert to singular form (simple heuristic)
404 |    */
405 |   private toSingular(word: string): string {
406 |     if (word.endsWith('ies')) {
407 |       return word.slice(0, -3) + 'y';
408 |     } else if (word.endsWith('es')) {
409 |       return word.slice(0, -2);
410 |     } else if (word.endsWith('s') && !word.endsWith('ss')) {
411 |       return word.slice(0, -1);
412 |     }
413 |     return word;
414 |   }
415 | 
416 |   /**
417 |    * Convert to plural form (simple heuristic)
418 |    */
419 |   private toPlural(word: string): string {
420 |     if (word.endsWith('y') && !['ay', 'ey', 'iy', 'oy', 'uy'].includes(word.slice(-2))) {
421 |       return word.slice(0, -1) + 'ies';
422 |     } else if (word.endsWith('s') || word.endsWith('x') || word.endsWith('z') ||
423 |                word.endsWith('ch') || word.endsWith('sh')) {
424 |       return word + 'es';
425 |     } else {
426 |       return word + 's';
427 |     }
428 |   }
429 | 
430 |   /**
431 |    * Calculate similarity between two strings using Levenshtein distance
432 |    */
433 |   private calculateSimilarity(str1: string, str2: string): number {
434 |     const s1 = str1.toLowerCase();
435 |     const s2 = str2.toLowerCase();
436 | 
437 |     // Exact match
438 |     if (s1 === s2) return 1.0;
439 | 
440 |     // One is substring of the other
441 |     if (s1.includes(s2) || s2.includes(s1)) {
442 |       const ratio = Math.min(s1.length, s2.length) / Math.max(s1.length, s2.length);
443 |       return Math.max(ResourceSimilarityService.CONFIDENCE_THRESHOLDS.MIN_SUBSTRING, ratio);
444 |     }
445 | 
446 |     // Calculate Levenshtein distance
447 |     const distance = this.levenshteinDistance(s1, s2);
448 |     const maxLength = Math.max(s1.length, s2.length);
449 | 
450 |     // Convert distance to similarity
451 |     let similarity = 1 - (distance / maxLength);
452 | 
453 |     // Boost confidence for single character typos and transpositions in short words
454 |     if (distance === 1 && maxLength <= 5) {
455 |       similarity = Math.max(similarity, 0.75);
456 |     } else if (distance === 2 && maxLength <= 5) {
457 |       // Boost for transpositions (e.g., "flie" -> "file")
458 |       similarity = Math.max(similarity, 0.72);
459 |     }
460 | 
461 |     return similarity;
462 |   }
463 | 
464 |   /**
465 |    * Calculate Levenshtein distance between two strings
466 |    */
467 |   private levenshteinDistance(str1: string, str2: string): number {
468 |     const m = str1.length;
469 |     const n = str2.length;
470 |     const dp: number[][] = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
471 | 
472 |     for (let i = 0; i <= m; i++) dp[i][0] = i;
473 |     for (let j = 0; j <= n; j++) dp[0][j] = j;
474 | 
475 |     for (let i = 1; i <= m; i++) {
476 |       for (let j = 1; j <= n; j++) {
477 |         if (str1[i - 1] === str2[j - 1]) {
478 |           dp[i][j] = dp[i - 1][j - 1];
479 |         } else {
480 |           dp[i][j] = Math.min(
481 |             dp[i - 1][j] + 1,    // deletion
482 |             dp[i][j - 1] + 1,    // insertion
483 |             dp[i - 1][j - 1] + 1 // substitution
484 |           );
485 |         }
486 |       }
487 |     }
488 | 
489 |     return dp[m][n];
490 |   }
491 | 
492 |   /**
493 |    * Generate a human-readable reason for the similarity
494 |    * @param confidence - Similarity confidence score
495 |    * @param invalid - The invalid resource string
496 |    * @param valid - The valid resource string
497 |    * @returns Human-readable explanation of the similarity
498 |    */
499 |   private getSimilarityReason(confidence: number, invalid: string, valid: string): string {
500 |     const { VERY_HIGH, HIGH, MEDIUM } = ResourceSimilarityService.CONFIDENCE_THRESHOLDS;
501 | 
502 |     if (confidence >= VERY_HIGH) {
503 |       return 'Almost exact match - likely a typo';
504 |     } else if (confidence >= HIGH) {
505 |       return 'Very similar - common variation';
506 |     } else if (confidence >= MEDIUM) {
507 |       return 'Similar resource name';
508 |     } else if (invalid.includes(valid) || valid.includes(invalid)) {
509 |       return 'Partial match';
510 |     } else {
511 |       return 'Possibly related resource';
512 |     }
513 |   }
514 | 
515 |   /**
516 |    * Clear caches
517 |    */
518 |   clearCache(): void {
519 |     this.resourceCache.clear();
520 |     this.suggestionCache.clear();
521 |   }
522 | }
```

--------------------------------------------------------------------------------
/tests/unit/parsers/property-extractor.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach } from 'vitest';
  2 | import { PropertyExtractor } from '@/parsers/property-extractor';
  3 | import {
  4 |   programmaticNodeFactory,
  5 |   declarativeNodeFactory,
  6 |   versionedNodeClassFactory,
  7 |   versionedNodeTypeClassFactory,
  8 |   nodeClassFactory,
  9 |   propertyFactory,
 10 |   stringPropertyFactory,
 11 |   numberPropertyFactory,
 12 |   booleanPropertyFactory,
 13 |   optionsPropertyFactory,
 14 |   collectionPropertyFactory,
 15 |   nestedPropertyFactory,
 16 |   resourcePropertyFactory,
 17 |   operationPropertyFactory,
 18 |   aiToolNodeFactory
 19 | } from '@tests/fixtures/factories/parser-node.factory';
 20 | 
 21 | describe('PropertyExtractor', () => {
 22 |   let extractor: PropertyExtractor;
 23 | 
 24 |   beforeEach(() => {
 25 |     extractor = new PropertyExtractor();
 26 |   });
 27 | 
 28 |   describe('extractProperties', () => {
 29 |     it('should extract properties from programmatic node', () => {
 30 |       const nodeDefinition = programmaticNodeFactory.build();
 31 |       const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
 32 |       
 33 |       const properties = extractor.extractProperties(NodeClass as any);
 34 |       
 35 |       expect(properties).toHaveLength(nodeDefinition.properties.length);
 36 |       expect(properties).toEqual(expect.arrayContaining(
 37 |         nodeDefinition.properties.map(prop => expect.objectContaining({
 38 |           displayName: prop.displayName,
 39 |           name: prop.name,
 40 |           type: prop.type,
 41 |           default: prop.default
 42 |         }))
 43 |       ));
 44 |     });
 45 | 
 46 |     it('should extract properties from versioned node latest version', () => {
 47 |       const versionedDef = versionedNodeClassFactory.build();
 48 |       const NodeClass = class {
 49 |         nodeVersions = versionedDef.nodeVersions;
 50 |         baseDescription = versionedDef.baseDescription;
 51 |       };
 52 |       
 53 |       const properties = extractor.extractProperties(NodeClass as any);
 54 |       
 55 |       // Should get properties from version 2 (latest)
 56 |       expect(properties).toHaveLength(versionedDef.nodeVersions![2].description.properties.length);
 57 |     });
 58 | 
 59 |     it('should extract properties from instance with nodeVersions', () => {
 60 |       const NodeClass = class {
 61 |         description = { name: 'test' };
 62 |         constructor() {
 63 |           (this as any).nodeVersions = {
 64 |             1: {
 65 |               description: {
 66 |                 properties: [propertyFactory.build({ name: 'v1prop' })]
 67 |               }
 68 |             },
 69 |             2: {
 70 |               description: {
 71 |                 properties: [
 72 |                   propertyFactory.build({ name: 'v2prop1' }),
 73 |                   propertyFactory.build({ name: 'v2prop2' })
 74 |                 ]
 75 |               }
 76 |             }
 77 |           };
 78 |         }
 79 |       };
 80 |       
 81 |       const properties = extractor.extractProperties(NodeClass as any);
 82 |       
 83 |       expect(properties).toHaveLength(2);
 84 |       expect(properties[0].name).toBe('v2prop1');
 85 |       expect(properties[1].name).toBe('v2prop2');
 86 |     });
 87 | 
 88 |     it('should normalize properties to consistent structure', () => {
 89 |       const rawProperties = [
 90 |         {
 91 |           displayName: 'Field 1',
 92 |           name: 'field1',
 93 |           type: 'string',
 94 |           default: 'value',
 95 |           description: 'Test field',
 96 |           required: true,
 97 |           displayOptions: { show: { resource: ['user'] } },
 98 |           typeOptions: { multipleValues: true },
 99 |           noDataExpression: false,
100 |           extraField: 'should be removed'
101 |         }
102 |       ];
103 |       
104 |       const NodeClass = nodeClassFactory.build({
105 |         description: { 
106 |           name: 'test',
107 |           properties: rawProperties 
108 |         }
109 |       });
110 |       
111 |       const properties = extractor.extractProperties(NodeClass as any);
112 |       
113 |       expect(properties[0]).toEqual({
114 |         displayName: 'Field 1',
115 |         name: 'field1',
116 |         type: 'string',
117 |         default: 'value',
118 |         description: 'Test field',
119 |         options: undefined,
120 |         required: true,
121 |         displayOptions: { show: { resource: ['user'] } },
122 |         typeOptions: { multipleValues: true },
123 |         noDataExpression: false
124 |       });
125 |       
126 |       expect(properties[0]).not.toHaveProperty('extraField');
127 |     });
128 | 
129 |     it('should handle nodes without properties', () => {
130 |       const NodeClass = nodeClassFactory.build({
131 |         description: {
132 |           name: 'test',
133 |           displayName: 'Test'
134 |           // No properties field
135 |         }
136 |       });
137 |       
138 |       const properties = extractor.extractProperties(NodeClass as any);
139 |       
140 |       expect(properties).toEqual([]);
141 |     });
142 | 
143 |     it('should handle failed instantiation', () => {
144 |       const NodeClass = class {
145 |         static description = {
146 |           name: 'test',
147 |           properties: [propertyFactory.build()]
148 |         };
149 |         constructor() {
150 |           throw new Error('Cannot instantiate');
151 |         }
152 |       };
153 |       
154 |       const properties = extractor.extractProperties(NodeClass as any);
155 |       
156 |       expect(properties).toHaveLength(1); // Should get static description property
157 |     });
158 | 
159 |     it('should extract from baseDescription when main description is missing', () => {
160 |       const NodeClass = class {
161 |         baseDescription = {
162 |           properties: [
163 |             stringPropertyFactory.build({ name: 'baseProp' })
164 |           ]
165 |         };
166 |       };
167 |       
168 |       const properties = extractor.extractProperties(NodeClass as any);
169 |       
170 |       expect(properties).toHaveLength(1);
171 |       expect(properties[0].name).toBe('baseProp');
172 |     });
173 | 
174 |     it('should handle complex nested properties', () => {
175 |       const nestedProp = nestedPropertyFactory.build();
176 |       const NodeClass = nodeClassFactory.build({
177 |         description: {
178 |           name: 'test',
179 |           properties: [nestedProp]
180 |         }
181 |       });
182 |       
183 |       const properties = extractor.extractProperties(NodeClass as any);
184 |       
185 |       expect(properties).toHaveLength(1);
186 |       expect(properties[0].type).toBe('collection');
187 |       expect(properties[0].options).toBeDefined();
188 |     });
189 | 
190 |     it('should handle non-function node classes', () => {
191 |       const nodeInstance = {
192 |         description: {
193 |           properties: [propertyFactory.build()]
194 |         }
195 |       };
196 | 
197 |       const properties = extractor.extractProperties(nodeInstance as any);
198 | 
199 |       expect(properties).toHaveLength(1);
200 |     });
201 |   });
202 | 
203 |   describe('extractOperations', () => {
204 |     it('should extract operations from declarative node routing', () => {
205 |       const nodeDefinition = declarativeNodeFactory.build();
206 |       const NodeClass = nodeClassFactory.build({ description: nodeDefinition });
207 |       
208 |       const operations = extractor.extractOperations(NodeClass as any);
209 |       
210 |       // Declarative node has 2 resources with 2 operations each = 4 total
211 |       expect(operations.length).toBe(4);
212 |       
213 |       // Check that we have operations for each resource
214 |       const userOps = operations.filter(op => op.resource === 'user');
215 |       const postOps = operations.filter(op => op.resource === 'post');
216 |       
217 |       expect(userOps.length).toBe(2); // Create and Get
218 |       expect(postOps.length).toBe(2); // Create and List
219 |       
220 |       // Verify operation structure
221 |       expect(userOps[0]).toMatchObject({
222 |         resource: 'user',
223 |         operation: expect.any(String),
224 |         name: expect.any(String),
225 |         action: expect.any(String)
226 |       });
227 |     });
228 | 
229 |     it('should extract operations when node has programmatic properties', () => {
230 |       const operationProp = operationPropertyFactory.build();
231 |       const NodeClass = nodeClassFactory.build({
232 |         description: {
233 |           name: 'test',
234 |           properties: [operationProp]
235 |         }
236 |       });
237 |       
238 |       const operations = extractor.extractOperations(NodeClass as any);
239 |       
240 |       expect(operations.length).toBe(operationProp.options!.length);
241 |       operations.forEach((op, idx) => {
242 |         expect(op).toMatchObject({
243 |           operation: operationProp.options![idx].value,
244 |           name: operationProp.options![idx].name,
245 |           description: operationProp.options![idx].description
246 |         });
247 |       });
248 |     });
249 | 
250 |     it('should extract operations when routing.operations structure exists', () => {
251 |       const NodeClass = nodeClassFactory.build({
252 |         description: {
253 |           name: 'test',
254 |           routing: {
255 |             operations: {
256 |               create: { displayName: 'Create Item' },
257 |               update: { displayName: 'Update Item' },
258 |               delete: { displayName: 'Delete Item' }
259 |             }
260 |           }
261 |         }
262 |       });
263 |       
264 |       const operations = extractor.extractOperations(NodeClass as any);
265 |       
266 |       // routing.operations is not currently extracted by the property extractor
267 |       // It only extracts from routing.request structure
268 |       expect(operations).toHaveLength(0);
269 |     });
270 | 
271 |     it('should handle operations when programmatic nodes have resource-based structure', () => {
272 |       const resourceProp = resourcePropertyFactory.build();
273 |       const operationProp = {
274 |         displayName: 'Operation',
275 |         name: 'operation',
276 |         type: 'options',
277 |         displayOptions: {
278 |           show: {
279 |             resource: ['user', 'post']
280 |           }
281 |         },
282 |         options: [
283 |           { name: 'Create', value: 'create', action: 'Create item' },
284 |           { name: 'Delete', value: 'delete', action: 'Delete item' }
285 |         ]
286 |       };
287 |       
288 |       const NodeClass = nodeClassFactory.build({
289 |         description: {
290 |           name: 'test',
291 |           properties: [resourceProp, operationProp]
292 |         }
293 |       });
294 |       
295 |       const operations = extractor.extractOperations(NodeClass as any);
296 |       
297 |       // PropertyExtractor only extracts operations, not resources
298 |       // It should find the operation property and extract its options
299 |       expect(operations).toHaveLength(operationProp.options.length);
300 |       expect(operations[0]).toMatchObject({
301 |         operation: 'create',
302 |         name: 'Create',
303 |         description: undefined // action field is not mapped to description
304 |       });
305 |       expect(operations[1]).toMatchObject({
306 |         operation: 'delete',
307 |         name: 'Delete',
308 |         description: undefined
309 |       });
310 |     });
311 | 
312 |     it('should return empty array when node has no operations', () => {
313 |       const NodeClass = nodeClassFactory.build({
314 |         description: {
315 |           name: 'test',
316 |           properties: [stringPropertyFactory.build()]
317 |         }
318 |       });
319 |       
320 |       const operations = extractor.extractOperations(NodeClass as any);
321 |       
322 |       expect(operations).toEqual([]);
323 |     });
324 | 
325 |     it('should extract operations when node has version structure', () => {
326 |       const NodeClass = class {
327 |         nodeVersions = {
328 |           1: {
329 |             description: {
330 |               properties: []
331 |             }
332 |           },
333 |           2: {
334 |             description: {
335 |               routing: {
336 |                 request: {
337 |                   resource: {
338 |                     options: [
339 |                       { name: 'User', value: 'user' }
340 |                     ]
341 |                   },
342 |                   operation: {
343 |                     options: {
344 |                       user: [
345 |                         { name: 'Get', value: 'get', action: 'Get a user' }
346 |                       ]
347 |                     }
348 |                   }
349 |                 }
350 |               }
351 |             }
352 |           }
353 |         };
354 |       };
355 |       
356 |       const operations = extractor.extractOperations(NodeClass as any);
357 |       
358 |       expect(operations).toHaveLength(1);
359 |       expect(operations[0]).toMatchObject({
360 |         resource: 'user',
361 |         operation: 'get',
362 |         name: 'User - Get',
363 |         action: 'Get a user'
364 |       });
365 |     });
366 | 
367 |     it('should handle extraction when property is named action instead of operation', () => {
368 |       const actionProp = {
369 |         displayName: 'Action',
370 |         name: 'action',
371 |         type: 'options',
372 |         options: [
373 |           { name: 'Send', value: 'send' },
374 |           { name: 'Receive', value: 'receive' }
375 |         ]
376 |       };
377 |       
378 |       const NodeClass = nodeClassFactory.build({
379 |         description: {
380 |           name: 'test',
381 |           properties: [actionProp]
382 |         }
383 |       });
384 |       
385 |       const operations = extractor.extractOperations(NodeClass as any);
386 |       
387 |       expect(operations).toHaveLength(2);
388 |       expect(operations[0].operation).toBe('send');
389 |     });
390 |   });
391 | 
392 |   describe('detectAIToolCapability', () => {
393 |     it('should detect AI capability when usableAsTool property is true', () => {
394 |       const NodeClass = nodeClassFactory.build({
395 |         description: {
396 |           name: 'test',
397 |           usableAsTool: true
398 |         }
399 |       });
400 |       
401 |       const isAITool = extractor.detectAIToolCapability(NodeClass as any);
402 |       
403 |       expect(isAITool).toBe(true);
404 |     });
405 | 
406 |     it('should detect AI capability when actions contain usableAsTool', () => {
407 |       const NodeClass = nodeClassFactory.build({
408 |         description: {
409 |           name: 'test',
410 |           actions: [
411 |             { name: 'action1', usableAsTool: false },
412 |             { name: 'action2', usableAsTool: true }
413 |           ]
414 |         }
415 |       });
416 |       
417 |       const isAITool = extractor.detectAIToolCapability(NodeClass as any);
418 |       
419 |       expect(isAITool).toBe(true);
420 |     });
421 | 
422 |     it('should detect AI capability when versioned node has usableAsTool', () => {
423 |       const NodeClass = {
424 |         nodeVersions: {
425 |           1: {
426 |             description: { usableAsTool: false }
427 |           },
428 |           2: {
429 |             description: { usableAsTool: true }
430 |           }
431 |         }
432 |       };
433 |       
434 |       const isAITool = extractor.detectAIToolCapability(NodeClass as any);
435 |       
436 |       expect(isAITool).toBe(true);
437 |     });
438 | 
439 |     it('should detect AI capability when node name contains AI-related terms', () => {
440 |       const aiNodeNames = ['openai', 'anthropic', 'huggingface', 'cohere', 'myai'];
441 |       
442 |       aiNodeNames.forEach(name => {
443 |         const NodeClass = nodeClassFactory.build({
444 |           description: { name }
445 |         });
446 |         
447 |         const isAITool = extractor.detectAIToolCapability(NodeClass as any);
448 |         
449 |         expect(isAITool).toBe(true);
450 |       });
451 |     });
452 | 
453 |     it('should return false when node is not AI-related', () => {
454 |       const NodeClass = nodeClassFactory.build({
455 |         description: {
456 |           name: 'slack',
457 |           usableAsTool: false
458 |         }
459 |       });
460 |       
461 |       const isAITool = extractor.detectAIToolCapability(NodeClass as any);
462 |       
463 |       expect(isAITool).toBe(false);
464 |     });
465 | 
466 |     it('should return false when node has no description', () => {
467 |       const NodeClass = class {};
468 |       
469 |       const isAITool = extractor.detectAIToolCapability(NodeClass as any);
470 |       
471 |       expect(isAITool).toBe(false);
472 |     });
473 |   });
474 | 
475 |   describe('extractCredentials', () => {
476 |     it('should extract credentials when node description contains them', () => {
477 |       const credentials = [
478 |         { name: 'apiKey', required: true },
479 |         { name: 'oauth2', required: false }
480 |       ];
481 |       
482 |       const NodeClass = nodeClassFactory.build({
483 |         description: {
484 |           name: 'test',
485 |           credentials
486 |         }
487 |       });
488 |       
489 |       const extracted = extractor.extractCredentials(NodeClass as any);
490 |       
491 |       expect(extracted).toEqual(credentials);
492 |     });
493 | 
494 |     it('should extract credentials when node has version structure', () => {
495 |       const NodeClass = class {
496 |         nodeVersions = {
497 |           1: {
498 |             description: {
499 |               credentials: [{ name: 'basic', required: true }]
500 |             }
501 |           },
502 |           2: {
503 |             description: {
504 |               credentials: [
505 |                 { name: 'oauth2', required: true },
506 |                 { name: 'apiKey', required: false }
507 |               ]
508 |             }
509 |           }
510 |         };
511 |       };
512 |       
513 |       const credentials = extractor.extractCredentials(NodeClass as any);
514 |       
515 |       expect(credentials).toHaveLength(2);
516 |       expect(credentials[0].name).toBe('oauth2');
517 |       expect(credentials[1].name).toBe('apiKey');
518 |     });
519 | 
520 |     it('should return empty array when node has no credentials', () => {
521 |       const NodeClass = nodeClassFactory.build({
522 |         description: {
523 |           name: 'test'
524 |           // No credentials field
525 |         }
526 |       });
527 |       
528 |       const credentials = extractor.extractCredentials(NodeClass as any);
529 |       
530 |       expect(credentials).toEqual([]);
531 |     });
532 | 
533 |     it('should extract credentials when only baseDescription has them', () => {
534 |       const NodeClass = class {
535 |         baseDescription = {
536 |           credentials: [{ name: 'token', required: true }]
537 |         };
538 |       };
539 |       
540 |       const credentials = extractor.extractCredentials(NodeClass as any);
541 |       
542 |       expect(credentials).toHaveLength(1);
543 |       expect(credentials[0].name).toBe('token');
544 |     });
545 | 
546 |     it('should extract credentials when they are defined at instance level', () => {
547 |       const NodeClass = class {
548 |         constructor() {
549 |           (this as any).description = {
550 |             credentials: [
551 |               { name: 'jwt', required: true }
552 |             ]
553 |           };
554 |         }
555 |       };
556 |       
557 |       const credentials = extractor.extractCredentials(NodeClass as any);
558 |       
559 |       expect(credentials).toHaveLength(1);
560 |       expect(credentials[0].name).toBe('jwt');
561 |     });
562 | 
563 |     it('should return empty array when instantiation fails', () => {
564 |       const NodeClass = class {
565 |         constructor() {
566 |           throw new Error('Cannot instantiate');
567 |         }
568 |       };
569 |       
570 |       const credentials = extractor.extractCredentials(NodeClass as any);
571 |       
572 |       expect(credentials).toEqual([]);
573 |     });
574 |   });
575 | 
576 |   describe('edge cases', () => {
577 |     it('should handle extraction when properties are deeply nested', () => {
578 |       const deepProperty = {
579 |         displayName: 'Deep Options',
580 |         name: 'deepOptions',
581 |         type: 'collection',
582 |         options: [
583 |           {
584 |             displayName: 'Level 1',
585 |             name: 'level1',
586 |             type: 'collection',
587 |             options: [
588 |               {
589 |                 displayName: 'Level 2',
590 |                 name: 'level2',
591 |                 type: 'collection',
592 |                 options: [
593 |                   stringPropertyFactory.build({ name: 'deepValue' })
594 |                 ]
595 |               }
596 |             ]
597 |           }
598 |         ]
599 |       };
600 |       
601 |       const NodeClass = nodeClassFactory.build({
602 |         description: {
603 |           name: 'test',
604 |           properties: [deepProperty]
605 |         }
606 |       });
607 |       
608 |       const properties = extractor.extractProperties(NodeClass as any);
609 |       
610 |       expect(properties).toHaveLength(1);
611 |       expect(properties[0].name).toBe('deepOptions');
612 |       expect(properties[0].options[0].options[0].options).toBeDefined();
613 |     });
614 | 
615 |     it('should not throw when node structure has circular references', () => {
616 |       const NodeClass = class {
617 |         description: any = { name: 'test' };
618 |         constructor() {
619 |           this.description.properties = [
620 |             {
621 |               name: 'prop1',
622 |               type: 'string',
623 |               parentRef: this.description // Circular reference
624 |             }
625 |           ];
626 |         }
627 |       };
628 |       
629 |       // Should not throw or hang
630 |       const properties = extractor.extractProperties(NodeClass as any);
631 |       
632 |       expect(properties).toBeDefined();
633 |     });
634 | 
635 |     it('should extract from all sources when multiple operation types exist', () => {
636 |       const NodeClass = nodeClassFactory.build({
637 |         description: {
638 |           name: 'test',
639 |           routing: {
640 |             request: {
641 |               resource: {
642 |                 options: [{ name: 'Resource1', value: 'res1' }]
643 |               }
644 |             },
645 |             operations: {
646 |               custom: { displayName: 'Custom Op' }
647 |             }
648 |           },
649 |           properties: [
650 |             operationPropertyFactory.build()
651 |           ]
652 |         }
653 |       });
654 |       
655 |       const operations = extractor.extractOperations(NodeClass as any);
656 |       
657 |       // Should extract from all sources
658 |       expect(operations.length).toBeGreaterThan(1);
659 |     });
660 |   });
661 | });
```

--------------------------------------------------------------------------------
/tests/integration/database/node-repository.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  2 | import Database from 'better-sqlite3';
  3 | import { NodeRepository } from '../../../src/database/node-repository';
  4 | import { DatabaseAdapter } from '../../../src/database/database-adapter';
  5 | import { TestDatabase, TestDataGenerator, MOCK_NODES, createTestDatabaseAdapter } from './test-utils';
  6 | import { ParsedNode } from '../../../src/parsers/node-parser';
  7 | 
  8 | describe('NodeRepository Integration Tests', () => {
  9 |   let testDb: TestDatabase;
 10 |   let db: Database.Database;
 11 |   let repository: NodeRepository;
 12 |   let adapter: DatabaseAdapter;
 13 | 
 14 |   beforeEach(async () => {
 15 |     testDb = new TestDatabase({ mode: 'memory' });
 16 |     db = await testDb.initialize();
 17 |     adapter = createTestDatabaseAdapter(db);
 18 |     repository = new NodeRepository(adapter);
 19 |   });
 20 | 
 21 |   afterEach(async () => {
 22 |     await testDb.cleanup();
 23 |   });
 24 | 
 25 |   describe('saveNode', () => {
 26 |     it('should save single node successfully', () => {
 27 |       const node = createParsedNode(MOCK_NODES.webhook);
 28 |       repository.saveNode(node);
 29 | 
 30 |       const saved = repository.getNode(node.nodeType);
 31 |       expect(saved).toBeTruthy();
 32 |       expect(saved.nodeType).toBe(node.nodeType);
 33 |       expect(saved.displayName).toBe(node.displayName);
 34 |     });
 35 | 
 36 |     it('should update existing nodes', () => {
 37 |       const node = createParsedNode(MOCK_NODES.webhook);
 38 |       
 39 |       // Save initial version
 40 |       repository.saveNode(node);
 41 |       
 42 |       // Update and save again
 43 |       const updated = { ...node, displayName: 'Updated Webhook' };
 44 |       repository.saveNode(updated);
 45 | 
 46 |       const saved = repository.getNode(node.nodeType);
 47 |       expect(saved?.displayName).toBe('Updated Webhook');
 48 |       
 49 |       // Should not create duplicate
 50 |       const count = repository.getNodeCount();
 51 |       expect(count).toBe(1);
 52 |     });
 53 | 
 54 |     it('should handle nodes with complex properties', () => {
 55 |       const complexNode: ParsedNode = {
 56 |         nodeType: 'n8n-nodes-base.complex',
 57 |         packageName: 'n8n-nodes-base',
 58 |         displayName: 'Complex Node',
 59 |         description: 'A complex node with many properties',
 60 |         category: 'automation',
 61 |         style: 'programmatic',
 62 |         isAITool: false,
 63 |         isTrigger: false,
 64 |         isWebhook: false,
 65 |         isVersioned: true,
 66 |         version: '1',
 67 |         documentation: 'Complex node documentation',
 68 |         properties: [
 69 |           {
 70 |             displayName: 'Resource',
 71 |             name: 'resource',
 72 |             type: 'options',
 73 |             options: [
 74 |               { name: 'User', value: 'user' },
 75 |               { name: 'Post', value: 'post' }
 76 |             ],
 77 |             default: 'user'
 78 |           },
 79 |           {
 80 |             displayName: 'Operation',
 81 |             name: 'operation',
 82 |             type: 'options',
 83 |             displayOptions: {
 84 |               show: {
 85 |                 resource: ['user']
 86 |               }
 87 |             },
 88 |             options: [
 89 |               { name: 'Create', value: 'create' },
 90 |               { name: 'Get', value: 'get' }
 91 |             ]
 92 |           }
 93 |         ],
 94 |         operations: [
 95 |           { resource: 'user', operation: 'create' },
 96 |           { resource: 'user', operation: 'get' }
 97 |         ],
 98 |         credentials: [
 99 |           {
100 |             name: 'httpBasicAuth',
101 |             required: false
102 |           }
103 |         ]
104 |       };
105 | 
106 |       repository.saveNode(complexNode);
107 |       
108 |       const saved = repository.getNode(complexNode.nodeType);
109 |       expect(saved).toBeTruthy();
110 |       expect(saved.properties).toHaveLength(2);
111 |       expect(saved.credentials).toHaveLength(1);
112 |       expect(saved.operations).toHaveLength(2);
113 |     });
114 | 
115 |     it('should handle very large nodes', () => {
116 |       const largeNode: ParsedNode = {
117 |         nodeType: 'n8n-nodes-base.large',
118 |         packageName: 'n8n-nodes-base',
119 |         displayName: 'Large Node',
120 |         description: 'A very large node',
121 |         category: 'automation',
122 |         style: 'programmatic',
123 |         isAITool: false,
124 |         isTrigger: false,
125 |         isWebhook: false,
126 |         isVersioned: true,
127 |         version: '1',
128 |         properties: Array.from({ length: 100 }, (_, i) => ({
129 |           displayName: `Property ${i}`,
130 |           name: `prop${i}`,
131 |           type: 'string',
132 |           default: ''
133 |         })),
134 |         operations: [],
135 |         credentials: []
136 |       };
137 | 
138 |       repository.saveNode(largeNode);
139 |       
140 |       const saved = repository.getNode(largeNode.nodeType);
141 |       expect(saved?.properties).toHaveLength(100);
142 |     });
143 |   });
144 | 
145 |   describe('getNode', () => {
146 |     beforeEach(() => {
147 |       repository.saveNode(createParsedNode(MOCK_NODES.webhook));
148 |       repository.saveNode(createParsedNode(MOCK_NODES.httpRequest));
149 |     });
150 | 
151 |     it('should retrieve node by type', () => {
152 |       const node = repository.getNode('n8n-nodes-base.webhook');
153 |       expect(node).toBeTruthy();
154 |       expect(node.displayName).toBe('Webhook');
155 |       expect(node.nodeType).toBe('n8n-nodes-base.webhook');
156 |       expect(node.package).toBe('n8n-nodes-base');
157 |     });
158 | 
159 |     it('should return null for non-existent node', () => {
160 |       const node = repository.getNode('n8n-nodes-base.nonExistent');
161 |       expect(node).toBeNull();
162 |     });
163 | 
164 |     it('should handle special characters in node types', () => {
165 |       const specialNode: ParsedNode = {
166 |         nodeType: 'n8n-nodes-base.special-chars_v2.node',
167 |         packageName: 'n8n-nodes-base',
168 |         displayName: 'Special Node',
169 |         description: 'Node with special characters',
170 |         category: 'automation',
171 |         style: 'programmatic',
172 |         isAITool: false,
173 |         isTrigger: false,
174 |         isWebhook: false,
175 |         isVersioned: true,
176 |         version: '2',
177 |         properties: [],
178 |         operations: [],
179 |         credentials: []
180 |       };
181 |       
182 |       repository.saveNode(specialNode);
183 |       const retrieved = repository.getNode(specialNode.nodeType);
184 |       expect(retrieved).toBeTruthy();
185 |     });
186 |   });
187 | 
188 |   describe('getAllNodes', () => {
189 |     it('should return empty array when no nodes', () => {
190 |       const nodes = repository.getAllNodes();
191 |       expect(nodes).toHaveLength(0);
192 |     });
193 | 
194 |     it('should return all nodes with limit', () => {
195 |       const nodes = Array.from({ length: 20 }, (_, i) => 
196 |         createParsedNode({
197 |           ...MOCK_NODES.webhook,
198 |           nodeType: `n8n-nodes-base.node${i}`,
199 |           displayName: `Node ${i}`
200 |         })
201 |       );
202 |       
203 |       nodes.forEach(node => repository.saveNode(node));
204 | 
205 |       const retrieved = repository.getAllNodes(10);
206 |       expect(retrieved).toHaveLength(10);
207 |     });
208 | 
209 |     it('should return all nodes without limit', () => {
210 |       const nodes = Array.from({ length: 20 }, (_, i) => 
211 |         createParsedNode({
212 |           ...MOCK_NODES.webhook,
213 |           nodeType: `n8n-nodes-base.node${i}`,
214 |           displayName: `Node ${i}`
215 |         })
216 |       );
217 |       
218 |       nodes.forEach(node => repository.saveNode(node));
219 | 
220 |       const retrieved = repository.getAllNodes();
221 |       expect(retrieved).toHaveLength(20);
222 |     });
223 | 
224 |     it('should handle very large result sets efficiently', () => {
225 |       const nodes = Array.from({ length: 1000 }, (_, i) => 
226 |         createParsedNode({
227 |           ...MOCK_NODES.webhook,
228 |           nodeType: `n8n-nodes-base.node${i}`,
229 |           displayName: `Node ${i}`
230 |         })
231 |       );
232 |       
233 |       const insertMany = db.transaction((nodes: ParsedNode[]) => {
234 |         nodes.forEach(node => repository.saveNode(node));
235 |       });
236 | 
237 |       const start = Date.now();
238 |       insertMany(nodes);
239 |       const duration = Date.now() - start;
240 | 
241 |       expect(duration).toBeLessThan(1000); // Should complete in under 1 second
242 | 
243 |       const retrieved = repository.getAllNodes();
244 |       expect(retrieved).toHaveLength(1000);
245 |     });
246 |   });
247 | 
248 |   describe('getNodesByPackage', () => {
249 |     beforeEach(() => {
250 |       const nodes = [
251 |         createParsedNode({ 
252 |           ...MOCK_NODES.webhook,
253 |           nodeType: 'n8n-nodes-base.node1',
254 |           packageName: 'n8n-nodes-base'
255 |         }),
256 |         createParsedNode({ 
257 |           ...MOCK_NODES.webhook,
258 |           nodeType: 'n8n-nodes-base.node2',
259 |           packageName: 'n8n-nodes-base' 
260 |         }),
261 |         createParsedNode({ 
262 |           ...MOCK_NODES.webhook,
263 |           nodeType: '@n8n/n8n-nodes-langchain.node3',
264 |           packageName: '@n8n/n8n-nodes-langchain' 
265 |         })
266 |       ];
267 |       nodes.forEach(node => repository.saveNode(node));
268 |     });
269 | 
270 |     it('should filter nodes by package', () => {
271 |       const baseNodes = repository.getNodesByPackage('n8n-nodes-base');
272 |       expect(baseNodes).toHaveLength(2);
273 | 
274 |       const langchainNodes = repository.getNodesByPackage('@n8n/n8n-nodes-langchain');
275 |       expect(langchainNodes).toHaveLength(1);
276 |     });
277 | 
278 |     it('should return empty array for non-existent package', () => {
279 |       const nodes = repository.getNodesByPackage('non-existent-package');
280 |       expect(nodes).toHaveLength(0);
281 |     });
282 |   });
283 | 
284 |   describe('getNodesByCategory', () => {
285 |     beforeEach(() => {
286 |       const nodes = [
287 |         createParsedNode({ 
288 |           ...MOCK_NODES.webhook,
289 |           nodeType: 'n8n-nodes-base.webhook',
290 |           category: 'trigger'
291 |         }),
292 |         createParsedNode({ 
293 |           ...MOCK_NODES.webhook,
294 |           nodeType: 'n8n-nodes-base.schedule',
295 |           displayName: 'Schedule',
296 |           category: 'trigger'
297 |         }),
298 |         createParsedNode({ 
299 |           ...MOCK_NODES.httpRequest,
300 |           nodeType: 'n8n-nodes-base.httpRequest',
301 |           category: 'automation'
302 |         })
303 |       ];
304 |       nodes.forEach(node => repository.saveNode(node));
305 |     });
306 | 
307 |     it('should filter nodes by category', () => {
308 |       const triggers = repository.getNodesByCategory('trigger');
309 |       expect(triggers).toHaveLength(2);
310 |       expect(triggers.every(n => n.category === 'trigger')).toBe(true);
311 | 
312 |       const automation = repository.getNodesByCategory('automation');
313 |       expect(automation).toHaveLength(1);
314 |       expect(automation[0].category).toBe('automation');
315 |     });
316 |   });
317 | 
318 |   describe('searchNodes', () => {
319 |     beforeEach(() => {
320 |       const nodes = [
321 |         createParsedNode({
322 |           ...MOCK_NODES.webhook,
323 |           description: 'Starts the workflow when webhook is called'
324 |         }),
325 |         createParsedNode({
326 |           ...MOCK_NODES.httpRequest,
327 |           description: 'Makes HTTP requests to external APIs'
328 |         }),
329 |         createParsedNode({
330 |           nodeType: 'n8n-nodes-base.emailSend',
331 |           packageName: 'n8n-nodes-base',
332 |           displayName: 'Send Email',
333 |           description: 'Sends emails via SMTP protocol',
334 |           category: 'communication',
335 |           developmentStyle: 'programmatic',
336 |           isAITool: false,
337 |           isTrigger: false,
338 |           isWebhook: false,
339 |           isVersioned: true,
340 |           version: '1',
341 |           properties: [],
342 |           operations: [],
343 |           credentials: []
344 |         })
345 |       ];
346 |       nodes.forEach(node => repository.saveNode(node));
347 |     });
348 | 
349 |     it('should search by node type', () => {
350 |       const results = repository.searchNodes('webhook');
351 |       expect(results).toHaveLength(1);
352 |       expect(results[0].nodeType).toBe('n8n-nodes-base.webhook');
353 |     });
354 | 
355 |     it('should search by display name', () => {
356 |       const results = repository.searchNodes('Send Email');
357 |       expect(results).toHaveLength(1);
358 |       expect(results[0].nodeType).toBe('n8n-nodes-base.emailSend');
359 |     });
360 | 
361 |     it('should search by description', () => {
362 |       const results = repository.searchNodes('SMTP');
363 |       expect(results).toHaveLength(1);
364 |       expect(results[0].nodeType).toBe('n8n-nodes-base.emailSend');
365 |     });
366 | 
367 |     it('should handle OR mode (default)', () => {
368 |       const results = repository.searchNodes('webhook email', 'OR');
369 |       expect(results).toHaveLength(2);
370 |       const nodeTypes = results.map(r => r.nodeType);
371 |       expect(nodeTypes).toContain('n8n-nodes-base.webhook');
372 |       expect(nodeTypes).toContain('n8n-nodes-base.emailSend');
373 |     });
374 | 
375 |     it('should handle AND mode', () => {
376 |       const results = repository.searchNodes('HTTP request', 'AND');
377 |       expect(results).toHaveLength(1);
378 |       expect(results[0].nodeType).toBe('n8n-nodes-base.httpRequest');
379 |     });
380 | 
381 |     it('should handle FUZZY mode', () => {
382 |       const results = repository.searchNodes('HTT', 'FUZZY');
383 |       expect(results).toHaveLength(1);
384 |       expect(results[0].nodeType).toBe('n8n-nodes-base.httpRequest');
385 |     });
386 | 
387 |     it('should handle case-insensitive search', () => {
388 |       const results = repository.searchNodes('WEBHOOK');
389 |       expect(results).toHaveLength(1);
390 |       expect(results[0].nodeType).toBe('n8n-nodes-base.webhook');
391 |     });
392 | 
393 |     it('should return empty array for no matches', () => {
394 |       const results = repository.searchNodes('nonexistent');
395 |       expect(results).toHaveLength(0);
396 |     });
397 | 
398 |     it('should respect limit parameter', () => {
399 |       // Add more nodes
400 |       const nodes = Array.from({ length: 10 }, (_, i) => 
401 |         createParsedNode({
402 |           ...MOCK_NODES.webhook,
403 |           nodeType: `n8n-nodes-base.test${i}`,
404 |           displayName: `Test Node ${i}`,
405 |           description: 'Test description'
406 |         })
407 |       );
408 |       nodes.forEach(node => repository.saveNode(node));
409 | 
410 |       const results = repository.searchNodes('test', 'OR', 5);
411 |       expect(results).toHaveLength(5);
412 |     });
413 |   });
414 | 
415 |   describe('getAITools', () => {
416 |     it('should return only AI tool nodes', () => {
417 |       const nodes = [
418 |         createParsedNode({ 
419 |           ...MOCK_NODES.webhook,
420 |           nodeType: 'n8n-nodes-base.webhook',
421 |           isAITool: false
422 |         }),
423 |         createParsedNode({ 
424 |           ...MOCK_NODES.webhook,
425 |           nodeType: '@n8n/n8n-nodes-langchain.agent',
426 |           displayName: 'AI Agent',
427 |           packageName: '@n8n/n8n-nodes-langchain',
428 |           isAITool: true
429 |         }),
430 |         createParsedNode({ 
431 |           ...MOCK_NODES.webhook,
432 |           nodeType: '@n8n/n8n-nodes-langchain.tool',
433 |           displayName: 'AI Tool',
434 |           packageName: '@n8n/n8n-nodes-langchain',
435 |           isAITool: true
436 |         })
437 |       ];
438 |       
439 |       nodes.forEach(node => repository.saveNode(node));
440 | 
441 |       const aiTools = repository.getAITools();
442 |       expect(aiTools).toHaveLength(2);
443 |       expect(aiTools.every(node => node.package.includes('langchain'))).toBe(true);
444 |       expect(aiTools[0].displayName).toBe('AI Agent');
445 |       expect(aiTools[1].displayName).toBe('AI Tool');
446 |     });
447 |   });
448 | 
449 |   describe('getNodeCount', () => {
450 |     it('should return correct node count', () => {
451 |       expect(repository.getNodeCount()).toBe(0);
452 | 
453 |       repository.saveNode(createParsedNode(MOCK_NODES.webhook));
454 |       expect(repository.getNodeCount()).toBe(1);
455 | 
456 |       repository.saveNode(createParsedNode(MOCK_NODES.httpRequest));
457 |       expect(repository.getNodeCount()).toBe(2);
458 |     });
459 |   });
460 | 
461 |   describe('searchNodeProperties', () => {
462 |     beforeEach(() => {
463 |       const node: ParsedNode = {
464 |         nodeType: 'n8n-nodes-base.complex',
465 |         packageName: 'n8n-nodes-base',
466 |         displayName: 'Complex Node',
467 |         description: 'A complex node',
468 |         category: 'automation',
469 |         style: 'programmatic',
470 |         isAITool: false,
471 |         isTrigger: false,
472 |         isWebhook: false,
473 |         isVersioned: true,
474 |         version: '1',
475 |         properties: [
476 |           {
477 |             displayName: 'Authentication',
478 |             name: 'authentication',
479 |             type: 'options',
480 |             options: [
481 |               { name: 'Basic', value: 'basic' },
482 |               { name: 'OAuth2', value: 'oauth2' }
483 |             ]
484 |           },
485 |           {
486 |             displayName: 'Headers',
487 |             name: 'headers',
488 |             type: 'collection',
489 |             default: {},
490 |             options: [
491 |               {
492 |                 displayName: 'Header',
493 |                 name: 'header',
494 |                 type: 'string'
495 |               }
496 |             ]
497 |           }
498 |         ],
499 |         operations: [],
500 |         credentials: []
501 |       };
502 |       repository.saveNode(node);
503 |     });
504 | 
505 |     it('should find properties by name', () => {
506 |       const results = repository.searchNodeProperties('n8n-nodes-base.complex', 'auth');
507 |       expect(results.length).toBeGreaterThan(0);
508 |       expect(results.some(r => r.path.includes('authentication'))).toBe(true);
509 |     });
510 | 
511 |     it('should find nested properties', () => {
512 |       const results = repository.searchNodeProperties('n8n-nodes-base.complex', 'header');
513 |       expect(results.length).toBeGreaterThan(0);
514 |     });
515 | 
516 |     it('should return empty array for non-existent node', () => {
517 |       const results = repository.searchNodeProperties('non-existent', 'test');
518 |       expect(results).toHaveLength(0);
519 |     });
520 |   });
521 | 
522 |   describe('Transaction handling', () => {
523 |     it('should handle errors gracefully', () => {
524 |       // Test with a node that violates database constraints
525 |       const invalidNode = {
526 |         nodeType: '', // Empty string should violate PRIMARY KEY constraint
527 |         packageName: null, // NULL should violate NOT NULL constraint
528 |         displayName: null, // NULL should violate NOT NULL constraint
529 |         description: '',
530 |         category: 'automation',
531 |         style: 'programmatic',
532 |         isAITool: false,
533 |         isTrigger: false,
534 |         isWebhook: false,
535 |         isVersioned: false,
536 |         version: '1',
537 |         properties: [],
538 |         operations: [],
539 |         credentials: []
540 |       } as any;
541 | 
542 |       expect(() => {
543 |         repository.saveNode(invalidNode);
544 |       }).toThrow();
545 | 
546 |       // Repository should still be functional
547 |       const count = repository.getNodeCount();
548 |       expect(count).toBe(0);
549 |     });
550 | 
551 |     it('should handle concurrent saves', () => {
552 |       const node = createParsedNode(MOCK_NODES.webhook);
553 |       
554 |       // Simulate concurrent saves of the same node with different display names
555 |       const promises = Array.from({ length: 10 }, (_, i) => {
556 |         const updatedNode = {
557 |           ...node,
558 |           displayName: `Display ${i}`
559 |         };
560 |         return Promise.resolve(repository.saveNode(updatedNode));
561 |       });
562 | 
563 |       Promise.all(promises);
564 | 
565 |       // Should have only one node
566 |       const count = repository.getNodeCount();
567 |       expect(count).toBe(1);
568 |       
569 |       // Should have the last update
570 |       const saved = repository.getNode(node.nodeType);
571 |       expect(saved).toBeTruthy();
572 |     });
573 |   });
574 | 
575 |   describe('Performance characteristics', () => {
576 |     it('should handle bulk operations efficiently', () => {
577 |       const nodeCount = 1000;
578 |       const nodes = Array.from({ length: nodeCount }, (_, i) => 
579 |         createParsedNode({
580 |           ...MOCK_NODES.webhook,
581 |           nodeType: `n8n-nodes-base.node${i}`,
582 |           displayName: `Node ${i}`,
583 |           description: `Description for node ${i}`
584 |         })
585 |       );
586 | 
587 |       const insertMany = db.transaction((nodes: ParsedNode[]) => {
588 |         nodes.forEach(node => repository.saveNode(node));
589 |       });
590 | 
591 |       const start = Date.now();
592 |       insertMany(nodes);
593 |       const saveDuration = Date.now() - start;
594 | 
595 |       expect(saveDuration).toBeLessThan(1000); // Should complete in under 1 second
596 | 
597 |       // Test search performance
598 |       const searchStart = Date.now();
599 |       const results = repository.searchNodes('node', 'OR', 100);
600 |       const searchDuration = Date.now() - searchStart;
601 | 
602 |       expect(searchDuration).toBeLessThan(50); // Search should be fast
603 |       expect(results.length).toBe(100); // Respects limit
604 |     });
605 |   });
606 | });
607 | 
608 | // Helper function to create ParsedNode from test data
609 | function createParsedNode(data: any): ParsedNode {
610 |   return {
611 |     nodeType: data.nodeType,
612 |     packageName: data.packageName,
613 |     displayName: data.displayName,
614 |     description: data.description || '',
615 |     category: data.category || 'automation',
616 |     style: data.developmentStyle || 'programmatic',
617 |     isAITool: data.isAITool || false,
618 |     isTrigger: data.isTrigger || false,
619 |     isWebhook: data.isWebhook || false,
620 |     isVersioned: data.isVersioned !== undefined ? data.isVersioned : true,
621 |     version: data.version || '1',
622 |     documentation: data.documentation || null,
623 |     properties: data.properties || [],
624 |     operations: data.operations || [],
625 |     credentials: data.credentials || []
626 |   };
627 | }
```
Page 28/59FirstPrevNextLast