#
tokens: 48004/50000 16/614 files (page 11/59)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 11 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

--------------------------------------------------------------------------------
/scripts/test-expression-format-validation.js:
--------------------------------------------------------------------------------

```javascript
  1 | #!/usr/bin/env node
  2 | 
  3 | /**
  4 |  * Test script for expression format validation
  5 |  * Tests the validation of expression prefixes and resource locator formats
  6 |  */
  7 | 
  8 | const { WorkflowValidator } = require('../dist/services/workflow-validator.js');
  9 | const { NodeRepository } = require('../dist/database/node-repository.js');
 10 | const { EnhancedConfigValidator } = require('../dist/services/enhanced-config-validator.js');
 11 | const { createDatabaseAdapter } = require('../dist/database/database-adapter.js');
 12 | const path = require('path');
 13 | 
 14 | async function runTests() {
 15 |   // Initialize database
 16 |   const dbPath = path.join(__dirname, '..', 'data', 'nodes.db');
 17 |   const adapter = await createDatabaseAdapter(dbPath);
 18 |   const db = adapter;
 19 | 
 20 |   const nodeRepository = new NodeRepository(db);
 21 |   const validator = new WorkflowValidator(nodeRepository, EnhancedConfigValidator);
 22 | 
 23 |   console.log('\n🧪 Testing Expression Format Validation\n');
 24 |   console.log('=' .repeat(60));
 25 | 
 26 |   // Test 1: Email node with missing = prefix
 27 |   console.log('\n📝 Test 1: Email Send node - Missing = prefix');
 28 |   console.log('-'.repeat(40));
 29 | 
 30 |   const emailWorkflowIncorrect = {
 31 |     nodes: [
 32 |       {
 33 |         id: 'b9dd1cfd-ee66-4049-97e7-1af6d976a4e0',
 34 |         name: 'Error Handler',
 35 |         type: 'n8n-nodes-base.emailSend',
 36 |         typeVersion: 2.1,
 37 |         position: [-128, 400],
 38 |         parameters: {
 39 |           fromEmail: '{{ $env.ADMIN_EMAIL }}',  // INCORRECT - missing =
 40 |           toEmail: '[email protected]',
 41 |           subject: 'GitHub Issue Workflow Error - HIGH PRIORITY',
 42 |           options: {}
 43 |         },
 44 |         credentials: {
 45 |           smtp: {
 46 |             id: '7AQ08VMFHubmfvzR',
 47 |             name: '[email protected]'
 48 |           }
 49 |         }
 50 |       }
 51 |     ],
 52 |     connections: {}
 53 |   };
 54 | 
 55 |   const result1 = await validator.validateWorkflow(emailWorkflowIncorrect);
 56 | 
 57 |   if (result1.errors.some(e => e.message.includes('Expression format'))) {
 58 |     console.log('✅ ERROR DETECTED (correct behavior):');
 59 |     const formatError = result1.errors.find(e => e.message.includes('Expression format'));
 60 |     console.log('\n' + formatError.message);
 61 |   } else {
 62 |     console.log('❌ No expression format error detected (should have detected missing prefix)');
 63 |   }
 64 | 
 65 |   // Test 2: Email node with correct = prefix
 66 |   console.log('\n📝 Test 2: Email Send node - Correct = prefix');
 67 |   console.log('-'.repeat(40));
 68 | 
 69 |   const emailWorkflowCorrect = {
 70 |     nodes: [
 71 |       {
 72 |         id: 'b9dd1cfd-ee66-4049-97e7-1af6d976a4e0',
 73 |         name: 'Error Handler',
 74 |         type: 'n8n-nodes-base.emailSend',
 75 |         typeVersion: 2.1,
 76 |         position: [-128, 400],
 77 |         parameters: {
 78 |           fromEmail: '={{ $env.ADMIN_EMAIL }}',  // CORRECT - has =
 79 |           toEmail: '[email protected]',
 80 |           subject: 'GitHub Issue Workflow Error - HIGH PRIORITY',
 81 |           options: {}
 82 |         }
 83 |       }
 84 |     ],
 85 |     connections: {}
 86 |   };
 87 | 
 88 |   const result2 = await validator.validateWorkflow(emailWorkflowCorrect);
 89 | 
 90 |   if (result2.errors.some(e => e.message.includes('Expression format'))) {
 91 |     console.log('❌ Unexpected expression format error (should accept = prefix)');
 92 |   } else {
 93 |     console.log('✅ No expression format errors (correct!)');
 94 |   }
 95 | 
 96 |   // Test 3: GitHub node without resource locator format
 97 |   console.log('\n📝 Test 3: GitHub node - Missing resource locator format');
 98 |   console.log('-'.repeat(40));
 99 | 
100 |   const githubWorkflowIncorrect = {
101 |     nodes: [
102 |       {
103 |         id: '3c742ca1-af8f-4d80-a47e-e68fb1ced491',
104 |         name: 'Send Welcome Comment',
105 |         type: 'n8n-nodes-base.github',
106 |         typeVersion: 1.1,
107 |         position: [-240, 96],
108 |         parameters: {
109 |           operation: 'createComment',
110 |           owner: '{{ $vars.GITHUB_OWNER }}',  // INCORRECT - needs RL format
111 |           repository: '{{ $vars.GITHUB_REPO }}',  // INCORRECT - needs RL format
112 |           issueNumber: null,
113 |           body: '👋 Hi @{{ $(\'Extract Issue Data\').first().json.author }}!'  // INCORRECT - missing =
114 |         },
115 |         credentials: {
116 |           githubApi: {
117 |             id: 'edgpwh6ldYN07MXx',
118 |             name: 'GitHub account'
119 |           }
120 |         }
121 |       }
122 |     ],
123 |     connections: {}
124 |   };
125 | 
126 |   const result3 = await validator.validateWorkflow(githubWorkflowIncorrect);
127 | 
128 |   const formatErrors = result3.errors.filter(e => e.message.includes('Expression format'));
129 |   console.log(`\nFound ${formatErrors.length} expression format errors:`);
130 | 
131 |   if (formatErrors.length >= 3) {
132 |     console.log('✅ All format issues detected:');
133 |     formatErrors.forEach((error, index) => {
134 |       const field = error.message.match(/Field '([^']+)'/)?.[1] || 'unknown';
135 |       console.log(`  ${index + 1}. Field '${field}' - ${error.message.includes('resource locator') ? 'Needs RL format' : 'Missing = prefix'}`);
136 |     });
137 |   } else {
138 |     console.log('❌ Not all format issues detected');
139 |   }
140 | 
141 |   // Test 4: GitHub node with correct resource locator format
142 |   console.log('\n📝 Test 4: GitHub node - Correct resource locator format');
143 |   console.log('-'.repeat(40));
144 | 
145 |   const githubWorkflowCorrect = {
146 |     nodes: [
147 |       {
148 |         id: '3c742ca1-af8f-4d80-a47e-e68fb1ced491',
149 |         name: 'Send Welcome Comment',
150 |         type: 'n8n-nodes-base.github',
151 |         typeVersion: 1.1,
152 |         position: [-240, 96],
153 |         parameters: {
154 |           operation: 'createComment',
155 |           owner: {
156 |             __rl: true,
157 |             value: '={{ $vars.GITHUB_OWNER }}',  // CORRECT - RL format with =
158 |             mode: 'expression'
159 |           },
160 |           repository: {
161 |             __rl: true,
162 |             value: '={{ $vars.GITHUB_REPO }}',  // CORRECT - RL format with =
163 |             mode: 'expression'
164 |           },
165 |           issueNumber: 123,
166 |           body: '=👋 Hi @{{ $(\'Extract Issue Data\').first().json.author }}!'  // CORRECT - has =
167 |         }
168 |       }
169 |     ],
170 |     connections: {}
171 |   };
172 | 
173 |   const result4 = await validator.validateWorkflow(githubWorkflowCorrect);
174 | 
175 |   const formatErrors4 = result4.errors.filter(e => e.message.includes('Expression format'));
176 |   if (formatErrors4.length === 0) {
177 |     console.log('✅ No expression format errors (correct!)');
178 |   } else {
179 |     console.log(`❌ Unexpected expression format errors: ${formatErrors4.length}`);
180 |     formatErrors4.forEach(e => console.log('  - ' + e.message.split('\n')[0]));
181 |   }
182 | 
183 |   // Test 5: Mixed content expressions
184 |   console.log('\n📝 Test 5: Mixed content with expressions');
185 |   console.log('-'.repeat(40));
186 | 
187 |   const mixedContentWorkflow = {
188 |     nodes: [
189 |       {
190 |         id: '1',
191 |         name: 'HTTP Request',
192 |         type: 'n8n-nodes-base.httpRequest',
193 |         typeVersion: 4,
194 |         position: [0, 0],
195 |         parameters: {
196 |           url: 'https://api.example.com/users/{{ $json.userId }}',  // INCORRECT
197 |           headers: {
198 |             'Authorization': '=Bearer {{ $env.API_TOKEN }}'  // CORRECT
199 |           }
200 |         }
201 |       }
202 |     ],
203 |     connections: {}
204 |   };
205 | 
206 |   const result5 = await validator.validateWorkflow(mixedContentWorkflow);
207 | 
208 |   const urlError = result5.errors.find(e => e.message.includes('url') && e.message.includes('Expression format'));
209 |   if (urlError) {
210 |     console.log('✅ Mixed content error detected for URL field');
211 |     console.log('  Should be: "=https://api.example.com/users/{{ $json.userId }}"');
212 |   } else {
213 |     console.log('❌ Mixed content error not detected');
214 |   }
215 | 
216 |   console.log('\n' + '='.repeat(60));
217 |   console.log('\n✨ Expression Format Validation Summary:');
218 |   console.log('  - Detects missing = prefix in expressions');
219 |   console.log('  - Identifies fields needing resource locator format');
220 |   console.log('  - Provides clear correction examples');
221 |   console.log('  - Handles mixed literal and expression content');
222 | 
223 |   // Close database
224 |   db.close();
225 | }
226 | 
227 | runTests().catch(error => {
228 |   console.error('Test failed:', error);
229 |   process.exit(1);
230 | });
```

--------------------------------------------------------------------------------
/tests/integration/n8n-api/workflows/get-workflow-details.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Integration Tests: handleGetWorkflowDetails
  3 |  *
  4 |  * Tests workflow details retrieval against a real n8n instance.
  5 |  * Covers basic workflows, metadata, version history, and execution stats.
  6 |  */
  7 | 
  8 | import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest';
  9 | import { createTestContext, TestContext, createTestWorkflowName } from '../utils/test-context';
 10 | import { getTestN8nClient } from '../utils/n8n-client';
 11 | import { N8nApiClient } from '../../../../src/services/n8n-api-client';
 12 | import { SIMPLE_WEBHOOK_WORKFLOW } from '../utils/fixtures';
 13 | import { cleanupOrphanedWorkflows } from '../utils/cleanup-helpers';
 14 | import { createMcpContext } from '../utils/mcp-context';
 15 | import { InstanceContext } from '../../../../src/types/instance-context';
 16 | import { handleGetWorkflowDetails } from '../../../../src/mcp/handlers-n8n-manager';
 17 | 
 18 | describe('Integration: handleGetWorkflowDetails', () => {
 19 |   let context: TestContext;
 20 |   let client: N8nApiClient;
 21 |   let mcpContext: InstanceContext;
 22 | 
 23 |   beforeEach(() => {
 24 |     context = createTestContext();
 25 |     client = getTestN8nClient();
 26 |     mcpContext = createMcpContext();
 27 |   });
 28 | 
 29 |   afterEach(async () => {
 30 |     await context.cleanup();
 31 |   });
 32 | 
 33 |   afterAll(async () => {
 34 |     if (!process.env.CI) {
 35 |       await cleanupOrphanedWorkflows();
 36 |     }
 37 |   });
 38 | 
 39 |   // ======================================================================
 40 |   // Basic Workflow Details
 41 |   // ======================================================================
 42 | 
 43 |   describe('Basic Workflow', () => {
 44 |     it('should retrieve workflow with basic details', async () => {
 45 |       // Create a simple workflow
 46 |       const workflow = {
 47 |         ...SIMPLE_WEBHOOK_WORKFLOW,
 48 |         name: createTestWorkflowName('Get Details - Basic'),
 49 |         tags: ['mcp-integration-test']
 50 |       };
 51 | 
 52 |       const created = await client.createWorkflow(workflow);
 53 |       expect(created).toBeDefined();
 54 |       expect(created.id).toBeTruthy();
 55 | 
 56 |       if (!created.id) throw new Error('Workflow ID is missing');
 57 |       context.trackWorkflow(created.id);
 58 | 
 59 |       // Retrieve detailed workflow information using MCP handler
 60 |       const response = await handleGetWorkflowDetails({ id: created.id }, mcpContext);
 61 | 
 62 |       // Verify MCP response structure
 63 |       expect(response.success).toBe(true);
 64 |       expect(response.data).toBeDefined();
 65 | 
 66 |       // handleGetWorkflowDetails returns { workflow, executionStats, hasWebhookTrigger, webhookPath }
 67 |       const details = (response.data as any).workflow;
 68 | 
 69 |       // Verify basic details
 70 |       expect(details).toBeDefined();
 71 |       expect(details.id).toBe(created.id);
 72 |       expect(details.name).toBe(workflow.name);
 73 |       expect(details.createdAt).toBeDefined();
 74 |       expect(details.updatedAt).toBeDefined();
 75 |       expect(details.active).toBeDefined();
 76 | 
 77 |       // Verify metadata fields
 78 |       expect(details.versionId).toBeDefined();
 79 |     });
 80 |   });
 81 | 
 82 |   // ======================================================================
 83 |   // Workflow with Metadata
 84 |   // ======================================================================
 85 | 
 86 |   describe('Workflow with Metadata', () => {
 87 |     it('should retrieve workflow with tags and settings metadata', async () => {
 88 |       // Create workflow with rich metadata
 89 |       const workflow = {
 90 |         ...SIMPLE_WEBHOOK_WORKFLOW,
 91 |         name: createTestWorkflowName('Get Details - With Metadata'),
 92 |         tags: [
 93 |           'mcp-integration-test',
 94 |           'test-category',
 95 |           'integration'
 96 |         ],
 97 |         settings: {
 98 |           executionOrder: 'v1' as const,
 99 |           timezone: 'America/New_York'
100 |         }
101 |       };
102 | 
103 |       const created = await client.createWorkflow(workflow);
104 |       expect(created).toBeDefined();
105 |       expect(created.id).toBeTruthy();
106 | 
107 |       if (!created.id) throw new Error('Workflow ID is missing');
108 |       context.trackWorkflow(created.id);
109 | 
110 |       // Retrieve workflow details using MCP handler
111 |       const response = await handleGetWorkflowDetails({ id: created.id }, mcpContext);
112 |       expect(response.success).toBe(true);
113 |       const details = (response.data as any).workflow;
114 | 
115 |       // Verify metadata is present (tags may be undefined in API response)
116 |       // Note: n8n API behavior for tags varies - they may not be returned
117 |       // in GET requests even if set during creation
118 |       if (details.tags) {
119 |         expect(details.tags.length).toBeGreaterThanOrEqual(0);
120 |       }
121 | 
122 |       // Verify settings
123 |       expect(details.settings).toBeDefined();
124 |       expect(details.settings!.executionOrder).toBe('v1');
125 |       expect(details.settings!.timezone).toBe('America/New_York');
126 |     });
127 |   });
128 | 
129 |   // ======================================================================
130 |   // Version History
131 |   // ======================================================================
132 | 
133 |   describe('Version History', () => {
134 |     it('should track version changes after updates', async () => {
135 |       // Create initial workflow
136 |       const workflow = {
137 |         ...SIMPLE_WEBHOOK_WORKFLOW,
138 |         name: createTestWorkflowName('Get Details - Version History'),
139 |         tags: ['mcp-integration-test']
140 |       };
141 | 
142 |       const created = await client.createWorkflow(workflow);
143 |       expect(created).toBeDefined();
144 |       expect(created.id).toBeTruthy();
145 | 
146 |       if (!created.id) throw new Error('Workflow ID is missing');
147 |       context.trackWorkflow(created.id);
148 | 
149 |       // Get initial version using MCP handler
150 |       const initialResponse = await handleGetWorkflowDetails({ id: created.id }, mcpContext);
151 |       expect(initialResponse.success).toBe(true);
152 |       const initialDetails = (initialResponse.data as any).workflow;
153 |       const initialVersionId = initialDetails.versionId;
154 |       const initialUpdatedAt = initialDetails.updatedAt;
155 | 
156 |       // Update the workflow
157 |       await client.updateWorkflow(created.id, {
158 |         name: createTestWorkflowName('Get Details - Version History (Updated)'),
159 |         nodes: workflow.nodes,
160 |         connections: workflow.connections
161 |       });
162 | 
163 |       // Get updated details using MCP handler
164 |       const updatedResponse = await handleGetWorkflowDetails({ id: created.id }, mcpContext);
165 |       expect(updatedResponse.success).toBe(true);
166 |       const updatedDetails = (updatedResponse.data as any).workflow;
167 | 
168 |       // Verify version changed
169 |       expect(updatedDetails.versionId).toBeDefined();
170 |       expect(updatedDetails.updatedAt).not.toBe(initialUpdatedAt);
171 | 
172 |       // Version ID should have changed after update
173 |       expect(updatedDetails.versionId).not.toBe(initialVersionId);
174 |     });
175 |   });
176 | 
177 |   // ======================================================================
178 |   // Execution Statistics
179 |   // ======================================================================
180 | 
181 |   describe('Execution Statistics', () => {
182 |     it('should include execution-related fields in details', async () => {
183 |       // Create workflow
184 |       const workflow = {
185 |         ...SIMPLE_WEBHOOK_WORKFLOW,
186 |         name: createTestWorkflowName('Get Details - Execution Stats'),
187 |         tags: ['mcp-integration-test']
188 |       };
189 | 
190 |       const created = await client.createWorkflow(workflow);
191 |       expect(created).toBeDefined();
192 |       expect(created.id).toBeTruthy();
193 | 
194 |       if (!created.id) throw new Error('Workflow ID is missing');
195 |       context.trackWorkflow(created.id);
196 | 
197 |       // Retrieve workflow details using MCP handler
198 |       const response = await handleGetWorkflowDetails({ id: created.id }, mcpContext);
199 |       expect(response.success).toBe(true);
200 |       const details = (response.data as any).workflow;
201 | 
202 |       // Verify execution-related fields exist
203 |       // Note: New workflows won't have executions, but fields should be present
204 |       expect(details).toHaveProperty('active');
205 | 
206 |       // The workflow should start inactive
207 |       expect(details.active).toBe(false);
208 |     });
209 |   });
210 | });
211 | 
```

--------------------------------------------------------------------------------
/tests/unit/test-env-example.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Example test demonstrating test environment configuration usage
  3 |  */
  4 | 
  5 | import { describe, it, expect, beforeAll, afterAll } from 'vitest';
  6 | import { 
  7 |   getTestConfig, 
  8 |   getTestTimeout, 
  9 |   isFeatureEnabled,
 10 |   isTestMode,
 11 |   loadTestEnvironment 
 12 | } from '@tests/setup/test-env';
 13 | import {
 14 |   withEnvOverrides,
 15 |   createTestDatabasePath,
 16 |   getMockApiUrl,
 17 |   measurePerformance,
 18 |   createTestLogger,
 19 |   waitForCondition
 20 | } from '@tests/helpers/env-helpers';
 21 | 
 22 | describe('Test Environment Configuration Example', () => {
 23 |   let config: ReturnType<typeof getTestConfig>;
 24 |   let logger: ReturnType<typeof createTestLogger>;
 25 |   
 26 |   beforeAll(() => {
 27 |     // Initialize config inside beforeAll to ensure environment is loaded
 28 |     config = getTestConfig();
 29 |     logger = createTestLogger('test-env-example');
 30 |     
 31 |     logger.info('Test suite starting with configuration:', {
 32 |       environment: config.nodeEnv,
 33 |       database: config.database.path,
 34 |       apiUrl: config.api.url
 35 |     });
 36 |   });
 37 |   
 38 |   afterAll(() => {
 39 |     logger.info('Test suite completed');
 40 |   });
 41 |   
 42 |   it('should be in test mode', () => {
 43 |     const testConfig = getTestConfig();
 44 |     expect(isTestMode()).toBe(true);
 45 |     expect(testConfig.nodeEnv).toBe('test');
 46 |     expect(testConfig.isTest).toBe(true);
 47 |   });
 48 |   
 49 |   it('should have proper database configuration', () => {
 50 |     const testConfig = getTestConfig();
 51 |     expect(testConfig.database.path).toBeDefined();
 52 |     expect(testConfig.database.rebuildOnStart).toBe(false);
 53 |     expect(testConfig.database.seedData).toBe(true);
 54 |   });
 55 |   
 56 |   it.skip('should have mock API configuration', () => {
 57 |     const testConfig = getTestConfig();
 58 |     // Add debug logging for CI
 59 |     if (process.env.CI) {
 60 |       console.log('CI Environment Debug:', {
 61 |         NODE_ENV: process.env.NODE_ENV,
 62 |         N8N_API_URL: process.env.N8N_API_URL,
 63 |         N8N_API_KEY: process.env.N8N_API_KEY,
 64 |         configUrl: testConfig.api.url,
 65 |         configKey: testConfig.api.key
 66 |       });
 67 |     }
 68 |     expect(testConfig.api.url).toMatch(/mock-api/);
 69 |     expect(testConfig.api.key).toBe('test-api-key-12345');
 70 |   });
 71 |   
 72 |   it('should respect test timeouts', { timeout: getTestTimeout('unit') }, async () => {
 73 |     const timeout = getTestTimeout('unit');
 74 |     expect(timeout).toBe(5000);
 75 |     
 76 |     // Simulate async operation
 77 |     await new Promise(resolve => setTimeout(resolve, 100));
 78 |   });
 79 |   
 80 |   it('should support environment overrides', () => {
 81 |     const testConfig = getTestConfig();
 82 |     const originalLogLevel = testConfig.logging.level;
 83 |     
 84 |     const result = withEnvOverrides({
 85 |       LOG_LEVEL: 'debug',
 86 |       DEBUG: 'true'
 87 |     }, () => {
 88 |       const newConfig = getTestConfig();
 89 |       expect(newConfig.logging.level).toBe('debug');
 90 |       expect(newConfig.logging.debug).toBe(true);
 91 |       return 'success';
 92 |     });
 93 |     
 94 |     expect(result).toBe('success');
 95 |     const configAfter = getTestConfig();
 96 |     expect(configAfter.logging.level).toBe(originalLogLevel);
 97 |   });
 98 |   
 99 |   it('should generate unique test database paths', () => {
100 |     const path1 = createTestDatabasePath('feature1');
101 |     const path2 = createTestDatabasePath('feature1');
102 |     
103 |     if (path1 !== ':memory:') {
104 |       expect(path1).not.toBe(path2);
105 |       expect(path1).toMatch(/test-feature1-\d+-\w+\.db$/);
106 |     }
107 |   });
108 |   
109 |   it('should construct mock API URLs', () => {
110 |     const testConfig = getTestConfig();
111 |     const baseUrl = getMockApiUrl();
112 |     const endpointUrl = getMockApiUrl('/nodes');
113 |     
114 |     expect(baseUrl).toBe(testConfig.api.url);
115 |     expect(endpointUrl).toBe(`${testConfig.api.url}/nodes`);
116 |   });
117 |   
118 |   it.skipIf(!isFeatureEnabled('mockExternalApis'))('should check feature flags', () => {
119 |     const testConfig = getTestConfig();
120 |     expect(testConfig.features.mockExternalApis).toBe(true);
121 |     expect(isFeatureEnabled('mockExternalApis')).toBe(true);
122 |   });
123 |   
124 |   it('should measure performance', () => {
125 |     const measure = measurePerformance('test-operation');
126 |     
127 |     // Test the performance measurement utility structure and behavior
128 |     // rather than relying on timing precision which is unreliable in CI
129 |     
130 |     // Capture initial state
131 |     const startTime = performance.now();
132 |     
133 |     // Add some marks
134 |     measure.mark('start-processing');
135 |     
136 |     // Do some minimal synchronous work
137 |     let sum = 0;
138 |     for (let i = 0; i < 10000; i++) {
139 |       sum += i;
140 |     }
141 |     
142 |     measure.mark('mid-processing');
143 |     
144 |     // Do a bit more work
145 |     for (let i = 0; i < 10000; i++) {
146 |       sum += i * 2;
147 |     }
148 |     
149 |     const results = measure.end();
150 |     const endTime = performance.now();
151 |     
152 |     // Test the utility's correctness rather than exact timing
153 |     expect(results).toHaveProperty('total');
154 |     expect(results).toHaveProperty('marks');
155 |     expect(typeof results.total).toBe('number');
156 |     expect(results.total).toBeGreaterThan(0);
157 |     
158 |     // Verify marks structure
159 |     expect(results.marks).toHaveProperty('start-processing');
160 |     expect(results.marks).toHaveProperty('mid-processing');
161 |     expect(typeof results.marks['start-processing']).toBe('number');
162 |     expect(typeof results.marks['mid-processing']).toBe('number');
163 |     
164 |     // Verify logical order of marks (this should always be true)
165 |     expect(results.marks['start-processing']).toBeLessThan(results.marks['mid-processing']);
166 |     expect(results.marks['start-processing']).toBeGreaterThanOrEqual(0);
167 |     expect(results.marks['mid-processing']).toBeLessThan(results.total);
168 |     
169 |     // Verify the total time is reasonable (should be between manual measurements)
170 |     const manualTotal = endTime - startTime;
171 |     expect(results.total).toBeLessThanOrEqual(manualTotal + 1); // Allow 1ms tolerance
172 |     
173 |     // Verify work was actually done
174 |     expect(sum).toBeGreaterThan(0);
175 |   });
176 |   
177 |   it('should wait for conditions', async () => {
178 |     let counter = 0;
179 |     const incrementCounter = setInterval(() => counter++, 100);
180 |     
181 |     try {
182 |       await waitForCondition(
183 |         () => counter >= 3,
184 |         { 
185 |           timeout: 1000, 
186 |           interval: 50,
187 |           message: 'Counter did not reach 3'
188 |         }
189 |       );
190 |       
191 |       expect(counter).toBeGreaterThanOrEqual(3);
192 |     } finally {
193 |       clearInterval(incrementCounter);
194 |     }
195 |   });
196 |   
197 |   it('should have proper logging configuration', () => {
198 |     const testConfig = getTestConfig();
199 |     expect(testConfig.logging.level).toBe('error');
200 |     expect(testConfig.logging.debug).toBe(false);
201 |     expect(testConfig.logging.showStack).toBe(true);
202 |     
203 |     // Logger should respect configuration
204 |     logger.debug('This should not appear in test output');
205 |     logger.error('This should appear in test output');
206 |   });
207 |   
208 |   it('should have performance thresholds', () => {
209 |     const testConfig = getTestConfig();
210 |     expect(testConfig.performance.thresholds.apiResponse).toBe(100);
211 |     expect(testConfig.performance.thresholds.dbQuery).toBe(50);
212 |     expect(testConfig.performance.thresholds.nodeParse).toBe(200);
213 |   });
214 |   
215 |   it('should disable caching and rate limiting in tests', () => {
216 |     const testConfig = getTestConfig();
217 |     expect(testConfig.cache.enabled).toBe(false);
218 |     expect(testConfig.cache.ttl).toBe(0);
219 |     expect(testConfig.rateLimiting.max).toBe(0);
220 |     expect(testConfig.rateLimiting.window).toBe(0);
221 |   });
222 |   
223 |   it('should configure test paths', () => {
224 |     const testConfig = getTestConfig();
225 |     expect(testConfig.paths.fixtures).toBe('./tests/fixtures');
226 |     expect(testConfig.paths.data).toBe('./tests/data');
227 |     expect(testConfig.paths.snapshots).toBe('./tests/__snapshots__');
228 |   });
229 |   
230 |   it('should support MSW configuration', () => {
231 |     // Ensure test environment is loaded
232 |     if (!process.env.MSW_ENABLED) {
233 |       loadTestEnvironment();
234 |     }
235 |     
236 |     const testConfig = getTestConfig();
237 |     expect(testConfig.mocking.msw.enabled).toBe(true);
238 |     expect(testConfig.mocking.msw.apiDelay).toBe(0);
239 |   });
240 | });
```

