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

# Directory Structure

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

# Files

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

```typescript
   1 | import { N8nApiClient } from '../services/n8n-api-client';
   2 | import { getN8nApiConfig, getN8nApiConfigFromContext } from '../config/n8n-api';
   3 | import {
   4 |   Workflow,
   5 |   WorkflowNode,
   6 |   WorkflowConnection,
   7 |   ExecutionStatus,
   8 |   WebhookRequest,
   9 |   McpToolResponse,
  10 |   ExecutionFilterOptions,
  11 |   ExecutionMode
  12 | } from '../types/n8n-api';
  13 | import {
  14 |   validateWorkflowStructure,
  15 |   hasWebhookTrigger,
  16 |   getWebhookUrl
  17 | } from '../services/n8n-validation';
  18 | import {
  19 |   N8nApiError,
  20 |   N8nNotFoundError,
  21 |   getUserFriendlyErrorMessage,
  22 |   formatExecutionError,
  23 |   formatNoExecutionError
  24 | } from '../utils/n8n-errors';
  25 | import { logger } from '../utils/logger';
  26 | import { z } from 'zod';
  27 | import { WorkflowValidator } from '../services/workflow-validator';
  28 | import { EnhancedConfigValidator } from '../services/enhanced-config-validator';
  29 | import { NodeRepository } from '../database/node-repository';
  30 | import { InstanceContext, validateInstanceContext } from '../types/instance-context';
  31 | import { NodeTypeNormalizer } from '../utils/node-type-normalizer';
  32 | import { WorkflowAutoFixer, AutoFixConfig } from '../services/workflow-auto-fixer';
  33 | import { ExpressionFormatValidator, ExpressionFormatIssue } from '../services/expression-format-validator';
  34 | import { handleUpdatePartialWorkflow } from './handlers-workflow-diff';
  35 | import { telemetry } from '../telemetry';
  36 | import {
  37 |   createCacheKey,
  38 |   createInstanceCache,
  39 |   CacheMutex,
  40 |   cacheMetrics,
  41 |   withRetry,
  42 |   getCacheStatistics
  43 | } from '../utils/cache-utils';
  44 | import { processExecution } from '../services/execution-processor';
  45 | import { checkNpmVersion, formatVersionMessage } from '../utils/npm-version-checker';
  46 | 
  47 | // ========================================================================
  48 | // TypeScript Interfaces for Type Safety
  49 | // ========================================================================
  50 | 
  51 | /**
  52 |  * Health Check Response Data Structure
  53 |  */
  54 | interface HealthCheckResponseData {
  55 |   status: string;
  56 |   instanceId?: string;
  57 |   n8nVersion?: string;
  58 |   features?: Record<string, unknown>;
  59 |   apiUrl?: string;
  60 |   mcpVersion: string;
  61 |   supportedN8nVersion?: string;
  62 |   versionCheck: {
  63 |     current: string;
  64 |     latest: string | null;
  65 |     upToDate: boolean;
  66 |     message: string;
  67 |     updateCommand?: string;
  68 |   };
  69 |   performance: {
  70 |     responseTimeMs: number;
  71 |     cacheHitRate: string;
  72 |     cachedInstances: number;
  73 |   };
  74 |   nextSteps?: string[];
  75 |   updateWarning?: string;
  76 | }
  77 | 
  78 | /**
  79 |  * Cloud Platform Guide Structure
  80 |  */
  81 | interface CloudPlatformGuide {
  82 |   name: string;
  83 |   troubleshooting: string[];
  84 | }
  85 | 
  86 | /**
  87 |  * Workflow Validation Response Data
  88 |  */
  89 | interface WorkflowValidationResponse {
  90 |   valid: boolean;
  91 |   workflowId?: string;
  92 |   workflowName?: string;
  93 |   summary: {
  94 |     totalNodes: number;
  95 |     enabledNodes: number;
  96 |     triggerNodes: number;
  97 |     validConnections: number;
  98 |     invalidConnections: number;
  99 |     expressionsValidated: number;
 100 |     errorCount: number;
 101 |     warningCount: number;
 102 |   };
 103 |   errors?: Array<{
 104 |     node: string;
 105 |     nodeName?: string;
 106 |     message: string;
 107 |     details?: Record<string, unknown>;
 108 |   }>;
 109 |   warnings?: Array<{
 110 |     node: string;
 111 |     nodeName?: string;
 112 |     message: string;
 113 |     details?: Record<string, unknown>;
 114 |   }>;
 115 |   suggestions?: unknown[];
 116 | }
 117 | 
 118 | /**
 119 |  * Diagnostic Response Data Structure
 120 |  */
 121 | interface DiagnosticResponseData {
 122 |   timestamp: string;
 123 |   environment: {
 124 |     N8N_API_URL: string | null;
 125 |     N8N_API_KEY: string | null;
 126 |     NODE_ENV: string;
 127 |     MCP_MODE: string;
 128 |     isDocker: boolean;
 129 |     cloudPlatform: string | null;
 130 |     nodeVersion: string;
 131 |     platform: string;
 132 |   };
 133 |   apiConfiguration: {
 134 |     configured: boolean;
 135 |     status: {
 136 |       configured: boolean;
 137 |       connected: boolean;
 138 |       error: string | null;
 139 |       version: string | null;
 140 |     };
 141 |     config: {
 142 |       baseUrl: string;
 143 |       timeout: number;
 144 |       maxRetries: number;
 145 |     } | null;
 146 |   };
 147 |   versionInfo: {
 148 |     current: string;
 149 |     latest: string | null;
 150 |     upToDate: boolean;
 151 |     message: string;
 152 |     updateCommand?: string;
 153 |   };
 154 |   toolsAvailability: {
 155 |     documentationTools: {
 156 |       count: number;
 157 |       enabled: boolean;
 158 |       description: string;
 159 |     };
 160 |     managementTools: {
 161 |       count: number;
 162 |       enabled: boolean;
 163 |       description: string;
 164 |     };
 165 |     totalAvailable: number;
 166 |   };
 167 |   performance: {
 168 |     diagnosticResponseTimeMs: number;
 169 |     cacheHitRate: string;
 170 |     cachedInstances: number;
 171 |   };
 172 |   modeSpecificDebug: Record<string, unknown>;
 173 |   dockerDebug?: Record<string, unknown>;
 174 |   cloudPlatformDebug?: CloudPlatformGuide;
 175 |   nextSteps?: Record<string, unknown>;
 176 |   troubleshooting?: Record<string, unknown>;
 177 |   setupGuide?: Record<string, unknown>;
 178 |   updateWarning?: Record<string, unknown>;
 179 |   debug?: Record<string, unknown>;
 180 |   [key: string]: unknown; // Allow dynamic property access for optional fields
 181 | }
 182 | 
 183 | // ========================================================================
 184 | // Singleton n8n API client instance (backward compatibility)
 185 | let defaultApiClient: N8nApiClient | null = null;
 186 | let lastDefaultConfigUrl: string | null = null;
 187 | 
 188 | // Mutex for cache operations to prevent race conditions
 189 | const cacheMutex = new CacheMutex();
 190 | 
 191 | // Instance-specific API clients cache with LRU eviction and TTL
 192 | const instanceClients = createInstanceCache<N8nApiClient>((client, key) => {
 193 |   // Clean up when evicting from cache
 194 |   logger.debug('Evicting API client from cache', {
 195 |     cacheKey: key.substring(0, 8) + '...' // Only log partial key for security
 196 |   });
 197 | });
 198 | 
 199 | /**
 200 |  * Get or create API client with flexible instance support
 201 |  * Supports both singleton mode (using environment variables) and instance-specific mode.
 202 |  * Uses LRU cache with mutex protection for thread-safe operations.
 203 |  *
 204 |  * @param context - Optional instance context for instance-specific configuration
 205 |  * @returns API client configured for the instance or environment, or null if not configured
 206 |  *
 207 |  * @example
 208 |  * // Using environment variables (singleton mode)
 209 |  * const client = getN8nApiClient();
 210 |  *
 211 |  * @example
 212 |  * // Using instance context
 213 |  * const client = getN8nApiClient({
 214 |  *   n8nApiUrl: 'https://customer.n8n.cloud',
 215 |  *   n8nApiKey: 'api-key-123',
 216 |  *   instanceId: 'customer-1'
 217 |  * });
 218 |  */
 219 | /**
 220 |  * Get cache statistics for monitoring
 221 |  * @returns Formatted cache statistics string
 222 |  */
 223 | export function getInstanceCacheStatistics(): string {
 224 |   return getCacheStatistics();
 225 | }
 226 | 
 227 | /**
 228 |  * Get raw cache metrics for detailed monitoring
 229 |  * @returns Raw cache metrics object
 230 |  */
 231 | export function getInstanceCacheMetrics() {
 232 |   return cacheMetrics.getMetrics();
 233 | }
 234 | 
 235 | /**
 236 |  * Clear the instance cache for testing or maintenance
 237 |  */
 238 | export function clearInstanceCache(): void {
 239 |   instanceClients.clear();
 240 |   cacheMetrics.recordClear();
 241 |   cacheMetrics.updateSize(0, instanceClients.max);
 242 | }
 243 | 
 244 | export function getN8nApiClient(context?: InstanceContext): N8nApiClient | null {
 245 |   // If context provided with n8n config, use instance-specific client
 246 |   if (context?.n8nApiUrl && context?.n8nApiKey) {
 247 |     // Validate context before using
 248 |     const validation = validateInstanceContext(context);
 249 |     if (!validation.valid) {
 250 |       logger.warn('Invalid instance context provided', {
 251 |         instanceId: context.instanceId,
 252 |         errors: validation.errors
 253 |       });
 254 |       return null;
 255 |     }
 256 |     // Create secure hash of credentials for cache key using memoization
 257 |     const cacheKey = createCacheKey(
 258 |       `${context.n8nApiUrl}:${context.n8nApiKey}:${context.instanceId || ''}`
 259 |     );
 260 | 
 261 |     // Check cache first
 262 |     if (instanceClients.has(cacheKey)) {
 263 |       cacheMetrics.recordHit();
 264 |       return instanceClients.get(cacheKey) || null;
 265 |     }
 266 | 
 267 |     cacheMetrics.recordMiss();
 268 | 
 269 |     // Check if already being created (simple lock check)
 270 |     if (cacheMutex.isLocked(cacheKey)) {
 271 |       // Wait briefly and check again
 272 |       const waitTime = 100; // 100ms
 273 |       const start = Date.now();
 274 |       while (cacheMutex.isLocked(cacheKey) && (Date.now() - start) < 1000) {
 275 |         // Busy wait for up to 1 second
 276 |       }
 277 |       // Check if it was created while waiting
 278 |       if (instanceClients.has(cacheKey)) {
 279 |         cacheMetrics.recordHit();
 280 |         return instanceClients.get(cacheKey) || null;
 281 |       }
 282 |     }
 283 | 
 284 |     const config = getN8nApiConfigFromContext(context);
 285 |     if (config) {
 286 |       // Sanitized logging - never log API keys
 287 |       logger.info('Creating instance-specific n8n API client', {
 288 |         url: config.baseUrl.replace(/^(https?:\/\/[^\/]+).*/, '$1'), // Only log domain
 289 |         instanceId: context.instanceId,
 290 |         cacheKey: cacheKey.substring(0, 8) + '...' // Only log partial hash
 291 |       });
 292 | 
 293 |       const client = new N8nApiClient(config);
 294 |       instanceClients.set(cacheKey, client);
 295 |       cacheMetrics.recordSet();
 296 |       cacheMetrics.updateSize(instanceClients.size, instanceClients.max);
 297 |       return client;
 298 |     }
 299 | 
 300 |     return null;
 301 |   }
 302 | 
 303 |   // Fall back to default singleton from environment
 304 |   logger.info('Falling back to environment configuration for n8n API client');
 305 |   const config = getN8nApiConfig();
 306 | 
 307 |   if (!config) {
 308 |     if (defaultApiClient) {
 309 |       logger.info('n8n API configuration removed, clearing default client');
 310 |       defaultApiClient = null;
 311 |       lastDefaultConfigUrl = null;
 312 |     }
 313 |     return null;
 314 |   }
 315 | 
 316 |   // Check if config has changed
 317 |   if (!defaultApiClient || lastDefaultConfigUrl !== config.baseUrl) {
 318 |     logger.info('n8n API client initialized from environment', { url: config.baseUrl });
 319 |     defaultApiClient = new N8nApiClient(config);
 320 |     lastDefaultConfigUrl = config.baseUrl;
 321 |   }
 322 | 
 323 |   return defaultApiClient;
 324 | }
 325 | 
 326 | /**
 327 |  * Helper to ensure API is configured
 328 |  * @param context - Optional instance context
 329 |  * @returns Configured API client
 330 |  * @throws Error if API is not configured
 331 |  */
 332 | function ensureApiConfigured(context?: InstanceContext): N8nApiClient {
 333 |   const client = getN8nApiClient(context);
 334 |   if (!client) {
 335 |     if (context?.instanceId) {
 336 |       throw new Error(`n8n API not configured for instance ${context.instanceId}. Please provide n8nApiUrl and n8nApiKey in the instance context.`);
 337 |     }
 338 |     throw new Error('n8n API not configured. Please set N8N_API_URL and N8N_API_KEY environment variables.');
 339 |   }
 340 |   return client;
 341 | }
 342 | 
 343 | // Zod schemas for input validation
 344 | const createWorkflowSchema = z.object({
 345 |   name: z.string(),
 346 |   nodes: z.array(z.any()),
 347 |   connections: z.record(z.any()),
 348 |   settings: z.object({
 349 |     executionOrder: z.enum(['v0', 'v1']).optional(),
 350 |     timezone: z.string().optional(),
 351 |     saveDataErrorExecution: z.enum(['all', 'none']).optional(),
 352 |     saveDataSuccessExecution: z.enum(['all', 'none']).optional(),
 353 |     saveManualExecutions: z.boolean().optional(),
 354 |     saveExecutionProgress: z.boolean().optional(),
 355 |     executionTimeout: z.number().optional(),
 356 |     errorWorkflow: z.string().optional(),
 357 |   }).optional(),
 358 | });
 359 | 
 360 | const updateWorkflowSchema = z.object({
 361 |   id: z.string(),
 362 |   name: z.string().optional(),
 363 |   nodes: z.array(z.any()).optional(),
 364 |   connections: z.record(z.any()).optional(),
 365 |   settings: z.any().optional(),
 366 | });
 367 | 
 368 | const listWorkflowsSchema = z.object({
 369 |   limit: z.number().min(1).max(100).optional(),
 370 |   cursor: z.string().optional(),
 371 |   active: z.boolean().optional(),
 372 |   tags: z.array(z.string()).optional(),
 373 |   projectId: z.string().optional(),
 374 |   excludePinnedData: z.boolean().optional(),
 375 | });
 376 | 
 377 | const validateWorkflowSchema = z.object({
 378 |   id: z.string(),
 379 |   options: z.object({
 380 |     validateNodes: z.boolean().optional(),
 381 |     validateConnections: z.boolean().optional(),
 382 |     validateExpressions: z.boolean().optional(),
 383 |     profile: z.enum(['minimal', 'runtime', 'ai-friendly', 'strict']).optional(),
 384 |   }).optional(),
 385 | });
 386 | 
 387 | const autofixWorkflowSchema = z.object({
 388 |   id: z.string(),
 389 |   applyFixes: z.boolean().optional().default(false),
 390 |   fixTypes: z.array(z.enum([
 391 |     'expression-format',
 392 |     'typeversion-correction',
 393 |     'error-output-config',
 394 |     'node-type-correction',
 395 |     'webhook-missing-path'
 396 |   ])).optional(),
 397 |   confidenceThreshold: z.enum(['high', 'medium', 'low']).optional().default('medium'),
 398 |   maxFixes: z.number().optional().default(50)
 399 | });
 400 | 
 401 | const triggerWebhookSchema = z.object({
 402 |   webhookUrl: z.string().url(),
 403 |   httpMethod: z.enum(['GET', 'POST', 'PUT', 'DELETE']).optional(),
 404 |   data: z.record(z.unknown()).optional(),
 405 |   headers: z.record(z.string()).optional(),
 406 |   waitForResponse: z.boolean().optional(),
 407 | });
 408 | 
 409 | const listExecutionsSchema = z.object({
 410 |   limit: z.number().min(1).max(100).optional(),
 411 |   cursor: z.string().optional(),
 412 |   workflowId: z.string().optional(),
 413 |   projectId: z.string().optional(),
 414 |   status: z.enum(['success', 'error', 'waiting']).optional(),
 415 |   includeData: z.boolean().optional(),
 416 | });
 417 | 
 418 | // Workflow Management Handlers
 419 | 
 420 | export async function handleCreateWorkflow(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
 421 |   try {
 422 |     const client = ensureApiConfigured(context);
 423 |     const input = createWorkflowSchema.parse(args);
 424 | 
 425 |     // Proactively detect SHORT form node types (common mistake)
 426 |     const shortFormErrors: string[] = [];
 427 |     input.nodes?.forEach((node: any, index: number) => {
 428 |       if (node.type?.startsWith('nodes-base.') || node.type?.startsWith('nodes-langchain.')) {
 429 |         const fullForm = node.type.startsWith('nodes-base.')
 430 |           ? node.type.replace('nodes-base.', 'n8n-nodes-base.')
 431 |           : node.type.replace('nodes-langchain.', '@n8n/n8n-nodes-langchain.');
 432 |         shortFormErrors.push(
 433 |           `Node ${index} ("${node.name}") uses SHORT form "${node.type}". ` +
 434 |           `The n8n API requires FULL form. Change to "${fullForm}"`
 435 |         );
 436 |       }
 437 |     });
 438 | 
 439 |     if (shortFormErrors.length > 0) {
 440 |       telemetry.trackWorkflowCreation(input, false);
 441 |       return {
 442 |         success: false,
 443 |         error: 'Node type format error: n8n API requires FULL form node types',
 444 |         details: {
 445 |           errors: shortFormErrors,
 446 |           hint: 'Use n8n-nodes-base.* instead of nodes-base.* for standard nodes'
 447 |         }
 448 |       };
 449 |     }
 450 | 
 451 |     // Validate workflow structure (n8n API expects FULL form: n8n-nodes-base.*)
 452 |     const errors = validateWorkflowStructure(input);
 453 |     if (errors.length > 0) {
 454 |       // Track validation failure
 455 |       telemetry.trackWorkflowCreation(input, false);
 456 | 
 457 |       return {
 458 |         success: false,
 459 |         error: 'Workflow validation failed',
 460 |         details: { errors }
 461 |       };
 462 |     }
 463 | 
 464 |     // Create workflow (n8n API expects node types in FULL form)
 465 |     const workflow = await client.createWorkflow(input);
 466 | 
 467 |     // Track successful workflow creation
 468 |     telemetry.trackWorkflowCreation(workflow, true);
 469 | 
 470 |     return {
 471 |       success: true,
 472 |       data: workflow,
 473 |       message: `Workflow "${workflow.name}" created successfully with ID: ${workflow.id}`
 474 |     };
 475 |   } catch (error) {
 476 |     if (error instanceof z.ZodError) {
 477 |       return {
 478 |         success: false,
 479 |         error: 'Invalid input',
 480 |         details: { errors: error.errors }
 481 |       };
 482 |     }
 483 |     
 484 |     if (error instanceof N8nApiError) {
 485 |       return {
 486 |         success: false,
 487 |         error: getUserFriendlyErrorMessage(error),
 488 |         code: error.code,
 489 |         details: error.details as Record<string, unknown> | undefined
 490 |       };
 491 |     }
 492 |     
 493 |     return {
 494 |       success: false,
 495 |       error: error instanceof Error ? error.message : 'Unknown error occurred'
 496 |     };
 497 |   }
 498 | }
 499 | 
 500 | export async function handleGetWorkflow(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
 501 |   try {
 502 |     const client = ensureApiConfigured(context);
 503 |     const { id } = z.object({ id: z.string() }).parse(args);
 504 |     
 505 |     const workflow = await client.getWorkflow(id);
 506 |     
 507 |     return {
 508 |       success: true,
 509 |       data: workflow
 510 |     };
 511 |   } catch (error) {
 512 |     if (error instanceof z.ZodError) {
 513 |       return {
 514 |         success: false,
 515 |         error: 'Invalid input',
 516 |         details: { errors: error.errors }
 517 |       };
 518 |     }
 519 |     
 520 |     if (error instanceof N8nApiError) {
 521 |       return {
 522 |         success: false,
 523 |         error: getUserFriendlyErrorMessage(error),
 524 |         code: error.code
 525 |       };
 526 |     }
 527 |     
 528 |     return {
 529 |       success: false,
 530 |       error: error instanceof Error ? error.message : 'Unknown error occurred'
 531 |     };
 532 |   }
 533 | }
 534 | 
 535 | export async function handleGetWorkflowDetails(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
 536 |   try {
 537 |     const client = ensureApiConfigured(context);
 538 |     const { id } = z.object({ id: z.string() }).parse(args);
 539 |     
 540 |     const workflow = await client.getWorkflow(id);
 541 |     
 542 |     // Get recent executions for this workflow
 543 |     const executions = await client.listExecutions({
 544 |       workflowId: id,
 545 |       limit: 10
 546 |     });
 547 |     
 548 |     // Calculate execution statistics
 549 |     const stats = {
 550 |       totalExecutions: executions.data.length,
 551 |       successCount: executions.data.filter(e => e.status === ExecutionStatus.SUCCESS).length,
 552 |       errorCount: executions.data.filter(e => e.status === ExecutionStatus.ERROR).length,
 553 |       lastExecutionTime: executions.data[0]?.startedAt || null
 554 |     };
 555 |     
 556 |     return {
 557 |       success: true,
 558 |       data: {
 559 |         workflow,
 560 |         executionStats: stats,
 561 |         hasWebhookTrigger: hasWebhookTrigger(workflow),
 562 |         webhookPath: getWebhookUrl(workflow)
 563 |       }
 564 |     };
 565 |   } catch (error) {
 566 |     if (error instanceof z.ZodError) {
 567 |       return {
 568 |         success: false,
 569 |         error: 'Invalid input',
 570 |         details: { errors: error.errors }
 571 |       };
 572 |     }
 573 |     
 574 |     if (error instanceof N8nApiError) {
 575 |       return {
 576 |         success: false,
 577 |         error: getUserFriendlyErrorMessage(error),
 578 |         code: error.code
 579 |       };
 580 |     }
 581 |     
 582 |     return {
 583 |       success: false,
 584 |       error: error instanceof Error ? error.message : 'Unknown error occurred'
 585 |     };
 586 |   }
 587 | }
 588 | 
 589 | export async function handleGetWorkflowStructure(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
 590 |   try {
 591 |     const client = ensureApiConfigured(context);
 592 |     const { id } = z.object({ id: z.string() }).parse(args);
 593 |     
 594 |     const workflow = await client.getWorkflow(id);
 595 |     
 596 |     // Simplify nodes to just essential structure
 597 |     const simplifiedNodes = workflow.nodes.map(node => ({
 598 |       id: node.id,
 599 |       name: node.name,
 600 |       type: node.type,
 601 |       position: node.position,
 602 |       disabled: node.disabled || false
 603 |     }));
 604 |     
 605 |     return {
 606 |       success: true,
 607 |       data: {
 608 |         id: workflow.id,
 609 |         name: workflow.name,
 610 |         active: workflow.active,
 611 |         isArchived: workflow.isArchived,
 612 |         nodes: simplifiedNodes,
 613 |         connections: workflow.connections,
 614 |         nodeCount: workflow.nodes.length,
 615 |         connectionCount: Object.keys(workflow.connections).length
 616 |       }
 617 |     };
 618 |   } catch (error) {
 619 |     if (error instanceof z.ZodError) {
 620 |       return {
 621 |         success: false,
 622 |         error: 'Invalid input',
 623 |         details: { errors: error.errors }
 624 |       };
 625 |     }
 626 |     
 627 |     if (error instanceof N8nApiError) {
 628 |       return {
 629 |         success: false,
 630 |         error: getUserFriendlyErrorMessage(error),
 631 |         code: error.code
 632 |       };
 633 |     }
 634 |     
 635 |     return {
 636 |       success: false,
 637 |       error: error instanceof Error ? error.message : 'Unknown error occurred'
 638 |     };
 639 |   }
 640 | }
 641 | 
 642 | export async function handleGetWorkflowMinimal(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
 643 |   try {
 644 |     const client = ensureApiConfigured(context);
 645 |     const { id } = z.object({ id: z.string() }).parse(args);
 646 |     
 647 |     const workflow = await client.getWorkflow(id);
 648 |     
 649 |     return {
 650 |       success: true,
 651 |       data: {
 652 |         id: workflow.id,
 653 |         name: workflow.name,
 654 |         active: workflow.active,
 655 |         isArchived: workflow.isArchived,
 656 |         tags: workflow.tags || [],
 657 |         createdAt: workflow.createdAt,
 658 |         updatedAt: workflow.updatedAt
 659 |       }
 660 |     };
 661 |   } catch (error) {
 662 |     if (error instanceof z.ZodError) {
 663 |       return {
 664 |         success: false,
 665 |         error: 'Invalid input',
 666 |         details: { errors: error.errors }
 667 |       };
 668 |     }
 669 |     
 670 |     if (error instanceof N8nApiError) {
 671 |       return {
 672 |         success: false,
 673 |         error: getUserFriendlyErrorMessage(error),
 674 |         code: error.code
 675 |       };
 676 |     }
 677 |     
 678 |     return {
 679 |       success: false,
 680 |       error: error instanceof Error ? error.message : 'Unknown error occurred'
 681 |     };
 682 |   }
 683 | }
 684 | 
 685 | export async function handleUpdateWorkflow(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
 686 |   try {
 687 |     const client = ensureApiConfigured(context);
 688 |     const input = updateWorkflowSchema.parse(args);
 689 |     const { id, ...updateData } = input;
 690 | 
 691 |     // If nodes/connections are being updated, validate the structure
 692 |     if (updateData.nodes || updateData.connections) {
 693 |       // Always fetch current workflow for validation (need all fields like name)
 694 |       const current = await client.getWorkflow(id);
 695 |       const fullWorkflow = {
 696 |         ...current,
 697 |         ...updateData
 698 |       };
 699 | 
 700 |       // Validate workflow structure (n8n API expects FULL form: n8n-nodes-base.*)
 701 |       const errors = validateWorkflowStructure(fullWorkflow);
 702 |       if (errors.length > 0) {
 703 |         return {
 704 |           success: false,
 705 |           error: 'Workflow validation failed',
 706 |           details: { errors }
 707 |         };
 708 |       }
 709 |     }
 710 |     
 711 |     // Update workflow
 712 |     const workflow = await client.updateWorkflow(id, updateData);
 713 |     
 714 |     return {
 715 |       success: true,
 716 |       data: workflow,
 717 |       message: `Workflow "${workflow.name}" updated successfully`
 718 |     };
 719 |   } catch (error) {
 720 |     if (error instanceof z.ZodError) {
 721 |       return {
 722 |         success: false,
 723 |         error: 'Invalid input',
 724 |         details: { errors: error.errors }
 725 |       };
 726 |     }
 727 |     
 728 |     if (error instanceof N8nApiError) {
 729 |       return {
 730 |         success: false,
 731 |         error: getUserFriendlyErrorMessage(error),
 732 |         code: error.code,
 733 |         details: error.details as Record<string, unknown> | undefined
 734 |       };
 735 |     }
 736 |     
 737 |     return {
 738 |       success: false,
 739 |       error: error instanceof Error ? error.message : 'Unknown error occurred'
 740 |     };
 741 |   }
 742 | }
 743 | 
 744 | export async function handleDeleteWorkflow(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
 745 |   try {
 746 |     const client = ensureApiConfigured(context);
 747 |     const { id } = z.object({ id: z.string() }).parse(args);
 748 | 
 749 |     const deleted = await client.deleteWorkflow(id);
 750 | 
 751 |     return {
 752 |       success: true,
 753 |       data: deleted,
 754 |       message: `Workflow ${id} deleted successfully`
 755 |     };
 756 |   } catch (error) {
 757 |     if (error instanceof z.ZodError) {
 758 |       return {
 759 |         success: false,
 760 |         error: 'Invalid input',
 761 |         details: { errors: error.errors }
 762 |       };
 763 |     }
 764 |     
 765 |     if (error instanceof N8nApiError) {
 766 |       return {
 767 |         success: false,
 768 |         error: getUserFriendlyErrorMessage(error),
 769 |         code: error.code
 770 |       };
 771 |     }
 772 |     
 773 |     return {
 774 |       success: false,
 775 |       error: error instanceof Error ? error.message : 'Unknown error occurred'
 776 |     };
 777 |   }
 778 | }
 779 | 
 780 | export async function handleListWorkflows(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
 781 |   try {
 782 |     const client = ensureApiConfigured(context);
 783 |     const input = listWorkflowsSchema.parse(args || {});
 784 | 
 785 |     // Convert tags array to comma-separated string (n8n API format)
 786 |     const tagsParam = input.tags && input.tags.length > 0
 787 |       ? input.tags.join(',')
 788 |       : undefined;
 789 | 
 790 |     const response = await client.listWorkflows({
 791 |       limit: input.limit || 100,
 792 |       cursor: input.cursor,
 793 |       active: input.active,
 794 |       tags: tagsParam as any,  // API expects string, not array
 795 |       projectId: input.projectId,
 796 |       excludePinnedData: input.excludePinnedData ?? true
 797 |     });
 798 |     
 799 |     // Strip down workflows to only essential metadata
 800 |     const minimalWorkflows = response.data.map(workflow => ({
 801 |       id: workflow.id,
 802 |       name: workflow.name,
 803 |       active: workflow.active,
 804 |       isArchived: workflow.isArchived,
 805 |       createdAt: workflow.createdAt,
 806 |       updatedAt: workflow.updatedAt,
 807 |       tags: workflow.tags || [],
 808 |       nodeCount: workflow.nodes?.length || 0
 809 |     }));
 810 | 
 811 |     return {
 812 |       success: true,
 813 |       data: {
 814 |         workflows: minimalWorkflows,
 815 |         returned: minimalWorkflows.length,
 816 |         nextCursor: response.nextCursor,
 817 |         hasMore: !!response.nextCursor,
 818 |         ...(response.nextCursor ? { 
 819 |           _note: "More workflows available. Use cursor to get next page." 
 820 |         } : {})
 821 |       }
 822 |     };
 823 |   } catch (error) {
 824 |     if (error instanceof z.ZodError) {
 825 |       return {
 826 |         success: false,
 827 |         error: 'Invalid input',
 828 |         details: { errors: error.errors }
 829 |       };
 830 |     }
 831 |     
 832 |     if (error instanceof N8nApiError) {
 833 |       return {
 834 |         success: false,
 835 |         error: getUserFriendlyErrorMessage(error),
 836 |         code: error.code
 837 |       };
 838 |     }
 839 |     
 840 |     return {
 841 |       success: false,
 842 |       error: error instanceof Error ? error.message : 'Unknown error occurred'
 843 |     };
 844 |   }
 845 | }
 846 | 
 847 | export async function handleValidateWorkflow(
 848 |   args: unknown,
 849 |   repository: NodeRepository,
 850 |   context?: InstanceContext
 851 | ): Promise<McpToolResponse> {
 852 |   try {
 853 |     const client = ensureApiConfigured(context);
 854 |     const input = validateWorkflowSchema.parse(args);
 855 |     
 856 |     // First, fetch the workflow from n8n
 857 |     const workflowResponse = await handleGetWorkflow({ id: input.id });
 858 |     
 859 |     if (!workflowResponse.success) {
 860 |       return workflowResponse; // Return the error from fetching
 861 |     }
 862 |     
 863 |     const workflow = workflowResponse.data as Workflow;
 864 |     
 865 |     // Create validator instance using the provided repository
 866 |     const validator = new WorkflowValidator(repository, EnhancedConfigValidator);
 867 |     
 868 |     // Run validation
 869 |     const validationResult = await validator.validateWorkflow(workflow, input.options);
 870 |     
 871 |     // Format the response (same format as the regular validate_workflow tool)
 872 |     const response: WorkflowValidationResponse = {
 873 |       valid: validationResult.valid,
 874 |       workflowId: workflow.id,
 875 |       workflowName: workflow.name,
 876 |       summary: {
 877 |         totalNodes: validationResult.statistics.totalNodes,
 878 |         enabledNodes: validationResult.statistics.enabledNodes,
 879 |         triggerNodes: validationResult.statistics.triggerNodes,
 880 |         validConnections: validationResult.statistics.validConnections,
 881 |         invalidConnections: validationResult.statistics.invalidConnections,
 882 |         expressionsValidated: validationResult.statistics.expressionsValidated,
 883 |         errorCount: validationResult.errors.length,
 884 |         warningCount: validationResult.warnings.length
 885 |       }
 886 |     };
 887 |     
 888 |     if (validationResult.errors.length > 0) {
 889 |       response.errors = validationResult.errors.map(e => ({
 890 |         node: e.nodeName || 'workflow',
 891 |         nodeName: e.nodeName, // Also set nodeName for compatibility
 892 |         message: e.message,
 893 |         details: e.details
 894 |       }));
 895 |     }
 896 | 
 897 |     if (validationResult.warnings.length > 0) {
 898 |       response.warnings = validationResult.warnings.map(w => ({
 899 |         node: w.nodeName || 'workflow',
 900 |         nodeName: w.nodeName, // Also set nodeName for compatibility
 901 |         message: w.message,
 902 |         details: w.details
 903 |       }));
 904 |     }
 905 |     
 906 |     if (validationResult.suggestions.length > 0) {
 907 |       response.suggestions = validationResult.suggestions;
 908 |     }
 909 | 
 910 |     // Track successfully validated workflows in telemetry
 911 |     if (validationResult.valid) {
 912 |       telemetry.trackWorkflowCreation(workflow, true);
 913 |     }
 914 | 
 915 |     return {
 916 |       success: true,
 917 |       data: response
 918 |     };
 919 |   } catch (error) {
 920 |     if (error instanceof z.ZodError) {
 921 |       return {
 922 |         success: false,
 923 |         error: 'Invalid input',
 924 |         details: { errors: error.errors }
 925 |       };
 926 |     }
 927 |     
 928 |     if (error instanceof N8nApiError) {
 929 |       return {
 930 |         success: false,
 931 |         error: getUserFriendlyErrorMessage(error),
 932 |         code: error.code
 933 |       };
 934 |     }
 935 |     
 936 |     return {
 937 |       success: false,
 938 |       error: error instanceof Error ? error.message : 'Unknown error occurred'
 939 |     };
 940 |   }
 941 | }
 942 | 
 943 | export async function handleAutofixWorkflow(
 944 |   args: unknown,
 945 |   repository: NodeRepository,
 946 |   context?: InstanceContext
 947 | ): Promise<McpToolResponse> {
 948 |   try {
 949 |     const client = ensureApiConfigured(context);
 950 |     const input = autofixWorkflowSchema.parse(args);
 951 | 
 952 |     // First, fetch the workflow from n8n
 953 |     const workflowResponse = await handleGetWorkflow({ id: input.id }, context);
 954 | 
 955 |     if (!workflowResponse.success) {
 956 |       return workflowResponse; // Return the error from fetching
 957 |     }
 958 | 
 959 |     const workflow = workflowResponse.data as Workflow;
 960 | 
 961 |     // Create validator instance using the provided repository
 962 |     const validator = new WorkflowValidator(repository, EnhancedConfigValidator);
 963 | 
 964 |     // Run validation to identify issues
 965 |     const validationResult = await validator.validateWorkflow(workflow, {
 966 |       validateNodes: true,
 967 |       validateConnections: true,
 968 |       validateExpressions: true,
 969 |       profile: 'ai-friendly'
 970 |     });
 971 | 
 972 |     // Check for expression format issues
 973 |     const allFormatIssues: ExpressionFormatIssue[] = [];
 974 |     for (const node of workflow.nodes) {
 975 |       const formatContext = {
 976 |         nodeType: node.type,
 977 |         nodeName: node.name,
 978 |         nodeId: node.id
 979 |       };
 980 | 
 981 |       const nodeFormatIssues = ExpressionFormatValidator.validateNodeParameters(
 982 |         node.parameters,
 983 |         formatContext
 984 |       );
 985 | 
 986 |       // Add node information to each format issue
 987 |       const enrichedIssues = nodeFormatIssues.map(issue => ({
 988 |         ...issue,
 989 |         nodeName: node.name,
 990 |         nodeId: node.id
 991 |       }));
 992 | 
 993 |       allFormatIssues.push(...enrichedIssues);
 994 |     }
 995 | 
 996 |     // Generate fixes using WorkflowAutoFixer
 997 |     const autoFixer = new WorkflowAutoFixer(repository);
 998 |     const fixResult = autoFixer.generateFixes(
 999 |       workflow,
1000 |       validationResult,
1001 |       allFormatIssues,
1002 |       {
1003 |         applyFixes: input.applyFixes,
1004 |         fixTypes: input.fixTypes,
1005 |         confidenceThreshold: input.confidenceThreshold,
1006 |         maxFixes: input.maxFixes
1007 |       }
1008 |     );
1009 | 
1010 |     // If no fixes available
1011 |     if (fixResult.fixes.length === 0) {
1012 |       return {
1013 |         success: true,
1014 |         data: {
1015 |           workflowId: workflow.id,
1016 |           workflowName: workflow.name,
1017 |           message: 'No automatic fixes available for this workflow',
1018 |           validationSummary: {
1019 |             errors: validationResult.errors.length,
1020 |             warnings: validationResult.warnings.length
1021 |           }
1022 |         }
1023 |       };
1024 |     }
1025 | 
1026 |     // If preview mode (applyFixes = false)
1027 |     if (!input.applyFixes) {
1028 |       return {
1029 |         success: true,
1030 |         data: {
1031 |           workflowId: workflow.id,
1032 |           workflowName: workflow.name,
1033 |           preview: true,
1034 |           fixesAvailable: fixResult.fixes.length,
1035 |           fixes: fixResult.fixes,
1036 |           summary: fixResult.summary,
1037 |           stats: fixResult.stats,
1038 |           message: `${fixResult.fixes.length} fixes available. Set applyFixes=true to apply them.`
1039 |         }
1040 |       };
1041 |     }
1042 | 
1043 |     // Apply fixes using the diff engine
1044 |     if (fixResult.operations.length > 0) {
1045 |       const updateResult = await handleUpdatePartialWorkflow(
1046 |         {
1047 |           id: workflow.id,
1048 |           operations: fixResult.operations
1049 |         },
1050 |         context
1051 |       );
1052 | 
1053 |       if (!updateResult.success) {
1054 |         return {
1055 |           success: false,
1056 |           error: 'Failed to apply fixes',
1057 |           details: {
1058 |             fixes: fixResult.fixes,
1059 |             updateError: updateResult.error
1060 |           }
1061 |         };
1062 |       }
1063 | 
1064 |       return {
1065 |         success: true,
1066 |         data: {
1067 |           workflowId: workflow.id,
1068 |           workflowName: workflow.name,
1069 |           fixesApplied: fixResult.fixes.length,
1070 |           fixes: fixResult.fixes,
1071 |           summary: fixResult.summary,
1072 |           stats: fixResult.stats,
1073 |           message: `Successfully applied ${fixResult.fixes.length} fixes to workflow "${workflow.name}"`
1074 |         }
1075 |       };
1076 |     }
1077 | 
1078 |     return {
1079 |       success: true,
1080 |       data: {
1081 |         workflowId: workflow.id,
1082 |         workflowName: workflow.name,
1083 |         message: 'No fixes needed'
1084 |       }
1085 |     };
1086 | 
1087 |   } catch (error) {
1088 |     if (error instanceof z.ZodError) {
1089 |       return {
1090 |         success: false,
1091 |         error: 'Invalid input',
1092 |         details: { errors: error.errors }
1093 |       };
1094 |     }
1095 | 
1096 |     if (error instanceof N8nApiError) {
1097 |       return {
1098 |         success: false,
1099 |         error: getUserFriendlyErrorMessage(error),
1100 |         code: error.code
1101 |       };
1102 |     }
1103 | 
1104 |     return {
1105 |       success: false,
1106 |       error: error instanceof Error ? error.message : 'Unknown error occurred'
1107 |     };
1108 |   }
1109 | }
1110 | 
1111 | // Execution Management Handlers
1112 | 
1113 | export async function handleTriggerWebhookWorkflow(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
1114 |   try {
1115 |     const client = ensureApiConfigured(context);
1116 |     const input = triggerWebhookSchema.parse(args);
1117 | 
1118 |     const webhookRequest: WebhookRequest = {
1119 |       webhookUrl: input.webhookUrl,
1120 |       httpMethod: input.httpMethod || 'POST',
1121 |       data: input.data,
1122 |       headers: input.headers,
1123 |       waitForResponse: input.waitForResponse ?? true
1124 |     };
1125 | 
1126 |     const response = await client.triggerWebhook(webhookRequest);
1127 | 
1128 |     return {
1129 |       success: true,
1130 |       data: response,
1131 |       message: 'Webhook triggered successfully'
1132 |     };
1133 |   } catch (error) {
1134 |     if (error instanceof z.ZodError) {
1135 |       return {
1136 |         success: false,
1137 |         error: 'Invalid input',
1138 |         details: { errors: error.errors }
1139 |       };
1140 |     }
1141 | 
1142 |     if (error instanceof N8nApiError) {
1143 |       // Try to extract execution context from error response
1144 |       const errorData = error.details as any;
1145 |       const executionId = errorData?.executionId || errorData?.id || errorData?.execution?.id;
1146 |       const workflowId = errorData?.workflowId || errorData?.workflow?.id;
1147 | 
1148 |       // If we have execution ID, provide specific guidance with n8n_get_execution
1149 |       if (executionId) {
1150 |         return {
1151 |           success: false,
1152 |           error: formatExecutionError(executionId, workflowId),
1153 |           code: error.code,
1154 |           executionId,
1155 |           workflowId: workflowId || undefined
1156 |         };
1157 |       }
1158 | 
1159 |       // No execution ID available - workflow likely didn't start
1160 |       // Provide guidance to check recent executions
1161 |       if (error.code === 'SERVER_ERROR' || error.statusCode && error.statusCode >= 500) {
1162 |         return {
1163 |           success: false,
1164 |           error: formatNoExecutionError(),
1165 |           code: error.code
1166 |         };
1167 |       }
1168 | 
1169 |       // For other errors (auth, validation, etc), use standard message
1170 |       return {
1171 |         success: false,
1172 |         error: getUserFriendlyErrorMessage(error),
1173 |         code: error.code,
1174 |         details: error.details as Record<string, unknown> | undefined
1175 |       };
1176 |     }
1177 | 
1178 |     return {
1179 |       success: false,
1180 |       error: error instanceof Error ? error.message : 'Unknown error occurred'
1181 |     };
1182 |   }
1183 | }
1184 | 
1185 | export async function handleGetExecution(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
1186 |   try {
1187 |     const client = ensureApiConfigured(context);
1188 | 
1189 |     // Parse and validate input with new parameters
1190 |     const schema = z.object({
1191 |       id: z.string(),
1192 |       // New filtering parameters
1193 |       mode: z.enum(['preview', 'summary', 'filtered', 'full']).optional(),
1194 |       nodeNames: z.array(z.string()).optional(),
1195 |       itemsLimit: z.number().optional(),
1196 |       includeInputData: z.boolean().optional(),
1197 |       // Legacy parameter (backward compatibility)
1198 |       includeData: z.boolean().optional()
1199 |     });
1200 | 
1201 |     const params = schema.parse(args);
1202 |     const { id, mode, nodeNames, itemsLimit, includeInputData, includeData } = params;
1203 | 
1204 |     /**
1205 |      * Map legacy includeData parameter to mode for backward compatibility
1206 |      *
1207 |      * Legacy behavior:
1208 |      * - includeData: undefined -> minimal execution summary (no data)
1209 |      * - includeData: false -> minimal execution summary (no data)
1210 |      * - includeData: true -> full execution data
1211 |      *
1212 |      * New behavior mapping:
1213 |      * - includeData: undefined -> no mode (minimal)
1214 |      * - includeData: false -> no mode (minimal)
1215 |      * - includeData: true -> mode: 'summary' (2 items per node, not full)
1216 |      *
1217 |      * Note: Legacy true behavior returned ALL data, which could exceed token limits.
1218 |      * New behavior caps at 2 items for safety. Users can use mode: 'full' for old behavior.
1219 |      */
1220 |     let effectiveMode = mode;
1221 |     if (!effectiveMode && includeData !== undefined) {
1222 |       effectiveMode = includeData ? 'summary' : undefined;
1223 |     }
1224 | 
1225 |     // Determine if we need to fetch full data from API
1226 |     // We fetch full data if any mode is specified (including preview) or legacy includeData is true
1227 |     // Preview mode needs the data to analyze structure and generate recommendations
1228 |     const fetchFullData = effectiveMode !== undefined || includeData === true;
1229 | 
1230 |     // Fetch execution from n8n API
1231 |     const execution = await client.getExecution(id, fetchFullData);
1232 | 
1233 |     // If no filtering options specified, return original execution (backward compatibility)
1234 |     if (!effectiveMode && !nodeNames && itemsLimit === undefined) {
1235 |       return {
1236 |         success: true,
1237 |         data: execution
1238 |       };
1239 |     }
1240 | 
1241 |     // Apply filtering using ExecutionProcessor
1242 |     const filterOptions: ExecutionFilterOptions = {
1243 |       mode: effectiveMode,
1244 |       nodeNames,
1245 |       itemsLimit,
1246 |       includeInputData
1247 |     };
1248 | 
1249 |     const processedExecution = processExecution(execution, filterOptions);
1250 | 
1251 |     return {
1252 |       success: true,
1253 |       data: processedExecution
1254 |     };
1255 |   } catch (error) {
1256 |     if (error instanceof z.ZodError) {
1257 |       return {
1258 |         success: false,
1259 |         error: 'Invalid input',
1260 |         details: { errors: error.errors }
1261 |       };
1262 |     }
1263 | 
1264 |     if (error instanceof N8nApiError) {
1265 |       return {
1266 |         success: false,
1267 |         error: getUserFriendlyErrorMessage(error),
1268 |         code: error.code
1269 |       };
1270 |     }
1271 | 
1272 |     return {
1273 |       success: false,
1274 |       error: error instanceof Error ? error.message : 'Unknown error occurred'
1275 |     };
1276 |   }
1277 | }
1278 | 
1279 | export async function handleListExecutions(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
1280 |   try {
1281 |     const client = ensureApiConfigured(context);
1282 |     const input = listExecutionsSchema.parse(args || {});
1283 |     
1284 |     const response = await client.listExecutions({
1285 |       limit: input.limit || 100,
1286 |       cursor: input.cursor,
1287 |       workflowId: input.workflowId,
1288 |       projectId: input.projectId,
1289 |       status: input.status as ExecutionStatus | undefined,
1290 |       includeData: input.includeData || false
1291 |     });
1292 |     
1293 |     return {
1294 |       success: true,
1295 |       data: {
1296 |         executions: response.data,
1297 |         returned: response.data.length,
1298 |         nextCursor: response.nextCursor,
1299 |         hasMore: !!response.nextCursor,
1300 |         ...(response.nextCursor ? { 
1301 |           _note: "More executions available. Use cursor to get next page." 
1302 |         } : {})
1303 |       }
1304 |     };
1305 |   } catch (error) {
1306 |     if (error instanceof z.ZodError) {
1307 |       return {
1308 |         success: false,
1309 |         error: 'Invalid input',
1310 |         details: { errors: error.errors }
1311 |       };
1312 |     }
1313 |     
1314 |     if (error instanceof N8nApiError) {
1315 |       return {
1316 |         success: false,
1317 |         error: getUserFriendlyErrorMessage(error),
1318 |         code: error.code
1319 |       };
1320 |     }
1321 |     
1322 |     return {
1323 |       success: false,
1324 |       error: error instanceof Error ? error.message : 'Unknown error occurred'
1325 |     };
1326 |   }
1327 | }
1328 | 
1329 | export async function handleDeleteExecution(args: unknown, context?: InstanceContext): Promise<McpToolResponse> {
1330 |   try {
1331 |     const client = ensureApiConfigured(context);
1332 |     const { id } = z.object({ id: z.string() }).parse(args);
1333 |     
1334 |     await client.deleteExecution(id);
1335 |     
1336 |     return {
1337 |       success: true,
1338 |       message: `Execution ${id} deleted successfully`
1339 |     };
1340 |   } catch (error) {
1341 |     if (error instanceof z.ZodError) {
1342 |       return {
1343 |         success: false,
1344 |         error: 'Invalid input',
1345 |         details: { errors: error.errors }
1346 |       };
1347 |     }
1348 |     
1349 |     if (error instanceof N8nApiError) {
1350 |       return {
1351 |         success: false,
1352 |         error: getUserFriendlyErrorMessage(error),
1353 |         code: error.code
1354 |       };
1355 |     }
1356 |     
1357 |     return {
1358 |       success: false,
1359 |       error: error instanceof Error ? error.message : 'Unknown error occurred'
1360 |     };
1361 |   }
1362 | }
1363 | 
1364 | // System Tools Handlers
1365 | 
1366 | export async function handleHealthCheck(context?: InstanceContext): Promise<McpToolResponse> {
1367 |   const startTime = Date.now();
1368 | 
1369 |   try {
1370 |     const client = ensureApiConfigured(context);
1371 |     const health = await client.healthCheck();
1372 | 
1373 |     // Get MCP version from package.json
1374 |     const packageJson = require('../../package.json');
1375 |     const mcpVersion = packageJson.version;
1376 |     const supportedN8nVersion = packageJson.dependencies?.n8n?.replace(/[^0-9.]/g, '');
1377 | 
1378 |     // Check npm for latest version (async, non-blocking)
1379 |     const versionCheck = await checkNpmVersion();
1380 | 
1381 |     // Get cache metrics for performance monitoring
1382 |     const cacheMetricsData = getInstanceCacheMetrics();
1383 | 
1384 |     // Calculate response time
1385 |     const responseTime = Date.now() - startTime;
1386 | 
1387 |     // Build response data
1388 |     const responseData: HealthCheckResponseData = {
1389 |       status: health.status,
1390 |       instanceId: health.instanceId,
1391 |       n8nVersion: health.n8nVersion,
1392 |       features: health.features,
1393 |       apiUrl: getN8nApiConfig()?.baseUrl,
1394 |       mcpVersion,
1395 |       supportedN8nVersion,
1396 |       versionCheck: {
1397 |         current: versionCheck.currentVersion,
1398 |         latest: versionCheck.latestVersion,
1399 |         upToDate: !versionCheck.isOutdated,
1400 |         message: formatVersionMessage(versionCheck),
1401 |         ...(versionCheck.updateCommand ? { updateCommand: versionCheck.updateCommand } : {})
1402 |       },
1403 |       performance: {
1404 |         responseTimeMs: responseTime,
1405 |         cacheHitRate: (cacheMetricsData.hits + cacheMetricsData.misses) > 0
1406 |           ? ((cacheMetricsData.hits / (cacheMetricsData.hits + cacheMetricsData.misses)) * 100).toFixed(2) + '%'
1407 |           : 'N/A',
1408 |         cachedInstances: cacheMetricsData.size
1409 |       }
1410 |     };
1411 | 
1412 |     // Add next steps guidance based on telemetry insights
1413 |     responseData.nextSteps = [
1414 |       '• Create workflow: n8n_create_workflow',
1415 |       '• List workflows: n8n_list_workflows',
1416 |       '• Search nodes: search_nodes',
1417 |       '• Browse templates: search_templates'
1418 |     ];
1419 | 
1420 |     // Add update warning if outdated
1421 |     if (versionCheck.isOutdated && versionCheck.latestVersion) {
1422 |       responseData.updateWarning = `⚠️  n8n-mcp v${versionCheck.latestVersion} is available (you have v${versionCheck.currentVersion}). Update recommended.`;
1423 |     }
1424 | 
1425 |     // Track result in telemetry
1426 |     telemetry.trackEvent('health_check_completed', {
1427 |       success: true,
1428 |       responseTimeMs: responseTime,
1429 |       upToDate: !versionCheck.isOutdated,
1430 |       apiConnected: true
1431 |     });
1432 | 
1433 |     return {
1434 |       success: true,
1435 |       data: responseData
1436 |     };
1437 |   } catch (error) {
1438 |     const responseTime = Date.now() - startTime;
1439 | 
1440 |     // Track failure in telemetry
1441 |     telemetry.trackEvent('health_check_failed', {
1442 |       success: false,
1443 |       responseTimeMs: responseTime,
1444 |       errorType: error instanceof N8nApiError ? error.code : 'unknown'
1445 |     });
1446 | 
1447 |     if (error instanceof N8nApiError) {
1448 |       return {
1449 |         success: false,
1450 |         error: getUserFriendlyErrorMessage(error),
1451 |         code: error.code,
1452 |         details: {
1453 |           apiUrl: getN8nApiConfig()?.baseUrl,
1454 |           hint: 'Check if n8n is running and API is enabled',
1455 |           troubleshooting: [
1456 |             '1. Verify n8n instance is running',
1457 |             '2. Check N8N_API_URL is correct',
1458 |             '3. Verify N8N_API_KEY has proper permissions',
1459 |             '4. Run n8n_diagnostic for detailed analysis'
1460 |           ]
1461 |         }
1462 |       };
1463 |     }
1464 | 
1465 |     return {
1466 |       success: false,
1467 |       error: error instanceof Error ? error.message : 'Unknown error occurred'
1468 |     };
1469 |   }
1470 | }
1471 | 
1472 | export async function handleListAvailableTools(context?: InstanceContext): Promise<McpToolResponse> {
1473 |   const tools = [
1474 |     {
1475 |       category: 'Workflow Management',
1476 |       tools: [
1477 |         { name: 'n8n_create_workflow', description: 'Create new workflows' },
1478 |         { name: 'n8n_get_workflow', description: 'Get workflow by ID' },
1479 |         { name: 'n8n_get_workflow_details', description: 'Get detailed workflow info with stats' },
1480 |         { name: 'n8n_get_workflow_structure', description: 'Get simplified workflow structure' },
1481 |         { name: 'n8n_get_workflow_minimal', description: 'Get minimal workflow info' },
1482 |         { name: 'n8n_update_workflow', description: 'Update existing workflows' },
1483 |         { name: 'n8n_delete_workflow', description: 'Delete workflows' },
1484 |         { name: 'n8n_list_workflows', description: 'List workflows with filters' },
1485 |         { name: 'n8n_validate_workflow', description: 'Validate workflow from n8n instance' },
1486 |         { name: 'n8n_autofix_workflow', description: 'Automatically fix common workflow errors' }
1487 |       ]
1488 |     },
1489 |     {
1490 |       category: 'Execution Management',
1491 |       tools: [
1492 |         { name: 'n8n_trigger_webhook_workflow', description: 'Trigger workflows via webhook' },
1493 |         { name: 'n8n_get_execution', description: 'Get execution details' },
1494 |         { name: 'n8n_list_executions', description: 'List executions with filters' },
1495 |         { name: 'n8n_delete_execution', description: 'Delete execution records' }
1496 |       ]
1497 |     },
1498 |     {
1499 |       category: 'System',
1500 |       tools: [
1501 |         { name: 'n8n_health_check', description: 'Check API connectivity' },
1502 |         { name: 'n8n_list_available_tools', description: 'List all available tools' }
1503 |       ]
1504 |     }
1505 |   ];
1506 |   
1507 |   const config = getN8nApiConfig();
1508 |   const apiConfigured = config !== null;
1509 |   
1510 |   return {
1511 |     success: true,
1512 |     data: {
1513 |       tools,
1514 |       apiConfigured,
1515 |       configuration: config ? {
1516 |         apiUrl: config.baseUrl,
1517 |         timeout: config.timeout,
1518 |         maxRetries: config.maxRetries
1519 |       } : null,
1520 |       limitations: [
1521 |         'Cannot activate/deactivate workflows via API',
1522 |         'Cannot execute workflows directly (must use webhooks)',
1523 |         'Cannot stop running executions',
1524 |         'Tags and credentials have limited API support'
1525 |       ]
1526 |     }
1527 |   };
1528 | }
1529 | 
1530 | // Environment-aware debugging helpers
1531 | 
1532 | /**
1533 |  * Detect cloud platform from environment variables
1534 |  * Returns platform name or null if not in cloud
1535 |  */
1536 | function detectCloudPlatform(): string | null {
1537 |   if (process.env.RAILWAY_ENVIRONMENT) return 'railway';
1538 |   if (process.env.RENDER) return 'render';
1539 |   if (process.env.FLY_APP_NAME) return 'fly';
1540 |   if (process.env.HEROKU_APP_NAME) return 'heroku';
1541 |   if (process.env.AWS_EXECUTION_ENV) return 'aws';
1542 |   if (process.env.KUBERNETES_SERVICE_HOST) return 'kubernetes';
1543 |   if (process.env.GOOGLE_CLOUD_PROJECT) return 'gcp';
1544 |   if (process.env.AZURE_FUNCTIONS_ENVIRONMENT) return 'azure';
1545 |   return null;
1546 | }
1547 | 
1548 | /**
1549 |  * Get mode-specific debugging suggestions
1550 |  */
1551 | function getModeSpecificDebug(mcpMode: string) {
1552 |   if (mcpMode === 'http') {
1553 |     const port = process.env.MCP_PORT || process.env.PORT || 3000;
1554 |     return {
1555 |       mode: 'HTTP Server',
1556 |       port,
1557 |       authTokenConfigured: !!(process.env.MCP_AUTH_TOKEN || process.env.AUTH_TOKEN),
1558 |       corsEnabled: true,
1559 |       serverUrl: `http://localhost:${port}`,
1560 |       healthCheckUrl: `http://localhost:${port}/health`,
1561 |       troubleshooting: [
1562 |         `1. Test server health: curl http://localhost:${port}/health`,
1563 |         '2. Check browser console for CORS errors',
1564 |         '3. Verify MCP_AUTH_TOKEN or AUTH_TOKEN if authentication enabled',
1565 |         `4. Ensure port ${port} is not in use: lsof -i :${port} (macOS/Linux) or netstat -ano | findstr :${port} (Windows)`,
1566 |         '5. Check firewall settings for port access',
1567 |         '6. Review server logs for connection errors'
1568 |       ],
1569 |       commonIssues: [
1570 |         'CORS policy blocking browser requests',
1571 |         'Port already in use by another application',
1572 |         'Authentication token mismatch',
1573 |         'Network firewall blocking connections'
1574 |       ]
1575 |     };
1576 |   } else {
1577 |     // stdio mode
1578 |     const configLocation = process.platform === 'darwin'
1579 |       ? '~/Library/Application Support/Claude/claude_desktop_config.json'
1580 |       : process.platform === 'win32'
1581 |       ? '%APPDATA%\\Claude\\claude_desktop_config.json'
1582 |       : '~/.config/Claude/claude_desktop_config.json';
1583 | 
1584 |     return {
1585 |       mode: 'Standard I/O (Claude Desktop)',
1586 |       configLocation,
1587 |       troubleshooting: [
1588 |         '1. Verify Claude Desktop config file exists and is valid JSON',
1589 |         '2. Check MCP server entry: {"mcpServers": {"n8n": {"command": "npx", "args": ["-y", "n8n-mcp"]}}}',
1590 |         '3. Restart Claude Desktop after config changes',
1591 |         '4. Check Claude Desktop logs for startup errors',
1592 |         '5. Test npx can run: npx -y n8n-mcp --version',
1593 |         '6. Verify executable permissions if using local installation'
1594 |       ],
1595 |       commonIssues: [
1596 |         'Invalid JSON in claude_desktop_config.json',
1597 |         'Incorrect command or args in MCP server config',
1598 |         'Claude Desktop not restarted after config changes',
1599 |         'npx unable to download or run package',
1600 |         'Missing execute permissions on local binary'
1601 |       ]
1602 |     };
1603 |   }
1604 | }
1605 | 
1606 | /**
1607 |  * Get Docker-specific debugging suggestions
1608 |  */
1609 | function getDockerDebug(isDocker: boolean) {
1610 |   if (!isDocker) return null;
1611 | 
1612 |   return {
1613 |     containerDetected: true,
1614 |     troubleshooting: [
1615 |       '1. Verify volume mounts for data/nodes.db',
1616 |       '2. Check network connectivity to n8n instance',
1617 |       '3. Ensure ports are correctly mapped',
1618 |       '4. Review container logs: docker logs <container-name>',
1619 |       '5. Verify environment variables passed to container',
1620 |       '6. Check IS_DOCKER=true is set correctly'
1621 |     ],
1622 |     commonIssues: [
1623 |       'Volume mount not persisting database',
1624 |       'Network isolation preventing n8n API access',
1625 |       'Port mapping conflicts',
1626 |       'Missing environment variables in container'
1627 |     ]
1628 |   };
1629 | }
1630 | 
1631 | /**
1632 |  * Get cloud platform-specific suggestions
1633 |  */
1634 | function getCloudPlatformDebug(cloudPlatform: string | null) {
1635 |   if (!cloudPlatform) return null;
1636 | 
1637 |   const platformGuides: Record<string, CloudPlatformGuide> = {
1638 |     railway: {
1639 |       name: 'Railway',
1640 |       troubleshooting: [
1641 |         '1. Check Railway environment variables are set',
1642 |         '2. Verify deployment logs in Railway dashboard',
1643 |         '3. Ensure PORT matches Railway assigned port (automatic)',
1644 |         '4. Check networking configuration for external access'
1645 |       ]
1646 |     },
1647 |     render: {
1648 |       name: 'Render',
1649 |       troubleshooting: [
1650 |         '1. Verify Render environment variables',
1651 |         '2. Check Render logs for startup errors',
1652 |         '3. Ensure health check endpoint is responding',
1653 |         '4. Verify instance type has sufficient resources'
1654 |       ]
1655 |     },
1656 |     fly: {
1657 |       name: 'Fly.io',
1658 |       troubleshooting: [
1659 |         '1. Check Fly.io logs: flyctl logs',
1660 |         '2. Verify fly.toml configuration',
1661 |         '3. Ensure volumes are properly mounted',
1662 |         '4. Check app status: flyctl status'
1663 |       ]
1664 |     },
1665 |     heroku: {
1666 |       name: 'Heroku',
1667 |       troubleshooting: [
1668 |         '1. Check Heroku logs: heroku logs --tail',
1669 |         '2. Verify Procfile configuration',
1670 |         '3. Ensure dynos are running: heroku ps',
1671 |         '4. Check environment variables: heroku config'
1672 |       ]
1673 |     },
1674 |     kubernetes: {
1675 |       name: 'Kubernetes',
1676 |       troubleshooting: [
1677 |         '1. Check pod logs: kubectl logs <pod-name>',
1678 |         '2. Verify service and ingress configuration',
1679 |         '3. Check persistent volume claims',
1680 |         '4. Verify resource limits and requests'
1681 |       ]
1682 |     },
1683 |     aws: {
1684 |       name: 'AWS',
1685 |       troubleshooting: [
1686 |         '1. Check CloudWatch logs',
1687 |         '2. Verify IAM roles and permissions',
1688 |         '3. Check security groups and networking',
1689 |         '4. Verify environment variables in service config'
1690 |       ]
1691 |     }
1692 |   };
1693 | 
1694 |   return platformGuides[cloudPlatform] || {
1695 |     name: cloudPlatform.toUpperCase(),
1696 |     troubleshooting: [
1697 |       '1. Check cloud platform logs',
1698 |       '2. Verify environment variables are set',
1699 |       '3. Check networking and port configuration',
1700 |       '4. Review platform-specific documentation'
1701 |     ]
1702 |   };
1703 | }
1704 | 
1705 | // Handler: n8n_diagnostic
1706 | export async function handleDiagnostic(request: any, context?: InstanceContext): Promise<McpToolResponse> {
1707 |   const startTime = Date.now();
1708 |   const verbose = request.params?.arguments?.verbose || false;
1709 | 
1710 |   // Detect environment for targeted debugging
1711 |   const mcpMode = process.env.MCP_MODE || 'stdio';
1712 |   const isDocker = process.env.IS_DOCKER === 'true';
1713 |   const cloudPlatform = detectCloudPlatform();
1714 | 
1715 |   // Check environment variables
1716 |   const envVars = {
1717 |     N8N_API_URL: process.env.N8N_API_URL || null,
1718 |     N8N_API_KEY: process.env.N8N_API_KEY ? '***configured***' : null,
1719 |     NODE_ENV: process.env.NODE_ENV || 'production',
1720 |     MCP_MODE: mcpMode,
1721 |     isDocker,
1722 |     cloudPlatform,
1723 |     nodeVersion: process.version,
1724 |     platform: process.platform
1725 |   };
1726 | 
1727 |   // Check API configuration
1728 |   const apiConfig = getN8nApiConfig();
1729 |   const apiConfigured = apiConfig !== null;
1730 |   const apiClient = getN8nApiClient(context);
1731 | 
1732 |   // Test API connectivity if configured
1733 |   let apiStatus = {
1734 |     configured: apiConfigured,
1735 |     connected: false,
1736 |     error: null as string | null,
1737 |     version: null as string | null
1738 |   };
1739 | 
1740 |   if (apiClient) {
1741 |     try {
1742 |       const health = await apiClient.healthCheck();
1743 |       apiStatus.connected = true;
1744 |       apiStatus.version = health.n8nVersion || 'unknown';
1745 |     } catch (error) {
1746 |       apiStatus.error = error instanceof Error ? error.message : 'Unknown error';
1747 |     }
1748 |   }
1749 | 
1750 |   // Check which tools are available
1751 |   const documentationTools = 22; // Base documentation tools
1752 |   const managementTools = apiConfigured ? 16 : 0;
1753 |   const totalTools = documentationTools + managementTools;
1754 | 
1755 |   // Check npm version
1756 |   const versionCheck = await checkNpmVersion();
1757 | 
1758 |   // Get performance metrics
1759 |   const cacheMetricsData = getInstanceCacheMetrics();
1760 |   const responseTime = Date.now() - startTime;
1761 | 
1762 |   // Build diagnostic report
1763 |   const diagnostic: DiagnosticResponseData = {
1764 |     timestamp: new Date().toISOString(),
1765 |     environment: envVars,
1766 |     apiConfiguration: {
1767 |       configured: apiConfigured,
1768 |       status: apiStatus,
1769 |       config: apiConfig ? {
1770 |         baseUrl: apiConfig.baseUrl,
1771 |         timeout: apiConfig.timeout,
1772 |         maxRetries: apiConfig.maxRetries
1773 |       } : null
1774 |     },
1775 |     versionInfo: {
1776 |       current: versionCheck.currentVersion,
1777 |       latest: versionCheck.latestVersion,
1778 |       upToDate: !versionCheck.isOutdated,
1779 |       message: formatVersionMessage(versionCheck),
1780 |       ...(versionCheck.updateCommand ? { updateCommand: versionCheck.updateCommand } : {})
1781 |     },
1782 |     toolsAvailability: {
1783 |       documentationTools: {
1784 |         count: documentationTools,
1785 |         enabled: true,
1786 |         description: 'Always available - node info, search, validation, etc.'
1787 |       },
1788 |       managementTools: {
1789 |         count: managementTools,
1790 |         enabled: apiConfigured,
1791 |         description: apiConfigured ?
1792 |           'Management tools are ENABLED - create, update, execute workflows' :
1793 |           'Management tools are DISABLED - configure N8N_API_URL and N8N_API_KEY to enable'
1794 |       },
1795 |       totalAvailable: totalTools
1796 |     },
1797 |     performance: {
1798 |       diagnosticResponseTimeMs: responseTime,
1799 |       cacheHitRate: (cacheMetricsData.hits + cacheMetricsData.misses) > 0
1800 |         ? ((cacheMetricsData.hits / (cacheMetricsData.hits + cacheMetricsData.misses)) * 100).toFixed(2) + '%'
1801 |         : 'N/A',
1802 |       cachedInstances: cacheMetricsData.size
1803 |     },
1804 |     modeSpecificDebug: getModeSpecificDebug(mcpMode)
1805 |   };
1806 | 
1807 |   // Enhanced guidance based on telemetry insights
1808 |   if (apiConfigured && apiStatus.connected) {
1809 |     // API is working - provide next steps
1810 |     diagnostic.nextSteps = {
1811 |       message: '✓ API connected! Here\'s what you can do:',
1812 |       recommended: [
1813 |         {
1814 |           action: 'n8n_list_workflows',
1815 |           description: 'See your existing workflows',
1816 |           timing: 'Fast (6 seconds median)'
1817 |         },
1818 |         {
1819 |           action: 'n8n_create_workflow',
1820 |           description: 'Create a new workflow',
1821 |           timing: 'Typically 6-14 minutes to build'
1822 |         },
1823 |         {
1824 |           action: 'search_nodes',
1825 |           description: 'Discover available nodes',
1826 |           timing: 'Fast - explore 500+ nodes'
1827 |         },
1828 |         {
1829 |           action: 'search_templates',
1830 |           description: 'Browse pre-built workflows',
1831 |           timing: 'Find examples quickly'
1832 |         }
1833 |       ],
1834 |       tips: [
1835 |         '82% of users start creating workflows after diagnostics - you\'re ready to go!',
1836 |         'Most common first action: n8n_update_partial_workflow (managing existing workflows)',
1837 |         'Use n8n_validate_workflow before deploying to catch issues early'
1838 |       ]
1839 |     };
1840 |   } else if (apiConfigured && !apiStatus.connected) {
1841 |     // API configured but not connecting - troubleshooting
1842 |     diagnostic.troubleshooting = {
1843 |       issue: '⚠️ API configured but connection failed',
1844 |       error: apiStatus.error,
1845 |       steps: [
1846 |         '1. Verify n8n instance is running and accessible',
1847 |         '2. Check N8N_API_URL is correct (currently: ' + apiConfig?.baseUrl + ')',
1848 |         '3. Test URL in browser: ' + apiConfig?.baseUrl + '/healthz',
1849 |         '4. Verify N8N_API_KEY has proper permissions',
1850 |         '5. Check firewall/network settings if using remote n8n',
1851 |         '6. Try running n8n_health_check again after fixes'
1852 |       ],
1853 |       commonIssues: [
1854 |         'Wrong port number in N8N_API_URL',
1855 |         'API key doesn\'t have sufficient permissions',
1856 |         'n8n instance not running or crashed',
1857 |         'Network firewall blocking connection'
1858 |       ],
1859 |       documentation: 'https://github.com/czlonkowski/n8n-mcp?tab=readme-ov-file#n8n-management-tools-optional---requires-api-configuration'
1860 |     };
1861 |   } else {
1862 |     // API not configured - setup guidance
1863 |     diagnostic.setupGuide = {
1864 |       message: 'n8n API not configured. You can still use documentation tools!',
1865 |       whatYouCanDoNow: {
1866 |         documentation: [
1867 |           {
1868 |             tool: 'search_nodes',
1869 |             description: 'Search 500+ n8n nodes',
1870 |             example: 'search_nodes({query: "slack"})'
1871 |           },
1872 |           {
1873 |             tool: 'get_node_essentials',
1874 |             description: 'Get node configuration details',
1875 |             example: 'get_node_essentials({nodeType: "nodes-base.httpRequest"})'
1876 |           },
1877 |           {
1878 |             tool: 'search_templates',
1879 |             description: 'Browse workflow templates',
1880 |             example: 'search_templates({query: "chatbot"})'
1881 |           },
1882 |           {
1883 |             tool: 'validate_workflow',
1884 |             description: 'Validate workflow JSON',
1885 |             example: 'validate_workflow({workflow: {...}})'
1886 |           }
1887 |         ],
1888 |         note: '22 documentation tools available without API configuration'
1889 |       },
1890 |       whatYouCannotDo: [
1891 |         '✗ Create/update workflows in n8n instance',
1892 |         '✗ List your workflows',
1893 |         '✗ Execute workflows',
1894 |         '✗ View execution results'
1895 |       ],
1896 |       howToEnable: {
1897 |         steps: [
1898 |           '1. Get your n8n API key: [Your n8n instance]/settings/api',
1899 |           '2. Set environment variables:',
1900 |           '   N8N_API_URL=https://your-n8n-instance.com',
1901 |           '   N8N_API_KEY=your_api_key_here',
1902 |           '3. Restart the MCP server',
1903 |           '4. Run n8n_diagnostic again to verify',
1904 |           '5. All 38 tools will be available!'
1905 |         ],
1906 |         documentation: 'https://github.com/czlonkowski/n8n-mcp?tab=readme-ov-file#n8n-management-tools-optional---requires-api-configuration'
1907 |       }
1908 |     };
1909 |   }
1910 | 
1911 |   // Add version warning if outdated
1912 |   if (versionCheck.isOutdated && versionCheck.latestVersion) {
1913 |     diagnostic.updateWarning = {
1914 |       message: `⚠️ Update available: v${versionCheck.currentVersion} → v${versionCheck.latestVersion}`,
1915 |       command: versionCheck.updateCommand,
1916 |       benefits: [
1917 |         'Latest bug fixes and improvements',
1918 |         'New features and tools',
1919 |         'Better performance and reliability'
1920 |       ]
1921 |     };
1922 |   }
1923 | 
1924 |   // Add Docker-specific debugging if in container
1925 |   const dockerDebug = getDockerDebug(isDocker);
1926 |   if (dockerDebug) {
1927 |     diagnostic.dockerDebug = dockerDebug;
1928 |   }
1929 | 
1930 |   // Add cloud platform-specific debugging if detected
1931 |   const cloudDebug = getCloudPlatformDebug(cloudPlatform);
1932 |   if (cloudDebug) {
1933 |     diagnostic.cloudPlatformDebug = cloudDebug;
1934 |   }
1935 | 
1936 |   // Add verbose debug info if requested
1937 |   if (verbose) {
1938 |     diagnostic.debug = {
1939 |       processEnv: Object.keys(process.env).filter(key =>
1940 |         key.startsWith('N8N_') || key.startsWith('MCP_')
1941 |       ),
1942 |       nodeVersion: process.version,
1943 |       platform: process.platform,
1944 |       workingDirectory: process.cwd(),
1945 |       cacheMetrics: cacheMetricsData
1946 |     };
1947 |   }
1948 | 
1949 |   // Track diagnostic usage with result data
1950 |   telemetry.trackEvent('diagnostic_completed', {
1951 |     success: true,
1952 |     apiConfigured,
1953 |     apiConnected: apiStatus.connected,
1954 |     toolsAvailable: totalTools,
1955 |     responseTimeMs: responseTime,
1956 |     upToDate: !versionCheck.isOutdated,
1957 |     verbose
1958 |   });
1959 | 
1960 |   return {
1961 |     success: true,
1962 |     data: diagnostic
1963 |   };
1964 | }
1965 | 
```

--------------------------------------------------------------------------------
/tests/unit/services/workflow-validator-comprehensive.test.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
   2 | import { WorkflowValidator } from '@/services/workflow-validator';
   3 | import { NodeRepository } from '@/database/node-repository';
   4 | import { EnhancedConfigValidator } from '@/services/enhanced-config-validator';
   5 | import { ExpressionValidator } from '@/services/expression-validator';
   6 | import { createWorkflow } from '@tests/utils/builders/workflow.builder';
   7 | import type { WorkflowNode, Workflow } from '@/types/n8n-api';
   8 | 
   9 | // Mock dependencies
  10 | vi.mock('@/database/node-repository');
  11 | vi.mock('@/services/enhanced-config-validator');
  12 | vi.mock('@/services/expression-validator');
  13 | vi.mock('@/utils/logger');
  14 | 
  15 | describe('WorkflowValidator - Comprehensive Tests', () => {
  16 |   let validator: WorkflowValidator;
  17 |   let mockNodeRepository: NodeRepository;
  18 |   let mockEnhancedConfigValidator: typeof EnhancedConfigValidator;
  19 | 
  20 |   beforeEach(() => {
  21 |     vi.clearAllMocks();
  22 | 
  23 |     // Create mock instances
  24 |     mockNodeRepository = new NodeRepository({} as any) as any;
  25 |     mockEnhancedConfigValidator = EnhancedConfigValidator as any;
  26 | 
  27 |     // Ensure the mock repository has all necessary methods
  28 |     if (!mockNodeRepository.getAllNodes) {
  29 |       mockNodeRepository.getAllNodes = vi.fn();
  30 |     }
  31 |     if (!mockNodeRepository.getNode) {
  32 |       mockNodeRepository.getNode = vi.fn();
  33 |     }
  34 | 
  35 |     // Mock common node types data
  36 |     const nodeTypes: Record<string, any> = {
  37 |       'nodes-base.webhook': {
  38 |         type: 'nodes-base.webhook',
  39 |         displayName: 'Webhook',
  40 |         package: 'n8n-nodes-base',
  41 |         version: 2,
  42 |         isVersioned: true,
  43 |         properties: [],
  44 |         category: 'trigger'
  45 |       },
  46 |       'nodes-base.httpRequest': {
  47 |         type: 'nodes-base.httpRequest',
  48 |         displayName: 'HTTP Request',
  49 |         package: 'n8n-nodes-base',
  50 |         version: 4,
  51 |         isVersioned: true,
  52 |         properties: [],
  53 |         category: 'network'
  54 |       },
  55 |       'nodes-base.set': {
  56 |         type: 'nodes-base.set',
  57 |         displayName: 'Set',
  58 |         package: 'n8n-nodes-base',
  59 |         version: 3,
  60 |         isVersioned: true,
  61 |         properties: [],
  62 |         category: 'data'
  63 |       },
  64 |       'nodes-base.code': {
  65 |         type: 'nodes-base.code',
  66 |         displayName: 'Code',
  67 |         package: 'n8n-nodes-base',
  68 |         version: 2,
  69 |         isVersioned: true,
  70 |         properties: [],
  71 |         category: 'code'
  72 |       },
  73 |       'nodes-base.manualTrigger': {
  74 |         type: 'nodes-base.manualTrigger',
  75 |         displayName: 'Manual Trigger',
  76 |         package: 'n8n-nodes-base',
  77 |         version: 1,
  78 |         isVersioned: true,
  79 |         properties: [],
  80 |         category: 'trigger'
  81 |       },
  82 |       'nodes-base.if': {
  83 |         type: 'nodes-base.if',
  84 |         displayName: 'IF',
  85 |         package: 'n8n-nodes-base',
  86 |         version: 2,
  87 |         isVersioned: true,
  88 |         properties: [],
  89 |         category: 'logic'
  90 |       },
  91 |       'nodes-base.slack': {
  92 |         type: 'nodes-base.slack',
  93 |         displayName: 'Slack',
  94 |         package: 'n8n-nodes-base',
  95 |         version: 2,
  96 |         isVersioned: true,
  97 |         properties: [],
  98 |         category: 'communication'
  99 |       },
 100 |       'nodes-base.googleSheets': {
 101 |         type: 'nodes-base.googleSheets',
 102 |         displayName: 'Google Sheets',
 103 |         package: 'n8n-nodes-base',
 104 |         version: 4,
 105 |         isVersioned: true,
 106 |         properties: [],
 107 |         category: 'data'
 108 |       },
 109 |       'nodes-langchain.agent': {
 110 |         type: 'nodes-langchain.agent',
 111 |         displayName: 'AI Agent',
 112 |         package: '@n8n/n8n-nodes-langchain',
 113 |         version: 1,
 114 |         isVersioned: true,
 115 |         properties: [],
 116 |         isAITool: true,
 117 |         category: 'ai'
 118 |       },
 119 |       'nodes-base.postgres': {
 120 |         type: 'nodes-base.postgres',
 121 |         displayName: 'Postgres',
 122 |         package: 'n8n-nodes-base',
 123 |         version: 2,
 124 |         isVersioned: true,
 125 |         properties: [],
 126 |         category: 'database'
 127 |       },
 128 |       'community.customNode': {
 129 |         type: 'community.customNode',
 130 |         displayName: 'Custom Node',
 131 |         package: 'n8n-nodes-custom',
 132 |         version: 1,
 133 |         isVersioned: false,
 134 |         properties: [],
 135 |         isAITool: false,
 136 |         category: 'custom'
 137 |       }
 138 |     };
 139 | 
 140 |     // Set up default mock behaviors
 141 |     vi.mocked(mockNodeRepository.getNode).mockImplementation((nodeType: string) => {
 142 |       // Handle normalization for custom nodes
 143 |       if (nodeType === 'n8n-nodes-custom.customNode') {
 144 |         return {
 145 |           type: 'n8n-nodes-custom.customNode',
 146 |           displayName: 'Custom Node',
 147 |           package: 'n8n-nodes-custom',
 148 |           version: 1,
 149 |           isVersioned: false,
 150 |           properties: [],
 151 |           isAITool: false
 152 |         };
 153 |       }
 154 | 
 155 |       return nodeTypes[nodeType] || null;
 156 |     });
 157 | 
 158 |     // Mock getAllNodes for NodeSimilarityService
 159 |     vi.mocked(mockNodeRepository.getAllNodes).mockReturnValue(Object.values(nodeTypes));
 160 | 
 161 |     vi.mocked(mockEnhancedConfigValidator.validateWithMode).mockReturnValue({
 162 |       errors: [],
 163 |       warnings: [],
 164 |       suggestions: [],
 165 |       mode: 'operation' as const,
 166 |       valid: true,
 167 |       visibleProperties: [],
 168 |       hiddenProperties: []
 169 |     } as any);
 170 | 
 171 |     vi.mocked(ExpressionValidator.validateNodeExpressions).mockReturnValue({
 172 |       valid: true,
 173 |       errors: [],
 174 |       warnings: [],
 175 |       usedVariables: new Set(),
 176 |       usedNodes: new Set()
 177 |     });
 178 | 
 179 |     // Create validator instance
 180 |     validator = new WorkflowValidator(mockNodeRepository, mockEnhancedConfigValidator);
 181 |   });
 182 | 
 183 |   describe('validateWorkflow', () => {
 184 |     it('should validate a minimal valid workflow', async () => {
 185 |       const workflow = createWorkflow('Test Workflow')
 186 |         .addWebhookNode({ name: 'Webhook' })
 187 |         .build();
 188 | 
 189 |       const result = await validator.validateWorkflow(workflow as any);
 190 | 
 191 |       expect(result.valid).toBe(true);
 192 |       expect(result.errors).toHaveLength(0);
 193 |       expect(result.statistics.totalNodes).toBe(1);
 194 |       expect(result.statistics.enabledNodes).toBe(1);
 195 |       expect(result.statistics.triggerNodes).toBe(1);
 196 |     });
 197 | 
 198 |     it('should validate a workflow with all options disabled', async () => {
 199 |       const workflow = createWorkflow('Test Workflow')
 200 |         .addWebhookNode({ name: 'Webhook' })
 201 |         .build();
 202 | 
 203 |       const result = await validator.validateWorkflow(workflow as any, {
 204 |         validateNodes: false,
 205 |         validateConnections: false,
 206 |         validateExpressions: false
 207 |       });
 208 | 
 209 |       expect(result.valid).toBe(true);
 210 |       expect(mockNodeRepository.getNode).not.toHaveBeenCalled();
 211 |       expect(ExpressionValidator.validateNodeExpressions).not.toHaveBeenCalled();
 212 |     });
 213 | 
 214 |     it('should handle validation errors gracefully', async () => {
 215 |       const workflow = createWorkflow('Test Workflow')
 216 |         .addWebhookNode({ name: 'Webhook' })
 217 |         .build();
 218 | 
 219 |       // Make the validation throw an error
 220 |       vi.mocked(mockNodeRepository.getNode).mockImplementation(() => {
 221 |         throw new Error('Database error');
 222 |       });
 223 | 
 224 |       const result = await validator.validateWorkflow(workflow as any);
 225 | 
 226 |       expect(result.valid).toBe(false);
 227 |       expect(result.errors.length).toBeGreaterThan(0);
 228 |       expect(result.errors.some(e => e.message.includes('Database error'))).toBe(true);
 229 |     });
 230 | 
 231 |     it('should use different validation profiles', async () => {
 232 |       const workflow = createWorkflow('Test Workflow')
 233 |         .addWebhookNode({ name: 'Webhook' })
 234 |         .build();
 235 | 
 236 |       const profiles = ['minimal', 'runtime', 'ai-friendly', 'strict'] as const;
 237 | 
 238 |       for (const profile of profiles) {
 239 |         const result = await validator.validateWorkflow(workflow as any, { profile });
 240 |         expect(result).toBeDefined();
 241 |         expect(mockEnhancedConfigValidator.validateWithMode).toHaveBeenCalledWith(
 242 |           expect.any(String),
 243 |           expect.any(Object),
 244 |           expect.any(Array),
 245 |           'operation',
 246 |           profile
 247 |         );
 248 |       }
 249 |     });
 250 |   });
 251 | 
 252 |   describe('validateWorkflowStructure', () => {
 253 |     it('should error when nodes array is missing', async () => {
 254 |       const workflow = { connections: {} } as any;
 255 | 
 256 |       const result = await validator.validateWorkflow(workflow as any);
 257 | 
 258 |       expect(result.valid).toBe(false);
 259 |       expect(result.errors.some(e => e.message === 'Workflow must have a nodes array')).toBe(true);
 260 |     });
 261 | 
 262 |     it('should error when connections object is missing', async () => {
 263 |       const workflow = { nodes: [] } as any;
 264 | 
 265 |       const result = await validator.validateWorkflow(workflow as any);
 266 | 
 267 |       expect(result.valid).toBe(false);
 268 |       expect(result.errors.some(e => e.message === 'Workflow must have a connections object')).toBe(true);
 269 |     });
 270 | 
 271 |     it('should warn when workflow has no nodes', async () => {
 272 |       const workflow = { nodes: [], connections: {} } as any;
 273 | 
 274 |       const result = await validator.validateWorkflow(workflow as any);
 275 | 
 276 |       expect(result.valid).toBe(true); // Empty workflows are valid but get a warning
 277 |       expect(result.warnings).toHaveLength(1);
 278 |       expect(result.warnings[0].message).toBe('Workflow is empty - no nodes defined');
 279 |     });
 280 | 
 281 |     it('should error for single non-webhook node workflow', async () => {
 282 |       const workflow = {
 283 |         nodes: [{
 284 |           id: '1',
 285 |           name: 'Set',
 286 |           type: 'n8n-nodes-base.set',
 287 |           position: [100, 100],
 288 |           parameters: {}
 289 |         }],
 290 |         connections: {}
 291 |       } as any;
 292 | 
 293 |       const result = await validator.validateWorkflow(workflow as any);
 294 | 
 295 |       expect(result.valid).toBe(false);
 296 |       expect(result.errors.some(e => e.message.includes('Single-node workflows are only valid for webhook endpoints'))).toBe(true);
 297 |     });
 298 | 
 299 |     it('should warn for webhook without connections', async () => {
 300 |       const workflow = {
 301 |         nodes: [{
 302 |           id: '1',
 303 |           name: 'Webhook',
 304 |           type: 'n8n-nodes-base.webhook',
 305 |           position: [100, 100],
 306 |           parameters: {},
 307 |           typeVersion: 2
 308 |         }],
 309 |         connections: {}
 310 |       } as any;
 311 | 
 312 |       const result = await validator.validateWorkflow(workflow as any);
 313 | 
 314 |       expect(result.valid).toBe(true);
 315 |       expect(result.warnings.some(w => w.message.includes('Webhook node has no connections'))).toBe(true);
 316 |     });
 317 | 
 318 |     it('should error for multi-node workflow without connections', async () => {
 319 |       const workflow = {
 320 |         nodes: [
 321 |           {
 322 |             id: '1',
 323 |             name: 'Webhook',
 324 |             type: 'n8n-nodes-base.webhook',
 325 |             position: [100, 100],
 326 |             parameters: {}
 327 |           },
 328 |           {
 329 |             id: '2',
 330 |             name: 'Set',
 331 |             type: 'n8n-nodes-base.set',
 332 |             position: [300, 100],
 333 |             parameters: {}
 334 |           }
 335 |         ],
 336 |         connections: {}
 337 |       } as any;
 338 | 
 339 |       const result = await validator.validateWorkflow(workflow as any);
 340 | 
 341 |       expect(result.valid).toBe(false);
 342 |       expect(result.errors.some(e => e.message.includes('Multi-node workflow has no connections'))).toBe(true);
 343 |     });
 344 | 
 345 |     it('should detect duplicate node names', async () => {
 346 |       const workflow = {
 347 |         nodes: [
 348 |           {
 349 |             id: '1',
 350 |             name: 'Webhook',
 351 |             type: 'n8n-nodes-base.webhook',
 352 |             position: [100, 100],
 353 |             parameters: {}
 354 |           },
 355 |           {
 356 |             id: '2',
 357 |             name: 'Webhook',
 358 |             type: 'n8n-nodes-base.webhook',
 359 |             position: [300, 100],
 360 |             parameters: {}
 361 |           }
 362 |         ],
 363 |         connections: {}
 364 |       } as any;
 365 | 
 366 |       const result = await validator.validateWorkflow(workflow as any);
 367 | 
 368 |       expect(result.errors.some(e => e.message.includes('Duplicate node name: "Webhook"'))).toBe(true);
 369 |     });
 370 | 
 371 |     it('should detect duplicate node IDs', async () => {
 372 |       const workflow = {
 373 |         nodes: [
 374 |           {
 375 |             id: '1',
 376 |             name: 'Webhook1',
 377 |             type: 'n8n-nodes-base.webhook',
 378 |             position: [100, 100],
 379 |             parameters: {}
 380 |           },
 381 |           {
 382 |             id: '1',
 383 |             name: 'Webhook2',
 384 |             type: 'n8n-nodes-base.webhook',
 385 |             position: [300, 100],
 386 |             parameters: {}
 387 |           }
 388 |         ],
 389 |         connections: {}
 390 |       } as any;
 391 | 
 392 |       const result = await validator.validateWorkflow(workflow as any);
 393 | 
 394 |       expect(result.errors.some(e => e.message.includes('Duplicate node ID: "1"'))).toBe(true);
 395 |     });
 396 | 
 397 |     it('should count trigger nodes correctly', async () => {
 398 |       const workflow = {
 399 |         nodes: [
 400 |           {
 401 |             id: '1',
 402 |             name: 'Webhook',
 403 |             type: 'n8n-nodes-base.webhook',
 404 |             position: [100, 100],
 405 |             parameters: {}
 406 |           },
 407 |           {
 408 |             id: '2',
 409 |             name: 'Schedule',
 410 |             type: 'n8n-nodes-base.scheduleTrigger',
 411 |             position: [100, 300],
 412 |             parameters: {}
 413 |           },
 414 |           {
 415 |             id: '3',
 416 |             name: 'Manual',
 417 |             type: 'n8n-nodes-base.manualTrigger',
 418 |             position: [100, 500],
 419 |             parameters: {}
 420 |           }
 421 |         ],
 422 |         connections: {}
 423 |       } as any;
 424 | 
 425 |       const result = await validator.validateWorkflow(workflow as any);
 426 | 
 427 |       expect(result.statistics.triggerNodes).toBe(3);
 428 |     });
 429 | 
 430 |     it('should warn when no trigger nodes exist', async () => {
 431 |       const workflow = {
 432 |         nodes: [
 433 |           {
 434 |             id: '1',
 435 |             name: 'Set',
 436 |             type: 'n8n-nodes-base.set',
 437 |             position: [100, 100],
 438 |             parameters: {}
 439 |           },
 440 |           {
 441 |             id: '2',
 442 |             name: 'Code',
 443 |             type: 'n8n-nodes-base.code',
 444 |             position: [300, 100],
 445 |             parameters: {}
 446 |           }
 447 |         ],
 448 |         connections: {
 449 |           'Set': {
 450 |             main: [[{ node: 'Code', type: 'main', index: 0 }]]
 451 |           }
 452 |         }
 453 |       } as any;
 454 | 
 455 |       const result = await validator.validateWorkflow(workflow as any);
 456 | 
 457 |       expect(result.warnings.some(w => w.message.includes('Workflow has no trigger nodes'))).toBe(true);
 458 |     });
 459 | 
 460 |     it('should not count disabled nodes in enabledNodes count', async () => {
 461 |       const workflow = {
 462 |         nodes: [
 463 |           {
 464 |             id: '1',
 465 |             name: 'Webhook',
 466 |             type: 'n8n-nodes-base.webhook',
 467 |             position: [100, 100],
 468 |             parameters: {},
 469 |             disabled: true
 470 |           },
 471 |           {
 472 |             id: '2',
 473 |             name: 'Set',
 474 |             type: 'n8n-nodes-base.set',
 475 |             position: [300, 100],
 476 |             parameters: {}
 477 |           }
 478 |         ],
 479 |         connections: {}
 480 |       } as any;
 481 | 
 482 |       const result = await validator.validateWorkflow(workflow as any);
 483 | 
 484 |       expect(result.statistics.totalNodes).toBe(2);
 485 |       expect(result.statistics.enabledNodes).toBe(1);
 486 |     });
 487 |   });
 488 | 
 489 |   describe('validateAllNodes', () => {
 490 |     it('should skip disabled nodes', async () => {
 491 |       const workflow = {
 492 |         nodes: [
 493 |           {
 494 |             id: '1',
 495 |             name: 'Webhook',
 496 |             type: 'n8n-nodes-base.webhook',
 497 |             position: [100, 100],
 498 |             parameters: {},
 499 |             disabled: true
 500 |           }
 501 |         ],
 502 |         connections: {}
 503 |       } as any;
 504 | 
 505 |       const result = await validator.validateWorkflow(workflow as any);
 506 | 
 507 |       expect(mockNodeRepository.getNode).not.toHaveBeenCalled();
 508 |     });
 509 | 
 510 |     it('should accept both nodes-base and n8n-nodes-base prefixes as valid', async () => {
 511 |       // This test verifies the fix for false positives - both prefixes are valid
 512 |       const workflow = {
 513 |         nodes: [
 514 |           {
 515 |             id: '1',
 516 |             name: 'Webhook',
 517 |             type: 'nodes-base.webhook', // This is now valid (normalized internally)
 518 |             position: [100, 100],
 519 |             parameters: {}
 520 |           }
 521 |         ],
 522 |         connections: {}
 523 |       } as any;
 524 | 
 525 |       // Mock the normalized node lookup
 526 |       (mockNodeRepository.getNode as any) = vi.fn((type: string) => {
 527 |         if (type === 'nodes-base.webhook') {
 528 |           return {
 529 |             nodeType: 'nodes-base.webhook',
 530 |             displayName: 'Webhook',
 531 |             properties: [],
 532 |             isVersioned: false
 533 |           };
 534 |         }
 535 |         return null;
 536 |       });
 537 | 
 538 |       const result = await validator.validateWorkflow(workflow as any);
 539 | 
 540 |       // Should NOT error for nodes-base prefix - it's valid!
 541 |       expect(result.valid).toBe(true);
 542 |       expect(result.errors.some(e => e.message.includes('Invalid node type'))).toBe(false);
 543 |     });
 544 | 
 545 |     it.skip('should handle unknown node types with suggestions', async () => {
 546 |       const workflow = {
 547 |         nodes: [
 548 |           {
 549 |             id: '1',
 550 |             name: 'HTTP',
 551 |             type: 'httpRequest', // Missing package prefix
 552 |             position: [100, 100],
 553 |             parameters: {}
 554 |           }
 555 |         ],
 556 |         connections: {}
 557 |       } as any;
 558 | 
 559 |       const result = await validator.validateWorkflow(workflow as any);
 560 | 
 561 |       expect(result.valid).toBe(false);
 562 |       expect(result.errors.some(e => e.message.includes('Unknown node type: "httpRequest"'))).toBe(true);
 563 |       expect(result.errors.some(e => e.message.includes('Did you mean "n8n-nodes-base.httpRequest"?'))).toBe(true);
 564 |     });
 565 | 
 566 |     it('should try normalized types for n8n-nodes-base', async () => {
 567 |       const workflow = {
 568 |         nodes: [
 569 |           {
 570 |             id: '1',
 571 |             name: 'Webhook',
 572 |             type: 'n8n-nodes-base.webhook',
 573 |             position: [100, 100],
 574 |             parameters: {}
 575 |           }
 576 |         ],
 577 |         connections: {}
 578 |       } as any;
 579 | 
 580 |       const result = await validator.validateWorkflow(workflow as any);
 581 | 
 582 |       expect(mockNodeRepository.getNode).toHaveBeenCalledWith('nodes-base.webhook');
 583 |     });
 584 | 
 585 |     it('should validate typeVersion but skip parameter validation for langchain nodes', async () => {
 586 |       const workflow = {
 587 |         nodes: [
 588 |           {
 589 |             id: '1',
 590 |             name: 'Agent',
 591 |             type: '@n8n/n8n-nodes-langchain.agent',
 592 |             typeVersion: 1,
 593 |             position: [100, 100],
 594 |             parameters: {}
 595 |           }
 596 |         ],
 597 |         connections: {}
 598 |       } as any;
 599 | 
 600 |       const result = await validator.validateWorkflow(workflow as any);
 601 | 
 602 |       // After v2.17.4 fix: Langchain nodes SHOULD call getNode for typeVersion validation
 603 |       // This prevents invalid typeVersion values from bypassing validation
 604 |       // But they skip parameter validation (handled by dedicated AI validators)
 605 |       expect(mockNodeRepository.getNode).toHaveBeenCalledWith('nodes-langchain.agent');
 606 | 
 607 |       // Should not have typeVersion validation errors (other AI-specific errors may exist)
 608 |       const typeVersionErrors = result.errors.filter(e => e.message.includes('typeVersion'));
 609 |       expect(typeVersionErrors).toEqual([]);
 610 |     });
 611 | 
 612 |     it('should catch invalid typeVersion for langchain nodes (v2.17.4 bug fix)', async () => {
 613 |       const workflow = {
 614 |         nodes: [
 615 |           {
 616 |             id: '1',
 617 |             name: 'Agent',
 618 |             type: '@n8n/n8n-nodes-langchain.agent',
 619 |             typeVersion: 99999, // Invalid - exceeds maximum
 620 |             position: [100, 100],
 621 |             parameters: {}
 622 |           }
 623 |         ],
 624 |         connections: {}
 625 |       } as any;
 626 | 
 627 |       const result = await validator.validateWorkflow(workflow as any);
 628 | 
 629 |       // Critical: Before v2.17.4, this would pass validation but fail at runtime
 630 |       // After v2.17.4: Invalid typeVersion is caught during validation
 631 |       expect(result.valid).toBe(false);
 632 |       expect(result.errors.some(e =>
 633 |         e.message.includes('typeVersion 99999 exceeds maximum')
 634 |       )).toBe(true);
 635 |     });
 636 | 
 637 |     it('should validate typeVersion for versioned nodes', async () => {
 638 |       const workflow = {
 639 |         nodes: [
 640 |           {
 641 |             id: '1',
 642 |             name: 'Webhook',
 643 |             type: 'n8n-nodes-base.webhook',
 644 |             position: [100, 100],
 645 |             parameters: {}
 646 |             // Missing typeVersion
 647 |           }
 648 |         ],
 649 |         connections: {}
 650 |       } as any;
 651 | 
 652 |       const result = await validator.validateWorkflow(workflow as any);
 653 | 
 654 |       expect(result.errors.some(e => e.message.includes('Missing required property \'typeVersion\''))).toBe(true);
 655 |     });
 656 | 
 657 |     it('should error for invalid typeVersion', async () => {
 658 |       const workflow = {
 659 |         nodes: [
 660 |           {
 661 |             id: '1',
 662 |             name: 'Webhook',
 663 |             type: 'n8n-nodes-base.webhook',
 664 |             position: [100, 100],
 665 |             parameters: {},
 666 |             typeVersion: 'invalid' as any
 667 |           }
 668 |         ],
 669 |         connections: {}
 670 |       } as any;
 671 | 
 672 |       const result = await validator.validateWorkflow(workflow as any);
 673 | 
 674 |       expect(result.errors.some(e => e.message.includes('Invalid typeVersion: invalid'))).toBe(true);
 675 |     });
 676 | 
 677 |     it('should warn for outdated typeVersion', async () => {
 678 |       const workflow = {
 679 |         nodes: [
 680 |           {
 681 |             id: '1',
 682 |             name: 'Webhook',
 683 |             type: 'n8n-nodes-base.webhook',
 684 |             position: [100, 100],
 685 |             parameters: {},
 686 |             typeVersion: 1 // Current version is 2
 687 |           }
 688 |         ],
 689 |         connections: {}
 690 |       } as any;
 691 | 
 692 |       const result = await validator.validateWorkflow(workflow as any);
 693 | 
 694 |       expect(result.warnings.some(w => w.message.includes('Outdated typeVersion: 1. Latest is 2'))).toBe(true);
 695 |     });
 696 | 
 697 |     it('should error for typeVersion exceeding maximum', async () => {
 698 |       const workflow = {
 699 |         nodes: [
 700 |           {
 701 |             id: '1',
 702 |             name: 'Webhook',
 703 |             type: 'n8n-nodes-base.webhook',
 704 |             position: [100, 100],
 705 |             parameters: {},
 706 |             typeVersion: 10 // Max is 2
 707 |           }
 708 |         ],
 709 |         connections: {}
 710 |       } as any;
 711 | 
 712 |       const result = await validator.validateWorkflow(workflow as any);
 713 | 
 714 |       expect(result.errors.some(e => e.message.includes('typeVersion 10 exceeds maximum supported version 2'))).toBe(true);
 715 |     });
 716 | 
 717 |     it('should add node validation errors and warnings', async () => {
 718 |       vi.mocked(mockEnhancedConfigValidator.validateWithMode).mockReturnValue({
 719 |         errors: [{ type: 'missing_required', property: 'url', message: 'Missing required field: url' }],
 720 |         warnings: [{ type: 'security', property: 'url', message: 'Consider using HTTPS' }],
 721 |         suggestions: [],
 722 |         mode: 'operation' as const,
 723 |         valid: false,
 724 |         visibleProperties: [],
 725 |         hiddenProperties: []
 726 |       } as any);
 727 | 
 728 |       const workflow = {
 729 |         nodes: [
 730 |           {
 731 |             id: '1',
 732 |             name: 'HTTP',
 733 |             type: 'n8n-nodes-base.httpRequest',
 734 |             position: [100, 100],
 735 |             parameters: {},
 736 |             typeVersion: 4
 737 |           }
 738 |         ],
 739 |         connections: {}
 740 |       } as any;
 741 | 
 742 |       const result = await validator.validateWorkflow(workflow as any);
 743 | 
 744 |       expect(result.errors.some(e => e.message.includes('Missing required field: url'))).toBe(true);
 745 |       expect(result.warnings.some(w => w.message.includes('Consider using HTTPS'))).toBe(true);
 746 |     });
 747 | 
 748 |     it('should handle node validation failures gracefully', async () => {
 749 |       vi.mocked(mockEnhancedConfigValidator.validateWithMode).mockImplementation(() => {
 750 |         throw new Error('Validation error');
 751 |       });
 752 | 
 753 |       const workflow = {
 754 |         nodes: [
 755 |           {
 756 |             id: '1',
 757 |             name: 'HTTP',
 758 |             type: 'n8n-nodes-base.httpRequest',
 759 |             position: [100, 100],
 760 |             parameters: {},
 761 |             typeVersion: 4
 762 |           }
 763 |         ],
 764 |         connections: {}
 765 |       } as any;
 766 | 
 767 |       const result = await validator.validateWorkflow(workflow as any);
 768 | 
 769 |       expect(result.errors.some(e => e.message.includes('Failed to validate node: Validation error'))).toBe(true);
 770 |     });
 771 |   });
 772 | 
 773 |   describe('validateConnections', () => {
 774 |     it('should validate valid connections', async () => {
 775 |       const workflow = {
 776 |         nodes: [
 777 |           {
 778 |             id: '1',
 779 |             name: 'Webhook',
 780 |             type: 'n8n-nodes-base.webhook',
 781 |             position: [100, 100],
 782 |             parameters: {}
 783 |           },
 784 |           {
 785 |             id: '2',
 786 |             name: 'Set',
 787 |             type: 'n8n-nodes-base.set',
 788 |             position: [300, 100],
 789 |             parameters: {}
 790 |           }
 791 |         ],
 792 |         connections: {
 793 |           'Webhook': {
 794 |             main: [[{ node: 'Set', type: 'main', index: 0 }]]
 795 |           }
 796 |         }
 797 |       } as any;
 798 | 
 799 |       const result = await validator.validateWorkflow(workflow as any);
 800 | 
 801 |       expect(result.statistics.validConnections).toBe(1);
 802 |       expect(result.statistics.invalidConnections).toBe(0);
 803 |     });
 804 | 
 805 |     it('should error for connection from non-existent node', async () => {
 806 |       const workflow = {
 807 |         nodes: [
 808 |           {
 809 |             id: '1',
 810 |             name: 'Webhook',
 811 |             type: 'n8n-nodes-base.webhook',
 812 |             position: [100, 100],
 813 |             parameters: {}
 814 |           }
 815 |         ],
 816 |         connections: {
 817 |           'NonExistent': {
 818 |             main: [[{ node: 'Webhook', type: 'main', index: 0 }]]
 819 |           }
 820 |         }
 821 |       } as any;
 822 | 
 823 |       const result = await validator.validateWorkflow(workflow as any);
 824 | 
 825 |       expect(result.errors.some(e => e.message.includes('Connection from non-existent node: "NonExistent"'))).toBe(true);
 826 |       expect(result.statistics.invalidConnections).toBe(1);
 827 |     });
 828 | 
 829 |     it('should error when using node ID instead of name in source', async () => {
 830 |       const workflow = {
 831 |         nodes: [
 832 |           {
 833 |             id: 'webhook-id',
 834 |             name: 'Webhook',
 835 |             type: 'n8n-nodes-base.webhook',
 836 |             position: [100, 100],
 837 |             parameters: {}
 838 |           },
 839 |           {
 840 |             id: 'set-id',
 841 |             name: 'Set',
 842 |             type: 'n8n-nodes-base.set',
 843 |             position: [300, 100],
 844 |             parameters: {}
 845 |           }
 846 |         ],
 847 |         connections: {
 848 |           'webhook-id': { // Using ID instead of name
 849 |             main: [[{ node: 'Set', type: 'main', index: 0 }]]
 850 |           }
 851 |         }
 852 |       } as any;
 853 | 
 854 |       const result = await validator.validateWorkflow(workflow as any);
 855 | 
 856 |       expect(result.errors.some(e => e.message.includes('Connection uses node ID \'webhook-id\' instead of node name \'Webhook\''))).toBe(true);
 857 |     });
 858 | 
 859 |     it('should error for connection to non-existent node', async () => {
 860 |       const workflow = {
 861 |         nodes: [
 862 |           {
 863 |             id: '1',
 864 |             name: 'Webhook',
 865 |             type: 'n8n-nodes-base.webhook',
 866 |             position: [100, 100],
 867 |             parameters: {}
 868 |           }
 869 |         ],
 870 |         connections: {
 871 |           'Webhook': {
 872 |             main: [[{ node: 'NonExistent', type: 'main', index: 0 }]]
 873 |           }
 874 |         }
 875 |       } as any;
 876 | 
 877 |       const result = await validator.validateWorkflow(workflow as any);
 878 | 
 879 |       expect(result.errors.some(e => e.message.includes('Connection to non-existent node: "NonExistent"'))).toBe(true);
 880 |       expect(result.statistics.invalidConnections).toBe(1);
 881 |     });
 882 | 
 883 |     it('should error when using node ID instead of name in target', async () => {
 884 |       const workflow = {
 885 |         nodes: [
 886 |           {
 887 |             id: 'webhook-id',
 888 |             name: 'Webhook',
 889 |             type: 'n8n-nodes-base.webhook',
 890 |             position: [100, 100],
 891 |             parameters: {}
 892 |           },
 893 |           {
 894 |             id: 'set-id',
 895 |             name: 'Set',
 896 |             type: 'n8n-nodes-base.set',
 897 |             position: [300, 100],
 898 |             parameters: {}
 899 |           }
 900 |         ],
 901 |         connections: {
 902 |           'Webhook': {
 903 |             main: [[{ node: 'set-id', type: 'main', index: 0 }]] // Using ID instead of name
 904 |           }
 905 |         }
 906 |       } as any;
 907 | 
 908 |       const result = await validator.validateWorkflow(workflow as any);
 909 | 
 910 |       expect(result.errors.some(e => e.message.includes('Connection target uses node ID \'set-id\' instead of node name \'Set\''))).toBe(true);
 911 |     });
 912 | 
 913 |     it('should warn for connection to disabled node', async () => {
 914 |       const workflow = {
 915 |         nodes: [
 916 |           {
 917 |             id: '1',
 918 |             name: 'Webhook',
 919 |             type: 'n8n-nodes-base.webhook',
 920 |             position: [100, 100],
 921 |             parameters: {}
 922 |           },
 923 |           {
 924 |             id: '2',
 925 |             name: 'Set',
 926 |             type: 'n8n-nodes-base.set',
 927 |             position: [300, 100],
 928 |             parameters: {},
 929 |             disabled: true
 930 |           }
 931 |         ],
 932 |         connections: {
 933 |           'Webhook': {
 934 |             main: [[{ node: 'Set', type: 'main', index: 0 }]]
 935 |           }
 936 |         }
 937 |       } as any;
 938 | 
 939 |       const result = await validator.validateWorkflow(workflow as any);
 940 | 
 941 |       expect(result.warnings.some(w => w.message.includes('Connection to disabled node: "Set"'))).toBe(true);
 942 |     });
 943 | 
 944 |     it('should validate error outputs', async () => {
 945 |       const workflow = {
 946 |         nodes: [
 947 |           {
 948 |             id: '1',
 949 |             name: 'HTTP',
 950 |             type: 'n8n-nodes-base.httpRequest',
 951 |             position: [100, 100],
 952 |             parameters: {}
 953 |           },
 954 |           {
 955 |             id: '2',
 956 |             name: 'Error Handler',
 957 |             type: 'n8n-nodes-base.set',
 958 |             position: [300, 100],
 959 |             parameters: {}
 960 |           }
 961 |         ],
 962 |         connections: {
 963 |           'HTTP': {
 964 |             error: [[{ node: 'Error Handler', type: 'main', index: 0 }]]
 965 |           }
 966 |         }
 967 |       } as any;
 968 | 
 969 |       const result = await validator.validateWorkflow(workflow as any);
 970 | 
 971 |       expect(result.statistics.validConnections).toBe(1);
 972 |     });
 973 | 
 974 |     it('should validate AI tool connections', async () => {
 975 |       const workflow = {
 976 |         nodes: [
 977 |           {
 978 |             id: '1',
 979 |             name: 'Agent',
 980 |             type: '@n8n/n8n-nodes-langchain.agent',
 981 |             position: [100, 100],
 982 |             parameters: {}
 983 |           },
 984 |           {
 985 |             id: '2',
 986 |             name: 'Tool',
 987 |             type: 'n8n-nodes-base.httpRequest',
 988 |             position: [300, 100],
 989 |             parameters: {}
 990 |           }
 991 |         ],
 992 |         connections: {
 993 |           'Agent': {
 994 |             ai_tool: [[{ node: 'Tool', type: 'main', index: 0 }]]
 995 |           }
 996 |         }
 997 |       } as any;
 998 | 
 999 |       const result = await validator.validateWorkflow(workflow as any);
1000 | 
1001 |       expect(result.statistics.validConnections).toBe(1);
1002 |     });
1003 | 
1004 |     it('should warn for community nodes used as AI tools', async () => {
1005 |       const workflow = {
1006 |         nodes: [
1007 |           {
1008 |             id: '1',
1009 |             name: 'Agent',
1010 |             type: '@n8n/n8n-nodes-langchain.agent',
1011 |             position: [100, 100],
1012 |             parameters: {},
1013 |             typeVersion: 1
1014 |           },
1015 |           {
1016 |             id: '2',
1017 |             name: 'CustomTool',
1018 |             type: 'n8n-nodes-custom.customNode',
1019 |             position: [300, 100],
1020 |             parameters: {},
1021 |             typeVersion: 1
1022 |           }
1023 |         ],
1024 |         connections: {
1025 |           'Agent': {
1026 |             ai_tool: [[{ node: 'CustomTool', type: 'main', index: 0 }]]
1027 |           }
1028 |         }
1029 |       } as any;
1030 | 
1031 |       const result = await validator.validateWorkflow(workflow as any);
1032 | 
1033 |       expect(result.warnings.some(w => w.message.includes('Community node "CustomTool" is being used as an AI tool'))).toBe(true);
1034 |     });
1035 | 
1036 |     it('should warn for orphaned nodes', async () => {
1037 |       const workflow = {
1038 |         nodes: [
1039 |           {
1040 |             id: '1',
1041 |             name: 'Webhook',
1042 |             type: 'n8n-nodes-base.webhook',
1043 |             position: [100, 100],
1044 |             parameters: {}
1045 |           },
1046 |           {
1047 |             id: '2',
1048 |             name: 'Set',
1049 |             type: 'n8n-nodes-base.set',
1050 |             position: [300, 100],
1051 |             parameters: {}
1052 |           },
1053 |           {
1054 |             id: '3',
1055 |             name: 'Orphaned',
1056 |             type: 'n8n-nodes-base.code',
1057 |             position: [500, 100],
1058 |             parameters: {}
1059 |           }
1060 |         ],
1061 |         connections: {
1062 |           'Webhook': {
1063 |             main: [[{ node: 'Set', type: 'main', index: 0 }]]
1064 |           }
1065 |         }
1066 |       } as any;
1067 | 
1068 |       const result = await validator.validateWorkflow(workflow as any);
1069 | 
1070 |       expect(result.warnings.some(w => w.message.includes('Node is not connected to any other nodes') && w.nodeName === 'Orphaned')).toBe(true);
1071 |     });
1072 | 
1073 |     it('should detect cycles in workflow', async () => {
1074 |       const workflow = {
1075 |         nodes: [
1076 |           {
1077 |             id: '1',
1078 |             name: 'Node1',
1079 |             type: 'n8n-nodes-base.set',
1080 |             position: [100, 100],
1081 |             parameters: {}
1082 |           },
1083 |           {
1084 |             id: '2',
1085 |             name: 'Node2',
1086 |             type: 'n8n-nodes-base.set',
1087 |             position: [300, 100],
1088 |             parameters: {}
1089 |           },
1090 |           {
1091 |             id: '3',
1092 |             name: 'Node3',
1093 |             type: 'n8n-nodes-base.set',
1094 |             position: [500, 100],
1095 |             parameters: {}
1096 |           }
1097 |         ],
1098 |         connections: {
1099 |           'Node1': {
1100 |             main: [[{ node: 'Node2', type: 'main', index: 0 }]]
1101 |           },
1102 |           'Node2': {
1103 |             main: [[{ node: 'Node3', type: 'main', index: 0 }]]
1104 |           },
1105 |           'Node3': {
1106 |             main: [[{ node: 'Node1', type: 'main', index: 0 }]] // Creates cycle
1107 |           }
1108 |         }
1109 |       } as any;
1110 | 
1111 |       const result = await validator.validateWorkflow(workflow as any);
1112 | 
1113 |       expect(result.errors.some(e => e.message.includes('Workflow contains a cycle'))).toBe(true);
1114 |     });
1115 | 
1116 |     it('should handle null connections properly', async () => {
1117 |       const workflow = {
1118 |         nodes: [
1119 |           {
1120 |             id: '1',
1121 |             name: 'IF',
1122 |             type: 'n8n-nodes-base.if',
1123 |             position: [100, 100],
1124 |             parameters: {},
1125 |             typeVersion: 2
1126 |           },
1127 |           {
1128 |             id: '2',
1129 |             name: 'True Branch',
1130 |             type: 'n8n-nodes-base.set',
1131 |             position: [300, 50],
1132 |             parameters: {},
1133 |             typeVersion: 3
1134 |           }
1135 |         ],
1136 |         connections: {
1137 |           'IF': {
1138 |             main: [
1139 |               [{ node: 'True Branch', type: 'main', index: 0 }],
1140 |               null // False branch not connected
1141 |             ]
1142 |           }
1143 |         }
1144 |       } as any;
1145 | 
1146 |       const result = await validator.validateWorkflow(workflow as any);
1147 | 
1148 |       expect(result.statistics.validConnections).toBe(1);
1149 |       expect(result.valid).toBe(true);
1150 |     });
1151 |   });
1152 | 
1153 |   describe('validateExpressions', () => {
1154 |     it('should validate expressions in node parameters', async () => {
1155 |       const workflow = {
1156 |         nodes: [
1157 |           {
1158 |             id: '1',
1159 |             name: 'Webhook',
1160 |             type: 'n8n-nodes-base.webhook',
1161 |             position: [100, 100],
1162 |             parameters: {}
1163 |           },
1164 |           {
1165 |             id: '2',
1166 |             name: 'Set',
1167 |             type: 'n8n-nodes-base.set',
1168 |             position: [300, 100],
1169 |             parameters: {
1170 |               values: {
1171 |                 string: [
1172 |                   {
1173 |                     name: 'field',
1174 |                     value: '={{ $json.data }}'
1175 |                   }
1176 |                 ]
1177 |               }
1178 |             }
1179 |           }
1180 |         ],
1181 |         connections: {
1182 |           'Webhook': {
1183 |             main: [[{ node: 'Set', type: 'main', index: 0 }]]
1184 |           }
1185 |         }
1186 |       } as any;
1187 | 
1188 |       const result = await validator.validateWorkflow(workflow as any);
1189 | 
1190 |       expect(ExpressionValidator.validateNodeExpressions).toHaveBeenCalledWith(
1191 |         expect.objectContaining({ values: expect.any(Object) }),
1192 |         expect.objectContaining({
1193 |           availableNodes: expect.arrayContaining(['Webhook']),
1194 |           currentNodeName: 'Set',
1195 |           hasInputData: true
1196 |         })
1197 |       );
1198 |     });
1199 | 
1200 |     it('should add expression errors to result', async () => {
1201 |       vi.mocked(ExpressionValidator.validateNodeExpressions).mockReturnValue({
1202 |         valid: false,
1203 |         errors: ['Invalid expression syntax'],
1204 |         warnings: ['Deprecated variable usage'],
1205 |         usedVariables: new Set(['$json']),
1206 |         usedNodes: new Set()
1207 |       });
1208 | 
1209 |       const workflow = {
1210 |         nodes: [
1211 |           {
1212 |             id: '1',
1213 |             name: 'Set',
1214 |             type: 'n8n-nodes-base.set',
1215 |             position: [100, 100],
1216 |             parameters: {
1217 |               value: '={{ invalid }}'
1218 |             }
1219 |           }
1220 |         ],
1221 |         connections: {}
1222 |       } as any;
1223 | 
1224 |       const result = await validator.validateWorkflow(workflow as any);
1225 | 
1226 |       expect(result.errors.some(e => e.message.includes('Expression error: Invalid expression syntax'))).toBe(true);
1227 |       expect(result.warnings.some(w => w.message.includes('Expression warning: Deprecated variable usage'))).toBe(true);
1228 |       expect(result.statistics.expressionsValidated).toBe(1);
1229 |     });
1230 | 
1231 |     it('should skip expression validation for disabled nodes', async () => {
1232 |       const workflow = {
1233 |         nodes: [
1234 |           {
1235 |             id: '1',
1236 |             name: 'Set',
1237 |             type: 'n8n-nodes-base.set',
1238 |             position: [100, 100],
1239 |             parameters: {
1240 |               value: '={{ $json.data }}'
1241 |             },
1242 |             disabled: true
1243 |           }
1244 |         ],
1245 |         connections: {}
1246 |       } as any;
1247 | 
1248 |       const result = await validator.validateWorkflow(workflow as any);
1249 | 
1250 |       expect(ExpressionValidator.validateNodeExpressions).not.toHaveBeenCalled();
1251 |     });
1252 |   });
1253 | 
1254 |   describe('checkWorkflowPatterns', () => {
1255 |     it('should suggest error handling for large workflows', async () => {
1256 |       const builder = createWorkflow('Large Workflow');
1257 |       
1258 |       // Add more than 3 nodes
1259 |       for (let i = 0; i < 5; i++) {
1260 |         builder.addCustomNode('n8n-nodes-base.set', 3, {}, { name: `Set${i}` });
1261 |       }
1262 | 
1263 |       const workflow = builder.build() as any;
1264 | 
1265 |       const result = await validator.validateWorkflow(workflow as any);
1266 | 
1267 |       expect(result.warnings.some(w => w.message.includes('Consider adding error handling'))).toBe(true);
1268 |     });
1269 | 
1270 |     it('should warn about long linear chains', async () => {
1271 |       const builder = createWorkflow('Linear Workflow');
1272 |       
1273 |       // Create a chain of 12 nodes
1274 |       const nodeNames: string[] = [];
1275 |       for (let i = 0; i < 12; i++) {
1276 |         const nodeName = `Node${i}`;
1277 |         builder.addCustomNode('n8n-nodes-base.set', 3, {}, { name: nodeName });
1278 |         nodeNames.push(nodeName);
1279 |       }
1280 | 
1281 |       // Connect them sequentially
1282 |       builder.connectSequentially(nodeNames);
1283 | 
1284 |       const workflow = builder.build() as any;
1285 | 
1286 |       const result = await validator.validateWorkflow(workflow as any);
1287 | 
1288 |       expect(result.warnings.some(w => w.message.includes('Long linear chain detected'))).toBe(true);
1289 |     });
1290 | 
1291 |     it('should warn about missing credentials', async () => {
1292 |       const workflow = {
1293 |         nodes: [
1294 |           {
1295 |             id: '1',
1296 |             name: 'Slack',
1297 |             type: 'n8n-nodes-base.slack',
1298 |             position: [100, 100],
1299 |             parameters: {},
1300 |             credentials: {
1301 |               slackApi: {} // Missing id
1302 |             }
1303 |           }
1304 |         ],
1305 |         connections: {}
1306 |       } as any;
1307 | 
1308 |       const result = await validator.validateWorkflow(workflow as any);
1309 | 
1310 |       expect(result.warnings.some(w => w.message.includes('Missing credentials configuration for slackApi'))).toBe(true);
1311 |     });
1312 | 
1313 |     it('should warn about AI agents without tools', async () => {
1314 |       const workflow = {
1315 |         nodes: [
1316 |           {
1317 |             id: '1',
1318 |             name: 'Agent',
1319 |             type: '@n8n/n8n-nodes-langchain.agent',
1320 |             position: [100, 100],
1321 |             parameters: {}
1322 |           }
1323 |         ],
1324 |         connections: {}
1325 |       } as any;
1326 | 
1327 |       const result = await validator.validateWorkflow(workflow as any);
1328 | 
1329 |       expect(result.warnings.some(w => w.message.includes('AI Agent has no tools connected'))).toBe(true);
1330 |     });
1331 | 
1332 |     it('should suggest community package setting for AI tools', async () => {
1333 |       const workflow = {
1334 |         nodes: [
1335 |           {
1336 |             id: '1',
1337 |             name: 'Agent',
1338 |             type: '@n8n/n8n-nodes-langchain.agent',
1339 |             position: [100, 100],
1340 |             parameters: {}
1341 |           },
1342 |           {
1343 |             id: '2',
1344 |             name: 'Tool',
1345 |             type: 'n8n-nodes-base.httpRequest',
1346 |             position: [300, 100],
1347 |             parameters: {}
1348 |           }
1349 |         ],
1350 |         connections: {
1351 |           'Agent': {
1352 |             ai_tool: [[{ node: 'Tool', type: 'main', index: 0 }]]
1353 |           }
1354 |         }
1355 |       } as any;
1356 | 
1357 |       const result = await validator.validateWorkflow(workflow as any);
1358 | 
1359 |       expect(result.suggestions.some(s => s.includes('N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE'))).toBe(true);
1360 |     });
1361 |   });
1362 | 
1363 |   describe('checkNodeErrorHandling', () => {
1364 |     it('should error when node-level properties are inside parameters', async () => {
1365 |       const workflow = {
1366 |         nodes: [
1367 |           {
1368 |             id: '1',
1369 |             name: 'HTTP',
1370 |             type: 'n8n-nodes-base.httpRequest',
1371 |             position: [100, 100],
1372 |             typeVersion: 4,
1373 |             parameters: {
1374 |               url: 'https://api.example.com',
1375 |               onError: 'continueRegularOutput', // Wrong location!
1376 |               retryOnFail: true, // Wrong location!
1377 |               credentials: {} // Wrong location!
1378 |             }
1379 |           }
1380 |         ],
1381 |         connections: {}
1382 |       } as any;
1383 | 
1384 |       const result = await validator.validateWorkflow(workflow as any);
1385 | 
1386 |       expect(result.errors.some(e => e.message.includes('Node-level properties onError, retryOnFail, credentials are in the wrong location'))).toBe(true);
1387 |       expect(result.errors.some(e => e.details?.fix?.includes('Move these properties from node.parameters to the node level'))).toBe(true);
1388 |     });
1389 | 
1390 |     it('should validate onError property values', async () => {
1391 |       const workflow = {
1392 |         nodes: [
1393 |           {
1394 |             id: '1',
1395 |             name: 'HTTP',
1396 |             type: 'n8n-nodes-base.httpRequest',
1397 |             position: [100, 100],
1398 |             parameters: {},
1399 |             onError: 'invalidValue' as any
1400 |           }
1401 |         ],
1402 |         connections: {}
1403 |       } as any;
1404 | 
1405 |       const result = await validator.validateWorkflow(workflow as any);
1406 | 
1407 |       expect(result.errors.some(e => e.message.includes('Invalid onError value: "invalidValue"'))).toBe(true);
1408 |     });
1409 | 
1410 |     it('should warn about deprecated continueOnFail', async () => {
1411 |       const workflow = {
1412 |         nodes: [
1413 |           {
1414 |             id: '1',
1415 |             name: 'HTTP',
1416 |             type: 'n8n-nodes-base.httpRequest',
1417 |             position: [100, 100],
1418 |             parameters: {},
1419 |             continueOnFail: true
1420 |           }
1421 |         ],
1422 |         connections: {}
1423 |       } as any;
1424 | 
1425 |       const result = await validator.validateWorkflow(workflow as any);
1426 | 
1427 |       expect(result.warnings.some(w => w.message.includes('Using deprecated "continueOnFail: true"'))).toBe(true);
1428 |     });
1429 | 
1430 |     it('should error for conflicting error handling properties', async () => {
1431 |       const workflow = {
1432 |         nodes: [
1433 |           {
1434 |             id: '1',
1435 |             name: 'HTTP',
1436 |             type: 'n8n-nodes-base.httpRequest',
1437 |             position: [100, 100],
1438 |             parameters: {},
1439 |             continueOnFail: true,
1440 |             onError: 'continueRegularOutput'
1441 |           }
1442 |         ],
1443 |         connections: {}
1444 |       } as any;
1445 | 
1446 |       const result = await validator.validateWorkflow(workflow as any);
1447 | 
1448 |       expect(result.errors.some(e => e.message.includes('Cannot use both "continueOnFail" and "onError" properties'))).toBe(true);
1449 |     });
1450 | 
1451 |     it('should validate retry configuration', async () => {
1452 |       const workflow = {
1453 |         nodes: [
1454 |           {
1455 |             id: '1',
1456 |             name: 'HTTP',
1457 |             type: 'n8n-nodes-base.httpRequest',
1458 |             position: [100, 100],
1459 |             parameters: {},
1460 |             retryOnFail: true,
1461 |             maxTries: 'invalid' as any,
1462 |             waitBetweenTries: -1000
1463 |           }
1464 |         ],
1465 |         connections: {}
1466 |       } as any;
1467 | 
1468 |       const result = await validator.validateWorkflow(workflow as any);
1469 | 
1470 |       expect(result.errors.some(e => e.message.includes('maxTries must be a positive number'))).toBe(true);
1471 |       expect(result.errors.some(e => e.message.includes('waitBetweenTries must be a non-negative number'))).toBe(true);
1472 |     });
1473 | 
1474 |     it('should warn about excessive retry values', async () => {
1475 |       const workflow = {
1476 |         nodes: [
1477 |           {
1478 |             id: '1',
1479 |             name: 'HTTP',
1480 |             type: 'n8n-nodes-base.httpRequest',
1481 |             position: [100, 100],
1482 |             parameters: {},
1483 |             retryOnFail: true,
1484 |             maxTries: 15,
1485 |             waitBetweenTries: 400000
1486 |           }
1487 |         ],
1488 |         connections: {}
1489 |       } as any;
1490 | 
1491 |       const result = await validator.validateWorkflow(workflow as any);
1492 | 
1493 |       expect(result.warnings.some(w => w.message.includes('maxTries is set to 15'))).toBe(true);
1494 |       expect(result.warnings.some(w => w.message.includes('waitBetweenTries is set to 400000ms'))).toBe(true);
1495 |     });
1496 | 
1497 |     it('should warn about retryOnFail without maxTries', async () => {
1498 |       const workflow = {
1499 |         nodes: [
1500 |           {
1501 |             id: '1',
1502 |             name: 'HTTP',
1503 |             type: 'n8n-nodes-base.httpRequest',
1504 |             position: [100, 100],
1505 |             parameters: {},
1506 |             retryOnFail: true
1507 |           }
1508 |         ],
1509 |         connections: {}
1510 |       } as any;
1511 | 
1512 |       const result = await validator.validateWorkflow(workflow as any);
1513 | 
1514 |       expect(result.warnings.some(w => w.message.includes('retryOnFail is enabled but maxTries is not specified'))).toBe(true);
1515 |     });
1516 | 
1517 |     it('should validate other node-level properties', async () => {
1518 |       const workflow = {
1519 |         nodes: [
1520 |           {
1521 |             id: '1',
1522 |             name: 'Set',
1523 |             type: 'n8n-nodes-base.set',
1524 |             position: [100, 100],
1525 |             parameters: {},
1526 |             typeVersion: 3,
1527 |             alwaysOutputData: 'invalid' as any,
1528 |             executeOnce: 'invalid' as any,
1529 |             disabled: 'invalid' as any,
1530 |             notesInFlow: 'invalid' as any,
1531 |             notes: 123 as any
1532 |           }
1533 |         ],
1534 |         connections: {}
1535 |       } as any;
1536 | 
1537 |       const result = await validator.validateWorkflow(workflow as any);
1538 | 
1539 | 
1540 |       expect(result.errors.some(e => e.message.includes('alwaysOutputData must be a boolean'))).toBe(true);
1541 |       expect(result.errors.some(e => e.message.includes('executeOnce must be a boolean'))).toBe(true);
1542 |       expect(result.errors.some(e => e.message.includes('disabled must be a boolean'))).toBe(true);
1543 |       expect(result.errors.some(e => e.message.includes('notesInFlow must be a boolean'))).toBe(true);
1544 |       expect(result.errors.some(e => e.message.includes('notes must be a string'))).toBe(true);
1545 |     });
1546 | 
1547 |     it('should warn about executeOnce', async () => {
1548 |       const workflow = {
1549 |         nodes: [
1550 |           {
1551 |             id: '1',
1552 |             name: 'Set',
1553 |             type: 'n8n-nodes-base.set',
1554 |             position: [100, 100],
1555 |             parameters: {},
1556 |             executeOnce: true
1557 |           }
1558 |         ],
1559 |         connections: {}
1560 |       } as any;
1561 | 
1562 |       const result = await validator.validateWorkflow(workflow as any);
1563 | 
1564 |       expect(result.warnings.some(w => w.message.includes('executeOnce is enabled'))).toBe(true);
1565 |     });
1566 | 
1567 |     it('should warn error-prone nodes without error handling', async () => {
1568 |       const errorProneNodes = [
1569 |         { type: 'n8n-nodes-base.httpRequest', message: 'HTTP Request', version: 4 },
1570 |         { type: 'n8n-nodes-base.webhook', message: 'Webhook', version: 2 },
1571 |         { type: 'n8n-nodes-base.postgres', message: 'Database operation', version: 2 },
1572 |         { type: 'n8n-nodes-base.slack', message: 'slack node', version: 2 }
1573 |       ];
1574 | 
1575 |       for (const nodeInfo of errorProneNodes) {
1576 |         const workflow = {
1577 |           nodes: [
1578 |             {
1579 |               id: '1',
1580 |               name: 'Node',
1581 |               type: nodeInfo.type,
1582 |               position: [100, 100],
1583 |               parameters: {},
1584 |               typeVersion: nodeInfo.version
1585 |             }
1586 |           ],
1587 |           connections: {}
1588 |         } as any;
1589 | 
1590 |         const result = await validator.validateWorkflow(workflow as any);
1591 | 
1592 |         expect(result.warnings.some(w => w.message.includes(nodeInfo.message) && w.message.includes('without error handling'))).toBe(true);
1593 |       }
1594 |     });
1595 | 
1596 |     it('should warn about conflicting error handling', async () => {
1597 |       const workflow = {
1598 |         nodes: [
1599 |           {
1600 |             id: '1',
1601 |             name: 'HTTP',
1602 |             type: 'n8n-nodes-base.httpRequest',
1603 |             position: [100, 100],
1604 |             parameters: {},
1605 |             continueOnFail: true,
1606 |             retryOnFail: true
1607 |           }
1608 |         ],
1609 |         connections: {}
1610 |       } as any;
1611 | 
1612 |       const result = await validator.validateWorkflow(workflow as any);
1613 | 
1614 |       expect(result.warnings.some(w => w.message.includes('Both continueOnFail and retryOnFail are enabled'))).toBe(true);
1615 |     });
1616 | 
1617 |     it('should suggest alwaysOutputData for debugging', async () => {
1618 |       const workflow = {
1619 |         nodes: [
1620 |           {
1621 |             id: '1',
1622 |             name: 'HTTP',
1623 |             type: 'n8n-nodes-base.httpRequest',
1624 |             position: [100, 100],
1625 |             parameters: {},
1626 |             retryOnFail: true
1627 |           }
1628 |         ],
1629 |         connections: {}
1630 |       } as any;
1631 | 
1632 |       const result = await validator.validateWorkflow(workflow as any);
1633 | 
1634 |       expect(result.suggestions.some(s => s.includes('Consider enabling alwaysOutputData'))).toBe(true);
1635 |     });
1636 | 
1637 |     it('should provide general error handling suggestions', async () => {
1638 |       const builder = createWorkflow('No Error Handling');
1639 |       
1640 |       // Add 6 nodes without error handling
1641 |       for (let i = 0; i < 6; i++) {
1642 |         builder.addCustomNode('n8n-nodes-base.httpRequest', 4, {}, { name: `HTTP${i}` });
1643 |       }
1644 | 
1645 |       const workflow = builder.build() as any;
1646 | 
1647 |       const result = await validator.validateWorkflow(workflow as any);
1648 | 
1649 |       expect(result.suggestions.some(s => s.includes('Most nodes lack error handling'))).toBe(true);
1650 |     });
1651 | 
1652 |     it('should suggest replacing deprecated error handling', async () => {
1653 |       const workflow = {
1654 |         nodes: [
1655 |           {
1656 |             id: '1',
1657 |             name: 'HTTP',
1658 |             type: 'n8n-nodes-base.httpRequest',
1659 |             position: [100, 100],
1660 |             parameters: {},
1661 |             continueOnFail: true
1662 |           }
1663 |         ],
1664 |         connections: {}
1665 |       } as any;
1666 | 
1667 |       const result = await validator.validateWorkflow(workflow as any);
1668 | 
1669 |       expect(result.suggestions.some(s => s.includes('Replace "continueOnFail: true" with "onError:'))).toBe(true);
1670 |     });
1671 |   });
1672 | 
1673 |   describe('generateSuggestions', () => {
1674 |     it('should suggest adding trigger for workflows without triggers', async () => {
1675 |       const workflow = {
1676 |         nodes: [
1677 |           {
1678 |             id: '1',
1679 |             name: 'Set',
1680 |             type: 'n8n-nodes-base.set',
1681 |             position: [100, 100],
1682 |             parameters: {}
1683 |           }
1684 |         ],
1685 |         connections: {}
1686 |       } as any;
1687 | 
1688 |       const result = await validator.validateWorkflow(workflow as any);
1689 | 
1690 |       expect(result.suggestions.some(s => s.includes('Add a trigger node'))).toBe(true);
1691 |     });
1692 | 
1693 |     it('should provide connection examples for connection errors', async () => {
1694 |       const workflow = {
1695 |         nodes: [
1696 |           {
1697 |             id: '1',
1698 |             name: 'Webhook',
1699 |             type: 'n8n-nodes-base.webhook',
1700 |             position: [100, 100],
1701 |             parameters: {}
1702 |           },
1703 |           {
1704 |             id: '2',
1705 |             name: 'Set',
1706 |             type: 'n8n-nodes-base.set',
1707 |             position: [300, 100],
1708 |             parameters: {}
1709 |           }
1710 |         ],
1711 |         connections: {} // Missing connections
1712 |       } as any;
1713 | 
1714 |       const result = await validator.validateWorkflow(workflow as any);
1715 | 
1716 |       expect(result.suggestions.some(s => s.includes('Example connection structure'))).toBe(true);
1717 |       expect(result.suggestions.some(s => s.includes('Use node NAMES (not IDs) in connections'))).toBe(true);
1718 |     });
1719 | 
1720 |     it('should suggest error handling when missing', async () => {
1721 |       const workflow = {
1722 |         nodes: [
1723 |           {
1724 |             id: '1',
1725 |             name: 'Webhook',
1726 |             type: 'n8n-nodes-base.webhook',
1727 |             position: [100, 100],
1728 |             parameters: {}
1729 |           },
1730 |           {
1731 |             id: '2',
1732 |             name: 'HTTP',
1733 |             type: 'n8n-nodes-base.httpRequest',
1734 |             position: [300, 100],
1735 |             parameters: {}
1736 |           }
1737 |         ],
1738 |         connections: {
1739 |           'Webhook': {
1740 |             main: [[{ node: 'HTTP', type: 'main', index: 0 }]]
1741 |           }
1742 |         }
1743 |       } as any;
1744 | 
1745 |       const result = await validator.validateWorkflow(workflow as any);
1746 | 
1747 |       expect(result.suggestions.some(s => s.includes('Add error handling'))).toBe(true);
1748 |     });
1749 | 
1750 |     it('should suggest breaking up large workflows', async () => {
1751 |       const builder = createWorkflow('Large Workflow');
1752 |       
1753 |       // Add 25 nodes
1754 |       for (let i = 0; i < 25; i++) {
1755 |         builder.addCustomNode('n8n-nodes-base.set', 3, {}, { name: `Node${i}` });
1756 |       }
1757 | 
1758 |       const workflow = builder.build() as any;
1759 | 
1760 |       const result = await validator.validateWorkflow(workflow as any);
1761 | 
1762 |       expect(result.suggestions.some(s => s.includes('Consider breaking this workflow into smaller sub-workflows'))).toBe(true);
1763 |     });
1764 | 
1765 |     it('should suggest Code node for complex expressions', async () => {
1766 |       const workflow = {
1767 |         nodes: [
1768 |           {
1769 |             id: '1',
1770 |             name: 'Complex',
1771 |             type: 'n8n-nodes-base.set',
1772 |             position: [100, 100],
1773 |             parameters: {
1774 |               field1: '={{ $json.a }}',
1775 |               field2: '={{ $json.b }}',
1776 |               field3: '={{ $json.c }}',
1777 |               field4: '={{ $json.d }}',
1778 |               field5: '={{ $json.e }}',
1779 |               field6: '={{ $json.f }}'
1780 |             }
1781 |           }
1782 |         ],
1783 |         connections: {}
1784 |       } as any;
1785 | 
1786 |       const result = await validator.validateWorkflow(workflow as any);
1787 | 
1788 |       expect(result.suggestions.some(s => s.includes('Consider using a Code node for complex data transformations'))).toBe(true);
1789 |     });
1790 | 
1791 |     it('should suggest minimal workflow structure', async () => {
1792 |       const workflow = {
1793 |         nodes: [
1794 |           {
1795 |             id: '1',
1796 |             name: 'Set',
1797 |             type: 'n8n-nodes-base.set',
1798 |             position: [100, 100],
1799 |             parameters: {}
1800 |           }
1801 |         ],
1802 |         connections: {}
1803 |       } as any;
1804 | 
1805 |       const result = await validator.validateWorkflow(workflow as any);
1806 | 
1807 |       expect(result.suggestions.some(s => s.includes('A minimal workflow needs'))).toBe(true);
1808 |     });
1809 |   });
1810 | 
1811 |   describe('findSimilarNodeTypes', () => {
1812 |     it.skip('should find similar node types for common mistakes', async () => {
1813 |       // Test that webhook without prefix gets suggestions
1814 |       const webhookWorkflow = {
1815 |         nodes: [
1816 |           {
1817 |             id: '1',
1818 |             name: 'Node',
1819 |             type: 'webhook',
1820 |             position: [100, 100],
1821 |             parameters: {}
1822 |           }
1823 |         ],
1824 |         connections: {}
1825 |       } as any;
1826 | 
1827 |       const webhookResult = await validator.validateWorkflow(webhookWorkflow);
1828 | 
1829 |       // Check that we get an unknown node error with suggestions
1830 |       const unknownNodeError = webhookResult.errors.find(e =>
1831 |         e.message && e.message.includes('Unknown node type')
1832 |       );
1833 |       expect(unknownNodeError).toBeDefined();
1834 | 
1835 |       // For webhook, it should definitely suggest nodes-base.webhook
1836 |       expect(unknownNodeError?.message).toContain('nodes-base.webhook');
1837 | 
1838 |       // Test that slack without prefix gets suggestions
1839 |       const slackWorkflow = {
1840 |         nodes: [
1841 |           {
1842 |             id: '1',
1843 |             name: 'Node',
1844 |             type: 'slack',
1845 |             position: [100, 100],
1846 |             parameters: {}
1847 |           }
1848 |         ],
1849 |         connections: {}
1850 |       } as any;
1851 | 
1852 |       const slackResult = await validator.validateWorkflow(slackWorkflow);
1853 |       const slackError = slackResult.errors.find(e =>
1854 |         e.message && e.message.includes('Unknown node type')
1855 |       );
1856 |       expect(slackError).toBeDefined();
1857 |       expect(slackError?.message).toContain('nodes-base.slack');
1858 |     });
1859 |   });
1860 | 
1861 |   describe('Integration Tests', () => {
1862 |     it('should validate a complex workflow with multiple issues', async () => {
1863 |       const workflow = {
1864 |         nodes: [
1865 |           // Valid trigger
1866 |           {
1867 |             id: '1',
1868 |             name: 'Webhook',
1869 |             type: 'n8n-nodes-base.webhook',
1870 |             position: [100, 100],
1871 |             parameters: {},
1872 |             typeVersion: 2
1873 |           },
1874 |           // Node with valid alternative prefix (no longer an error)
1875 |           {
1876 |             id: '2',
1877 |             name: 'HTTP1',
1878 |             type: 'nodes-base.httpRequest', // Valid prefix (normalized internally)
1879 |             position: [300, 100],
1880 |             parameters: {}
1881 |           },
1882 |           // Node with missing typeVersion
1883 |           {
1884 |             id: '3',
1885 |             name: 'Slack',
1886 |             type: 'n8n-nodes-base.slack',
1887 |             position: [500, 100],
1888 |             parameters: {}
1889 |           },
1890 |           // Disabled node
1891 |           {
1892 |             id: '4',
1893 |             name: 'Disabled',
1894 |             type: 'n8n-nodes-base.set',
1895 |             position: [700, 100],
1896 |             parameters: {},
1897 |             disabled: true
1898 |           },
1899 |           // Node with error handling in wrong place
1900 |           {
1901 |             id: '5',
1902 |             name: 'HTTP2',
1903 |             type: 'n8n-nodes-base.httpRequest',
1904 |             position: [900, 100],
1905 |             parameters: {
1906 |               onError: 'continueRegularOutput'
1907 |             },
1908 |             typeVersion: 4
1909 |           },
1910 |           // Orphaned node
1911 |           {
1912 |             id: '6',
1913 |             name: 'Orphaned',
1914 |             type: 'n8n-nodes-base.code',
1915 |             position: [1100, 100],
1916 |             parameters: {},
1917 |             typeVersion: 2
1918 |           },
1919 |           // AI Agent without tools
1920 |           {
1921 |             id: '7',
1922 |             name: 'Agent',
1923 |             type: '@n8n/n8n-nodes-langchain.agent',
1924 |             position: [100, 300],
1925 |             parameters: {},
1926 |             typeVersion: 1
1927 |           }
1928 |         ],
1929 |         connections: {
1930 |           'Webhook': {
1931 |             main: [[{ node: 'HTTP1', type: 'main', index: 0 }]]
1932 |           },
1933 |           'HTTP1': {
1934 |             main: [[{ node: 'Slack', type: 'main', index: 0 }]]
1935 |           },
1936 |           'Slack': {
1937 |             main: [[{ node: 'Disabled', type: 'main', index: 0 }]]
1938 |           },
1939 |           // Using ID instead of name
1940 |           '5': {
1941 |             main: [[{ node: 'Agent', type: 'main', index: 0 }]]
1942 |           }
1943 |         }
1944 |       } as any;
1945 | 
1946 |       const result = await validator.validateWorkflow(workflow as any);
1947 | 
1948 |       // Should have multiple errors (but not for the nodes-base prefix)
1949 |       expect(result.valid).toBe(false);
1950 |       expect(result.errors.length).toBeGreaterThan(2); // Reduced by 1 since nodes-base prefix is now valid
1951 | 
1952 |       // Specific errors (removed the invalid node type error as it's no longer invalid)
1953 |       expect(result.errors.some(e => e.message.includes('Missing required property \'typeVersion\''))).toBe(true);
1954 |       expect(result.errors.some(e => e.message.includes('Node-level properties onError are in the wrong location'))).toBe(true);
1955 |       expect(result.errors.some(e => e.message.includes('Connection uses node ID \'5\' instead of node name'))).toBe(true);
1956 | 
1957 |       // Warnings
1958 |       expect(result.warnings.some(w => w.message.includes('Connection to disabled node'))).toBe(true);
1959 |       expect(result.warnings.some(w => w.message.includes('Node is not connected') && w.nodeName === 'Orphaned')).toBe(true);
1960 |       expect(result.warnings.some(w => w.message.includes('AI Agent has no tools connected'))).toBe(true);
1961 | 
1962 |       // Statistics
1963 |       expect(result.statistics.totalNodes).toBe(7);
1964 |       expect(result.statistics.enabledNodes).toBe(6);
1965 |       expect(result.statistics.triggerNodes).toBe(1);
1966 |       expect(result.statistics.invalidConnections).toBeGreaterThan(0);
1967 | 
1968 |       // Suggestions
1969 |       expect(result.suggestions.length).toBeGreaterThan(0);
1970 |     });
1971 | 
1972 |     it('should validate a perfect workflow', async () => {
1973 |       const workflow = {
1974 |         nodes: [
1975 |           {
1976 |             id: '1',
1977 |             name: 'Manual Trigger',
1978 |             type: 'n8n-nodes-base.manualTrigger',
1979 |             position: [250, 300],
1980 |             parameters: {},
1981 |             typeVersion: 1
1982 |           },
1983 |           {
1984 |             id: '2',
1985 |             name: 'HTTP Request',
1986 |             type: 'n8n-nodes-base.httpRequest',
1987 |             position: [450, 300],
1988 |             parameters: {
1989 |               url: 'https://api.example.com/data',
1990 |               method: 'GET'
1991 |             },
1992 |             typeVersion: 4,
1993 |             onError: 'continueErrorOutput',
1994 |             retryOnFail: true,
1995 |             maxTries: 3,
1996 |             waitBetweenTries: 1000
1997 |           },
1998 |           {
1999 |             id: '3',
2000 |             name: 'Process Data',
2001 |             type: 'n8n-nodes-base.code',
2002 |             position: [650, 300],
2003 |             parameters: {
2004 |               jsCode: 'return items;'
2005 |             },
2006 |             typeVersion: 2
2007 |           },
2008 |           {
2009 |             id: '4',
2010 |             name: 'Error Handler',
2011 |             type: 'n8n-nodes-base.set',
2012 |             position: [650, 500],
2013 |             parameters: {
2014 |               values: {
2015 |                 string: [
2016 |                   {
2017 |                     name: 'error',
2018 |                     value: 'An error occurred'
2019 |                   }
2020 |                 ]
2021 |               }
2022 |             },
2023 |             typeVersion: 3
2024 |           }
2025 |         ],
2026 |         connections: {
2027 |           'Manual Trigger': {
2028 |             main: [[{ node: 'HTTP Request', type: 'main', index: 0 }]]
2029 |           },
2030 |           'HTTP Request': {
2031 |             main: [
2032 |               [{ node: 'Process Data', type: 'main', index: 0 }],
2033 |               [{ node: 'Error Handler', type: 'main', index: 0 }]
2034 |             ]
2035 |           }
2036 |         }
2037 |       } as any;
2038 | 
2039 |       const result = await validator.validateWorkflow(workflow as any);
2040 | 
2041 |       expect(result.valid).toBe(true);
2042 |       expect(result.errors).toHaveLength(0);
2043 |       expect(result.warnings).toHaveLength(0);
2044 |       expect(result.statistics.validConnections).toBe(3);
2045 |       expect(result.statistics.invalidConnections).toBe(0);
2046 |     });
2047 |   });
2048 | });
```
Page 48/59FirstPrevNextLast