--------------------------------------------------------------------------------
/tests/unit/examples/using-n8n-nodes-base-mock.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, vi } from 'vitest';
  2 | import { getNodeTypes, mockNodeBehavior, resetAllMocks } from '../__mocks__/n8n-nodes-base';
  3 | 
  4 | // Example service that uses n8n-nodes-base
  5 | class WorkflowService {
  6 |   async getNodeDescription(nodeName: string) {
  7 |     const nodeTypes = getNodeTypes();
  8 |     const node = nodeTypes.getByName(nodeName);
  9 |     return node?.description;
 10 |   }
 11 | 
 12 |   async executeNode(nodeName: string, context: any) {
 13 |     const nodeTypes = getNodeTypes();
 14 |     const node = nodeTypes.getByName(nodeName);
 15 |     
 16 |     if (!node?.execute) {
 17 |       throw new Error(`Node ${nodeName} does not have an execute method`);
 18 |     }
 19 |     
 20 |     return node.execute.call(context);
 21 |   }
 22 | 
 23 |   async validateSlackMessage(channel: string, text: string) {
 24 |     if (!channel || !text) {
 25 |       throw new Error('Channel and text are required');
 26 |     }
 27 |     
 28 |     const nodeTypes = getNodeTypes();
 29 |     const slackNode = nodeTypes.getByName('slack');
 30 |     
 31 |     if (!slackNode) {
 32 |       throw new Error('Slack node not found');
 33 |     }
 34 |     
 35 |     // Check if required properties exist
 36 |     const channelProp = slackNode.description.properties.find(p => p.name === 'channel');
 37 |     const textProp = slackNode.description.properties.find(p => p.name === 'text');
 38 |     
 39 |     return !!(channelProp && textProp);
 40 |   }
 41 | }
 42 | 
 43 | // Mock the module at the top level
 44 | vi.mock('n8n-nodes-base', () => {
 45 |   const { getNodeTypes: mockGetNodeTypes } = require('../__mocks__/n8n-nodes-base');
 46 |   return {
 47 |     getNodeTypes: mockGetNodeTypes
 48 |   };
 49 | });
 50 | 
 51 | describe('WorkflowService with n8n-nodes-base mock', () => {
 52 |   let service: WorkflowService;
 53 | 
 54 |   beforeEach(() => {
 55 |     resetAllMocks();
 56 |     service = new WorkflowService();
 57 |   });
 58 | 
 59 |   describe('getNodeDescription', () => {
 60 |     it('should get webhook node description', async () => {
 61 |       const description = await service.getNodeDescription('webhook');
 62 |       
 63 |       expect(description).toBeDefined();
 64 |       expect(description?.name).toBe('webhook');
 65 |       expect(description?.group).toContain('trigger');
 66 |       expect(description?.webhooks).toBeDefined();
 67 |     });
 68 | 
 69 |     it('should get httpRequest node description', async () => {
 70 |       const description = await service.getNodeDescription('httpRequest');
 71 |       
 72 |       expect(description).toBeDefined();
 73 |       expect(description?.name).toBe('httpRequest');
 74 |       expect(description?.version).toBe(3);
 75 |       
 76 |       const methodProp = description?.properties.find(p => p.name === 'method');
 77 |       expect(methodProp).toBeDefined();
 78 |       expect(methodProp?.options).toHaveLength(6);
 79 |     });
 80 |   });
 81 | 
 82 |   describe('executeNode', () => {
 83 |     it('should execute httpRequest node with custom response', async () => {
 84 |       // Override the httpRequest node behavior for this test
 85 |       mockNodeBehavior('httpRequest', {
 86 |         execute: vi.fn(async function(this: any) {
 87 |           const url = this.getNodeParameter('url', 0);
 88 |           return [[{ 
 89 |             json: { 
 90 |               statusCode: 200,
 91 |               url,
 92 |               customData: 'mocked response' 
 93 |             } 
 94 |           }]];
 95 |         })
 96 |       });
 97 | 
 98 |       const mockContext = {
 99 |         getInputData: vi.fn(() => [{ json: { input: 'data' } }]),
100 |         getNodeParameter: vi.fn((name: string) => {
101 |           if (name === 'url') return 'https://test.com/api';
102 |           return '';
103 |         })
104 |       };
105 | 
106 |       const result = await service.executeNode('httpRequest', mockContext);
107 |       
108 |       expect(result).toBeDefined();
109 |       expect(result[0][0].json).toMatchObject({
110 |         statusCode: 200,
111 |         url: 'https://test.com/api',
112 |         customData: 'mocked response'
113 |       });
114 |     });
115 | 
116 |     it('should execute slack node and track calls', async () => {
117 |       const mockContext = {
118 |         getInputData: vi.fn(() => [{ json: { message: 'test' } }]),
119 |         getNodeParameter: vi.fn((name: string, index: number) => {
120 |           const params: Record<string, string> = {
121 |             resource: 'message',
122 |             operation: 'post',
123 |             channel: '#general',
124 |             text: 'Hello from test!'
125 |           };
126 |           return params[name] || '';
127 |         }),
128 |         getCredentials: vi.fn(async () => ({ token: 'mock-token' }))
129 |       };
130 | 
131 |       const result = await service.executeNode('slack', mockContext);
132 |       
133 |       expect(result).toBeDefined();
134 |       expect(result[0][0].json).toMatchObject({
135 |         ok: true,
136 |         channel: '#general',
137 |         message: {
138 |           text: 'Hello from test!'
139 |         }
140 |       });
141 |       
142 |       // Verify the mock was called
143 |       expect(mockContext.getNodeParameter).toHaveBeenCalledWith('channel', 0, '');
144 |       expect(mockContext.getNodeParameter).toHaveBeenCalledWith('text', 0, '');
145 |     });
146 | 
147 |     it('should throw error for non-executable node', async () => {
148 |       // Create a trigger-only node
149 |       mockNodeBehavior('webhook', {
150 |         execute: undefined // Remove execute method
151 |       });
152 | 
153 |       await expect(
154 |         service.executeNode('webhook', {})
155 |       ).rejects.toThrow('Node webhook does not have an execute method');
156 |     });
157 |   });
158 | 
159 |   describe('validateSlackMessage', () => {
160 |     it('should validate slack message parameters', async () => {
161 |       const isValid = await service.validateSlackMessage('#general', 'Hello');
162 |       expect(isValid).toBe(true);
163 |     });
164 | 
165 |     it('should throw error for missing parameters', async () => {
166 |       await expect(
167 |         service.validateSlackMessage('', 'Hello')
168 |       ).rejects.toThrow('Channel and text are required');
169 | 
170 |       await expect(
171 |         service.validateSlackMessage('#general', '')
172 |       ).rejects.toThrow('Channel and text are required');
173 |     });
174 | 
175 |     it('should handle missing slack node', async () => {
176 |       // Save the original mock implementation
177 |       const originalImplementation = vi.mocked(getNodeTypes).getMockImplementation();
178 |       
179 |       // Override getNodeTypes to return undefined for slack
180 |       vi.mocked(getNodeTypes).mockImplementation(() => ({
181 |         getByName: vi.fn((name: string) => {
182 |           if (name === 'slack') return undefined;
183 |           // Return the actual mock implementation for other nodes
184 |           const actualRegistry = originalImplementation ? originalImplementation() : getNodeTypes();
185 |           return actualRegistry.getByName(name);
186 |         }),
187 |         getByNameAndVersion: vi.fn()
188 |       }));
189 | 
190 |       await expect(
191 |         service.validateSlackMessage('#general', 'Hello')
192 |       ).rejects.toThrow('Slack node not found');
193 |       
194 |       // Restore the original implementation
195 |       if (originalImplementation) {
196 |         vi.mocked(getNodeTypes).mockImplementation(originalImplementation);
197 |       }
198 |     });
199 |   });
200 | 
201 |   describe('complex workflow scenarios', () => {
202 |     it('should handle if node branching', async () => {
203 |       const mockContext = {
204 |         getInputData: vi.fn(() => [
205 |           { json: { status: 'active' } },
206 |           { json: { status: 'inactive' } },
207 |           { json: { status: 'active' } },
208 |         ]),
209 |         getNodeParameter: vi.fn()
210 |       };
211 | 
212 |       const result = await service.executeNode('if', mockContext);
213 |       
214 |       expect(result).toHaveLength(2); // true and false branches
215 |       expect(result[0]).toHaveLength(2); // items at index 0 and 2
216 |       expect(result[1]).toHaveLength(1); // item at index 1
217 |     });
218 | 
219 |     it('should handle merge node combining inputs', async () => {
220 |       const mockContext = {
221 |         getInputData: vi.fn((inputIndex?: number) => {
222 |           if (inputIndex === 0) return [{ json: { source: 'input1' } }];
223 |           if (inputIndex === 1) return [{ json: { source: 'input2' } }];
224 |           return [{ json: { source: 'input1' } }];
225 |         }),
226 |         getNodeParameter: vi.fn(() => 'append')
227 |       };
228 | 
229 |       const result = await service.executeNode('merge', mockContext);
230 |       
231 |       expect(result).toBeDefined();
232 |       expect(result[0]).toHaveLength(1);
233 |     });
234 |   });
235 | });
```

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

```typescript
  1 | import { describe, it, expect } from 'vitest';
  2 | import { UniversalExpressionValidator } from '../../../src/services/universal-expression-validator';
  3 | 
  4 | describe('UniversalExpressionValidator', () => {
  5 |   describe('validateExpressionPrefix', () => {
  6 |     it('should detect missing prefix in pure expression', () => {
  7 |       const result = UniversalExpressionValidator.validateExpressionPrefix('{{ $json.value }}');
  8 | 
  9 |       expect(result.isValid).toBe(false);
 10 |       expect(result.hasExpression).toBe(true);
 11 |       expect(result.needsPrefix).toBe(true);
 12 |       expect(result.isMixedContent).toBe(false);
 13 |       expect(result.confidence).toBe(1.0);
 14 |       expect(result.suggestion).toBe('={{ $json.value }}');
 15 |     });
 16 | 
 17 |     it('should detect missing prefix in mixed content', () => {
 18 |       const result = UniversalExpressionValidator.validateExpressionPrefix(
 19 |         'Hello {{ $json.name }}'
 20 |       );
 21 | 
 22 |       expect(result.isValid).toBe(false);
 23 |       expect(result.hasExpression).toBe(true);
 24 |       expect(result.needsPrefix).toBe(true);
 25 |       expect(result.isMixedContent).toBe(true);
 26 |       expect(result.confidence).toBe(1.0);
 27 |       expect(result.suggestion).toBe('=Hello {{ $json.name }}');
 28 |     });
 29 | 
 30 |     it('should accept properly prefixed expression', () => {
 31 |       const result = UniversalExpressionValidator.validateExpressionPrefix('={{ $json.value }}');
 32 | 
 33 |       expect(result.isValid).toBe(true);
 34 |       expect(result.hasExpression).toBe(true);
 35 |       expect(result.needsPrefix).toBe(false);
 36 |       expect(result.confidence).toBe(1.0);
 37 |     });
 38 | 
 39 |     it('should accept properly prefixed mixed content', () => {
 40 |       const result = UniversalExpressionValidator.validateExpressionPrefix(
 41 |         '=Hello {{ $json.name }}!'
 42 |       );
 43 | 
 44 |       expect(result.isValid).toBe(true);
 45 |       expect(result.hasExpression).toBe(true);
 46 |       expect(result.isMixedContent).toBe(true);
 47 |       expect(result.confidence).toBe(1.0);
 48 |     });
 49 | 
 50 |     it('should ignore non-string values', () => {
 51 |       const result = UniversalExpressionValidator.validateExpressionPrefix(123);
 52 | 
 53 |       expect(result.isValid).toBe(true);
 54 |       expect(result.hasExpression).toBe(false);
 55 |       expect(result.confidence).toBe(1.0);
 56 |     });
 57 | 
 58 |     it('should ignore strings without expressions', () => {
 59 |       const result = UniversalExpressionValidator.validateExpressionPrefix('plain text');
 60 | 
 61 |       expect(result.isValid).toBe(true);
 62 |       expect(result.hasExpression).toBe(false);
 63 |       expect(result.confidence).toBe(1.0);
 64 |     });
 65 |   });
 66 | 
 67 |   describe('validateExpressionSyntax', () => {
 68 |     it('should detect unclosed brackets', () => {
 69 |       const result = UniversalExpressionValidator.validateExpressionSyntax('={{ $json.value }');
 70 | 
 71 |       expect(result.isValid).toBe(false);
 72 |       expect(result.explanation).toContain('Unmatched expression brackets');
 73 |     });
 74 | 
 75 |     it('should detect empty expressions', () => {
 76 |       const result = UniversalExpressionValidator.validateExpressionSyntax('={{  }}');
 77 | 
 78 |       expect(result.isValid).toBe(false);
 79 |       expect(result.explanation).toContain('Empty expression');
 80 |     });
 81 | 
 82 |     it('should accept valid syntax', () => {
 83 |       const result = UniversalExpressionValidator.validateExpressionSyntax('={{ $json.value }}');
 84 | 
 85 |       expect(result.isValid).toBe(true);
 86 |       expect(result.hasExpression).toBe(true);
 87 |     });
 88 | 
 89 |     it('should handle multiple expressions', () => {
 90 |       const result = UniversalExpressionValidator.validateExpressionSyntax(
 91 |         '={{ $json.first }} and {{ $json.second }}'
 92 |       );
 93 | 
 94 |       expect(result.isValid).toBe(true);
 95 |       expect(result.hasExpression).toBe(true);
 96 |       expect(result.isMixedContent).toBe(true);
 97 |     });
 98 |   });
 99 | 
100 |   describe('validateCommonPatterns', () => {
101 |     it('should detect template literal syntax', () => {
102 |       const result = UniversalExpressionValidator.validateCommonPatterns('={{ ${json.value} }}');
103 | 
104 |       expect(result.isValid).toBe(false);
105 |       expect(result.explanation).toContain('Template literal syntax');
106 |     });
107 | 
108 |     it('should detect double prefix', () => {
109 |       const result = UniversalExpressionValidator.validateCommonPatterns('={{ =$json.value }}');
110 | 
111 |       expect(result.isValid).toBe(false);
112 |       expect(result.explanation).toContain('Double prefix');
113 |     });
114 | 
115 |     it('should detect nested brackets', () => {
116 |       const result = UniversalExpressionValidator.validateCommonPatterns(
117 |         '={{ $json.items[{{ $json.index }}] }}'
118 |       );
119 | 
120 |       expect(result.isValid).toBe(false);
121 |       expect(result.explanation).toContain('Nested brackets');
122 |     });
123 | 
124 |     it('should accept valid patterns', () => {
125 |       const result = UniversalExpressionValidator.validateCommonPatterns(
126 |         '={{ $json.items[$json.index] }}'
127 |       );
128 | 
129 |       expect(result.isValid).toBe(true);
130 |     });
131 |   });
132 | 
133 |   describe('validate (comprehensive)', () => {
134 |     it('should return all validation issues', () => {
135 |       const results = UniversalExpressionValidator.validate('{{ ${json.value} }}');
136 | 
137 |       expect(results.length).toBeGreaterThan(0);
138 |       const issues = results.filter(r => !r.isValid);
139 |       expect(issues.length).toBeGreaterThan(0);
140 | 
141 |       // Should detect both missing prefix and template literal syntax
142 |       const prefixIssue = issues.find(i => i.needsPrefix);
143 |       const patternIssue = issues.find(i => i.explanation.includes('Template literal'));
144 | 
145 |       expect(prefixIssue).toBeTruthy();
146 |       expect(patternIssue).toBeTruthy();
147 |     });
148 | 
149 |     it('should return success for valid expression', () => {
150 |       const results = UniversalExpressionValidator.validate('={{ $json.value }}');
151 | 
152 |       expect(results).toHaveLength(1);
153 |       expect(results[0].isValid).toBe(true);
154 |       expect(results[0].confidence).toBe(1.0);
155 |     });
156 | 
157 |     it('should handle non-expression strings', () => {
158 |       const results = UniversalExpressionValidator.validate('plain text');
159 | 
160 |       expect(results).toHaveLength(1);
161 |       expect(results[0].isValid).toBe(true);
162 |       expect(results[0].hasExpression).toBe(false);
163 |     });
164 |   });
165 | 
166 |   describe('getCorrectedValue', () => {
167 |     it('should add prefix to expression', () => {
168 |       const corrected = UniversalExpressionValidator.getCorrectedValue('{{ $json.value }}');
169 |       expect(corrected).toBe('={{ $json.value }}');
170 |     });
171 | 
172 |     it('should add prefix to mixed content', () => {
173 |       const corrected = UniversalExpressionValidator.getCorrectedValue(
174 |         'Hello {{ $json.name }}'
175 |       );
176 |       expect(corrected).toBe('=Hello {{ $json.name }}');
177 |     });
178 | 
179 |     it('should not modify already prefixed expressions', () => {
180 |       const corrected = UniversalExpressionValidator.getCorrectedValue('={{ $json.value }}');
181 |       expect(corrected).toBe('={{ $json.value }}');
182 |     });
183 | 
184 |     it('should not modify non-expressions', () => {
185 |       const corrected = UniversalExpressionValidator.getCorrectedValue('plain text');
186 |       expect(corrected).toBe('plain text');
187 |     });
188 |   });
189 | 
190 |   describe('hasMixedContent', () => {
191 |     it('should detect URLs with expressions', () => {
192 |       const result = UniversalExpressionValidator.validateExpressionPrefix(
193 |         'https://api.example.com/users/{{ $json.id }}'
194 |       );
195 |       expect(result.isMixedContent).toBe(true);
196 |     });
197 | 
198 |     it('should detect text with expressions', () => {
199 |       const result = UniversalExpressionValidator.validateExpressionPrefix(
200 |         'Welcome {{ $json.name }} to our service'
201 |       );
202 |       expect(result.isMixedContent).toBe(true);
203 |     });
204 | 
205 |     it('should identify pure expressions', () => {
206 |       const result = UniversalExpressionValidator.validateExpressionPrefix('{{ $json.value }}');
207 |       expect(result.isMixedContent).toBe(false);
208 |     });
209 | 
210 |     it('should identify pure expressions with spaces', () => {
211 |       const result = UniversalExpressionValidator.validateExpressionPrefix(
212 |         '  {{ $json.value }}  '
213 |       );
214 |       expect(result.isMixedContent).toBe(false);
215 |     });
216 |   });
217 | });
```

--------------------------------------------------------------------------------
/tests/integration/mcp-protocol/test-helpers.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
  2 | import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
  3 | import { 
  4 |   CallToolRequestSchema, 
  5 |   ListToolsRequestSchema,
  6 |   InitializeRequestSchema,
  7 | } from '@modelcontextprotocol/sdk/types.js';
  8 | import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
  9 | import { N8NDocumentationMCPServer } from '../../../src/mcp/server';
 10 | 
 11 | let sharedMcpServer: N8NDocumentationMCPServer | null = null;
 12 | 
 13 | export class TestableN8NMCPServer {
 14 |   private mcpServer: N8NDocumentationMCPServer;
 15 |   private server: Server;
 16 |   private transports = new Set<Transport>();
 17 |   private connections = new Set<any>();
 18 |   private static instanceCount = 0;
 19 |   private testDbPath: string;
 20 | 
 21 |   constructor() {
 22 |     // Use a unique test database for each instance to avoid conflicts
 23 |     // This prevents concurrent test issues with database locking
 24 |     const instanceId = TestableN8NMCPServer.instanceCount++;
 25 |     this.testDbPath = `/tmp/n8n-mcp-test-${process.pid}-${instanceId}.db`;
 26 |     process.env.NODE_DB_PATH = this.testDbPath;
 27 |     
 28 |     this.server = new Server({
 29 |       name: 'n8n-documentation-mcp',
 30 |       version: '1.0.0'
 31 |     }, {
 32 |       capabilities: {
 33 |         tools: {}
 34 |       }
 35 |     });
 36 |     
 37 |     this.mcpServer = new N8NDocumentationMCPServer();
 38 |     this.setupHandlers();
 39 |   }
 40 | 
 41 |   private setupHandlers() {
 42 |     // Initialize handler
 43 |     this.server.setRequestHandler(InitializeRequestSchema, async () => {
 44 |       return {
 45 |         protocolVersion: '2024-11-05',
 46 |         capabilities: {
 47 |           tools: {}
 48 |         },
 49 |         serverInfo: {
 50 |           name: 'n8n-documentation-mcp',
 51 |           version: '1.0.0'
 52 |         }
 53 |       };
 54 |     });
 55 | 
 56 |     // List tools handler
 57 |     this.server.setRequestHandler(ListToolsRequestSchema, async () => {
 58 |       // Import the tools directly from the tools module
 59 |       const { n8nDocumentationToolsFinal } = await import('../../../src/mcp/tools');
 60 |       const { n8nManagementTools } = await import('../../../src/mcp/tools-n8n-manager');
 61 |       const { isN8nApiConfigured } = await import('../../../src/config/n8n-api');
 62 |       
 63 |       // Combine documentation tools with management tools if API is configured
 64 |       const tools = [...n8nDocumentationToolsFinal];
 65 |       if (isN8nApiConfigured()) {
 66 |         tools.push(...n8nManagementTools);
 67 |       }
 68 |       
 69 |       return { tools };
 70 |     });
 71 | 
 72 |     // Call tool handler
 73 |     this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
 74 |       try {
 75 |         // The mcpServer.executeTool returns raw data, we need to wrap it in the MCP response format
 76 |         const result = await this.mcpServer.executeTool(request.params.name, request.params.arguments || {});
 77 |         
 78 |         return {
 79 |           content: [
 80 |             {
 81 |               type: 'text' as const,
 82 |               text: typeof result === 'string' ? result : JSON.stringify(result, null, 2)
 83 |             }
 84 |           ]
 85 |         };
 86 |       } catch (error: any) {
 87 |         // If it's already an MCP error, throw it as is
 88 |         if (error.code && error.message) {
 89 |           throw error;
 90 |         }
 91 |         // Otherwise, wrap it in an MCP error
 92 |         throw new McpError(
 93 |           ErrorCode.InternalError,
 94 |           error.message || 'Unknown error'
 95 |         );
 96 |       }
 97 |     });
 98 |   }
 99 | 
100 |   async initialize(): Promise<void> {
101 |     // Copy production database to test location for realistic testing
102 |     try {
103 |       const fs = await import('fs');
104 |       const path = await import('path');
105 |       const prodDbPath = path.join(process.cwd(), 'data', 'nodes.db');
106 |       
107 |       if (await fs.promises.access(prodDbPath).then(() => true).catch(() => false)) {
108 |         await fs.promises.copyFile(prodDbPath, this.testDbPath);
109 |       }
110 |     } catch (error) {
111 |       // Ignore copy errors, database will be created fresh
112 |     }
113 |     
114 |     // The MCP server initializes its database lazily
115 |     // We can trigger initialization by calling executeTool
116 |     try {
117 |       await this.mcpServer.executeTool('get_database_statistics', {});
118 |     } catch (error) {
119 |       // Ignore errors, we just want to trigger initialization
120 |     }
121 |   }
122 | 
123 |   async connectToTransport(transport: Transport): Promise<void> {
124 |     // Ensure transport has required properties before connecting
125 |     if (!transport || typeof transport !== 'object') {
126 |       throw new Error('Invalid transport provided');
127 |     }
128 |     
129 |     // Set up any missing transport handlers to prevent "Cannot set properties of undefined" errors
130 |     if (transport && typeof transport === 'object') {
131 |       const transportAny = transport as any;
132 |       if (transportAny.serverTransport && !transportAny.serverTransport.onclose) {
133 |         transportAny.serverTransport.onclose = () => {};
134 |       }
135 |     }
136 |     
137 |     // Track this transport for cleanup
138 |     this.transports.add(transport);
139 |     
140 |     const connection = await this.server.connect(transport);
141 |     this.connections.add(connection);
142 |   }
143 | 
144 |   async close(): Promise<void> {
145 |     // Use a timeout to prevent hanging during cleanup
146 |     const closeTimeout = new Promise<void>((resolve) => {
147 |       setTimeout(() => {
148 |         console.warn('TestableN8NMCPServer close timeout - forcing cleanup');
149 |         resolve();
150 |       }, 3000);
151 |     });
152 | 
153 |     const performClose = async () => {
154 |       // Close all connections first with timeout protection
155 |       const connectionPromises = Array.from(this.connections).map(async (connection) => {
156 |         const connTimeout = new Promise<void>((resolve) => setTimeout(resolve, 500));
157 |         
158 |         try {
159 |           if (connection && typeof connection.close === 'function') {
160 |             await Promise.race([connection.close(), connTimeout]);
161 |           }
162 |         } catch (error) {
163 |           // Ignore errors during connection cleanup
164 |         }
165 |       });
166 |       
167 |       await Promise.allSettled(connectionPromises);
168 |       this.connections.clear();
169 |       
170 |       // Close all tracked transports with timeout protection
171 |       const transportPromises: Promise<void>[] = [];
172 |       
173 |       for (const transport of this.transports) {
174 |         const transportTimeout = new Promise<void>((resolve) => setTimeout(resolve, 500));
175 |         
176 |         try {
177 |           // Force close all transports
178 |           const transportAny = transport as any;
179 |           
180 |           // Try different close methods
181 |           if (transportAny.close && typeof transportAny.close === 'function') {
182 |             transportPromises.push(
183 |               Promise.race([transportAny.close(), transportTimeout])
184 |             );
185 |           }
186 |           if (transportAny.serverTransport?.close) {
187 |             transportPromises.push(
188 |               Promise.race([transportAny.serverTransport.close(), transportTimeout])
189 |             );
190 |           }
191 |           if (transportAny.clientTransport?.close) {
192 |             transportPromises.push(
193 |               Promise.race([transportAny.clientTransport.close(), transportTimeout])
194 |             );
195 |           }
196 |         } catch (error) {
197 |           // Ignore errors during transport cleanup
198 |         }
199 |       }
200 |       
201 |       // Wait for all transports to close with timeout
202 |       await Promise.allSettled(transportPromises);
203 |       
204 |       // Clear the transports set
205 |       this.transports.clear();
206 |       
207 |       // Don't shut down the shared MCP server instance
208 |     };
209 | 
210 |     // Race between actual close and timeout
211 |     await Promise.race([performClose(), closeTimeout]);
212 |     
213 |     // Clean up test database
214 |     if (this.testDbPath) {
215 |       try {
216 |         const fs = await import('fs');
217 |         await fs.promises.unlink(this.testDbPath).catch(() => {});
218 |         await fs.promises.unlink(`${this.testDbPath}-shm`).catch(() => {});
219 |         await fs.promises.unlink(`${this.testDbPath}-wal`).catch(() => {});
220 |       } catch (error) {
221 |         // Ignore cleanup errors
222 |       }
223 |     }
224 |   }
225 |   
226 |   static async shutdownShared(): Promise<void> {
227 |     if (sharedMcpServer) {
228 |       await sharedMcpServer.shutdown();
229 |       sharedMcpServer = null;
230 |     }
231 |   }
232 | }
```

--------------------------------------------------------------------------------
/scripts/test-error-output-validation.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env npx tsx
  2 | 
  3 | /**
  4 |  * Test script for error output validation improvements
  5 |  * Tests both incorrect and correct error output configurations
  6 |  */
  7 | 
  8 | import { WorkflowValidator } from '../dist/services/workflow-validator.js';
  9 | import { NodeRepository } from '../dist/database/node-repository.js';
 10 | import { EnhancedConfigValidator } from '../dist/services/enhanced-config-validator.js';
 11 | import { DatabaseAdapter } from '../dist/database/database-adapter.js';
 12 | import { Logger } from '../dist/utils/logger.js';
 13 | import path from 'path';
 14 | import { fileURLToPath } from 'url';
 15 | 
 16 | const __filename = fileURLToPath(import.meta.url);
 17 | const __dirname = path.dirname(__filename);
 18 | 
 19 | const logger = new Logger({ prefix: '[TestErrorValidation]' });
 20 | 
 21 | async function runTests() {
 22 |   // Initialize database
 23 |   const dbPath = path.join(__dirname, '..', 'data', 'n8n-nodes.db');
 24 |   const adapter = new DatabaseAdapter();
 25 |   adapter.initialize({
 26 |     type: 'better-sqlite3',
 27 |     filename: dbPath
 28 |   });
 29 |   const db = adapter.getDatabase();
 30 | 
 31 |   const nodeRepository = new NodeRepository(db);
 32 |   const validator = new WorkflowValidator(nodeRepository, EnhancedConfigValidator);
 33 | 
 34 |   console.log('\n🧪 Testing Error Output Validation Improvements\n');
 35 |   console.log('=' .repeat(60));
 36 | 
 37 |   // Test 1: Incorrect configuration - multiple nodes in same array
 38 |   console.log('\n📝 Test 1: INCORRECT - Multiple nodes in main[0]');
 39 |   console.log('-'.repeat(40));
 40 | 
 41 |   const incorrectWorkflow = {
 42 |     nodes: [
 43 |       {
 44 |         id: '132ef0dc-87af-41de-a95d-cabe3a0a5342',
 45 |         name: 'Validate Input',
 46 |         type: 'n8n-nodes-base.set',
 47 |         typeVersion: 3.4,
 48 |         position: [-400, 64] as [number, number],
 49 |         parameters: {}
 50 |       },
 51 |       {
 52 |         id: '5dedf217-63f9-409f-b34e-7780b22e199a',
 53 |         name: 'Filter URLs',
 54 |         type: 'n8n-nodes-base.filter',
 55 |         typeVersion: 2.2,
 56 |         position: [-176, 64] as [number, number],
 57 |         parameters: {}
 58 |       },
 59 |       {
 60 |         id: '9d5407cc-ca5a-4966-b4b7-0e5dfbf54ad3',
 61 |         name: 'Error Response1',
 62 |         type: 'n8n-nodes-base.respondToWebhook',
 63 |         typeVersion: 1.5,
 64 |         position: [-160, 240] as [number, number],
 65 |         parameters: {}
 66 |       }
 67 |     ],
 68 |     connections: {
 69 |       'Validate Input': {
 70 |         main: [
 71 |           [
 72 |             { node: 'Filter URLs', type: 'main', index: 0 },
 73 |             { node: 'Error Response1', type: 'main', index: 0 }  // WRONG!
 74 |           ]
 75 |         ]
 76 |       }
 77 |     }
 78 |   };
 79 | 
 80 |   const result1 = await validator.validateWorkflow(incorrectWorkflow);
 81 | 
 82 |   if (result1.errors.length > 0) {
 83 |     console.log('❌ ERROR DETECTED (as expected):');
 84 |     const errorMessage = result1.errors.find(e =>
 85 |       e.message.includes('Incorrect error output configuration')
 86 |     );
 87 |     if (errorMessage) {
 88 |       console.log('\n' + errorMessage.message);
 89 |     }
 90 |   } else {
 91 |     console.log('✅ No errors found (but should have detected the issue!)');
 92 |   }
 93 | 
 94 |   // Test 2: Correct configuration - separate arrays
 95 |   console.log('\n📝 Test 2: CORRECT - Separate main[0] and main[1]');
 96 |   console.log('-'.repeat(40));
 97 | 
 98 |   const correctWorkflow = {
 99 |     nodes: [
100 |       {
101 |         id: '132ef0dc-87af-41de-a95d-cabe3a0a5342',
102 |         name: 'Validate Input',
103 |         type: 'n8n-nodes-base.set',
104 |         typeVersion: 3.4,
105 |         position: [-400, 64] as [number, number],
106 |         parameters: {},
107 |         onError: 'continueErrorOutput' as const
108 |       },
109 |       {
110 |         id: '5dedf217-63f9-409f-b34e-7780b22e199a',
111 |         name: 'Filter URLs',
112 |         type: 'n8n-nodes-base.filter',
113 |         typeVersion: 2.2,
114 |         position: [-176, 64] as [number, number],
115 |         parameters: {}
116 |       },
117 |       {
118 |         id: '9d5407cc-ca5a-4966-b4b7-0e5dfbf54ad3',
119 |         name: 'Error Response1',
120 |         type: 'n8n-nodes-base.respondToWebhook',
121 |         typeVersion: 1.5,
122 |         position: [-160, 240] as [number, number],
123 |         parameters: {}
124 |       }
125 |     ],
126 |     connections: {
127 |       'Validate Input': {
128 |         main: [
129 |           [
130 |             { node: 'Filter URLs', type: 'main', index: 0 }
131 |           ],
132 |           [
133 |             { node: 'Error Response1', type: 'main', index: 0 }  // CORRECT!
134 |           ]
135 |         ]
136 |       }
137 |     }
138 |   };
139 | 
140 |   const result2 = await validator.validateWorkflow(correctWorkflow);
141 | 
142 |   const hasIncorrectError = result2.errors.some(e =>
143 |     e.message.includes('Incorrect error output configuration')
144 |   );
145 | 
146 |   if (!hasIncorrectError) {
147 |     console.log('✅ No error output configuration issues (correct!)');
148 |   } else {
149 |     console.log('❌ Unexpected error found');
150 |   }
151 | 
152 |   // Test 3: onError without error connections
153 |   console.log('\n📝 Test 3: onError without error connections');
154 |   console.log('-'.repeat(40));
155 | 
156 |   const mismatchWorkflow = {
157 |     nodes: [
158 |       {
159 |         id: '1',
160 |         name: 'HTTP Request',
161 |         type: 'n8n-nodes-base.httpRequest',
162 |         typeVersion: 4,
163 |         position: [100, 100] as [number, number],
164 |         parameters: {},
165 |         onError: 'continueErrorOutput' as const
166 |       },
167 |       {
168 |         id: '2',
169 |         name: 'Process Data',
170 |         type: 'n8n-nodes-base.set',
171 |         typeVersion: 2,
172 |         position: [300, 100] as [number, number],
173 |         parameters: {}
174 |       }
175 |     ],
176 |     connections: {
177 |       'HTTP Request': {
178 |         main: [
179 |           [
180 |             { node: 'Process Data', type: 'main', index: 0 }
181 |           ]
182 |           // No main[1] for error output
183 |         ]
184 |       }
185 |     }
186 |   };
187 | 
188 |   const result3 = await validator.validateWorkflow(mismatchWorkflow);
189 | 
190 |   const mismatchError = result3.errors.find(e =>
191 |     e.message.includes("has onError: 'continueErrorOutput' but no error output connections")
192 |   );
193 | 
194 |   if (mismatchError) {
195 |     console.log('❌ ERROR DETECTED (as expected):');
196 |     console.log(`Node: ${mismatchError.nodeName}`);
197 |     console.log(`Message: ${mismatchError.message}`);
198 |   } else {
199 |     console.log('✅ No mismatch detected (but should have!)');
200 |   }
201 | 
202 |   // Test 4: Error connections without onError
203 |   console.log('\n📝 Test 4: Error connections without onError property');
204 |   console.log('-'.repeat(40));
205 | 
206 |   const missingOnErrorWorkflow = {
207 |     nodes: [
208 |       {
209 |         id: '1',
210 |         name: 'HTTP Request',
211 |         type: 'n8n-nodes-base.httpRequest',
212 |         typeVersion: 4,
213 |         position: [100, 100] as [number, number],
214 |         parameters: {}
215 |         // Missing onError property
216 |       },
217 |       {
218 |         id: '2',
219 |         name: 'Process Data',
220 |         type: 'n8n-nodes-base.set',
221 |         position: [300, 100] as [number, number],
222 |         parameters: {}
223 |       },
224 |       {
225 |         id: '3',
226 |         name: 'Error Handler',
227 |         type: 'n8n-nodes-base.set',
228 |         position: [300, 300] as [number, number],
229 |         parameters: {}
230 |       }
231 |     ],
232 |     connections: {
233 |       'HTTP Request': {
234 |         main: [
235 |           [
236 |             { node: 'Process Data', type: 'main', index: 0 }
237 |           ],
238 |           [
239 |             { node: 'Error Handler', type: 'main', index: 0 }
240 |           ]
241 |         ]
242 |       }
243 |     }
244 |   };
245 | 
246 |   const result4 = await validator.validateWorkflow(missingOnErrorWorkflow);
247 | 
248 |   const missingOnErrorWarning = result4.warnings.find(w =>
249 |     w.message.includes('error output connections in main[1] but missing onError')
250 |   );
251 | 
252 |   if (missingOnErrorWarning) {
253 |     console.log('⚠️  WARNING DETECTED (as expected):');
254 |     console.log(`Node: ${missingOnErrorWarning.nodeName}`);
255 |     console.log(`Message: ${missingOnErrorWarning.message}`);
256 |   } else {
257 |     console.log('✅ No warning (but should have warned!)');
258 |   }
259 | 
260 |   console.log('\n' + '='.repeat(60));
261 |   console.log('\n📊 Summary:');
262 |   console.log('- Error output validation is working correctly');
263 |   console.log('- Detects incorrect configurations (multiple nodes in main[0])');
264 |   console.log('- Validates onError property matches connections');
265 |   console.log('- Provides clear error messages with fix examples');
266 | 
267 |   // Close database
268 |   adapter.close();
269 | }
270 | 
271 | runTests().catch(error => {
272 |   console.error('Test failed:', error);
273 |   process.exit(1);
274 | });
```

--------------------------------------------------------------------------------
/tests/integration/n8n-api/utils/factories.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Test Data Factories
  3 |  *
  4 |  * Provides factory functions for generating test data dynamically.
  5 |  * Useful for creating variations of workflows, nodes, and parameters.
  6 |  */
  7 | 
  8 | import { Workflow, WorkflowNode } from '../../../../src/types/n8n-api';
  9 | import { createTestWorkflowName } from './test-context';
 10 | 
 11 | /**
 12 |  * Create a webhook node with custom parameters
 13 |  *
 14 |  * @param options - Node options
 15 |  * @returns WorkflowNode
 16 |  */
 17 | export function createWebhookNode(options: {
 18 |   id?: string;
 19 |   name?: string;
 20 |   httpMethod?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
 21 |   path?: string;
 22 |   position?: [number, number];
 23 |   responseMode?: 'onReceived' | 'lastNode';
 24 | }): WorkflowNode {
 25 |   return {
 26 |     id: options.id || `webhook-${Date.now()}`,
 27 |     name: options.name || 'Webhook',
 28 |     type: 'n8n-nodes-base.webhook',
 29 |     typeVersion: 2,
 30 |     position: options.position || [250, 300],
 31 |     parameters: {
 32 |       httpMethod: options.httpMethod || 'GET',
 33 |       path: options.path || `test-${Date.now()}`,
 34 |       responseMode: options.responseMode || 'lastNode'
 35 |     }
 36 |   };
 37 | }
 38 | 
 39 | /**
 40 |  * Create an HTTP Request node with custom parameters
 41 |  *
 42 |  * @param options - Node options
 43 |  * @returns WorkflowNode
 44 |  */
 45 | export function createHttpRequestNode(options: {
 46 |   id?: string;
 47 |   name?: string;
 48 |   url?: string;
 49 |   method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
 50 |   position?: [number, number];
 51 |   authentication?: string;
 52 | }): WorkflowNode {
 53 |   return {
 54 |     id: options.id || `http-${Date.now()}`,
 55 |     name: options.name || 'HTTP Request',
 56 |     type: 'n8n-nodes-base.httpRequest',
 57 |     typeVersion: 4.2,
 58 |     position: options.position || [450, 300],
 59 |     parameters: {
 60 |       url: options.url || 'https://httpbin.org/get',
 61 |       method: options.method || 'GET',
 62 |       authentication: options.authentication || 'none'
 63 |     }
 64 |   };
 65 | }
 66 | 
 67 | /**
 68 |  * Create a Set node with custom assignments
 69 |  *
 70 |  * @param options - Node options
 71 |  * @returns WorkflowNode
 72 |  */
 73 | export function createSetNode(options: {
 74 |   id?: string;
 75 |   name?: string;
 76 |   position?: [number, number];
 77 |   assignments?: Array<{
 78 |     name: string;
 79 |     value: any;
 80 |     type?: 'string' | 'number' | 'boolean' | 'object' | 'array';
 81 |   }>;
 82 | }): WorkflowNode {
 83 |   const assignments = options.assignments || [
 84 |     { name: 'key', value: 'value', type: 'string' as const }
 85 |   ];
 86 | 
 87 |   return {
 88 |     id: options.id || `set-${Date.now()}`,
 89 |     name: options.name || 'Set',
 90 |     type: 'n8n-nodes-base.set',
 91 |     typeVersion: 3.4,
 92 |     position: options.position || [450, 300],
 93 |     parameters: {
 94 |       assignments: {
 95 |         assignments: assignments.map((a, idx) => ({
 96 |           id: `assign-${idx}`,
 97 |           name: a.name,
 98 |           value: a.value,
 99 |           type: a.type || 'string'
100 |         }))
101 |       },
102 |       options: {}
103 |     }
104 |   };
105 | }
106 | 
107 | /**
108 |  * Create a Manual Trigger node
109 |  *
110 |  * @param options - Node options
111 |  * @returns WorkflowNode
112 |  */
113 | export function createManualTriggerNode(options: {
114 |   id?: string;
115 |   name?: string;
116 |   position?: [number, number];
117 | } = {}): WorkflowNode {
118 |   return {
119 |     id: options.id || `manual-${Date.now()}`,
120 |     name: options.name || 'When clicking "Test workflow"',
121 |     type: 'n8n-nodes-base.manualTrigger',
122 |     typeVersion: 1,
123 |     position: options.position || [250, 300],
124 |     parameters: {}
125 |   };
126 | }
127 | 
128 | /**
129 |  * Create a simple connection between two nodes
130 |  *
131 |  * @param from - Source node name
132 |  * @param to - Target node name
133 |  * @param options - Connection options
134 |  * @returns Connection object
135 |  */
136 | export function createConnection(
137 |   from: string,
138 |   to: string,
139 |   options: {
140 |     sourceOutput?: string;
141 |     targetInput?: string;
142 |     sourceIndex?: number;
143 |     targetIndex?: number;
144 |   } = {}
145 | ): Record<string, any> {
146 |   const sourceOutput = options.sourceOutput || 'main';
147 |   const targetInput = options.targetInput || 'main';
148 |   const sourceIndex = options.sourceIndex || 0;
149 |   const targetIndex = options.targetIndex || 0;
150 | 
151 |   return {
152 |     [from]: {
153 |       [sourceOutput]: [
154 |         [
155 |           {
156 |             node: to,
157 |             type: targetInput,
158 |             index: targetIndex
159 |           }
160 |         ]
161 |       ]
162 |     }
163 |   };
164 | }
165 | 
166 | /**
167 |  * Create a workflow from nodes with automatic connections
168 |  *
169 |  * Connects nodes in sequence: node1 -> node2 -> node3, etc.
170 |  *
171 |  * @param name - Workflow name
172 |  * @param nodes - Array of nodes
173 |  * @returns Partial workflow
174 |  */
175 | export function createSequentialWorkflow(
176 |   name: string,
177 |   nodes: WorkflowNode[]
178 | ): Partial<Workflow> {
179 |   const connections: Record<string, any> = {};
180 | 
181 |   // Create connections between sequential nodes
182 |   for (let i = 0; i < nodes.length - 1; i++) {
183 |     const currentNode = nodes[i];
184 |     const nextNode = nodes[i + 1];
185 | 
186 |     connections[currentNode.name] = {
187 |       main: [[{ node: nextNode.name, type: 'main', index: 0 }]]
188 |     };
189 |   }
190 | 
191 |   return {
192 |     name: createTestWorkflowName(name),
193 |     nodes,
194 |     connections,
195 |     settings: {
196 |       executionOrder: 'v1'
197 |     }
198 |   };
199 | }
200 | 
201 | /**
202 |  * Create a workflow with parallel branches
203 |  *
204 |  * Creates a workflow with one trigger node that splits into multiple
205 |  * parallel execution paths.
206 |  *
207 |  * @param name - Workflow name
208 |  * @param trigger - Trigger node
209 |  * @param branches - Array of branch nodes
210 |  * @returns Partial workflow
211 |  */
212 | export function createParallelWorkflow(
213 |   name: string,
214 |   trigger: WorkflowNode,
215 |   branches: WorkflowNode[]
216 | ): Partial<Workflow> {
217 |   const connections: Record<string, any> = {
218 |     [trigger.name]: {
219 |       main: [branches.map(node => ({ node: node.name, type: 'main', index: 0 }))]
220 |     }
221 |   };
222 | 
223 |   return {
224 |     name: createTestWorkflowName(name),
225 |     nodes: [trigger, ...branches],
226 |     connections,
227 |     settings: {
228 |       executionOrder: 'v1'
229 |     }
230 |   };
231 | }
232 | 
233 | /**
234 |  * Generate a random string for test data
235 |  *
236 |  * @param length - String length (default: 8)
237 |  * @returns Random string
238 |  */
239 | export function randomString(length: number = 8): string {
240 |   const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
241 |   let result = '';
242 |   for (let i = 0; i < length; i++) {
243 |     result += chars.charAt(Math.floor(Math.random() * chars.length));
244 |   }
245 |   return result;
246 | }
247 | 
248 | /**
249 |  * Generate a unique ID for testing
250 |  *
251 |  * @param prefix - Optional prefix
252 |  * @returns Unique ID
253 |  */
254 | export function uniqueId(prefix: string = 'test'): string {
255 |   return `${prefix}-${Date.now()}-${randomString(4)}`;
256 | }
257 | 
258 | /**
259 |  * Create a workflow with error handling
260 |  *
261 |  * @param name - Workflow name
262 |  * @param mainNode - Main processing node
263 |  * @param errorNode - Error handling node
264 |  * @returns Partial workflow with error handling configured
265 |  */
266 | export function createErrorHandlingWorkflow(
267 |   name: string,
268 |   mainNode: WorkflowNode,
269 |   errorNode: WorkflowNode
270 | ): Partial<Workflow> {
271 |   const trigger = createWebhookNode({
272 |     name: 'Trigger',
273 |     position: [250, 300]
274 |   });
275 | 
276 |   // Configure main node for error handling
277 |   const mainNodeWithError = {
278 |     ...mainNode,
279 |     continueOnFail: true,
280 |     onError: 'continueErrorOutput' as const
281 |   };
282 | 
283 |   const connections: Record<string, any> = {
284 |     [trigger.name]: {
285 |       main: [[{ node: mainNode.name, type: 'main', index: 0 }]]
286 |     },
287 |     [mainNode.name]: {
288 |       error: [[{ node: errorNode.name, type: 'main', index: 0 }]]
289 |     }
290 |   };
291 | 
292 |   return {
293 |     name: createTestWorkflowName(name),
294 |     nodes: [trigger, mainNodeWithError, errorNode],
295 |     connections,
296 |     settings: {
297 |       executionOrder: 'v1'
298 |     }
299 |   };
300 | }
301 | 
302 | /**
303 |  * Create test workflow tags
304 |  *
305 |  * @param additional - Additional tags to include
306 |  * @returns Array of tags for test workflows
307 |  */
308 | export function createTestTags(additional: string[] = []): string[] {
309 |   return ['mcp-integration-test', ...additional];
310 | }
311 | 
312 | /**
313 |  * Create workflow settings with common test configurations
314 |  *
315 |  * @param overrides - Settings to override
316 |  * @returns Workflow settings object
317 |  */
318 | export function createWorkflowSettings(overrides: Record<string, any> = {}): Record<string, any> {
319 |   return {
320 |     executionOrder: 'v1',
321 |     saveDataErrorExecution: 'all',
322 |     saveDataSuccessExecution: 'all',
323 |     saveManualExecutions: true,
324 |     ...overrides
325 |   };
326 | }
327 | 
```

--------------------------------------------------------------------------------
/tests/http-server-auth.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { readFileSync, writeFileSync, mkdirSync, rmSync } from 'fs';
  2 | import { join } from 'path';
  3 | import { tmpdir } from 'os';
  4 | import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
  5 | import type { MockedFunction } from 'vitest';
  6 | 
  7 | // Import the actual functions we'll be testing
  8 | import { loadAuthToken, startFixedHTTPServer } from '../src/http-server';
  9 | 
 10 | // Mock dependencies
 11 | vi.mock('../src/utils/logger', () => ({
 12 |   logger: {
 13 |     info: vi.fn(),
 14 |     error: vi.fn(),
 15 |     warn: vi.fn(),
 16 |     debug: vi.fn()
 17 |   },
 18 |   Logger: vi.fn().mockImplementation(() => ({
 19 |     info: vi.fn(),
 20 |     error: vi.fn(),
 21 |     warn: vi.fn(),
 22 |     debug: vi.fn()
 23 |   })),
 24 |   LogLevel: {
 25 |     ERROR: 0,
 26 |     WARN: 1,
 27 |     INFO: 2,
 28 |     DEBUG: 3
 29 |   }
 30 | }));
 31 | 
 32 | vi.mock('dotenv');
 33 | 
 34 | // Mock other dependencies to prevent side effects
 35 | vi.mock('../src/mcp/server', () => ({
 36 |   N8NDocumentationMCPServer: vi.fn().mockImplementation(() => ({
 37 |     executeTool: vi.fn()
 38 |   }))
 39 | }));
 40 | 
 41 | vi.mock('../src/mcp/tools', () => ({
 42 |   n8nDocumentationToolsFinal: []
 43 | }));
 44 | 
 45 | vi.mock('../src/mcp/tools-n8n-manager', () => ({
 46 |   n8nManagementTools: []
 47 | }));
 48 | 
 49 | vi.mock('../src/utils/version', () => ({
 50 |   PROJECT_VERSION: '2.7.4'
 51 | }));
 52 | 
 53 | vi.mock('../src/config/n8n-api', () => ({
 54 |   isN8nApiConfigured: vi.fn().mockReturnValue(false)
 55 | }));
 56 | 
 57 | vi.mock('../src/utils/url-detector', () => ({
 58 |   getStartupBaseUrl: vi.fn().mockReturnValue('http://localhost:3000'),
 59 |   formatEndpointUrls: vi.fn().mockReturnValue({
 60 |     health: 'http://localhost:3000/health',
 61 |     mcp: 'http://localhost:3000/mcp'
 62 |   }),
 63 |   detectBaseUrl: vi.fn().mockReturnValue('http://localhost:3000')
 64 | }));
 65 | 
 66 | // Create mock server instance
 67 | const mockServer = {
 68 |   on: vi.fn(),
 69 |   close: vi.fn((callback) => callback())
 70 | };
 71 | 
 72 | // Mock Express to prevent server from starting
 73 | const mockExpressApp = {
 74 |   use: vi.fn(),
 75 |   get: vi.fn(),
 76 |   post: vi.fn(),
 77 |   listen: vi.fn((port: any, host: any, callback: any) => {
 78 |     // Call the callback immediately to simulate server start
 79 |     if (callback) callback();
 80 |     return mockServer;
 81 |   }),
 82 |   set: vi.fn()
 83 | };
 84 | 
 85 | vi.mock('express', () => {
 86 |   const express: any = vi.fn(() => mockExpressApp);
 87 |   express.json = vi.fn();
 88 |   express.urlencoded = vi.fn();
 89 |   express.static = vi.fn();
 90 |   express.Request = {};
 91 |   express.Response = {};
 92 |   express.NextFunction = {};
 93 |   return { default: express };
 94 | });
 95 | 
 96 | describe('HTTP Server Authentication', () => {
 97 |   const originalEnv = process.env;
 98 |   let tempDir: string;
 99 |   let authTokenFile: string;
100 | 
101 |   beforeEach(() => {
102 |     // Reset modules and environment
103 |     vi.clearAllMocks();
104 |     vi.resetModules();
105 |     process.env = { ...originalEnv };
106 |     
107 |     // Create temporary directory for test files
108 |     tempDir = join(tmpdir(), `http-server-auth-test-${Date.now()}`);
109 |     mkdirSync(tempDir, { recursive: true });
110 |     authTokenFile = join(tempDir, 'auth-token');
111 |   });
112 | 
113 |   afterEach(() => {
114 |     // Restore original environment
115 |     process.env = originalEnv;
116 |     
117 |     // Clean up temporary directory
118 |     try {
119 |       rmSync(tempDir, { recursive: true, force: true });
120 |     } catch (error) {
121 |       // Ignore cleanup errors
122 |     }
123 |   });
124 | 
125 |   describe('loadAuthToken', () => {
126 |     it('should load token when AUTH_TOKEN environment variable is set', () => {
127 |       process.env.AUTH_TOKEN = 'test-token-from-env';
128 |       delete process.env.AUTH_TOKEN_FILE;
129 | 
130 |       const token = loadAuthToken();
131 |       expect(token).toBe('test-token-from-env');
132 |     });
133 | 
134 |     it('should load token from file when only AUTH_TOKEN_FILE is set', () => {
135 |       delete process.env.AUTH_TOKEN;
136 |       process.env.AUTH_TOKEN_FILE = authTokenFile;
137 |       
138 |       // Write test token to file
139 |       writeFileSync(authTokenFile, 'test-token-from-file\n');
140 | 
141 |       const token = loadAuthToken();
142 |       expect(token).toBe('test-token-from-file');
143 |     });
144 | 
145 |     it('should trim whitespace when reading token from file', () => {
146 |       delete process.env.AUTH_TOKEN;
147 |       process.env.AUTH_TOKEN_FILE = authTokenFile;
148 |       
149 |       // Write token with whitespace
150 |       writeFileSync(authTokenFile, '  test-token-with-spaces  \n\n');
151 | 
152 |       const token = loadAuthToken();
153 |       expect(token).toBe('test-token-with-spaces');
154 |     });
155 | 
156 |     it('should prefer AUTH_TOKEN when both variables are set', () => {
157 |       process.env.AUTH_TOKEN = 'env-token';
158 |       process.env.AUTH_TOKEN_FILE = authTokenFile;
159 |       writeFileSync(authTokenFile, 'file-token');
160 | 
161 |       const token = loadAuthToken();
162 |       expect(token).toBe('env-token');
163 |     });
164 | 
165 |     it('should return null when AUTH_TOKEN_FILE points to non-existent file', async () => {
166 |       delete process.env.AUTH_TOKEN;
167 |       process.env.AUTH_TOKEN_FILE = join(tempDir, 'non-existent-file');
168 | 
169 |       // Import logger to check calls
170 |       const { logger } = await import('../src/utils/logger');
171 |       
172 |       // Clear any previous mock calls
173 |       vi.clearAllMocks();
174 |       
175 |       const token = loadAuthToken();
176 |       expect(token).toBeNull();
177 |       expect(logger.error).toHaveBeenCalled();
178 |       const errorCall = (logger.error as MockedFunction<any>).mock.calls[0];
179 |       expect(errorCall[0]).toContain('Failed to read AUTH_TOKEN_FILE');
180 |       // Check that the second argument exists and is truthy (the error object)
181 |       expect(errorCall[1]).toBeTruthy();
182 |     });
183 | 
184 |     it('should return null when no auth variables are set', () => {
185 |       delete process.env.AUTH_TOKEN;
186 |       delete process.env.AUTH_TOKEN_FILE;
187 | 
188 |       const token = loadAuthToken();
189 |       expect(token).toBeNull();
190 |     });
191 |   });
192 | 
193 |   describe('validateEnvironment', () => {
194 |     it('should exit process when no auth token is available', async () => {
195 |       delete process.env.AUTH_TOKEN;
196 |       delete process.env.AUTH_TOKEN_FILE;
197 | 
198 |       const mockExit = vi.spyOn(process, 'exit').mockImplementation((code?: string | number | null | undefined) => {
199 |         throw new Error('Process exited');
200 |       });
201 | 
202 |       // validateEnvironment is called when starting the server
203 |       await expect(async () => {
204 |         await startFixedHTTPServer();
205 |       }).rejects.toThrow('Process exited');
206 | 
207 |       expect(mockExit).toHaveBeenCalledWith(1);
208 |       mockExit.mockRestore();
209 |     });
210 | 
211 |     it('should warn when token length is less than 32 characters', async () => {
212 |       process.env.AUTH_TOKEN = 'short-token';
213 | 
214 |       // Import logger to check calls
215 |       const { logger } = await import('../src/utils/logger');
216 |       
217 |       // Clear any previous mock calls
218 |       vi.clearAllMocks();
219 |       
220 |       // Ensure the mock server is properly configured
221 |       mockExpressApp.listen.mockReturnValue(mockServer);
222 |       mockServer.on.mockReturnValue(undefined);
223 |       
224 |       // Start the server which will trigger validateEnvironment
225 |       await startFixedHTTPServer();
226 |       
227 |       expect(logger.warn).toHaveBeenCalledWith(
228 |         'AUTH_TOKEN should be at least 32 characters for security'
229 |       );
230 |     });
231 |   });
232 | 
233 |   describe('Integration test scenarios', () => {
234 |     it('should authenticate successfully when token is loaded from file', () => {
235 |       // This is more of an integration test placeholder
236 |       // In a real scenario, you'd start the server and make HTTP requests
237 |       
238 |       writeFileSync(authTokenFile, 'very-secure-token-with-more-than-32-characters');
239 |       process.env.AUTH_TOKEN_FILE = authTokenFile;
240 |       delete process.env.AUTH_TOKEN;
241 | 
242 |       const token = loadAuthToken();
243 |       expect(token).toBe('very-secure-token-with-more-than-32-characters');
244 |     });
245 | 
246 |     it('should load token when using Docker secrets pattern', () => {
247 |       // Docker secrets are typically mounted at /run/secrets/
248 |       const dockerSecretPath = join(tempDir, 'run', 'secrets', 'auth_token');
249 |       mkdirSync(join(tempDir, 'run', 'secrets'), { recursive: true });
250 |       writeFileSync(dockerSecretPath, 'docker-secret-token');
251 |       
252 |       process.env.AUTH_TOKEN_FILE = dockerSecretPath;
253 |       delete process.env.AUTH_TOKEN;
254 | 
255 |       const token = loadAuthToken();
256 |       expect(token).toBe('docker-secret-token');
257 |     });
258 |   });
259 | });
```

--------------------------------------------------------------------------------
/src/n8n/MCPNode.node.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 |   IExecuteFunctions,
  3 |   INodeExecutionData,
  4 |   INodeType,
  5 |   INodeTypeDescription,
  6 |   NodeOperationError,
  7 | } from 'n8n-workflow';
  8 | import { MCPClient } from '../utils/mcp-client';
  9 | import { N8NMCPBridge } from '../utils/bridge';
 10 | 
 11 | export class MCPNode implements INodeType {
 12 |   description: INodeTypeDescription = {
 13 |     displayName: 'MCP',
 14 |     name: 'mcp',
 15 |     icon: 'file:mcp.svg',
 16 |     group: ['transform'],
 17 |     version: 1,
 18 |     description: 'Interact with Model Context Protocol (MCP) servers',
 19 |     defaults: {
 20 |       name: 'MCP',
 21 |     },
 22 |     inputs: ['main'],
 23 |     outputs: ['main'],
 24 |     credentials: [
 25 |       {
 26 |         name: 'mcpApi',
 27 |         required: true,
 28 |       },
 29 |     ],
 30 |     properties: [
 31 |       {
 32 |         displayName: 'Operation',
 33 |         name: 'operation',
 34 |         type: 'options',
 35 |         noDataExpression: true,
 36 |         options: [
 37 |           {
 38 |             name: 'Call Tool',
 39 |             value: 'callTool',
 40 |             description: 'Execute an MCP tool',
 41 |           },
 42 |           {
 43 |             name: 'List Tools',
 44 |             value: 'listTools',
 45 |             description: 'List available MCP tools',
 46 |           },
 47 |           {
 48 |             name: 'Read Resource',
 49 |             value: 'readResource',
 50 |             description: 'Read an MCP resource',
 51 |           },
 52 |           {
 53 |             name: 'List Resources',
 54 |             value: 'listResources',
 55 |             description: 'List available MCP resources',
 56 |           },
 57 |           {
 58 |             name: 'Get Prompt',
 59 |             value: 'getPrompt',
 60 |             description: 'Get an MCP prompt',
 61 |           },
 62 |           {
 63 |             name: 'List Prompts',
 64 |             value: 'listPrompts',
 65 |             description: 'List available MCP prompts',
 66 |           },
 67 |         ],
 68 |         default: 'callTool',
 69 |       },
 70 |       // Tool-specific fields
 71 |       {
 72 |         displayName: 'Tool Name',
 73 |         name: 'toolName',
 74 |         type: 'string',
 75 |         required: true,
 76 |         displayOptions: {
 77 |           show: {
 78 |             operation: ['callTool'],
 79 |           },
 80 |         },
 81 |         default: '',
 82 |         description: 'Name of the MCP tool to execute',
 83 |       },
 84 |       {
 85 |         displayName: 'Tool Arguments',
 86 |         name: 'toolArguments',
 87 |         type: 'json',
 88 |         required: false,
 89 |         displayOptions: {
 90 |           show: {
 91 |             operation: ['callTool'],
 92 |           },
 93 |         },
 94 |         default: '{}',
 95 |         description: 'Arguments to pass to the MCP tool',
 96 |       },
 97 |       // Resource-specific fields
 98 |       {
 99 |         displayName: 'Resource URI',
100 |         name: 'resourceUri',
101 |         type: 'string',
102 |         required: true,
103 |         displayOptions: {
104 |           show: {
105 |             operation: ['readResource'],
106 |           },
107 |         },
108 |         default: '',
109 |         description: 'URI of the MCP resource to read',
110 |       },
111 |       // Prompt-specific fields
112 |       {
113 |         displayName: 'Prompt Name',
114 |         name: 'promptName',
115 |         type: 'string',
116 |         required: true,
117 |         displayOptions: {
118 |           show: {
119 |             operation: ['getPrompt'],
120 |           },
121 |         },
122 |         default: '',
123 |         description: 'Name of the MCP prompt to retrieve',
124 |       },
125 |       {
126 |         displayName: 'Prompt Arguments',
127 |         name: 'promptArguments',
128 |         type: 'json',
129 |         required: false,
130 |         displayOptions: {
131 |           show: {
132 |             operation: ['getPrompt'],
133 |           },
134 |         },
135 |         default: '{}',
136 |         description: 'Arguments to pass to the MCP prompt',
137 |       },
138 |     ],
139 |   };
140 | 
141 |   async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
142 |     const items = this.getInputData();
143 |     const returnData: INodeExecutionData[] = [];
144 |     const operation = this.getNodeParameter('operation', 0) as string;
145 | 
146 |     // Get credentials
147 |     const credentials = await this.getCredentials('mcpApi');
148 |     
149 |     for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
150 |       try {
151 |         let result: any;
152 | 
153 |         switch (operation) {
154 |           case 'callTool':
155 |             const toolName = this.getNodeParameter('toolName', itemIndex) as string;
156 |             const toolArgumentsJson = this.getNodeParameter('toolArguments', itemIndex) as string;
157 |             const toolArguments = JSON.parse(toolArgumentsJson);
158 |             
159 |             result = await (this as any).callMCPTool(credentials, toolName, toolArguments);
160 |             break;
161 | 
162 |           case 'listTools':
163 |             result = await (this as any).listMCPTools(credentials);
164 |             break;
165 | 
166 |           case 'readResource':
167 |             const resourceUri = this.getNodeParameter('resourceUri', itemIndex) as string;
168 |             result = await (this as any).readMCPResource(credentials, resourceUri);
169 |             break;
170 | 
171 |           case 'listResources':
172 |             result = await (this as any).listMCPResources(credentials);
173 |             break;
174 | 
175 |           case 'getPrompt':
176 |             const promptName = this.getNodeParameter('promptName', itemIndex) as string;
177 |             const promptArgumentsJson = this.getNodeParameter('promptArguments', itemIndex) as string;
178 |             const promptArguments = JSON.parse(promptArgumentsJson);
179 |             
180 |             result = await (this as any).getMCPPrompt(credentials, promptName, promptArguments);
181 |             break;
182 | 
183 |           case 'listPrompts':
184 |             result = await (this as any).listMCPPrompts(credentials);
185 |             break;
186 | 
187 |           default:
188 |             throw new NodeOperationError(this.getNode(), `Unknown operation: ${operation}`);
189 |         }
190 | 
191 |         returnData.push({
192 |           json: result,
193 |           pairedItem: itemIndex,
194 |         });
195 |       } catch (error) {
196 |         if (this.continueOnFail()) {
197 |           returnData.push({
198 |             json: {
199 |               error: error instanceof Error ? error.message : 'Unknown error',
200 |             },
201 |             pairedItem: itemIndex,
202 |           });
203 |           continue;
204 |         }
205 |         throw error;
206 |       }
207 |     }
208 | 
209 |     return [returnData];
210 |   }
211 | 
212 |   // MCP client methods
213 |   private async getMCPClient(credentials: any): Promise<MCPClient> {
214 |     const client = new MCPClient({
215 |       serverUrl: credentials.serverUrl,
216 |       authToken: credentials.authToken,
217 |       connectionType: credentials.connectionType || 'websocket',
218 |     });
219 |     await client.connect();
220 |     return client;
221 |   }
222 | 
223 |   private async callMCPTool(credentials: any, toolName: string, args: any): Promise<any> {
224 |     const client = await this.getMCPClient(credentials);
225 |     try {
226 |       const result = await client.callTool(toolName, args);
227 |       return N8NMCPBridge.mcpToN8NExecutionData(result).json;
228 |     } finally {
229 |       await client.disconnect();
230 |     }
231 |   }
232 | 
233 |   private async listMCPTools(credentials: any): Promise<any> {
234 |     const client = await this.getMCPClient(credentials);
235 |     try {
236 |       return await client.listTools();
237 |     } finally {
238 |       await client.disconnect();
239 |     }
240 |   }
241 | 
242 |   private async readMCPResource(credentials: any, uri: string): Promise<any> {
243 |     const client = await this.getMCPClient(credentials);
244 |     try {
245 |       const result = await client.readResource(uri);
246 |       return N8NMCPBridge.mcpToN8NExecutionData(result).json;
247 |     } finally {
248 |       await client.disconnect();
249 |     }
250 |   }
251 | 
252 |   private async listMCPResources(credentials: any): Promise<any> {
253 |     const client = await this.getMCPClient(credentials);
254 |     try {
255 |       return await client.listResources();
256 |     } finally {
257 |       await client.disconnect();
258 |     }
259 |   }
260 | 
261 |   private async getMCPPrompt(credentials: any, promptName: string, args: any): Promise<any> {
262 |     const client = await this.getMCPClient(credentials);
263 |     try {
264 |       const result = await client.getPrompt(promptName, args);
265 |       return N8NMCPBridge.mcpPromptArgsToN8N(result);
266 |     } finally {
267 |       await client.disconnect();
268 |     }
269 |   }
270 | 
271 |   private async listMCPPrompts(credentials: any): Promise<any> {
272 |     const client = await this.getMCPClient(credentials);
273 |     try {
274 |       return await client.listPrompts();
275 |     } finally {
276 |       await client.disconnect();
277 |     }
278 |   }
279 | }
```

--------------------------------------------------------------------------------
/tests/integration/n8n-api/utils/fixtures.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Workflow Fixtures for Integration Tests
  3 |  *
  4 |  * Provides reusable workflow templates for testing.
  5 |  * All fixtures use FULL node type format (n8n-nodes-base.*)
  6 |  * as required by the n8n API.
  7 |  */
  8 | 
  9 | import { Workflow, WorkflowNode } from '../../../../src/types/n8n-api';
 10 | 
 11 | /**
 12 |  * Simple webhook workflow with a single Webhook node
 13 |  *
 14 |  * Use this for basic workflow creation tests.
 15 |  */
 16 | export const SIMPLE_WEBHOOK_WORKFLOW: Partial<Workflow> = {
 17 |   nodes: [
 18 |     {
 19 |       id: 'webhook-1',
 20 |       name: 'Webhook',
 21 |       type: 'n8n-nodes-base.webhook',
 22 |       typeVersion: 2,
 23 |       position: [250, 300],
 24 |       parameters: {
 25 |         httpMethod: 'GET',
 26 |         path: 'test-webhook'
 27 |       }
 28 |     }
 29 |   ],
 30 |   connections: {},
 31 |   settings: {
 32 |     executionOrder: 'v1'
 33 |   }
 34 | };
 35 | 
 36 | /**
 37 |  * Simple HTTP request workflow
 38 |  *
 39 |  * Contains a Webhook trigger and an HTTP Request node.
 40 |  * Tests basic workflow connections.
 41 |  */
 42 | export const SIMPLE_HTTP_WORKFLOW: Partial<Workflow> = {
 43 |   nodes: [
 44 |     {
 45 |       id: 'webhook-1',
 46 |       name: 'Webhook',
 47 |       type: 'n8n-nodes-base.webhook',
 48 |       typeVersion: 2,
 49 |       position: [250, 300],
 50 |       parameters: {
 51 |         httpMethod: 'GET',
 52 |         path: 'trigger'
 53 |       }
 54 |     },
 55 |     {
 56 |       id: 'http-1',
 57 |       name: 'HTTP Request',
 58 |       type: 'n8n-nodes-base.httpRequest',
 59 |       typeVersion: 4.2,
 60 |       position: [450, 300],
 61 |       parameters: {
 62 |         url: 'https://httpbin.org/get',
 63 |         method: 'GET'
 64 |       }
 65 |     }
 66 |   ],
 67 |   connections: {
 68 |     Webhook: {
 69 |       main: [[{ node: 'HTTP Request', type: 'main', index: 0 }]]
 70 |     }
 71 |   },
 72 |   settings: {
 73 |     executionOrder: 'v1'
 74 |   }
 75 | };
 76 | 
 77 | /**
 78 |  * Multi-node workflow with branching
 79 |  *
 80 |  * Tests complex connections and multiple execution paths.
 81 |  */
 82 | export const MULTI_NODE_WORKFLOW: Partial<Workflow> = {
 83 |   nodes: [
 84 |     {
 85 |       id: 'webhook-1',
 86 |       name: 'Webhook',
 87 |       type: 'n8n-nodes-base.webhook',
 88 |       typeVersion: 2,
 89 |       position: [250, 300],
 90 |       parameters: {
 91 |         httpMethod: 'POST',
 92 |         path: 'multi-node'
 93 |       }
 94 |     },
 95 |     {
 96 |       id: 'set-1',
 97 |       name: 'Set 1',
 98 |       type: 'n8n-nodes-base.set',
 99 |       typeVersion: 3.4,
100 |       position: [450, 200],
101 |       parameters: {
102 |         assignments: {
103 |           assignments: [
104 |             {
105 |               id: 'assign-1',
106 |               name: 'branch',
107 |               value: 'top',
108 |               type: 'string'
109 |             }
110 |           ]
111 |         },
112 |         options: {}
113 |       }
114 |     },
115 |     {
116 |       id: 'set-2',
117 |       name: 'Set 2',
118 |       type: 'n8n-nodes-base.set',
119 |       typeVersion: 3.4,
120 |       position: [450, 400],
121 |       parameters: {
122 |         assignments: {
123 |           assignments: [
124 |             {
125 |               id: 'assign-2',
126 |               name: 'branch',
127 |               value: 'bottom',
128 |               type: 'string'
129 |             }
130 |           ]
131 |         },
132 |         options: {}
133 |       }
134 |     },
135 |     {
136 |       id: 'merge-1',
137 |       name: 'Merge',
138 |       type: 'n8n-nodes-base.merge',
139 |       typeVersion: 3,
140 |       position: [650, 300],
141 |       parameters: {
142 |         mode: 'append',
143 |         options: {}
144 |       }
145 |     }
146 |   ],
147 |   connections: {
148 |     Webhook: {
149 |       main: [
150 |         [
151 |           { node: 'Set 1', type: 'main', index: 0 },
152 |           { node: 'Set 2', type: 'main', index: 0 }
153 |         ]
154 |       ]
155 |     },
156 |     'Set 1': {
157 |       main: [[{ node: 'Merge', type: 'main', index: 0 }]]
158 |     },
159 |     'Set 2': {
160 |       main: [[{ node: 'Merge', type: 'main', index: 1 }]]
161 |     }
162 |   },
163 |   settings: {
164 |     executionOrder: 'v1'
165 |   }
166 | };
167 | 
168 | /**
169 |  * Workflow with error handling
170 |  *
171 |  * Tests error output configuration and error workflows.
172 |  */
173 | export const ERROR_HANDLING_WORKFLOW: Partial<Workflow> = {
174 |   nodes: [
175 |     {
176 |       id: 'webhook-1',
177 |       name: 'Webhook',
178 |       type: 'n8n-nodes-base.webhook',
179 |       typeVersion: 2,
180 |       position: [250, 300],
181 |       parameters: {
182 |         httpMethod: 'GET',
183 |         path: 'error-test'
184 |       }
185 |     },
186 |     {
187 |       id: 'http-1',
188 |       name: 'HTTP Request',
189 |       type: 'n8n-nodes-base.httpRequest',
190 |       typeVersion: 4.2,
191 |       position: [450, 300],
192 |       parameters: {
193 |         url: 'https://httpbin.org/status/500',
194 |         method: 'GET'
195 |       },
196 |       continueOnFail: true,
197 |       onError: 'continueErrorOutput'
198 |     },
199 |     {
200 |       id: 'set-error',
201 |       name: 'Handle Error',
202 |       type: 'n8n-nodes-base.set',
203 |       typeVersion: 3.4,
204 |       position: [650, 400],
205 |       parameters: {
206 |         assignments: {
207 |           assignments: [
208 |             {
209 |               id: 'error-assign',
210 |               name: 'error_handled',
211 |               value: 'true',
212 |               type: 'boolean'
213 |             }
214 |           ]
215 |         },
216 |         options: {}
217 |       }
218 |     }
219 |   ],
220 |   connections: {
221 |     Webhook: {
222 |       main: [[{ node: 'HTTP Request', type: 'main', index: 0 }]]
223 |     },
224 |     'HTTP Request': {
225 |       main: [[{ node: 'Handle Error', type: 'main', index: 0 }]],
226 |       error: [[{ node: 'Handle Error', type: 'main', index: 0 }]]
227 |     }
228 |   },
229 |   settings: {
230 |     executionOrder: 'v1'
231 |   }
232 | };
233 | 
234 | /**
235 |  * AI Agent workflow (langchain nodes)
236 |  *
237 |  * Tests langchain node support.
238 |  */
239 | export const AI_AGENT_WORKFLOW: Partial<Workflow> = {
240 |   nodes: [
241 |     {
242 |       id: 'manual-1',
243 |       name: 'When clicking "Test workflow"',
244 |       type: 'n8n-nodes-base.manualTrigger',
245 |       typeVersion: 1,
246 |       position: [250, 300],
247 |       parameters: {}
248 |     },
249 |     {
250 |       id: 'agent-1',
251 |       name: 'AI Agent',
252 |       type: '@n8n/n8n-nodes-langchain.agent',
253 |       typeVersion: 1.7,
254 |       position: [450, 300],
255 |       parameters: {
256 |         promptType: 'define',
257 |         text: '={{ $json.input }}',
258 |         options: {}
259 |       }
260 |     }
261 |   ],
262 |   connections: {
263 |     'When clicking "Test workflow"': {
264 |       main: [[{ node: 'AI Agent', type: 'main', index: 0 }]]
265 |     }
266 |   },
267 |   settings: {
268 |     executionOrder: 'v1'
269 |   }
270 | };
271 | 
272 | /**
273 |  * Workflow with n8n expressions
274 |  *
275 |  * Tests expression validation.
276 |  */
277 | export const EXPRESSION_WORKFLOW: Partial<Workflow> = {
278 |   nodes: [
279 |     {
280 |       id: 'manual-1',
281 |       name: 'Manual Trigger',
282 |       type: 'n8n-nodes-base.manualTrigger',
283 |       typeVersion: 1,
284 |       position: [250, 300],
285 |       parameters: {}
286 |     },
287 |     {
288 |       id: 'set-1',
289 |       name: 'Set Variables',
290 |       type: 'n8n-nodes-base.set',
291 |       typeVersion: 3.4,
292 |       position: [450, 300],
293 |       parameters: {
294 |         assignments: {
295 |           assignments: [
296 |             {
297 |               id: 'expr-1',
298 |               name: 'timestamp',
299 |               value: '={{ $now }}',
300 |               type: 'string'
301 |             },
302 |             {
303 |               id: 'expr-2',
304 |               name: 'item_count',
305 |               value: '={{ $json.items.length }}',
306 |               type: 'number'
307 |             },
308 |             {
309 |               id: 'expr-3',
310 |               name: 'first_item',
311 |               value: '={{ $node["Manual Trigger"].json }}',
312 |               type: 'object'
313 |             }
314 |           ]
315 |         },
316 |         options: {}
317 |       }
318 |     }
319 |   ],
320 |   connections: {
321 |     'Manual Trigger': {
322 |       main: [[{ node: 'Set Variables', type: 'main', index: 0 }]]
323 |     }
324 |   },
325 |   settings: {
326 |     executionOrder: 'v1'
327 |   }
328 | };
329 | 
330 | /**
331 |  * Get a fixture by name
332 |  *
333 |  * @param name - Fixture name
334 |  * @returns Workflow fixture
335 |  */
336 | export function getFixture(
337 |   name:
338 |     | 'simple-webhook'
339 |     | 'simple-http'
340 |     | 'multi-node'
341 |     | 'error-handling'
342 |     | 'ai-agent'
343 |     | 'expression'
344 | ): Partial<Workflow> {
345 |   const fixtures = {
346 |     'simple-webhook': SIMPLE_WEBHOOK_WORKFLOW,
347 |     'simple-http': SIMPLE_HTTP_WORKFLOW,
348 |     'multi-node': MULTI_NODE_WORKFLOW,
349 |     'error-handling': ERROR_HANDLING_WORKFLOW,
350 |     'ai-agent': AI_AGENT_WORKFLOW,
351 |     expression: EXPRESSION_WORKFLOW
352 |   };
353 | 
354 |   return JSON.parse(JSON.stringify(fixtures[name])); // Deep clone
355 | }
356 | 
357 | /**
358 |  * Create a minimal workflow with custom nodes
359 |  *
360 |  * @param nodes - Array of workflow nodes
361 |  * @param connections - Optional connections object
362 |  * @returns Workflow fixture
363 |  */
364 | export function createCustomWorkflow(
365 |   nodes: WorkflowNode[],
366 |   connections: Record<string, any> = {}
367 | ): Partial<Workflow> {
368 |   return {
369 |     nodes,
370 |     connections,
371 |     settings: {
372 |       executionOrder: 'v1'
373 |     }
374 |   };
375 | }
376 | 
```

--------------------------------------------------------------------------------
/tests/integration/n8n-api/system/list-tools.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Integration Tests: handleListAvailableTools
  3 |  *
  4 |  * Tests tool listing functionality.
  5 |  * Covers tool discovery and configuration status.
  6 |  */
  7 | 
  8 | import { describe, it, expect, beforeEach } from 'vitest';
  9 | import { createMcpContext } from '../utils/mcp-context';
 10 | import { InstanceContext } from '../../../../src/types/instance-context';
 11 | import { handleListAvailableTools } from '../../../../src/mcp/handlers-n8n-manager';
 12 | import { ListToolsResponse } from '../utils/response-types';
 13 | 
 14 | describe('Integration: handleListAvailableTools', () => {
 15 |   let mcpContext: InstanceContext;
 16 | 
 17 |   beforeEach(() => {
 18 |     mcpContext = createMcpContext();
 19 |   });
 20 | 
 21 |   // ======================================================================
 22 |   // List All Tools
 23 |   // ======================================================================
 24 | 
 25 |   describe('Tool Listing', () => {
 26 |     it('should list all available tools organized by category', async () => {
 27 |       const response = await handleListAvailableTools(mcpContext);
 28 | 
 29 |       expect(response.success).toBe(true);
 30 |       expect(response.data).toBeDefined();
 31 | 
 32 |       const data = response.data as ListToolsResponse;
 33 | 
 34 |       // Verify tools array exists
 35 |       expect(data).toHaveProperty('tools');
 36 |       expect(Array.isArray(data.tools)).toBe(true);
 37 |       expect(data.tools.length).toBeGreaterThan(0);
 38 | 
 39 |       // Verify tool categories
 40 |       const categories = data.tools.map((cat: any) => cat.category);
 41 |       expect(categories).toContain('Workflow Management');
 42 |       expect(categories).toContain('Execution Management');
 43 |       expect(categories).toContain('System');
 44 | 
 45 |       // Verify each category has tools
 46 |       data.tools.forEach(category => {
 47 |         expect(category).toHaveProperty('category');
 48 |         expect(category).toHaveProperty('tools');
 49 |         expect(Array.isArray(category.tools)).toBe(true);
 50 |         expect(category.tools.length).toBeGreaterThan(0);
 51 | 
 52 |         // Verify each tool has required fields
 53 |         category.tools.forEach(tool => {
 54 |           expect(tool).toHaveProperty('name');
 55 |           expect(tool).toHaveProperty('description');
 56 |           expect(typeof tool.name).toBe('string');
 57 |           expect(typeof tool.description).toBe('string');
 58 |         });
 59 |       });
 60 |     });
 61 | 
 62 |     it('should include API configuration status', async () => {
 63 |       const response = await handleListAvailableTools(mcpContext);
 64 | 
 65 |       expect(response.success).toBe(true);
 66 |       const data = response.data as ListToolsResponse;
 67 | 
 68 |       // Verify configuration status
 69 |       expect(data).toHaveProperty('apiConfigured');
 70 |       expect(typeof data.apiConfigured).toBe('boolean');
 71 | 
 72 |       // Since tests run with API configured, should be true
 73 |       expect(data.apiConfigured).toBe(true);
 74 | 
 75 |       // Verify configuration details are present when configured
 76 |       if (data.apiConfigured) {
 77 |         expect(data).toHaveProperty('configuration');
 78 |         expect(data.configuration).toBeDefined();
 79 |         expect(data.configuration).toHaveProperty('apiUrl');
 80 |         expect(data.configuration).toHaveProperty('timeout');
 81 |         expect(data.configuration).toHaveProperty('maxRetries');
 82 |       }
 83 |     });
 84 | 
 85 |     it('should include API limitations information', async () => {
 86 |       const response = await handleListAvailableTools(mcpContext);
 87 | 
 88 |       expect(response.success).toBe(true);
 89 |       const data = response.data as ListToolsResponse;
 90 | 
 91 |       // Verify limitations are documented
 92 |       expect(data).toHaveProperty('limitations');
 93 |       expect(Array.isArray(data.limitations)).toBe(true);
 94 |       expect(data.limitations.length).toBeGreaterThan(0);
 95 | 
 96 |       // Verify limitations are informative strings
 97 |       data.limitations.forEach(limitation => {
 98 |         expect(typeof limitation).toBe('string');
 99 |         expect(limitation.length).toBeGreaterThan(0);
100 |       });
101 | 
102 |       // Common known limitations
103 |       const limitationsText = data.limitations.join(' ');
104 |       expect(limitationsText).toContain('Cannot activate');
105 |       expect(limitationsText).toContain('Cannot execute workflows directly');
106 |     });
107 |   });
108 | 
109 |   // ======================================================================
110 |   // Workflow Management Tools
111 |   // ======================================================================
112 | 
113 |   describe('Workflow Management Tools', () => {
114 |     it('should include all workflow management tools', async () => {
115 |       const response = await handleListAvailableTools(mcpContext);
116 |       const data = response.data as ListToolsResponse;
117 | 
118 |       const workflowCategory = data.tools.find(cat => cat.category === 'Workflow Management');
119 |       expect(workflowCategory).toBeDefined();
120 | 
121 |       const toolNames = workflowCategory!.tools.map(t => t.name);
122 | 
123 |       // Core workflow tools
124 |       expect(toolNames).toContain('n8n_create_workflow');
125 |       expect(toolNames).toContain('n8n_get_workflow');
126 |       expect(toolNames).toContain('n8n_update_workflow');
127 |       expect(toolNames).toContain('n8n_delete_workflow');
128 |       expect(toolNames).toContain('n8n_list_workflows');
129 | 
130 |       // Enhanced workflow tools
131 |       expect(toolNames).toContain('n8n_get_workflow_details');
132 |       expect(toolNames).toContain('n8n_get_workflow_structure');
133 |       expect(toolNames).toContain('n8n_get_workflow_minimal');
134 |       expect(toolNames).toContain('n8n_validate_workflow');
135 |       expect(toolNames).toContain('n8n_autofix_workflow');
136 |     });
137 |   });
138 | 
139 |   // ======================================================================
140 |   // Execution Management Tools
141 |   // ======================================================================
142 | 
143 |   describe('Execution Management Tools', () => {
144 |     it('should include all execution management tools', async () => {
145 |       const response = await handleListAvailableTools(mcpContext);
146 |       const data = response.data as ListToolsResponse;
147 | 
148 |       const executionCategory = data.tools.find(cat => cat.category === 'Execution Management');
149 |       expect(executionCategory).toBeDefined();
150 | 
151 |       const toolNames = executionCategory!.tools.map(t => t.name);
152 | 
153 |       expect(toolNames).toContain('n8n_trigger_webhook_workflow');
154 |       expect(toolNames).toContain('n8n_get_execution');
155 |       expect(toolNames).toContain('n8n_list_executions');
156 |       expect(toolNames).toContain('n8n_delete_execution');
157 |     });
158 |   });
159 | 
160 |   // ======================================================================
161 |   // System Tools
162 |   // ======================================================================
163 | 
164 |   describe('System Tools', () => {
165 |     it('should include system tools', async () => {
166 |       const response = await handleListAvailableTools(mcpContext);
167 |       const data = response.data as ListToolsResponse;
168 | 
169 |       const systemCategory = data.tools.find(cat => cat.category === 'System');
170 |       expect(systemCategory).toBeDefined();
171 | 
172 |       const toolNames = systemCategory!.tools.map(t => t.name);
173 | 
174 |       expect(toolNames).toContain('n8n_health_check');
175 |       expect(toolNames).toContain('n8n_list_available_tools');
176 |     });
177 |   });
178 | 
179 |   // ======================================================================
180 |   // Response Format Verification
181 |   // ======================================================================
182 | 
183 |   describe('Response Format', () => {
184 |     it('should return complete tool list response structure', async () => {
185 |       const response = await handleListAvailableTools(mcpContext);
186 | 
187 |       expect(response.success).toBe(true);
188 |       expect(response.data).toBeDefined();
189 | 
190 |       const data = response.data as ListToolsResponse;
191 | 
192 |       // Verify all required fields
193 |       expect(data).toHaveProperty('tools');
194 |       expect(data).toHaveProperty('apiConfigured');
195 |       expect(data).toHaveProperty('limitations');
196 | 
197 |       // Verify optional configuration field
198 |       if (data.apiConfigured) {
199 |         expect(data).toHaveProperty('configuration');
200 |       }
201 | 
202 |       // Verify data types
203 |       expect(Array.isArray(data.tools)).toBe(true);
204 |       expect(typeof data.apiConfigured).toBe('boolean');
205 |       expect(Array.isArray(data.limitations)).toBe(true);
206 |     });
207 |   });
208 | });
209 | 
```

--------------------------------------------------------------------------------
/tests/unit/services/resource-similarity-service.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for ResourceSimilarityService
  3 |  */
  4 | 
  5 | import { describe, it, expect, beforeEach } from 'vitest';
  6 | import { ResourceSimilarityService } from '../../../src/services/resource-similarity-service';
  7 | import { NodeRepository } from '../../../src/database/node-repository';
  8 | import { createTestDatabase } from '../../utils/database-utils';
  9 | 
 10 | describe('ResourceSimilarityService', () => {
 11 |   let service: ResourceSimilarityService;
 12 |   let repository: NodeRepository;
 13 |   let testDb: any;
 14 | 
 15 |   beforeEach(async () => {
 16 |     testDb = await createTestDatabase();
 17 |     repository = testDb.nodeRepository;
 18 |     service = new ResourceSimilarityService(repository);
 19 | 
 20 |     // Add test node with resources
 21 |     const testNode = {
 22 |       nodeType: 'nodes-base.googleDrive',
 23 |       packageName: 'n8n-nodes-base',
 24 |       displayName: 'Google Drive',
 25 |       description: 'Access Google Drive',
 26 |       category: 'transform',
 27 |       style: 'declarative' as const,
 28 |       isAITool: false,
 29 |       isTrigger: false,
 30 |       isWebhook: false,
 31 |       isVersioned: true,
 32 |       version: '1',
 33 |       properties: [
 34 |         {
 35 |           name: 'resource',
 36 |           type: 'options',
 37 |           options: [
 38 |             { value: 'file', name: 'File' },
 39 |             { value: 'folder', name: 'Folder' },
 40 |             { value: 'drive', name: 'Shared Drive' },
 41 |             { value: 'fileFolder', name: 'File & Folder' }
 42 |           ]
 43 |         }
 44 |       ],
 45 |       operations: [],
 46 |       credentials: []
 47 |     };
 48 | 
 49 |     repository.saveNode(testNode);
 50 | 
 51 |     // Add Slack node for testing different patterns
 52 |     const slackNode = {
 53 |       nodeType: 'nodes-base.slack',
 54 |       packageName: 'n8n-nodes-base',
 55 |       displayName: 'Slack',
 56 |       description: 'Send messages to Slack',
 57 |       category: 'communication',
 58 |       style: 'declarative' as const,
 59 |       isAITool: false,
 60 |       isTrigger: false,
 61 |       isWebhook: false,
 62 |       isVersioned: true,
 63 |       version: '2',
 64 |       properties: [
 65 |         {
 66 |           name: 'resource',
 67 |           type: 'options',
 68 |           options: [
 69 |             { value: 'channel', name: 'Channel' },
 70 |             { value: 'message', name: 'Message' },
 71 |             { value: 'user', name: 'User' },
 72 |             { value: 'file', name: 'File' },
 73 |             { value: 'star', name: 'Star' }
 74 |           ]
 75 |         }
 76 |       ],
 77 |       operations: [],
 78 |       credentials: []
 79 |     };
 80 | 
 81 |     repository.saveNode(slackNode);
 82 |   });
 83 | 
 84 |   afterEach(async () => {
 85 |     if (testDb) {
 86 |       await testDb.cleanup();
 87 |     }
 88 |   });
 89 | 
 90 |   describe('findSimilarResources', () => {
 91 |     it('should find exact match', () => {
 92 |       const suggestions = service.findSimilarResources(
 93 |         'nodes-base.googleDrive',
 94 |         'file',
 95 |         5
 96 |       );
 97 | 
 98 |       expect(suggestions).toHaveLength(0); // No suggestions for valid resource
 99 |     });
100 | 
101 |     it('should suggest singular form for plural input', () => {
102 |       const suggestions = service.findSimilarResources(
103 |         'nodes-base.googleDrive',
104 |         'files',
105 |         5
106 |       );
107 | 
108 |       expect(suggestions.length).toBeGreaterThan(0);
109 |       expect(suggestions[0].value).toBe('file');
110 |       expect(suggestions[0].confidence).toBeGreaterThanOrEqual(0.9);
111 |       expect(suggestions[0].reason).toContain('singular');
112 |     });
113 | 
114 |     it('should suggest singular form for folders', () => {
115 |       const suggestions = service.findSimilarResources(
116 |         'nodes-base.googleDrive',
117 |         'folders',
118 |         5
119 |       );
120 | 
121 |       expect(suggestions.length).toBeGreaterThan(0);
122 |       expect(suggestions[0].value).toBe('folder');
123 |       expect(suggestions[0].confidence).toBeGreaterThanOrEqual(0.9);
124 |     });
125 | 
126 |     it('should handle typos with Levenshtein distance', () => {
127 |       const suggestions = service.findSimilarResources(
128 |         'nodes-base.googleDrive',
129 |         'flie',
130 |         5
131 |       );
132 | 
133 |       expect(suggestions.length).toBeGreaterThan(0);
134 |       expect(suggestions[0].value).toBe('file');
135 |       expect(suggestions[0].confidence).toBeGreaterThan(0.7);
136 |     });
137 | 
138 |     it('should handle combined resources', () => {
139 |       const suggestions = service.findSimilarResources(
140 |         'nodes-base.googleDrive',
141 |         'fileAndFolder',
142 |         5
143 |       );
144 | 
145 |       expect(suggestions.length).toBeGreaterThan(0);
146 |       // Should suggest 'fileFolder' (the actual combined resource)
147 |       const fileFolderSuggestion = suggestions.find(s => s.value === 'fileFolder');
148 |       expect(fileFolderSuggestion).toBeDefined();
149 |     });
150 | 
151 |     it('should return empty array for node not found', () => {
152 |       const suggestions = service.findSimilarResources(
153 |         'nodes-base.nonexistent',
154 |         'resource',
155 |         5
156 |       );
157 | 
158 |       expect(suggestions).toEqual([]);
159 |     });
160 |   });
161 | 
162 |   describe('plural/singular detection', () => {
163 |     it('should handle regular plurals (s)', () => {
164 |       const suggestions = service.findSimilarResources(
165 |         'nodes-base.slack',
166 |         'channels',
167 |         5
168 |       );
169 | 
170 |       expect(suggestions.length).toBeGreaterThan(0);
171 |       expect(suggestions[0].value).toBe('channel');
172 |     });
173 | 
174 |     it('should handle plural ending in es', () => {
175 |       const suggestions = service.findSimilarResources(
176 |         'nodes-base.slack',
177 |         'messages',
178 |         5
179 |       );
180 | 
181 |       expect(suggestions.length).toBeGreaterThan(0);
182 |       expect(suggestions[0].value).toBe('message');
183 |     });
184 | 
185 |     it('should handle plural ending in ies', () => {
186 |       // Test with a hypothetical 'entities' -> 'entity' conversion
187 |       const suggestions = service.findSimilarResources(
188 |         'nodes-base.googleDrive',
189 |         'entities',
190 |         5
191 |       );
192 | 
193 |       // Should not crash and provide some suggestions
194 |       expect(suggestions).toBeDefined();
195 |     });
196 |   });
197 | 
198 |   describe('node-specific patterns', () => {
199 |     it('should apply Google Drive specific patterns', () => {
200 |       const suggestions = service.findSimilarResources(
201 |         'nodes-base.googleDrive',
202 |         'sharedDrives',
203 |         5
204 |       );
205 | 
206 |       expect(suggestions.length).toBeGreaterThan(0);
207 |       const driveSuggestion = suggestions.find(s => s.value === 'drive');
208 |       expect(driveSuggestion).toBeDefined();
209 |     });
210 | 
211 |     it('should apply Slack specific patterns', () => {
212 |       const suggestions = service.findSimilarResources(
213 |         'nodes-base.slack',
214 |         'users',
215 |         5
216 |       );
217 | 
218 |       expect(suggestions.length).toBeGreaterThan(0);
219 |       expect(suggestions[0].value).toBe('user');
220 |     });
221 |   });
222 | 
223 |   describe('similarity calculation', () => {
224 |     it('should rank exact matches highest', () => {
225 |       const suggestions = service.findSimilarResources(
226 |         'nodes-base.googleDrive',
227 |         'file',
228 |         5
229 |       );
230 | 
231 |       expect(suggestions).toHaveLength(0); // Exact match, no suggestions
232 |     });
233 | 
234 |     it('should rank substring matches high', () => {
235 |       const suggestions = service.findSimilarResources(
236 |         'nodes-base.googleDrive',
237 |         'fil',
238 |         5
239 |       );
240 | 
241 |       expect(suggestions.length).toBeGreaterThan(0);
242 |       const fileSuggestion = suggestions.find(s => s.value === 'file');
243 |       expect(fileSuggestion).toBeDefined();
244 |       expect(fileSuggestion!.confidence).toBeGreaterThanOrEqual(0.7);
245 |     });
246 |   });
247 | 
248 |   describe('caching', () => {
249 |     it('should cache results for repeated queries', () => {
250 |       // First call
251 |       const suggestions1 = service.findSimilarResources(
252 |         'nodes-base.googleDrive',
253 |         'files',
254 |         5
255 |       );
256 | 
257 |       // Second call with same params
258 |       const suggestions2 = service.findSimilarResources(
259 |         'nodes-base.googleDrive',
260 |         'files',
261 |         5
262 |       );
263 | 
264 |       expect(suggestions1).toEqual(suggestions2);
265 |     });
266 | 
267 |     it('should clear cache when requested', () => {
268 |       // Add to cache
269 |       service.findSimilarResources(
270 |         'nodes-base.googleDrive',
271 |         'test',
272 |         5
273 |       );
274 | 
275 |       // Clear cache
276 |       service.clearCache();
277 | 
278 |       // This would fetch fresh data (behavior is the same, just uncached)
279 |       const suggestions = service.findSimilarResources(
280 |         'nodes-base.googleDrive',
281 |         'test',
282 |         5
283 |       );
284 | 
285 |       expect(suggestions).toBeDefined();
286 |     });
287 |   });
288 | });
```

--------------------------------------------------------------------------------
/tests/unit/__mocks__/n8n-nodes-base.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, vi } from 'vitest';
  2 | import { getNodeTypes, mockNodeBehavior, resetAllMocks, registerMockNode } from './n8n-nodes-base';
  3 | 
  4 | describe('n8n-nodes-base mock', () => {
  5 |   beforeEach(() => {
  6 |     resetAllMocks();
  7 |   });
  8 | 
  9 |   describe('getNodeTypes', () => {
 10 |     it('should return node types registry', () => {
 11 |       const registry = getNodeTypes();
 12 |       expect(registry).toBeDefined();
 13 |       expect(registry.getByName).toBeDefined();
 14 |       expect(registry.getByNameAndVersion).toBeDefined();
 15 |     });
 16 | 
 17 |     it('should retrieve webhook node', () => {
 18 |       const registry = getNodeTypes();
 19 |       const webhookNode = registry.getByName('webhook');
 20 |       
 21 |       expect(webhookNode).toBeDefined();
 22 |       expect(webhookNode?.description.name).toBe('webhook');
 23 |       expect(webhookNode?.description.group).toContain('trigger');
 24 |       expect(webhookNode?.webhook).toBeDefined();
 25 |     });
 26 | 
 27 |     it('should retrieve httpRequest node', () => {
 28 |       const registry = getNodeTypes();
 29 |       const httpNode = registry.getByName('httpRequest');
 30 |       
 31 |       expect(httpNode).toBeDefined();
 32 |       expect(httpNode?.description.name).toBe('httpRequest');
 33 |       expect(httpNode?.description.version).toBe(3);
 34 |       expect(httpNode?.execute).toBeDefined();
 35 |     });
 36 | 
 37 |     it('should retrieve slack node', () => {
 38 |       const registry = getNodeTypes();
 39 |       const slackNode = registry.getByName('slack');
 40 |       
 41 |       expect(slackNode).toBeDefined();
 42 |       expect(slackNode?.description.credentials).toHaveLength(1);
 43 |       expect(slackNode?.description.credentials?.[0].name).toBe('slackApi');
 44 |     });
 45 |   });
 46 | 
 47 |   describe('node execution', () => {
 48 |     it('should execute webhook node', async () => {
 49 |       const registry = getNodeTypes();
 50 |       const webhookNode = registry.getByName('webhook');
 51 |       
 52 |       const mockContext = {
 53 |         getWebhookName: vi.fn(() => 'default'),
 54 |         getBodyData: vi.fn(() => ({ test: 'data' })),
 55 |         getHeaderData: vi.fn(() => ({ 'content-type': 'application/json' })),
 56 |         getQueryData: vi.fn(() => ({ query: 'param' })),
 57 |         getRequestObject: vi.fn(),
 58 |         getResponseObject: vi.fn(),
 59 |         helpers: {
 60 |           returnJsonArray: vi.fn((data) => [{ json: data }]),
 61 |         },
 62 |       };
 63 | 
 64 |       const result = await webhookNode?.webhook?.call(mockContext as any);
 65 |       
 66 |       expect(result).toBeDefined();
 67 |       expect(result?.workflowData).toBeDefined();
 68 |       expect(result?.workflowData[0]).toHaveLength(1);
 69 |       expect(result?.workflowData[0][0].json).toMatchObject({
 70 |         headers: { 'content-type': 'application/json' },
 71 |         params: { query: 'param' },
 72 |         body: { test: 'data' },
 73 |       });
 74 |     });
 75 | 
 76 |     it('should execute httpRequest node', async () => {
 77 |       const registry = getNodeTypes();
 78 |       const httpNode = registry.getByName('httpRequest');
 79 |       
 80 |       const mockContext = {
 81 |         getInputData: vi.fn(() => [{ json: { test: 'input' } }]),
 82 |         getNodeParameter: vi.fn((name: string) => {
 83 |           if (name === 'method') return 'POST';
 84 |           if (name === 'url') return 'https://api.example.com';
 85 |           return '';
 86 |         }),
 87 |         getCredentials: vi.fn(),
 88 |         helpers: {
 89 |           returnJsonArray: vi.fn((data) => [{ json: data }]),
 90 |           httpRequest: vi.fn(),
 91 |           webhook: vi.fn(),
 92 |         },
 93 |       };
 94 | 
 95 |       const result = await httpNode?.execute?.call(mockContext as any);
 96 |       
 97 |       expect(result).toBeDefined();
 98 |       expect(result!).toHaveLength(1);
 99 |       expect(result![0]).toHaveLength(1);
100 |       expect(result![0][0].json).toMatchObject({
101 |         statusCode: 200,
102 |         body: {
103 |           success: true,
104 |           method: 'POST',
105 |           url: 'https://api.example.com',
106 |         },
107 |       });
108 |     });
109 |   });
110 | 
111 |   describe('mockNodeBehavior', () => {
112 |     it('should override node execution behavior', async () => {
113 |       const customExecute = vi.fn(async function() {
114 |         return [[{ json: { custom: 'response' } }]];
115 |       });
116 | 
117 |       mockNodeBehavior('httpRequest', {
118 |         execute: customExecute,
119 |       });
120 | 
121 |       const registry = getNodeTypes();
122 |       const httpNode = registry.getByName('httpRequest');
123 |       
124 |       const mockContext = {
125 |         getInputData: vi.fn(() => []),
126 |         getNodeParameter: vi.fn(),
127 |         getCredentials: vi.fn(),
128 |         helpers: {
129 |           returnJsonArray: vi.fn(),
130 |           httpRequest: vi.fn(),
131 |           webhook: vi.fn(),
132 |         },
133 |       };
134 | 
135 |       const result = await httpNode?.execute?.call(mockContext as any);
136 |       
137 |       expect(customExecute).toHaveBeenCalled();
138 |       expect(result).toEqual([[{ json: { custom: 'response' } }]]);
139 |     });
140 | 
141 |     it('should override node description', () => {
142 |       mockNodeBehavior('slack', {
143 |         description: {
144 |           displayName: 'Custom Slack',
145 |           version: 3,
146 |           name: 'slack',
147 |           group: ['output'],
148 |           description: 'Send messages to Slack',
149 |           defaults: { name: 'Slack' },
150 |           inputs: ['main'],
151 |           outputs: ['main'],
152 |           properties: [],
153 |         },
154 |       });
155 | 
156 |       const registry = getNodeTypes();
157 |       const slackNode = registry.getByName('slack');
158 |       
159 |       expect(slackNode?.description.displayName).toBe('Custom Slack');
160 |       expect(slackNode?.description.version).toBe(3);
161 |       expect(slackNode?.description.name).toBe('slack'); // Original preserved
162 |     });
163 |   });
164 | 
165 |   describe('registerMockNode', () => {
166 |     it('should register custom node', () => {
167 |       const customNode = {
168 |         description: {
169 |           displayName: 'Custom Node',
170 |           name: 'customNode',
171 |           group: ['transform'],
172 |           version: 1,
173 |           description: 'A custom test node',
174 |           defaults: { name: 'Custom' },
175 |           inputs: ['main'],
176 |           outputs: ['main'],
177 |           properties: [],
178 |         },
179 |         execute: vi.fn(async function() {
180 |           return [[{ json: { custom: true } }]];
181 |         }),
182 |       };
183 | 
184 |       registerMockNode('customNode', customNode);
185 | 
186 |       const registry = getNodeTypes();
187 |       const retrievedNode = registry.getByName('customNode');
188 |       
189 |       expect(retrievedNode).toBe(customNode);
190 |       expect(retrievedNode?.description.name).toBe('customNode');
191 |     });
192 |   });
193 | 
194 |   describe('conditional nodes', () => {
195 |     it('should execute if node with two outputs', async () => {
196 |       const registry = getNodeTypes();
197 |       const ifNode = registry.getByName('if');
198 |       
199 |       const mockContext = {
200 |         getInputData: vi.fn(() => [
201 |           { json: { value: 1 } },
202 |           { json: { value: 2 } },
203 |           { json: { value: 3 } },
204 |           { json: { value: 4 } },
205 |         ]),
206 |         getNodeParameter: vi.fn(),
207 |         getCredentials: vi.fn(),
208 |         helpers: {
209 |           returnJsonArray: vi.fn(),
210 |           httpRequest: vi.fn(),
211 |           webhook: vi.fn(),
212 |         },
213 |       };
214 | 
215 |       const result = await ifNode?.execute?.call(mockContext as any);
216 |       
217 |       expect(result!).toHaveLength(2); // true and false outputs
218 |       expect(result![0]).toHaveLength(2); // even indices
219 |       expect(result![1]).toHaveLength(2); // odd indices
220 |     });
221 | 
222 |     it('should execute switch node with multiple outputs', async () => {
223 |       const registry = getNodeTypes();
224 |       const switchNode = registry.getByName('switch');
225 |       
226 |       const mockContext = {
227 |         getInputData: vi.fn(() => [
228 |           { json: { value: 1 } },
229 |           { json: { value: 2 } },
230 |           { json: { value: 3 } },
231 |           { json: { value: 4 } },
232 |         ]),
233 |         getNodeParameter: vi.fn(),
234 |         getCredentials: vi.fn(),
235 |         helpers: {
236 |           returnJsonArray: vi.fn(),
237 |           httpRequest: vi.fn(),
238 |           webhook: vi.fn(),
239 |         },
240 |       };
241 | 
242 |       const result = await switchNode?.execute?.call(mockContext as any);
243 |       
244 |       expect(result!).toHaveLength(4); // 4 outputs
245 |       expect(result![0]).toHaveLength(1); // item 0
246 |       expect(result![1]).toHaveLength(1); // item 1
247 |       expect(result![2]).toHaveLength(1); // item 2
248 |       expect(result![3]).toHaveLength(1); // item 3
249 |     });
250 |   });
251 | });
```

--------------------------------------------------------------------------------
/src/types/n8n-api.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // n8n API Types - Ported from n8n-manager-for-ai-agents
  2 | // These types define the structure of n8n API requests and responses
  3 | 
  4 | // Resource Locator Types
  5 | export interface ResourceLocatorValue {
  6 |   __rl: true;
  7 |   value: string;
  8 |   mode: 'id' | 'url' | 'expression' | string;
  9 | }
 10 | 
 11 | // Expression Format Types
 12 | export type ExpressionValue = string | ResourceLocatorValue;
 13 | 
 14 | // Workflow Node Types
 15 | export interface WorkflowNode {
 16 |   id: string;
 17 |   name: string;
 18 |   type: string;
 19 |   typeVersion: number;
 20 |   position: [number, number];
 21 |   parameters: Record<string, unknown>;
 22 |   credentials?: Record<string, unknown>;
 23 |   disabled?: boolean;
 24 |   notes?: string;
 25 |   notesInFlow?: boolean;
 26 |   continueOnFail?: boolean;
 27 |   onError?: 'continueRegularOutput' | 'continueErrorOutput' | 'stopWorkflow';
 28 |   retryOnFail?: boolean;
 29 |   maxTries?: number;
 30 |   waitBetweenTries?: number;
 31 |   alwaysOutputData?: boolean;
 32 |   executeOnce?: boolean;
 33 | }
 34 | 
 35 | export interface WorkflowConnection {
 36 |   [sourceNodeId: string]: {
 37 |     [outputType: string]: Array<Array<{
 38 |       node: string;
 39 |       type: string;
 40 |       index: number;
 41 |     }>>;
 42 |   };
 43 | }
 44 | 
 45 | export interface WorkflowSettings {
 46 |   executionOrder?: 'v0' | 'v1';
 47 |   timezone?: string;
 48 |   saveDataErrorExecution?: 'all' | 'none';
 49 |   saveDataSuccessExecution?: 'all' | 'none';
 50 |   saveManualExecutions?: boolean;
 51 |   saveExecutionProgress?: boolean;
 52 |   executionTimeout?: number;
 53 |   errorWorkflow?: string;
 54 | }
 55 | 
 56 | export interface Workflow {
 57 |   id?: string;
 58 |   name: string;
 59 |   nodes: WorkflowNode[];
 60 |   connections: WorkflowConnection;
 61 |   active?: boolean; // Optional for creation as it's read-only
 62 |   isArchived?: boolean; // Optional, available in newer n8n versions
 63 |   settings?: WorkflowSettings;
 64 |   staticData?: Record<string, unknown>;
 65 |   tags?: string[];
 66 |   updatedAt?: string;
 67 |   createdAt?: string;
 68 |   versionId?: string;
 69 |   meta?: {
 70 |     instanceId?: string;
 71 |   };
 72 | }
 73 | 
 74 | // Execution Types
 75 | export enum ExecutionStatus {
 76 |   SUCCESS = 'success',
 77 |   ERROR = 'error',
 78 |   WAITING = 'waiting',
 79 |   // Note: 'running' status is not returned by the API
 80 | }
 81 | 
 82 | export interface ExecutionSummary {
 83 |   id: string;
 84 |   finished: boolean;
 85 |   mode: string;
 86 |   retryOf?: string;
 87 |   retrySuccessId?: string;
 88 |   status: ExecutionStatus;
 89 |   startedAt: string;
 90 |   stoppedAt?: string;
 91 |   workflowId: string;
 92 |   workflowName?: string;
 93 |   waitTill?: string;
 94 | }
 95 | 
 96 | export interface ExecutionData {
 97 |   startData?: Record<string, unknown>;
 98 |   resultData: {
 99 |     runData: Record<string, unknown>;
100 |     lastNodeExecuted?: string;
101 |     error?: Record<string, unknown>;
102 |   };
103 |   executionData?: Record<string, unknown>;
104 | }
105 | 
106 | export interface Execution extends ExecutionSummary {
107 |   data?: ExecutionData;
108 | }
109 | 
110 | // Credential Types
111 | export interface Credential {
112 |   id?: string;
113 |   name: string;
114 |   type: string;
115 |   data?: Record<string, unknown>;
116 |   nodesAccess?: Array<{
117 |     nodeType: string;
118 |     date?: string;
119 |   }>;
120 |   createdAt?: string;
121 |   updatedAt?: string;
122 | }
123 | 
124 | // Tag Types
125 | export interface Tag {
126 |   id?: string;
127 |   name: string;
128 |   workflowIds?: string[];
129 |   createdAt?: string;
130 |   updatedAt?: string;
131 | }
132 | 
133 | // Variable Types
134 | export interface Variable {
135 |   id?: string;
136 |   key: string;
137 |   value: string;
138 |   type?: 'string';
139 | }
140 | 
141 | // Import/Export Types
142 | export interface WorkflowExport {
143 |   id: string;
144 |   name: string;
145 |   active: boolean;
146 |   createdAt: string;
147 |   updatedAt: string;
148 |   nodes: WorkflowNode[];
149 |   connections: WorkflowConnection;
150 |   settings?: WorkflowSettings;
151 |   staticData?: Record<string, unknown>;
152 |   tags?: string[];
153 |   pinData?: Record<string, unknown>;
154 |   versionId?: string;
155 |   meta?: Record<string, unknown>;
156 | }
157 | 
158 | export interface WorkflowImport {
159 |   name: string;
160 |   nodes: WorkflowNode[];
161 |   connections: WorkflowConnection;
162 |   settings?: WorkflowSettings;
163 |   staticData?: Record<string, unknown>;
164 |   tags?: string[];
165 |   pinData?: Record<string, unknown>;
166 | }
167 | 
168 | // Source Control Types
169 | export interface SourceControlStatus {
170 |   ahead: number;
171 |   behind: number;
172 |   conflicted: string[];
173 |   created: string[];
174 |   current: string;
175 |   deleted: string[];
176 |   detached: boolean;
177 |   files: Array<{
178 |     path: string;
179 |     status: string;
180 |   }>;
181 |   modified: string[];
182 |   notAdded: string[];
183 |   renamed: Array<{
184 |     from: string;
185 |     to: string;
186 |   }>;
187 |   staged: string[];
188 |   tracking: string;
189 | }
190 | 
191 | export interface SourceControlPullResult {
192 |   conflicts: string[];
193 |   files: Array<{
194 |     path: string;
195 |     status: string;
196 |   }>;
197 |   mergeConflicts: boolean;
198 |   pullResult: 'success' | 'conflict' | 'error';
199 | }
200 | 
201 | export interface SourceControlPushResult {
202 |   ahead: number;
203 |   conflicts: string[];
204 |   files: Array<{
205 |     path: string;
206 |     status: string;
207 |   }>;
208 |   pushResult: 'success' | 'conflict' | 'error';
209 | }
210 | 
211 | // Health Check Types
212 | export interface HealthCheckResponse {
213 |   status: 'ok' | 'error';
214 |   instanceId?: string;
215 |   n8nVersion?: string;
216 |   features?: {
217 |     sourceControl?: boolean;
218 |     externalHooks?: boolean;
219 |     workers?: boolean;
220 |     [key: string]: boolean | undefined;
221 |   };
222 | }
223 | 
224 | // Request Parameter Types
225 | export interface WorkflowListParams {
226 |   limit?: number;
227 |   cursor?: string;
228 |   active?: boolean;
229 |   tags?: string | null;  // Comma-separated string per n8n API spec
230 |   projectId?: string;
231 |   excludePinnedData?: boolean;
232 |   instance?: string;
233 | }
234 | 
235 | export interface WorkflowListResponse {
236 |   data: Workflow[];
237 |   nextCursor?: string | null;
238 | }
239 | 
240 | export interface ExecutionListParams {
241 |   limit?: number;
242 |   cursor?: string;
243 |   workflowId?: string;
244 |   projectId?: string;
245 |   status?: ExecutionStatus;
246 |   includeData?: boolean;
247 | }
248 | 
249 | export interface ExecutionListResponse {
250 |   data: Execution[];
251 |   nextCursor?: string | null;
252 | }
253 | 
254 | export interface CredentialListParams {
255 |   limit?: number;
256 |   cursor?: string;
257 |   filter?: Record<string, unknown>;
258 | }
259 | 
260 | export interface CredentialListResponse {
261 |   data: Credential[];
262 |   nextCursor?: string | null;
263 | }
264 | 
265 | export interface TagListParams {
266 |   limit?: number;
267 |   cursor?: string;
268 |   withUsageCount?: boolean;
269 | }
270 | 
271 | export interface TagListResponse {
272 |   data: Tag[];
273 |   nextCursor?: string | null;
274 | }
275 | 
276 | // Webhook Request Type
277 | export interface WebhookRequest {
278 |   webhookUrl: string;
279 |   httpMethod: 'GET' | 'POST' | 'PUT' | 'DELETE';
280 |   data?: Record<string, unknown>;
281 |   headers?: Record<string, string>;
282 |   waitForResponse?: boolean;
283 | }
284 | 
285 | // MCP Tool Response Type
286 | export interface McpToolResponse {
287 |   success: boolean;
288 |   data?: unknown;
289 |   error?: string;
290 |   message?: string;
291 |   code?: string;
292 |   details?: Record<string, unknown>;
293 |   executionId?: string;
294 |   workflowId?: string;
295 | }
296 | 
297 | // Execution Filtering Types
298 | export type ExecutionMode = 'preview' | 'summary' | 'filtered' | 'full';
299 | 
300 | export interface ExecutionPreview {
301 |   totalNodes: number;
302 |   executedNodes: number;
303 |   estimatedSizeKB: number;
304 |   nodes: Record<string, NodePreview>;
305 | }
306 | 
307 | export interface NodePreview {
308 |   status: 'success' | 'error';
309 |   itemCounts: {
310 |     input: number;
311 |     output: number;
312 |   };
313 |   dataStructure: Record<string, any>;
314 |   estimatedSizeKB: number;
315 |   error?: string;
316 | }
317 | 
318 | export interface ExecutionRecommendation {
319 |   canFetchFull: boolean;
320 |   suggestedMode: ExecutionMode;
321 |   suggestedItemsLimit?: number;
322 |   reason: string;
323 | }
324 | 
325 | export interface ExecutionFilterOptions {
326 |   mode?: ExecutionMode;
327 |   nodeNames?: string[];
328 |   itemsLimit?: number;
329 |   includeInputData?: boolean;
330 |   fieldsToInclude?: string[];
331 | }
332 | 
333 | export interface FilteredExecutionResponse {
334 |   id: string;
335 |   workflowId: string;
336 |   status: ExecutionStatus;
337 |   mode: ExecutionMode;
338 |   startedAt: string;
339 |   stoppedAt?: string;
340 |   duration?: number;
341 |   finished: boolean;
342 | 
343 |   // Preview-specific data
344 |   preview?: ExecutionPreview;
345 |   recommendation?: ExecutionRecommendation;
346 | 
347 |   // Summary/Filtered data
348 |   summary?: {
349 |     totalNodes: number;
350 |     executedNodes: number;
351 |     totalItems: number;
352 |     hasMoreData: boolean;
353 |   };
354 |   nodes?: Record<string, FilteredNodeData>;
355 | 
356 |   // Error information
357 |   error?: Record<string, unknown>;
358 | }
359 | 
360 | export interface FilteredNodeData {
361 |   executionTime?: number;
362 |   itemsInput: number;
363 |   itemsOutput: number;
364 |   status: 'success' | 'error';
365 |   error?: string;
366 |   data?: {
367 |     input?: any[][];
368 |     output?: any[][];
369 |     metadata: {
370 |       totalItems: number;
371 |       itemsShown: number;
372 |       truncated: boolean;
373 |     };
374 |   };
375 | }
```

--------------------------------------------------------------------------------
/tests/mocks/n8n-api/handlers.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { http, HttpResponse, RequestHandler } from 'msw';
  2 | import { mockWorkflows } from './data/workflows';
  3 | import { mockExecutions } from './data/executions';
  4 | import { mockCredentials } from './data/credentials';
  5 | 
  6 | // Base URL for n8n API (will be overridden by actual URL in tests)
  7 | const API_BASE = process.env.N8N_API_URL || 'http://localhost:5678';
  8 | 
  9 | /**
 10 |  * Default handlers for n8n API endpoints
 11 |  * These can be overridden in specific tests using server.use()
 12 |  */
 13 | export const handlers: RequestHandler[] = [
 14 |   // Health check endpoint
 15 |   http.get('*/api/v1/health', () => {
 16 |     return HttpResponse.json({
 17 |       status: 'ok',
 18 |       version: '1.103.2',
 19 |       features: {
 20 |         workflows: true,
 21 |         executions: true,
 22 |         credentials: true,
 23 |         webhooks: true,
 24 |       }
 25 |     });
 26 |   }),
 27 | 
 28 |   // Workflow endpoints
 29 |   http.get('*/api/v1/workflows', ({ request }) => {
 30 |     const url = new URL(request.url);
 31 |     const limit = parseInt(url.searchParams.get('limit') || '100');
 32 |     const cursor = url.searchParams.get('cursor');
 33 |     const active = url.searchParams.get('active');
 34 |     
 35 |     let filtered = mockWorkflows;
 36 |     
 37 |     // Filter by active status if provided
 38 |     if (active !== null) {
 39 |       filtered = filtered.filter(w => w.active === (active === 'true'));
 40 |     }
 41 |     
 42 |     // Simple pagination simulation
 43 |     const startIndex = cursor ? parseInt(cursor) : 0;
 44 |     const paginatedData = filtered.slice(startIndex, startIndex + limit);
 45 |     const hasMore = startIndex + limit < filtered.length;
 46 |     const nextCursor = hasMore ? String(startIndex + limit) : null;
 47 |     
 48 |     return HttpResponse.json({
 49 |       data: paginatedData,
 50 |       nextCursor,
 51 |       hasMore
 52 |     });
 53 |   }),
 54 | 
 55 |   http.get('*/api/v1/workflows/:id', ({ params }) => {
 56 |     const workflow = mockWorkflows.find(w => w.id === params.id);
 57 |     
 58 |     if (!workflow) {
 59 |       return HttpResponse.json(
 60 |         { message: 'Workflow not found', code: 'NOT_FOUND' },
 61 |         { status: 404 }
 62 |       );
 63 |     }
 64 |     
 65 |     return HttpResponse.json({ data: workflow });
 66 |   }),
 67 | 
 68 |   http.post('*/api/v1/workflows', async ({ request }) => {
 69 |     const body = await request.json() as any;
 70 |     
 71 |     // Validate required fields
 72 |     if (!body.name || !body.nodes || !body.connections) {
 73 |       return HttpResponse.json(
 74 |         { 
 75 |           message: 'Validation failed', 
 76 |           errors: {
 77 |             name: !body.name ? 'Name is required' : undefined,
 78 |             nodes: !body.nodes ? 'Nodes are required' : undefined,
 79 |             connections: !body.connections ? 'Connections are required' : undefined,
 80 |           },
 81 |           code: 'VALIDATION_ERROR' 
 82 |         },
 83 |         { status: 400 }
 84 |       );
 85 |     }
 86 |     
 87 |     const newWorkflow = {
 88 |       id: `workflow_${Date.now()}`,
 89 |       name: body.name,
 90 |       active: body.active || false,
 91 |       nodes: body.nodes,
 92 |       connections: body.connections,
 93 |       settings: body.settings || {},
 94 |       tags: body.tags || [],
 95 |       createdAt: new Date().toISOString(),
 96 |       updatedAt: new Date().toISOString(),
 97 |       versionId: '1'
 98 |     };
 99 |     
100 |     mockWorkflows.push(newWorkflow);
101 |     
102 |     return HttpResponse.json({ data: newWorkflow }, { status: 201 });
103 |   }),
104 | 
105 |   http.patch('*/api/v1/workflows/:id', async ({ params, request }) => {
106 |     const workflowIndex = mockWorkflows.findIndex(w => w.id === params.id);
107 |     
108 |     if (workflowIndex === -1) {
109 |       return HttpResponse.json(
110 |         { message: 'Workflow not found', code: 'NOT_FOUND' },
111 |         { status: 404 }
112 |       );
113 |     }
114 |     
115 |     const body = await request.json() as any;
116 |     const updatedWorkflow = {
117 |       ...mockWorkflows[workflowIndex],
118 |       ...body,
119 |       id: params.id, // Ensure ID doesn't change
120 |       updatedAt: new Date().toISOString(),
121 |       versionId: String(parseInt(mockWorkflows[workflowIndex].versionId) + 1)
122 |     };
123 |     
124 |     mockWorkflows[workflowIndex] = updatedWorkflow;
125 |     
126 |     return HttpResponse.json({ data: updatedWorkflow });
127 |   }),
128 | 
129 |   http.delete('*/api/v1/workflows/:id', ({ params }) => {
130 |     const workflowIndex = mockWorkflows.findIndex(w => w.id === params.id);
131 |     
132 |     if (workflowIndex === -1) {
133 |       return HttpResponse.json(
134 |         { message: 'Workflow not found', code: 'NOT_FOUND' },
135 |         { status: 404 }
136 |       );
137 |     }
138 |     
139 |     mockWorkflows.splice(workflowIndex, 1);
140 |     
141 |     return HttpResponse.json({ success: true });
142 |   }),
143 | 
144 |   // Execution endpoints
145 |   http.get('*/api/v1/executions', ({ request }) => {
146 |     const url = new URL(request.url);
147 |     const limit = parseInt(url.searchParams.get('limit') || '100');
148 |     const cursor = url.searchParams.get('cursor');
149 |     const workflowId = url.searchParams.get('workflowId');
150 |     const status = url.searchParams.get('status');
151 |     
152 |     let filtered = mockExecutions;
153 |     
154 |     // Filter by workflow ID if provided
155 |     if (workflowId) {
156 |       filtered = filtered.filter(e => e.workflowId === workflowId);
157 |     }
158 |     
159 |     // Filter by status if provided
160 |     if (status) {
161 |       filtered = filtered.filter(e => e.status === status);
162 |     }
163 |     
164 |     // Simple pagination simulation
165 |     const startIndex = cursor ? parseInt(cursor) : 0;
166 |     const paginatedData = filtered.slice(startIndex, startIndex + limit);
167 |     const hasMore = startIndex + limit < filtered.length;
168 |     const nextCursor = hasMore ? String(startIndex + limit) : null;
169 |     
170 |     return HttpResponse.json({
171 |       data: paginatedData,
172 |       nextCursor,
173 |       hasMore
174 |     });
175 |   }),
176 | 
177 |   http.get('*/api/v1/executions/:id', ({ params }) => {
178 |     const execution = mockExecutions.find(e => e.id === params.id);
179 |     
180 |     if (!execution) {
181 |       return HttpResponse.json(
182 |         { message: 'Execution not found', code: 'NOT_FOUND' },
183 |         { status: 404 }
184 |       );
185 |     }
186 |     
187 |     return HttpResponse.json({ data: execution });
188 |   }),
189 | 
190 |   http.delete('*/api/v1/executions/:id', ({ params }) => {
191 |     const executionIndex = mockExecutions.findIndex(e => e.id === params.id);
192 |     
193 |     if (executionIndex === -1) {
194 |       return HttpResponse.json(
195 |         { message: 'Execution not found', code: 'NOT_FOUND' },
196 |         { status: 404 }
197 |       );
198 |     }
199 |     
200 |     mockExecutions.splice(executionIndex, 1);
201 |     
202 |     return HttpResponse.json({ success: true });
203 |   }),
204 | 
205 |   // Webhook endpoints (dynamic handling)
206 |   http.all('*/webhook/*', async ({ request }) => {
207 |     const url = new URL(request.url);
208 |     const method = request.method;
209 |     const body = request.body ? await request.json() : undefined;
210 |     
211 |     // Log webhook trigger in debug mode
212 |     if (process.env.MSW_DEBUG === 'true') {
213 |       console.log('[MSW] Webhook triggered:', {
214 |         url: url.pathname,
215 |         method,
216 |         body
217 |       });
218 |     }
219 |     
220 |     // Return success response by default
221 |     return HttpResponse.json({
222 |       success: true,
223 |       webhookUrl: url.pathname,
224 |       method,
225 |       timestamp: new Date().toISOString(),
226 |       data: body
227 |     });
228 |   }),
229 | 
230 |   // Catch-all for unhandled API routes (helps identify missing handlers)
231 |   http.all('*/api/*', ({ request }) => {
232 |     console.warn('[MSW] Unhandled API request:', request.method, request.url);
233 |     
234 |     return HttpResponse.json(
235 |       { 
236 |         message: 'Not implemented in mock', 
237 |         code: 'NOT_IMPLEMENTED',
238 |         path: new URL(request.url).pathname,
239 |         method: request.method
240 |       },
241 |       { status: 501 }
242 |     );
243 |   }),
244 | ];
245 | 
246 | /**
247 |  * Dynamic handler registration helpers
248 |  */
249 | export const dynamicHandlers = {
250 |   /**
251 |    * Add a workflow that will be returned by GET requests
252 |    */
253 |   addWorkflow: (workflow: any) => {
254 |     mockWorkflows.push(workflow);
255 |   },
256 | 
257 |   /**
258 |    * Clear all mock workflows
259 |    */
260 |   clearWorkflows: () => {
261 |     mockWorkflows.length = 0;
262 |   },
263 | 
264 |   /**
265 |    * Add an execution that will be returned by GET requests
266 |    */
267 |   addExecution: (execution: any) => {
268 |     mockExecutions.push(execution);
269 |   },
270 | 
271 |   /**
272 |    * Clear all mock executions
273 |    */
274 |   clearExecutions: () => {
275 |     mockExecutions.length = 0;
276 |   },
277 | 
278 |   /**
279 |    * Reset all mock data to initial state
280 |    */
281 |   resetAll: () => {
282 |     // Reset arrays to initial state (implementation depends on data modules)
283 |     mockWorkflows.length = 0;
284 |     mockExecutions.length = 0;
285 |     mockCredentials.length = 0;
286 |   }
287 | };
```
Page 11/59FirstPrevNextLast