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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/src/mcp/tool-docs/guides/ai-agents-guide.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ToolDocumentation } from '../types';
  2 | 
  3 | export const aiAgentsGuide: ToolDocumentation = {
  4 |   name: 'ai_agents_guide',
  5 |   category: 'guides',
  6 |   essentials: {
  7 |     description: 'Comprehensive guide to building AI Agent workflows in n8n. Covers architecture, connections, tools, validation, and best practices for production AI systems.',
  8 |     keyParameters: [],
  9 |     example: 'Use tools_documentation({topic: "ai_agents_guide"}) to access this guide',
 10 |     performance: 'N/A - Documentation only',
 11 |     tips: [
 12 |       'Start with Chat Trigger → AI Agent → Language Model pattern',
 13 |       'Always connect language model BEFORE enabling AI Agent',
 14 |       'Use proper toolDescription for all AI tools (15+ characters)',
 15 |       'Validate workflows with n8n_validate_workflow before deployment',
 16 |       'Use includeExamples=true when searching for AI nodes',
 17 |       'Check FINAL_AI_VALIDATION_SPEC.md for detailed requirements'
 18 |     ]
 19 |   },
 20 |   full: {
 21 |     description: `# Complete Guide to AI Agents in n8n
 22 | 
 23 | This comprehensive guide covers everything you need to build production-ready AI Agent workflows in n8n.
 24 | 
 25 | ## Table of Contents
 26 | 1. [AI Agent Architecture](#architecture)
 27 | 2. [Essential Connection Types](#connections)
 28 | 3. [Building Your First AI Agent](#first-agent)
 29 | 4. [AI Tools Deep Dive](#tools)
 30 | 5. [Advanced Patterns](#advanced)
 31 | 6. [Validation & Best Practices](#validation)
 32 | 7. [Troubleshooting](#troubleshooting)
 33 | 
 34 | ---
 35 | 
 36 | ## 1. AI Agent Architecture {#architecture}
 37 | 
 38 | ### Core Components
 39 | 
 40 | An n8n AI Agent workflow typically consists of:
 41 | 
 42 | 1. **Chat Trigger**: Entry point for user interactions
 43 |    - Webhook-based or manual trigger
 44 |    - Supports streaming responses (responseMode)
 45 |    - Passes user message to AI Agent
 46 | 
 47 | 2. **AI Agent**: The orchestrator
 48 |    - Manages conversation flow
 49 |    - Decides when to use tools
 50 |    - Iterates until task is complete
 51 |    - Supports fallback models (v2.1+)
 52 | 
 53 | 3. **Language Model**: The AI brain
 54 |    - OpenAI GPT-4, Claude, Gemini, etc.
 55 |    - Connected via ai_languageModel port
 56 |    - Can have primary + fallback for reliability
 57 | 
 58 | 4. **Tools**: AI Agent's capabilities
 59 |    - HTTP Request, Code, Vector Store, etc.
 60 |    - Connected via ai_tool port
 61 |    - Each tool needs clear toolDescription
 62 | 
 63 | 5. **Optional Components**:
 64 |    - Memory (conversation history)
 65 |    - Output Parser (structured responses)
 66 |    - Vector Store (knowledge retrieval)
 67 | 
 68 | ### Connection Flow
 69 | 
 70 | **CRITICAL**: AI connections flow TO the consumer (reversed from standard n8n):
 71 | 
 72 | \`\`\`
 73 | Standard n8n:  [Source] --main--> [Target]
 74 | AI pattern:    [Language Model] --ai_languageModel--> [AI Agent]
 75 |                [HTTP Tool] --ai_tool--> [AI Agent]
 76 | \`\`\`
 77 | 
 78 | This is why you use \`sourceOutput: "ai_languageModel"\` when connecting components.
 79 | 
 80 | ---
 81 | 
 82 | ## 2. Essential Connection Types {#connections}
 83 | 
 84 | ### The 8 AI Connection Types
 85 | 
 86 | 1. **ai_languageModel**
 87 |    - FROM: OpenAI Chat Model, Anthropic, Google Gemini, etc.
 88 |    - TO: AI Agent, Basic LLM Chain
 89 |    - REQUIRED: Every AI Agent needs 1-2 language models
 90 |    - Example: \`{type: "addConnection", source: "OpenAI", target: "AI Agent", sourceOutput: "ai_languageModel"}\`
 91 | 
 92 | 2. **ai_tool**
 93 |    - FROM: Any tool node (HTTP Request Tool, Code Tool, etc.)
 94 |    - TO: AI Agent
 95 |    - REQUIRED: At least 1 tool recommended
 96 |    - Example: \`{type: "addConnection", source: "HTTP Request Tool", target: "AI Agent", sourceOutput: "ai_tool"}\`
 97 | 
 98 | 3. **ai_memory**
 99 |    - FROM: Window Buffer Memory, Conversation Summary, etc.
100 |    - TO: AI Agent
101 |    - OPTIONAL: 0-1 memory system
102 |    - Enables conversation history tracking
103 | 
104 | 4. **ai_outputParser**
105 |    - FROM: Structured Output Parser, JSON Parser, etc.
106 |    - TO: AI Agent
107 |    - OPTIONAL: For structured responses
108 |    - Must set hasOutputParser=true on AI Agent
109 | 
110 | 5. **ai_embedding**
111 |    - FROM: Embeddings OpenAI, Embeddings Google, etc.
112 |    - TO: Vector Store (Pinecone, In-Memory, etc.)
113 |    - REQUIRED: For vector-based retrieval
114 | 
115 | 6. **ai_vectorStore**
116 |    - FROM: Vector Store node
117 |    - TO: Vector Store Tool
118 |    - REQUIRED: For retrieval-augmented generation (RAG)
119 | 
120 | 7. **ai_document**
121 |    - FROM: Document Loader, Default Data Loader
122 |    - TO: Vector Store
123 |    - REQUIRED: Provides data for vector storage
124 | 
125 | 8. **ai_textSplitter**
126 |    - FROM: Text Splitter nodes
127 |    - TO: Document processing chains
128 |    - OPTIONAL: Chunk large documents
129 | 
130 | ### Connection Examples
131 | 
132 | \`\`\`typescript
133 | // Basic AI Agent setup
134 | n8n_update_partial_workflow({
135 |   id: "workflow_id",
136 |   operations: [
137 |     // Connect language model (REQUIRED)
138 |     {
139 |       type: "addConnection",
140 |       source: "OpenAI Chat Model",
141 |       target: "AI Agent",
142 |       sourceOutput: "ai_languageModel"
143 |     },
144 |     // Connect tools
145 |     {
146 |       type: "addConnection",
147 |       source: "HTTP Request Tool",
148 |       target: "AI Agent",
149 |       sourceOutput: "ai_tool"
150 |     },
151 |     {
152 |       type: "addConnection",
153 |       source: "Code Tool",
154 |       target: "AI Agent",
155 |       sourceOutput: "ai_tool"
156 |     },
157 |     // Add memory (optional)
158 |     {
159 |       type: "addConnection",
160 |       source: "Window Buffer Memory",
161 |       target: "AI Agent",
162 |       sourceOutput: "ai_memory"
163 |     }
164 |   ]
165 | })
166 | \`\`\`
167 | 
168 | ---
169 | 
170 | ## 3. Building Your First AI Agent {#first-agent}
171 | 
172 | ### Step-by-Step Tutorial
173 | 
174 | #### Step 1: Create Chat Trigger
175 | 
176 | Use \`n8n_create_workflow\` or manually create a workflow with:
177 | 
178 | \`\`\`typescript
179 | {
180 |   name: "My First AI Agent",
181 |   nodes: [
182 |     {
183 |       id: "chat_trigger",
184 |       name: "Chat Trigger",
185 |       type: "@n8n/n8n-nodes-langchain.chatTrigger",
186 |       position: [100, 100],
187 |       parameters: {
188 |         options: {
189 |           responseMode: "lastNode"  // or "streaming" for real-time
190 |         }
191 |       }
192 |     }
193 |   ],
194 |   connections: {}
195 | }
196 | \`\`\`
197 | 
198 | #### Step 2: Add Language Model
199 | 
200 | \`\`\`typescript
201 | n8n_update_partial_workflow({
202 |   id: "workflow_id",
203 |   operations: [
204 |     {
205 |       type: "addNode",
206 |       node: {
207 |         name: "OpenAI Chat Model",
208 |         type: "@n8n/n8n-nodes-langchain.lmChatOpenAi",
209 |         position: [300, 50],
210 |         parameters: {
211 |           model: "gpt-4",
212 |           temperature: 0.7
213 |         }
214 |       }
215 |     }
216 |   ]
217 | })
218 | \`\`\`
219 | 
220 | #### Step 3: Add AI Agent
221 | 
222 | \`\`\`typescript
223 | n8n_update_partial_workflow({
224 |   id: "workflow_id",
225 |   operations: [
226 |     {
227 |       type: "addNode",
228 |       node: {
229 |         name: "AI Agent",
230 |         type: "@n8n/n8n-nodes-langchain.agent",
231 |         position: [300, 150],
232 |         parameters: {
233 |           promptType: "auto",
234 |           systemMessage: "You are a helpful assistant. Be concise and accurate."
235 |         }
236 |       }
237 |     }
238 |   ]
239 | })
240 | \`\`\`
241 | 
242 | #### Step 4: Connect Components
243 | 
244 | \`\`\`typescript
245 | n8n_update_partial_workflow({
246 |   id: "workflow_id",
247 |   operations: [
248 |     // Chat Trigger → AI Agent (main connection)
249 |     {
250 |       type: "addConnection",
251 |       source: "Chat Trigger",
252 |       target: "AI Agent"
253 |     },
254 |     // Language Model → AI Agent (AI connection)
255 |     {
256 |       type: "addConnection",
257 |       source: "OpenAI Chat Model",
258 |       target: "AI Agent",
259 |       sourceOutput: "ai_languageModel"
260 |     }
261 |   ]
262 | })
263 | \`\`\`
264 | 
265 | #### Step 5: Validate
266 | 
267 | \`\`\`typescript
268 | n8n_validate_workflow({id: "workflow_id"})
269 | \`\`\`
270 | 
271 | ---
272 | 
273 | ## 4. AI Tools Deep Dive {#tools}
274 | 
275 | ### Tool Types and When to Use Them
276 | 
277 | #### 1. HTTP Request Tool
278 | **Use when**: AI needs to call external APIs
279 | 
280 | **Critical Requirements**:
281 | - \`toolDescription\`: Clear, 15+ character description
282 | - \`url\`: API endpoint (can include placeholders)
283 | - \`placeholderDefinitions\`: Define all {placeholders}
284 | - Proper authentication if needed
285 | 
286 | **Example**:
287 | \`\`\`typescript
288 | {
289 |   type: "addNode",
290 |   node: {
291 |     name: "GitHub Issues Tool",
292 |     type: "@n8n/n8n-nodes-langchain.toolHttpRequest",
293 |     position: [500, 100],
294 |     parameters: {
295 |       method: "POST",
296 |       url: "https://api.github.com/repos/{owner}/{repo}/issues",
297 |       toolDescription: "Create GitHub issues. Requires owner (username), repo (repository name), title, and body.",
298 |       placeholderDefinitions: {
299 |         values: [
300 |           {name: "owner", description: "Repository owner username"},
301 |           {name: "repo", description: "Repository name"},
302 |           {name: "title", description: "Issue title"},
303 |           {name: "body", description: "Issue description"}
304 |         ]
305 |       },
306 |       sendBody: true,
307 |       jsonBody: "={{ { title: $json.title, body: $json.body } }}"
308 |     }
309 |   }
310 | }
311 | \`\`\`
312 | 
313 | #### 2. Code Tool
314 | **Use when**: AI needs to run custom logic
315 | 
316 | **Critical Requirements**:
317 | - \`name\`: Function name (alphanumeric + underscore)
318 | - \`description\`: 10+ character explanation
319 | - \`code\`: JavaScript or Python code
320 | - \`inputSchema\`: Define expected inputs (recommended)
321 | 
322 | **Example**:
323 | \`\`\`typescript
324 | {
325 |   type: "addNode",
326 |   node: {
327 |     name: "Calculate Shipping",
328 |     type: "@n8n/n8n-nodes-langchain.toolCode",
329 |     position: [500, 200],
330 |     parameters: {
331 |       name: "calculate_shipping",
332 |       description: "Calculate shipping cost based on weight (kg) and distance (km)",
333 |       language: "javaScript",
334 |       code: "const cost = 5 + ($input.weight * 2) + ($input.distance * 0.1); return { cost };",
335 |       specifyInputSchema: true,
336 |       inputSchema: "{ \\"type\\": \\"object\\", \\"properties\\": { \\"weight\\": { \\"type\\": \\"number\\" }, \\"distance\\": { \\"type\\": \\"number\\" } } }"
337 |     }
338 |   }
339 | }
340 | \`\`\`
341 | 
342 | #### 3. Vector Store Tool
343 | **Use when**: AI needs to search knowledge base
344 | 
345 | **Setup**: Requires Vector Store + Embeddings + Documents
346 | 
347 | **Example**:
348 | \`\`\`typescript
349 | // Step 1: Create Vector Store with embeddings and documents
350 | n8n_update_partial_workflow({
351 |   operations: [
352 |     {type: "addConnection", source: "Embeddings OpenAI", target: "Pinecone", sourceOutput: "ai_embedding"},
353 |     {type: "addConnection", source: "Document Loader", target: "Pinecone", sourceOutput: "ai_document"}
354 |   ]
355 | })
356 | 
357 | // Step 2: Connect Vector Store to Vector Store Tool
358 | n8n_update_partial_workflow({
359 |   operations: [
360 |     {type: "addConnection", source: "Pinecone", target: "Vector Store Tool", sourceOutput: "ai_vectorStore"}
361 |   ]
362 | })
363 | 
364 | // Step 3: Connect tool to AI Agent
365 | n8n_update_partial_workflow({
366 |   operations: [
367 |     {type: "addConnection", source: "Vector Store Tool", target: "AI Agent", sourceOutput: "ai_tool"}
368 |   ]
369 | })
370 | \`\`\`
371 | 
372 | #### 4. AI Agent Tool (Sub-Agents)
373 | **Use when**: Need specialized expertise
374 | 
375 | **Example**: Research specialist sub-agent
376 | \`\`\`typescript
377 | {
378 |   type: "addNode",
379 |   node: {
380 |     name: "Research Specialist",
381 |     type: "@n8n/n8n-nodes-langchain.agentTool",
382 |     position: [500, 300],
383 |     parameters: {
384 |       name: "research_specialist",
385 |       description: "Expert researcher that searches multiple sources and synthesizes information. Use for detailed research tasks.",
386 |       systemMessage: "You are a research specialist. Search thoroughly, cite sources, and provide comprehensive analysis."
387 |     }
388 |   }
389 | }
390 | \`\`\`
391 | 
392 | #### 5. MCP Client Tool
393 | **Use when**: Need to use Model Context Protocol servers
394 | 
395 | **Example**: Filesystem access
396 | \`\`\`typescript
397 | {
398 |   type: "addNode",
399 |   node: {
400 |     name: "Filesystem Tool",
401 |     type: "@n8n/n8n-nodes-langchain.mcpClientTool",
402 |     position: [500, 400],
403 |     parameters: {
404 |       description: "Access file system to read files, list directories, and search content",
405 |       mcpServer: {
406 |         transport: "stdio",
407 |         command: "npx",
408 |         args: ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"]
409 |       },
410 |       tool: "read_file"
411 |     }
412 |   }
413 | }
414 | \`\`\`
415 | 
416 | ---
417 | 
418 | ## 5. Advanced Patterns {#advanced}
419 | 
420 | ### Pattern 1: Streaming Responses
421 | 
422 | For real-time user experience:
423 | 
424 | \`\`\`typescript
425 | // Set Chat Trigger to streaming mode
426 | {
427 |   parameters: {
428 |     options: {
429 |       responseMode: "streaming"
430 |     }
431 |   }
432 | }
433 | 
434 | // CRITICAL: AI Agent must NOT have main output connections in streaming mode
435 | // Responses stream back through Chat Trigger automatically
436 | \`\`\`
437 | 
438 | **Validation will fail if**:
439 | - Chat Trigger has streaming but target is not AI Agent
440 | - AI Agent in streaming mode has main output connections
441 | 
442 | ### Pattern 2: Fallback Language Models
443 | 
444 | For production reliability (requires AI Agent v2.1+):
445 | 
446 | \`\`\`typescript
447 | n8n_update_partial_workflow({
448 |   operations: [
449 |     // Primary model
450 |     {
451 |       type: "addConnection",
452 |       source: "OpenAI GPT-4",
453 |       target: "AI Agent",
454 |       sourceOutput: "ai_languageModel",
455 |       targetIndex: 0
456 |     },
457 |     // Fallback model
458 |     {
459 |       type: "addConnection",
460 |       source: "Anthropic Claude",
461 |       target: "AI Agent",
462 |       sourceOutput: "ai_languageModel",
463 |       targetIndex: 1
464 |     }
465 |   ]
466 | })
467 | 
468 | // Enable fallback on AI Agent
469 | {
470 |   type: "updateNode",
471 |   nodeName: "AI Agent",
472 |   updates: {
473 |     "parameters.needsFallback": true
474 |   }
475 | }
476 | \`\`\`
477 | 
478 | ### Pattern 3: RAG (Retrieval-Augmented Generation)
479 | 
480 | Complete knowledge base setup:
481 | 
482 | \`\`\`typescript
483 | // 1. Load documents
484 | {type: "addConnection", source: "PDF Loader", target: "Text Splitter", sourceOutput: "ai_document"}
485 | 
486 | // 2. Split and embed
487 | {type: "addConnection", source: "Text Splitter", target: "Vector Store"}
488 | {type: "addConnection", source: "Embeddings", target: "Vector Store", sourceOutput: "ai_embedding"}
489 | 
490 | // 3. Create search tool
491 | {type: "addConnection", source: "Vector Store", target: "Vector Store Tool", sourceOutput: "ai_vectorStore"}
492 | 
493 | // 4. Give tool to agent
494 | {type: "addConnection", source: "Vector Store Tool", target: "AI Agent", sourceOutput: "ai_tool"}
495 | \`\`\`
496 | 
497 | ### Pattern 4: Multi-Agent Systems
498 | 
499 | Specialized sub-agents for complex tasks:
500 | 
501 | \`\`\`typescript
502 | // Create sub-agents with specific expertise
503 | [
504 |   {name: "research_agent", description: "Deep research specialist"},
505 |   {name: "data_analyst", description: "Data analysis expert"},
506 |   {name: "writer_agent", description: "Content writing specialist"}
507 | ].forEach(agent => {
508 |   // Add as AI Agent Tool to main coordinator agent
509 |   {
510 |     type: "addConnection",
511 |     source: agent.name,
512 |     target: "Coordinator Agent",
513 |     sourceOutput: "ai_tool"
514 |   }
515 | })
516 | \`\`\`
517 | 
518 | ---
519 | 
520 | ## 6. Validation & Best Practices {#validation}
521 | 
522 | ### Always Validate Before Deployment
523 | 
524 | \`\`\`typescript
525 | const result = n8n_validate_workflow({id: "workflow_id"})
526 | 
527 | if (!result.valid) {
528 |   console.log("Errors:", result.errors)
529 |   console.log("Warnings:", result.warnings)
530 |   console.log("Suggestions:", result.suggestions)
531 | }
532 | \`\`\`
533 | 
534 | ### Common Validation Errors
535 | 
536 | 1. **MISSING_LANGUAGE_MODEL**
537 |    - Problem: AI Agent has no ai_languageModel connection
538 |    - Fix: Connect a language model before creating AI Agent
539 | 
540 | 2. **MISSING_TOOL_DESCRIPTION**
541 |    - Problem: HTTP Request Tool has no toolDescription
542 |    - Fix: Add clear description (15+ characters)
543 | 
544 | 3. **STREAMING_WITH_MAIN_OUTPUT**
545 |    - Problem: AI Agent in streaming mode has outgoing main connections
546 |    - Fix: Remove main connections when using streaming
547 | 
548 | 4. **FALLBACK_MISSING_SECOND_MODEL**
549 |    - Problem: needsFallback=true but only 1 language model
550 |    - Fix: Add second language model or disable needsFallback
551 | 
552 | ### Best Practices Checklist
553 | 
554 | ✅ **Before Creating AI Agent**:
555 | - [ ] Language model is connected first
556 | - [ ] At least one tool is prepared (or will be added)
557 | - [ ] System message is thoughtful and specific
558 | 
559 | ✅ **For Each Tool**:
560 | - [ ] Has toolDescription/description (15+ characters)
561 | - [ ] toolDescription explains WHEN to use the tool
562 | - [ ] All required parameters are configured
563 | - [ ] Credentials are set up if needed
564 | 
565 | ✅ **For Production**:
566 | - [ ] Workflow validated with n8n_validate_workflow
567 | - [ ] Tested with real user queries
568 | - [ ] Fallback model configured for reliability
569 | - [ ] Error handling in place
570 | - [ ] maxIterations set appropriately (default 10, max 50)
571 | 
572 | ---
573 | 
574 | ## 7. Troubleshooting {#troubleshooting}
575 | 
576 | ### Problem: "AI Agent has no language model"
577 | 
578 | **Cause**: Connection created AFTER AI Agent or using wrong sourceOutput
579 | 
580 | **Solution**:
581 | \`\`\`typescript
582 | n8n_update_partial_workflow({
583 |   operations: [
584 |     {
585 |       type: "addConnection",
586 |       source: "OpenAI Chat Model",
587 |       target: "AI Agent",
588 |       sourceOutput: "ai_languageModel"  // ← CRITICAL
589 |     }
590 |   ]
591 | })
592 | \`\`\`
593 | 
594 | ### Problem: "Tool has no description"
595 | 
596 | **Cause**: HTTP Request Tool or Code Tool missing toolDescription/description
597 | 
598 | **Solution**:
599 | \`\`\`typescript
600 | {
601 |   type: "updateNode",
602 |   nodeName: "HTTP Request Tool",
603 |   updates: {
604 |     "parameters.toolDescription": "Call weather API to get current conditions for a city"
605 |   }
606 | }
607 | \`\`\`
608 | 
609 | ### Problem: "Streaming mode not working"
610 | 
611 | **Causes**:
612 | 1. Chat Trigger not set to streaming
613 | 2. AI Agent has main output connections
614 | 3. Target of Chat Trigger is not AI Agent
615 | 
616 | **Solution**:
617 | \`\`\`typescript
618 | // 1. Set Chat Trigger to streaming
619 | {
620 |   type: "updateNode",
621 |   nodeName: "Chat Trigger",
622 |   updates: {
623 |     "parameters.options.responseMode": "streaming"
624 |   }
625 | }
626 | 
627 | // 2. Remove AI Agent main outputs
628 | {
629 |   type: "removeConnection",
630 |   source: "AI Agent",
631 |   target: "Any Output Node"
632 | }
633 | \`\`\`
634 | 
635 | ### Problem: "Agent keeps looping"
636 | 
637 | **Cause**: Tool not returning proper response or agent stuck in reasoning loop
638 | 
639 | **Solutions**:
640 | 1. Set maxIterations lower: \`"parameters.maxIterations": 5\`
641 | 2. Improve tool descriptions to be more specific
642 | 3. Add system message guidance: "Use tools efficiently, don't repeat actions"
643 | 
644 | ---
645 | 
646 | ## Quick Reference
647 | 
648 | ### Essential Tools
649 | 
650 | | Tool | Purpose | Key Parameters |
651 | |------|---------|----------------|
652 | | HTTP Request Tool | API calls | toolDescription, url, placeholders |
653 | | Code Tool | Custom logic | name, description, code, inputSchema |
654 | | Vector Store Tool | Knowledge search | description, topK |
655 | | AI Agent Tool | Sub-agents | name, description, systemMessage |
656 | | MCP Client Tool | MCP protocol | description, mcpServer, tool |
657 | 
658 | ### Connection Quick Codes
659 | 
660 | \`\`\`typescript
661 | // Language Model → AI Agent
662 | sourceOutput: "ai_languageModel"
663 | 
664 | // Tool → AI Agent
665 | sourceOutput: "ai_tool"
666 | 
667 | // Memory → AI Agent
668 | sourceOutput: "ai_memory"
669 | 
670 | // Parser → AI Agent
671 | sourceOutput: "ai_outputParser"
672 | 
673 | // Embeddings → Vector Store
674 | sourceOutput: "ai_embedding"
675 | 
676 | // Vector Store → Vector Store Tool
677 | sourceOutput: "ai_vectorStore"
678 | \`\`\`
679 | 
680 | ### Validation Command
681 | 
682 | \`\`\`typescript
683 | n8n_validate_workflow({id: "workflow_id"})
684 | \`\`\`
685 | 
686 | ---
687 | 
688 | ## Related Resources
689 | 
690 | - **FINAL_AI_VALIDATION_SPEC.md**: Complete validation rules
691 | - **n8n_update_partial_workflow**: Workflow modification tool
692 | - **search_nodes({query: "AI", includeExamples: true})**: Find AI nodes with examples
693 | - **get_node_essentials({nodeType: "...", includeExamples: true})**: Node details with examples
694 | 
695 | ---
696 | 
697 | *This guide is part of the n8n-mcp documentation system. For questions or issues, refer to the validation spec or use tools_documentation() for specific topics.*`,
698 |     parameters: {},
699 |     returns: 'Complete AI Agents guide with architecture, patterns, validation, and troubleshooting',
700 |     examples: [
701 |       'tools_documentation({topic: "ai_agents_guide"}) - Full guide',
702 |       'tools_documentation({topic: "ai_agents_guide", depth: "essentials"}) - Quick reference',
703 |       'When user asks about AI Agents, Chat Trigger, or building AI workflows → Point to this guide'
704 |     ],
705 |     useCases: [
706 |       'Learning AI Agent architecture in n8n',
707 |       'Understanding AI connection types and patterns',
708 |       'Building first AI Agent workflow step-by-step',
709 |       'Implementing advanced patterns (streaming, fallback, RAG, multi-agent)',
710 |       'Troubleshooting AI workflow issues',
711 |       'Validating AI workflows before deployment',
712 |       'Quick reference for connection types and tools'
713 |     ],
714 |     performance: 'N/A - Static documentation',
715 |     bestPractices: [
716 |       'Reference this guide when users ask about AI Agents',
717 |       'Point to specific sections based on user needs',
718 |       'Combine with search_nodes(includeExamples=true) for working examples',
719 |       'Validate workflows after following guide instructions',
720 |       'Use FINAL_AI_VALIDATION_SPEC.md for detailed requirements'
721 |     ],
722 |     pitfalls: [
723 |       'This is a guide, not an executable tool',
724 |       'Always validate workflows after making changes',
725 |       'AI connections require sourceOutput parameter',
726 |       'Streaming mode has specific constraints',
727 |       'Some features require specific AI Agent versions (v2.1+ for fallback)'
728 |     ],
729 |     relatedTools: [
730 |       'n8n_create_workflow',
731 |       'n8n_update_partial_workflow',
732 |       'n8n_validate_workflow',
733 |       'search_nodes',
734 |       'get_node_essentials',
735 |       'list_ai_tools'
736 |     ]
737 |   }
738 | };
739 | 
```

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

```typescript
  1 | import { describe, it, expect, beforeEach, vi } from 'vitest';
  2 | import { NodeRepository } from '@/database/node-repository';
  3 | import { DatabaseAdapter, PreparedStatement, RunResult } from '@/database/database-adapter';
  4 | 
  5 | // Mock DatabaseAdapter for testing the new operation methods
  6 | class MockDatabaseAdapter implements DatabaseAdapter {
  7 |   private statements = new Map<string, MockPreparedStatement>();
  8 |   private mockNodes = new Map<string, any>();
  9 | 
 10 |   prepare = vi.fn((sql: string) => {
 11 |     if (!this.statements.has(sql)) {
 12 |       this.statements.set(sql, new MockPreparedStatement(sql, this.mockNodes));
 13 |     }
 14 |     return this.statements.get(sql)!;
 15 |   });
 16 | 
 17 |   exec = vi.fn();
 18 |   close = vi.fn();
 19 |   pragma = vi.fn();
 20 |   transaction = vi.fn((fn: () => any) => fn());
 21 |   checkFTS5Support = vi.fn(() => true);
 22 |   inTransaction = false;
 23 | 
 24 |   // Test helper to set mock data
 25 |   _setMockNode(nodeType: string, value: any) {
 26 |     this.mockNodes.set(nodeType, value);
 27 |   }
 28 | }
 29 | 
 30 | class MockPreparedStatement implements PreparedStatement {
 31 |   run = vi.fn((...params: any[]): RunResult => ({ changes: 1, lastInsertRowid: 1 }));
 32 |   get = vi.fn();
 33 |   all = vi.fn(() => []);
 34 |   iterate = vi.fn();
 35 |   pluck = vi.fn(() => this);
 36 |   expand = vi.fn(() => this);
 37 |   raw = vi.fn(() => this);
 38 |   columns = vi.fn(() => []);
 39 |   bind = vi.fn(() => this);
 40 | 
 41 |   constructor(private sql: string, private mockNodes: Map<string, any>) {
 42 |     // Configure get() to return node data
 43 |     if (sql.includes('SELECT * FROM nodes WHERE node_type = ?')) {
 44 |       this.get = vi.fn((nodeType: string) => this.mockNodes.get(nodeType));
 45 |     }
 46 | 
 47 |     // Configure all() for getAllNodes
 48 |     if (sql.includes('SELECT * FROM nodes ORDER BY display_name')) {
 49 |       this.all = vi.fn(() => Array.from(this.mockNodes.values()));
 50 |     }
 51 |   }
 52 | }
 53 | 
 54 | describe('NodeRepository - Operations and Resources', () => {
 55 |   let repository: NodeRepository;
 56 |   let mockAdapter: MockDatabaseAdapter;
 57 | 
 58 |   beforeEach(() => {
 59 |     mockAdapter = new MockDatabaseAdapter();
 60 |     repository = new NodeRepository(mockAdapter);
 61 |   });
 62 | 
 63 |   describe('getNodeOperations', () => {
 64 |     it('should extract operations from array format', () => {
 65 |       const mockNode = {
 66 |         node_type: 'nodes-base.httpRequest',
 67 |         display_name: 'HTTP Request',
 68 |         operations: JSON.stringify([
 69 |           { name: 'get', displayName: 'GET' },
 70 |           { name: 'post', displayName: 'POST' }
 71 |         ]),
 72 |         properties_schema: '[]',
 73 |         credentials_required: '[]'
 74 |       };
 75 | 
 76 |       mockAdapter._setMockNode('nodes-base.httpRequest', mockNode);
 77 | 
 78 |       const operations = repository.getNodeOperations('nodes-base.httpRequest');
 79 | 
 80 |       expect(operations).toEqual([
 81 |         { name: 'get', displayName: 'GET' },
 82 |         { name: 'post', displayName: 'POST' }
 83 |       ]);
 84 |     });
 85 | 
 86 |     it('should extract operations from object format grouped by resource', () => {
 87 |       const mockNode = {
 88 |         node_type: 'nodes-base.slack',
 89 |         display_name: 'Slack',
 90 |         operations: JSON.stringify({
 91 |           message: [
 92 |             { name: 'send', displayName: 'Send Message' },
 93 |             { name: 'update', displayName: 'Update Message' }
 94 |           ],
 95 |           channel: [
 96 |             { name: 'create', displayName: 'Create Channel' },
 97 |             { name: 'archive', displayName: 'Archive Channel' }
 98 |           ]
 99 |         }),
100 |         properties_schema: '[]',
101 |         credentials_required: '[]'
102 |       };
103 | 
104 |       mockAdapter._setMockNode('nodes-base.slack', mockNode);
105 | 
106 |       const allOperations = repository.getNodeOperations('nodes-base.slack');
107 |       const messageOperations = repository.getNodeOperations('nodes-base.slack', 'message');
108 | 
109 |       expect(allOperations).toHaveLength(4);
110 |       expect(messageOperations).toEqual([
111 |         { name: 'send', displayName: 'Send Message' },
112 |         { name: 'update', displayName: 'Update Message' }
113 |       ]);
114 |     });
115 | 
116 |     it('should extract operations from properties with operation field', () => {
117 |       const mockNode = {
118 |         node_type: 'nodes-base.googleSheets',
119 |         display_name: 'Google Sheets',
120 |         operations: '[]',
121 |         properties_schema: JSON.stringify([
122 |           {
123 |             name: 'resource',
124 |             type: 'options',
125 |             options: [{ name: 'sheet', displayName: 'Sheet' }]
126 |           },
127 |           {
128 |             name: 'operation',
129 |             type: 'options',
130 |             displayOptions: {
131 |               show: {
132 |                 resource: ['sheet']
133 |               }
134 |             },
135 |             options: [
136 |               { name: 'append', displayName: 'Append Row' },
137 |               { name: 'read', displayName: 'Read Rows' }
138 |             ]
139 |           }
140 |         ]),
141 |         credentials_required: '[]'
142 |       };
143 | 
144 |       mockAdapter._setMockNode('nodes-base.googleSheets', mockNode);
145 | 
146 |       const operations = repository.getNodeOperations('nodes-base.googleSheets');
147 | 
148 |       expect(operations).toEqual([
149 |         { name: 'append', displayName: 'Append Row' },
150 |         { name: 'read', displayName: 'Read Rows' }
151 |       ]);
152 |     });
153 | 
154 |     it('should filter operations by resource when specified', () => {
155 |       const mockNode = {
156 |         node_type: 'nodes-base.googleSheets',
157 |         display_name: 'Google Sheets',
158 |         operations: '[]',
159 |         properties_schema: JSON.stringify([
160 |           {
161 |             name: 'operation',
162 |             type: 'options',
163 |             displayOptions: {
164 |               show: {
165 |                 resource: ['sheet']
166 |               }
167 |             },
168 |             options: [
169 |               { name: 'append', displayName: 'Append Row' }
170 |             ]
171 |           },
172 |           {
173 |             name: 'operation',
174 |             type: 'options',
175 |             displayOptions: {
176 |               show: {
177 |                 resource: ['cell']
178 |               }
179 |             },
180 |             options: [
181 |               { name: 'update', displayName: 'Update Cell' }
182 |             ]
183 |           }
184 |         ]),
185 |         credentials_required: '[]'
186 |       };
187 | 
188 |       mockAdapter._setMockNode('nodes-base.googleSheets', mockNode);
189 | 
190 |       const sheetOperations = repository.getNodeOperations('nodes-base.googleSheets', 'sheet');
191 |       const cellOperations = repository.getNodeOperations('nodes-base.googleSheets', 'cell');
192 | 
193 |       expect(sheetOperations).toEqual([{ name: 'append', displayName: 'Append Row' }]);
194 |       expect(cellOperations).toEqual([{ name: 'update', displayName: 'Update Cell' }]);
195 |     });
196 | 
197 |     it('should return empty array for non-existent node', () => {
198 |       const operations = repository.getNodeOperations('nodes-base.nonexistent');
199 |       expect(operations).toEqual([]);
200 |     });
201 | 
202 |     it('should handle nodes without operations', () => {
203 |       const mockNode = {
204 |         node_type: 'nodes-base.simple',
205 |         display_name: 'Simple Node',
206 |         operations: '[]',
207 |         properties_schema: '[]',
208 |         credentials_required: '[]'
209 |       };
210 | 
211 |       mockAdapter._setMockNode('nodes-base.simple', mockNode);
212 | 
213 |       const operations = repository.getNodeOperations('nodes-base.simple');
214 |       expect(operations).toEqual([]);
215 |     });
216 | 
217 |     it('should handle malformed operations JSON gracefully', () => {
218 |       const mockNode = {
219 |         node_type: 'nodes-base.broken',
220 |         display_name: 'Broken Node',
221 |         operations: '{invalid json}',
222 |         properties_schema: '[]',
223 |         credentials_required: '[]'
224 |       };
225 | 
226 |       mockAdapter._setMockNode('nodes-base.broken', mockNode);
227 | 
228 |       const operations = repository.getNodeOperations('nodes-base.broken');
229 |       expect(operations).toEqual([]);
230 |     });
231 |   });
232 | 
233 |   describe('getNodeResources', () => {
234 |     it('should extract resources from properties', () => {
235 |       const mockNode = {
236 |         node_type: 'nodes-base.slack',
237 |         display_name: 'Slack',
238 |         operations: '[]',
239 |         properties_schema: JSON.stringify([
240 |           {
241 |             name: 'resource',
242 |             type: 'options',
243 |             options: [
244 |               { name: 'message', displayName: 'Message' },
245 |               { name: 'channel', displayName: 'Channel' },
246 |               { name: 'user', displayName: 'User' }
247 |             ]
248 |           }
249 |         ]),
250 |         credentials_required: '[]'
251 |       };
252 | 
253 |       mockAdapter._setMockNode('nodes-base.slack', mockNode);
254 | 
255 |       const resources = repository.getNodeResources('nodes-base.slack');
256 | 
257 |       expect(resources).toEqual([
258 |         { name: 'message', displayName: 'Message' },
259 |         { name: 'channel', displayName: 'Channel' },
260 |         { name: 'user', displayName: 'User' }
261 |       ]);
262 |     });
263 | 
264 |     it('should return empty array for node without resources', () => {
265 |       const mockNode = {
266 |         node_type: 'nodes-base.simple',
267 |         display_name: 'Simple Node',
268 |         operations: '[]',
269 |         properties_schema: JSON.stringify([
270 |           { name: 'url', type: 'string' }
271 |         ]),
272 |         credentials_required: '[]'
273 |       };
274 | 
275 |       mockAdapter._setMockNode('nodes-base.simple', mockNode);
276 | 
277 |       const resources = repository.getNodeResources('nodes-base.simple');
278 |       expect(resources).toEqual([]);
279 |     });
280 | 
281 |     it('should return empty array for non-existent node', () => {
282 |       const resources = repository.getNodeResources('nodes-base.nonexistent');
283 |       expect(resources).toEqual([]);
284 |     });
285 | 
286 |     it('should handle multiple resource properties', () => {
287 |       const mockNode = {
288 |         node_type: 'nodes-base.multi',
289 |         display_name: 'Multi Resource Node',
290 |         operations: '[]',
291 |         properties_schema: JSON.stringify([
292 |           {
293 |             name: 'resource',
294 |             type: 'options',
295 |             options: [{ name: 'type1', displayName: 'Type 1' }]
296 |           },
297 |           {
298 |             name: 'resource',
299 |             type: 'options',
300 |             options: [{ name: 'type2', displayName: 'Type 2' }]
301 |           }
302 |         ]),
303 |         credentials_required: '[]'
304 |       };
305 | 
306 |       mockAdapter._setMockNode('nodes-base.multi', mockNode);
307 | 
308 |       const resources = repository.getNodeResources('nodes-base.multi');
309 | 
310 |       expect(resources).toEqual([
311 |         { name: 'type1', displayName: 'Type 1' },
312 |         { name: 'type2', displayName: 'Type 2' }
313 |       ]);
314 |     });
315 |   });
316 | 
317 |   describe('getOperationsForResource', () => {
318 |     it('should return operations for specific resource', () => {
319 |       const mockNode = {
320 |         node_type: 'nodes-base.slack',
321 |         display_name: 'Slack',
322 |         operations: '[]',
323 |         properties_schema: JSON.stringify([
324 |           {
325 |             name: 'operation',
326 |             type: 'options',
327 |             displayOptions: {
328 |               show: {
329 |                 resource: ['message']
330 |               }
331 |             },
332 |             options: [
333 |               { name: 'send', displayName: 'Send Message' },
334 |               { name: 'update', displayName: 'Update Message' }
335 |             ]
336 |           },
337 |           {
338 |             name: 'operation',
339 |             type: 'options',
340 |             displayOptions: {
341 |               show: {
342 |                 resource: ['channel']
343 |               }
344 |             },
345 |             options: [
346 |               { name: 'create', displayName: 'Create Channel' }
347 |             ]
348 |           }
349 |         ]),
350 |         credentials_required: '[]'
351 |       };
352 | 
353 |       mockAdapter._setMockNode('nodes-base.slack', mockNode);
354 | 
355 |       const messageOps = repository.getOperationsForResource('nodes-base.slack', 'message');
356 |       const channelOps = repository.getOperationsForResource('nodes-base.slack', 'channel');
357 |       const nonExistentOps = repository.getOperationsForResource('nodes-base.slack', 'nonexistent');
358 | 
359 |       expect(messageOps).toEqual([
360 |         { name: 'send', displayName: 'Send Message' },
361 |         { name: 'update', displayName: 'Update Message' }
362 |       ]);
363 |       expect(channelOps).toEqual([
364 |         { name: 'create', displayName: 'Create Channel' }
365 |       ]);
366 |       expect(nonExistentOps).toEqual([]);
367 |     });
368 | 
369 |     it('should handle array format for resource display options', () => {
370 |       const mockNode = {
371 |         node_type: 'nodes-base.multi',
372 |         display_name: 'Multi Node',
373 |         operations: '[]',
374 |         properties_schema: JSON.stringify([
375 |           {
376 |             name: 'operation',
377 |             type: 'options',
378 |             displayOptions: {
379 |               show: {
380 |                 resource: ['message', 'channel'] // Array format
381 |               }
382 |             },
383 |             options: [
384 |               { name: 'list', displayName: 'List Items' }
385 |             ]
386 |           }
387 |         ]),
388 |         credentials_required: '[]'
389 |       };
390 | 
391 |       mockAdapter._setMockNode('nodes-base.multi', mockNode);
392 | 
393 |       const messageOps = repository.getOperationsForResource('nodes-base.multi', 'message');
394 |       const channelOps = repository.getOperationsForResource('nodes-base.multi', 'channel');
395 |       const otherOps = repository.getOperationsForResource('nodes-base.multi', 'other');
396 | 
397 |       expect(messageOps).toEqual([{ name: 'list', displayName: 'List Items' }]);
398 |       expect(channelOps).toEqual([{ name: 'list', displayName: 'List Items' }]);
399 |       expect(otherOps).toEqual([]);
400 |     });
401 | 
402 |     it('should return empty array for non-existent node', () => {
403 |       const operations = repository.getOperationsForResource('nodes-base.nonexistent', 'message');
404 |       expect(operations).toEqual([]);
405 |     });
406 | 
407 |     it('should handle string format for single resource', () => {
408 |       const mockNode = {
409 |         node_type: 'nodes-base.single',
410 |         display_name: 'Single Node',
411 |         operations: '[]',
412 |         properties_schema: JSON.stringify([
413 |           {
414 |             name: 'operation',
415 |             type: 'options',
416 |             displayOptions: {
417 |               show: {
418 |                 resource: 'document' // String format
419 |               }
420 |             },
421 |             options: [
422 |               { name: 'create', displayName: 'Create Document' }
423 |             ]
424 |           }
425 |         ]),
426 |         credentials_required: '[]'
427 |       };
428 | 
429 |       mockAdapter._setMockNode('nodes-base.single', mockNode);
430 | 
431 |       const operations = repository.getOperationsForResource('nodes-base.single', 'document');
432 |       expect(operations).toEqual([{ name: 'create', displayName: 'Create Document' }]);
433 |     });
434 |   });
435 | 
436 |   describe('getAllOperations', () => {
437 |     it('should collect operations from all nodes', () => {
438 |       const mockNodes = [
439 |         {
440 |           node_type: 'nodes-base.httpRequest',
441 |           display_name: 'HTTP Request',
442 |           operations: JSON.stringify([{ name: 'execute' }]),
443 |           properties_schema: '[]',
444 |           credentials_required: '[]'
445 |         },
446 |         {
447 |           node_type: 'nodes-base.slack',
448 |           display_name: 'Slack',
449 |           operations: JSON.stringify([{ name: 'send' }]),
450 |           properties_schema: '[]',
451 |           credentials_required: '[]'
452 |         },
453 |         {
454 |           node_type: 'nodes-base.empty',
455 |           display_name: 'Empty Node',
456 |           operations: '[]',
457 |           properties_schema: '[]',
458 |           credentials_required: '[]'
459 |         }
460 |       ];
461 | 
462 |       mockNodes.forEach(node => {
463 |         mockAdapter._setMockNode(node.node_type, node);
464 |       });
465 | 
466 |       const allOperations = repository.getAllOperations();
467 | 
468 |       expect(allOperations.size).toBe(2); // Only nodes with operations
469 |       expect(allOperations.get('nodes-base.httpRequest')).toEqual([{ name: 'execute' }]);
470 |       expect(allOperations.get('nodes-base.slack')).toEqual([{ name: 'send' }]);
471 |       expect(allOperations.has('nodes-base.empty')).toBe(false);
472 |     });
473 | 
474 |     it('should handle empty node list', () => {
475 |       const allOperations = repository.getAllOperations();
476 |       expect(allOperations.size).toBe(0);
477 |     });
478 |   });
479 | 
480 |   describe('getAllResources', () => {
481 |     it('should collect resources from all nodes', () => {
482 |       const mockNodes = [
483 |         {
484 |           node_type: 'nodes-base.slack',
485 |           display_name: 'Slack',
486 |           operations: '[]',
487 |           properties_schema: JSON.stringify([
488 |             {
489 |               name: 'resource',
490 |               options: [{ name: 'message' }, { name: 'channel' }]
491 |             }
492 |           ]),
493 |           credentials_required: '[]'
494 |         },
495 |         {
496 |           node_type: 'nodes-base.sheets',
497 |           display_name: 'Google Sheets',
498 |           operations: '[]',
499 |           properties_schema: JSON.stringify([
500 |             {
501 |               name: 'resource',
502 |               options: [{ name: 'sheet' }]
503 |             }
504 |           ]),
505 |           credentials_required: '[]'
506 |         },
507 |         {
508 |           node_type: 'nodes-base.simple',
509 |           display_name: 'Simple Node',
510 |           operations: '[]',
511 |           properties_schema: '[]', // No resources
512 |           credentials_required: '[]'
513 |         }
514 |       ];
515 | 
516 |       mockNodes.forEach(node => {
517 |         mockAdapter._setMockNode(node.node_type, node);
518 |       });
519 | 
520 |       const allResources = repository.getAllResources();
521 | 
522 |       expect(allResources.size).toBe(2); // Only nodes with resources
523 |       expect(allResources.get('nodes-base.slack')).toEqual([
524 |         { name: 'message' },
525 |         { name: 'channel' }
526 |       ]);
527 |       expect(allResources.get('nodes-base.sheets')).toEqual([{ name: 'sheet' }]);
528 |       expect(allResources.has('nodes-base.simple')).toBe(false);
529 |     });
530 | 
531 |     it('should handle empty node list', () => {
532 |       const allResources = repository.getAllResources();
533 |       expect(allResources.size).toBe(0);
534 |     });
535 |   });
536 | 
537 |   describe('edge cases and error handling', () => {
538 |     it('should handle null or undefined properties gracefully', () => {
539 |       const mockNode = {
540 |         node_type: 'nodes-base.null',
541 |         display_name: 'Null Node',
542 |         operations: null,
543 |         properties_schema: null,
544 |         credentials_required: null
545 |       };
546 | 
547 |       mockAdapter._setMockNode('nodes-base.null', mockNode);
548 | 
549 |       const operations = repository.getNodeOperations('nodes-base.null');
550 |       const resources = repository.getNodeResources('nodes-base.null');
551 | 
552 |       expect(operations).toEqual([]);
553 |       expect(resources).toEqual([]);
554 |     });
555 | 
556 |     it('should handle complex nested operation properties', () => {
557 |       const mockNode = {
558 |         node_type: 'nodes-base.complex',
559 |         display_name: 'Complex Node',
560 |         operations: '[]',
561 |         properties_schema: JSON.stringify([
562 |           {
563 |             name: 'operation',
564 |             type: 'options',
565 |             displayOptions: {
566 |               show: {
567 |                 resource: ['message'],
568 |                 mode: ['advanced']
569 |               }
570 |             },
571 |             options: [
572 |               { name: 'complexOperation', displayName: 'Complex Operation' }
573 |             ]
574 |           }
575 |         ]),
576 |         credentials_required: '[]'
577 |       };
578 | 
579 |       mockAdapter._setMockNode('nodes-base.complex', mockNode);
580 | 
581 |       const operations = repository.getNodeOperations('nodes-base.complex');
582 |       expect(operations).toEqual([{ name: 'complexOperation', displayName: 'Complex Operation' }]);
583 |     });
584 | 
585 |     it('should handle operations with mixed data types', () => {
586 |       const mockNode = {
587 |         node_type: 'nodes-base.mixed',
588 |         display_name: 'Mixed Node',
589 |         operations: JSON.stringify({
590 |           string_operation: 'invalid', // Should be array
591 |           valid_operations: [{ name: 'valid' }],
592 |           nested_object: { inner: [{ name: 'nested' }] }
593 |         }),
594 |         properties_schema: '[]',
595 |         credentials_required: '[]'
596 |       };
597 | 
598 |       mockAdapter._setMockNode('nodes-base.mixed', mockNode);
599 | 
600 |       const operations = repository.getNodeOperations('nodes-base.mixed');
601 |       expect(operations).toEqual([{ name: 'valid' }]); // Only valid array operations
602 |     });
603 | 
604 |     it('should handle very deeply nested properties', () => {
605 |       const deepProperties = [
606 |         {
607 |           name: 'resource',
608 |           options: [{ name: 'deep', displayName: 'Deep Resource' }],
609 |           nested: {
610 |             level1: {
611 |               level2: {
612 |                 operations: [{ name: 'deep_operation' }]
613 |               }
614 |             }
615 |           }
616 |         }
617 |       ];
618 | 
619 |       const mockNode = {
620 |         node_type: 'nodes-base.deep',
621 |         display_name: 'Deep Node',
622 |         operations: '[]',
623 |         properties_schema: JSON.stringify(deepProperties),
624 |         credentials_required: '[]'
625 |       };
626 | 
627 |       mockAdapter._setMockNode('nodes-base.deep', mockNode);
628 | 
629 |       const resources = repository.getNodeResources('nodes-base.deep');
630 |       expect(resources).toEqual([{ name: 'deep', displayName: 'Deep Resource' }]);
631 |     });
632 |   });
633 | });
```

--------------------------------------------------------------------------------
/tests/unit/templates/template-repository-security.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, vi } from 'vitest';
  2 | import { TemplateRepository } from '../../../src/templates/template-repository';
  3 | import { DatabaseAdapter, PreparedStatement, RunResult } from '../../../src/database/database-adapter';
  4 | 
  5 | // Mock logger
  6 | vi.mock('../../../src/utils/logger', () => ({
  7 |   logger: {
  8 |     info: vi.fn(),
  9 |     warn: vi.fn(),
 10 |     error: vi.fn(),
 11 |     debug: vi.fn()
 12 |   }
 13 | }));
 14 | 
 15 | // Mock template sanitizer
 16 | vi.mock('../../../src/utils/template-sanitizer', () => {
 17 |   class MockTemplateSanitizer {
 18 |     sanitizeWorkflow = vi.fn((workflow) => ({ sanitized: workflow, wasModified: false }));
 19 |     detectTokens = vi.fn(() => []);
 20 |   }
 21 |   
 22 |   return {
 23 |     TemplateSanitizer: MockTemplateSanitizer
 24 |   };
 25 | });
 26 | 
 27 | // Create mock database adapter
 28 | class MockDatabaseAdapter implements DatabaseAdapter {
 29 |   private statements = new Map<string, MockPreparedStatement>();
 30 |   private execCalls: string[] = [];
 31 |   private _fts5Support = true;
 32 |   
 33 |   prepare = vi.fn((sql: string) => {
 34 |     if (!this.statements.has(sql)) {
 35 |       this.statements.set(sql, new MockPreparedStatement(sql));
 36 |     }
 37 |     return this.statements.get(sql)!;
 38 |   });
 39 |   
 40 |   exec = vi.fn((sql: string) => {
 41 |     this.execCalls.push(sql);
 42 |   });
 43 |   close = vi.fn();
 44 |   pragma = vi.fn();
 45 |   transaction = vi.fn((fn: () => any) => fn());
 46 |   checkFTS5Support = vi.fn(() => this._fts5Support);
 47 |   inTransaction = false;
 48 |   
 49 |   // Test helpers
 50 |   _setFTS5Support(supported: boolean) {
 51 |     this._fts5Support = supported;
 52 |   }
 53 |   
 54 |   _getStatement(sql: string) {
 55 |     return this.statements.get(sql);
 56 |   }
 57 |   
 58 |   _getExecCalls() {
 59 |     return this.execCalls;
 60 |   }
 61 |   
 62 |   _clearExecCalls() {
 63 |     this.execCalls = [];
 64 |   }
 65 | }
 66 | 
 67 | class MockPreparedStatement implements PreparedStatement {
 68 |   public mockResults: any[] = [];
 69 |   public capturedParams: any[][] = [];
 70 |   
 71 |   run = vi.fn((...params: any[]): RunResult => {
 72 |     this.capturedParams.push(params);
 73 |     return { changes: 1, lastInsertRowid: 1 };
 74 |   });
 75 |   
 76 |   get = vi.fn((...params: any[]) => {
 77 |     this.capturedParams.push(params);
 78 |     return this.mockResults[0] || null;
 79 |   });
 80 |   
 81 |   all = vi.fn((...params: any[]) => {
 82 |     this.capturedParams.push(params);
 83 |     return this.mockResults;
 84 |   });
 85 |   
 86 |   iterate = vi.fn();
 87 |   pluck = vi.fn(() => this);
 88 |   expand = vi.fn(() => this);
 89 |   raw = vi.fn(() => this);
 90 |   columns = vi.fn(() => []);
 91 |   bind = vi.fn(() => this);
 92 |   
 93 |   constructor(private sql: string) {}
 94 |   
 95 |   // Test helpers
 96 |   _setMockResults(results: any[]) {
 97 |     this.mockResults = results;
 98 |   }
 99 |   
100 |   _getCapturedParams() {
101 |     return this.capturedParams;
102 |   }
103 | }
104 | 
105 | describe('TemplateRepository - Security Tests', () => {
106 |   let repository: TemplateRepository;
107 |   let mockAdapter: MockDatabaseAdapter;
108 |   
109 |   beforeEach(() => {
110 |     vi.clearAllMocks();
111 |     mockAdapter = new MockDatabaseAdapter();
112 |     repository = new TemplateRepository(mockAdapter);
113 |   });
114 |   
115 |   describe('SQL Injection Prevention', () => {
116 |     describe('searchTemplatesByMetadata', () => {
117 |       it('should prevent SQL injection in category parameter', () => {
118 |         const maliciousCategory = "'; DROP TABLE templates; --";
119 |         
120 |         const stmt = new MockPreparedStatement('');
121 |         stmt._setMockResults([]);
122 |         mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
123 |         
124 |         repository.searchTemplatesByMetadata({
125 |           category: maliciousCategory}, 10, 0);
126 |         
127 |         // Should use parameterized queries, not inject SQL
128 |         const capturedParams = stmt._getCapturedParams();
129 |         expect(capturedParams.length).toBeGreaterThan(0);
130 |         // The parameter should be the sanitized version (JSON.stringify then slice to remove quotes)
131 |         const expectedParam = JSON.stringify(maliciousCategory).slice(1, -1);
132 |         // capturedParams[0] is the first call's parameters array
133 |         expect(capturedParams[0][0]).toBe(expectedParam);
134 |         
135 |         // Verify the SQL doesn't contain the malicious content directly
136 |         const prepareCall = mockAdapter.prepare.mock.calls[0][0];
137 |         expect(prepareCall).not.toContain('DROP TABLE');
138 |         expect(prepareCall).toContain("json_extract(metadata_json, '$.categories') LIKE '%' || ? || '%'");
139 |       });
140 | 
141 |       it('should prevent SQL injection in requiredService parameter', () => {
142 |         const maliciousService = "'; UNION SELECT * FROM sqlite_master; --";
143 |         
144 |         const stmt = new MockPreparedStatement('');
145 |         stmt._setMockResults([]);
146 |         mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
147 |         
148 |         repository.searchTemplatesByMetadata({
149 |           requiredService: maliciousService}, 10, 0);
150 |         
151 |         const capturedParams = stmt._getCapturedParams();
152 |         const expectedParam = JSON.stringify(maliciousService).slice(1, -1);
153 |         // capturedParams[0] is the first call's parameters array
154 |         expect(capturedParams[0][0]).toBe(expectedParam);
155 |         
156 |         const prepareCall = mockAdapter.prepare.mock.calls[0][0];
157 |         expect(prepareCall).not.toContain('UNION SELECT');
158 |         expect(prepareCall).toContain("json_extract(metadata_json, '$.required_services') LIKE '%' || ? || '%'");
159 |       });
160 | 
161 |       it('should prevent SQL injection in targetAudience parameter', () => {
162 |         const maliciousAudience = "administrators'; DELETE FROM templates WHERE '1'='1";
163 |         
164 |         const stmt = new MockPreparedStatement('');
165 |         stmt._setMockResults([]);
166 |         mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
167 |         
168 |         repository.searchTemplatesByMetadata({
169 |           targetAudience: maliciousAudience}, 10, 0);
170 |         
171 |         const capturedParams = stmt._getCapturedParams();
172 |         const expectedParam = JSON.stringify(maliciousAudience).slice(1, -1);
173 |         // capturedParams[0] is the first call's parameters array
174 |         expect(capturedParams[0][0]).toBe(expectedParam);
175 |         
176 |         const prepareCall = mockAdapter.prepare.mock.calls[0][0];
177 |         expect(prepareCall).not.toContain('DELETE FROM');
178 |         expect(prepareCall).toContain("json_extract(metadata_json, '$.target_audience') LIKE '%' || ? || '%'");
179 |       });
180 | 
181 |       it('should safely handle special characters in parameters', () => {
182 |         const specialChars = "test'with\"quotes\\and%wildcards_and[brackets]";
183 |         
184 |         const stmt = new MockPreparedStatement('');
185 |         stmt._setMockResults([]);
186 |         mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
187 |         
188 |         repository.searchTemplatesByMetadata({
189 |           category: specialChars}, 10, 0);
190 |         
191 |         const capturedParams = stmt._getCapturedParams();
192 |         const expectedParam = JSON.stringify(specialChars).slice(1, -1);
193 |         // capturedParams[0] is the first call's parameters array
194 |         expect(capturedParams[0][0]).toBe(expectedParam);
195 |         
196 |         // Should use parameterized query
197 |         const prepareCall = mockAdapter.prepare.mock.calls[0][0];
198 |         expect(prepareCall).toContain("json_extract(metadata_json, '$.categories') LIKE '%' || ? || '%'");
199 |       });
200 | 
201 |       it('should prevent injection through numeric parameters', () => {
202 |         const stmt = new MockPreparedStatement('');
203 |         stmt._setMockResults([]);
204 |         mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
205 |         
206 |         // Try to inject through numeric parameters
207 |         repository.searchTemplatesByMetadata({maxSetupMinutes: 999999999, // Large number
208 |           minSetupMinutes: -999999999 // Negative number
209 |         }, 10, 0);
210 |         
211 |         const capturedParams = stmt._getCapturedParams();
212 |         // capturedParams[0] is the first call's parameters array
213 |         expect(capturedParams[0]).toContain(999999999);
214 |         expect(capturedParams[0]).toContain(-999999999);
215 |         
216 |         // Should use CAST and parameterized queries
217 |         const prepareCall = mockAdapter.prepare.mock.calls[0][0];
218 |         expect(prepareCall).toContain('CAST(json_extract(metadata_json, \'$.estimated_setup_minutes\') AS INTEGER)');
219 |       });
220 |     });
221 | 
222 |     describe('getMetadataSearchCount', () => {
223 |       it('should use parameterized queries for count operations', () => {
224 |         const maliciousCategory = "'; DROP TABLE templates; SELECT COUNT(*) FROM sqlite_master WHERE name LIKE '%";
225 |         
226 |         const stmt = new MockPreparedStatement('');
227 |         stmt._setMockResults([{ count: 0 }]);
228 |         mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
229 |         
230 |         repository.getMetadataSearchCount({
231 |           category: maliciousCategory
232 |         });
233 |         
234 |         const capturedParams = stmt._getCapturedParams();
235 |         const expectedParam = JSON.stringify(maliciousCategory).slice(1, -1);
236 |         // capturedParams[0] is the first call's parameters array
237 |         expect(capturedParams[0][0]).toBe(expectedParam);
238 |         
239 |         const prepareCall = mockAdapter.prepare.mock.calls[0][0];
240 |         expect(prepareCall).not.toContain('DROP TABLE');
241 |         expect(prepareCall).toContain('SELECT COUNT(*) as count FROM templates');
242 |       });
243 |     });
244 | 
245 |     describe('updateTemplateMetadata', () => {
246 |       it('should safely handle metadata with special characters', () => {
247 |         const maliciousMetadata = {
248 |           categories: ["automation'; DROP TABLE templates; --"],
249 |           complexity: "simple",
250 |           use_cases: ['SQL injection"test'],
251 |           estimated_setup_minutes: 30,
252 |           required_services: ['api"with\\"quotes'],
253 |           key_features: ["feature's test"],
254 |           target_audience: ['developers\\administrators']
255 |         };
256 |         
257 |         const stmt = new MockPreparedStatement('');
258 |         mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
259 |         
260 |         repository.updateTemplateMetadata(123, maliciousMetadata);
261 |         
262 |         const capturedParams = stmt._getCapturedParams();
263 |         expect(capturedParams[0][0]).toBe(JSON.stringify(maliciousMetadata));
264 |         expect(capturedParams[0][1]).toBe(123);
265 |         
266 |         // Should use parameterized UPDATE
267 |         const prepareCall = mockAdapter.prepare.mock.calls[0][0];
268 |         expect(prepareCall).toContain('UPDATE templates');
269 |         expect(prepareCall).toContain('metadata_json = ?');
270 |         expect(prepareCall).toContain('WHERE id = ?');
271 |         expect(prepareCall).not.toContain('DROP TABLE');
272 |       });
273 |     });
274 | 
275 |     describe('batchUpdateMetadata', () => {
276 |       it('should safely handle batch updates with malicious data', () => {
277 |         const maliciousData = new Map();
278 |         maliciousData.set(1, { categories: ["'; DROP TABLE templates; --"] });
279 |         maliciousData.set(2, { categories: ["normal category"] });
280 |         
281 |         const stmt = new MockPreparedStatement('');
282 |         mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
283 |         
284 |         repository.batchUpdateMetadata(maliciousData);
285 |         
286 |         const capturedParams = stmt._getCapturedParams();
287 |         expect(capturedParams).toHaveLength(2);
288 |         
289 |         // Both calls should be parameterized
290 |         const firstJson = capturedParams[0][0];
291 |         const secondJson = capturedParams[1][0];
292 |         expect(firstJson).toContain("'; DROP TABLE templates; --"); // Should be JSON-encoded
293 |         expect(capturedParams[0][1]).toBe(1);
294 |         expect(secondJson).toContain('normal category');
295 |         expect(capturedParams[1][1]).toBe(2);
296 |       });
297 |     });
298 |   });
299 | 
300 |   describe('JSON Extraction Security', () => {
301 |     it('should safely extract categories from JSON', () => {
302 |       const stmt = new MockPreparedStatement('');
303 |       stmt._setMockResults([]);
304 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
305 |       
306 |       repository.getUniqueCategories();
307 |       
308 |       const prepareCall = mockAdapter.prepare.mock.calls[0][0];
309 |       expect(prepareCall).toContain('json_each(metadata_json, \'$.categories\')');
310 |       expect(prepareCall).not.toContain('eval(');
311 |       expect(prepareCall).not.toContain('exec(');
312 |     });
313 | 
314 |     it('should safely extract target audiences from JSON', () => {
315 |       const stmt = new MockPreparedStatement('');
316 |       stmt._setMockResults([]);
317 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
318 |       
319 |       repository.getUniqueTargetAudiences();
320 |       
321 |       const prepareCall = mockAdapter.prepare.mock.calls[0][0];
322 |       expect(prepareCall).toContain('json_each(metadata_json, \'$.target_audience\')');
323 |       expect(prepareCall).not.toContain('eval(');
324 |     });
325 | 
326 |     it('should safely handle complex JSON structures', () => {
327 |       const stmt = new MockPreparedStatement('');
328 |       stmt._setMockResults([]);
329 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
330 |       
331 |       repository.getTemplatesByCategory('test');
332 |       
333 |       const prepareCall = mockAdapter.prepare.mock.calls[0][0];
334 |       expect(prepareCall).toContain("json_extract(metadata_json, '$.categories') LIKE '%' || ? || '%'");
335 |       
336 |       const capturedParams = stmt._getCapturedParams();
337 |       // Check if parameters were captured
338 |       expect(capturedParams.length).toBeGreaterThan(0);
339 |       // Find the parameter that contains 'test'
340 |       const testParam = capturedParams[0].find((p: any) => typeof p === 'string' && p.includes('test'));
341 |       expect(testParam).toBe('test');
342 |     });
343 |   });
344 | 
345 |   describe('Input Validation and Sanitization', () => {
346 |     it('should handle null and undefined parameters safely', () => {
347 |       const stmt = new MockPreparedStatement('');
348 |       stmt._setMockResults([]);
349 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
350 |       
351 |       repository.searchTemplatesByMetadata({
352 |         category: undefined as any,
353 |         complexity: null as any}, 10, 0);
354 |       
355 |       // Should not break and should exclude undefined/null filters
356 |       const prepareCall = mockAdapter.prepare.mock.calls[0][0];
357 |       expect(prepareCall).toContain('metadata_json IS NOT NULL');
358 |       expect(prepareCall).not.toContain('undefined');
359 |       expect(prepareCall).not.toContain('null');
360 |     });
361 | 
362 |     it('should handle empty string parameters', () => {
363 |       const stmt = new MockPreparedStatement('');
364 |       stmt._setMockResults([]);
365 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
366 |       
367 |       repository.searchTemplatesByMetadata({
368 |         category: '',
369 |         requiredService: '',
370 |         targetAudience: ''}, 10, 0);
371 |       
372 |       // Empty strings should still be processed (might be valid searches)
373 |       const capturedParams = stmt._getCapturedParams();
374 |       const expectedParam = JSON.stringify("").slice(1, -1); // Results in empty string
375 |       // Check if parameters were captured
376 |       expect(capturedParams.length).toBeGreaterThan(0);
377 |       // Check if empty string parameters are present
378 |       const hasEmptyString = capturedParams[0].includes(expectedParam);
379 |       expect(hasEmptyString).toBe(true);
380 |     });
381 | 
382 |     it('should validate numeric ranges', () => {
383 |       const stmt = new MockPreparedStatement('');
384 |       stmt._setMockResults([]);
385 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
386 |       
387 |       repository.searchTemplatesByMetadata({
388 |         maxSetupMinutes: Number.MAX_SAFE_INTEGER,
389 |         minSetupMinutes: Number.MIN_SAFE_INTEGER}, 10, 0);
390 |       
391 |       // Should handle extreme values without breaking
392 |       const capturedParams = stmt._getCapturedParams();
393 |       expect(capturedParams[0]).toContain(Number.MAX_SAFE_INTEGER);
394 |       expect(capturedParams[0]).toContain(Number.MIN_SAFE_INTEGER);
395 |     });
396 | 
397 |     it('should handle Unicode and international characters', () => {
398 |       const unicodeCategory = '自動化'; // Japanese for "automation"
399 |       const emojiAudience = '👩‍💻 developers';
400 |       
401 |       const stmt = new MockPreparedStatement('');
402 |       stmt._setMockResults([]);
403 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
404 |       
405 |       repository.searchTemplatesByMetadata({
406 |         category: unicodeCategory,
407 |         targetAudience: emojiAudience}, 10, 0);
408 |       
409 |       const capturedParams = stmt._getCapturedParams();
410 |       const expectedCategoryParam = JSON.stringify(unicodeCategory).slice(1, -1);
411 |       const expectedAudienceParam = JSON.stringify(emojiAudience).slice(1, -1);
412 |       // capturedParams[0] is the first call's parameters array
413 |       expect(capturedParams[0][0]).toBe(expectedCategoryParam);
414 |       expect(capturedParams[0][1]).toBe(expectedAudienceParam);
415 |     });
416 |   });
417 | 
418 |   describe('Database Schema Security', () => {
419 |     it('should use proper column names without injection', () => {
420 |       const stmt = new MockPreparedStatement('');
421 |       stmt._setMockResults([]);
422 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
423 |       
424 |       repository.searchTemplatesByMetadata({
425 |         category: 'test'}, 10, 0);
426 |       
427 |       const prepareCall = mockAdapter.prepare.mock.calls[0][0];
428 |       
429 |       // Should reference proper column names
430 |       expect(prepareCall).toContain('metadata_json');
431 |       expect(prepareCall).toContain('templates');
432 |       
433 |       // Should not contain dynamic column names that could be injected
434 |       expect(prepareCall).not.toMatch(/SELECT \* FROM \w+;/);
435 |       expect(prepareCall).not.toContain('information_schema');
436 |       expect(prepareCall).not.toContain('sqlite_master');
437 |     });
438 | 
439 |     it('should use proper JSON path syntax', () => {
440 |       const stmt = new MockPreparedStatement('');
441 |       stmt._setMockResults([]);
442 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
443 |       
444 |       repository.getUniqueCategories();
445 |       
446 |       const prepareCall = mockAdapter.prepare.mock.calls[0][0];
447 |       
448 |       // Should use safe JSON path syntax
449 |       expect(prepareCall).toContain('$.categories');
450 |       expect(prepareCall).not.toContain('$[');
451 |       expect(prepareCall).not.toContain('eval(');
452 |     });
453 |   });
454 | 
455 |   describe('Transaction Safety', () => {
456 |     it('should handle transaction rollback on metadata update errors', () => {
457 |       const stmt = new MockPreparedStatement('');
458 |       stmt.run = vi.fn().mockImplementation(() => {
459 |         throw new Error('Database error');
460 |       });
461 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
462 |       
463 |       const maliciousData = new Map();
464 |       maliciousData.set(1, { categories: ["'; DROP TABLE templates; --"] });
465 |       
466 |       expect(() => {
467 |         repository.batchUpdateMetadata(maliciousData);
468 |       }).toThrow('Database error');
469 |       
470 |       // The error is thrown when running the statement, not during transaction setup
471 |       // So we just verify that the error was thrown correctly
472 |     });
473 |   });
474 | 
475 |   describe('Error Message Security', () => {
476 |     it('should not expose sensitive information in error messages', () => {
477 |       const stmt = new MockPreparedStatement('');
478 |       stmt.get = vi.fn().mockImplementation(() => {
479 |         throw new Error('SQLITE_ERROR: syntax error near "DROP TABLE"');
480 |       });
481 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
482 |       
483 |       expect(() => {
484 |         repository.getMetadataSearchCount({
485 |           category: "'; DROP TABLE templates; --"
486 |         });
487 |       }).toThrow(); // Should throw, but not expose SQL details
488 |     });
489 |   });
490 | 
491 |   describe('Performance and DoS Protection', () => {
492 |     it('should handle large limit values safely', () => {
493 |       const stmt = new MockPreparedStatement('');
494 |       stmt._setMockResults([]);
495 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
496 |       
497 |       repository.searchTemplatesByMetadata({}, 999999999, 0); // Very large limit
498 |       
499 |       const capturedParams = stmt._getCapturedParams();
500 |       // Check if parameters were captured
501 |       expect(capturedParams.length).toBeGreaterThan(0);
502 |       // Check if the large limit value is present (might be capped)
503 |       const hasLargeLimit = capturedParams[0].includes(999999999) || capturedParams[0].includes(20);
504 |       expect(hasLargeLimit).toBe(true);
505 |       
506 |       // Should still work but might be limited by database constraints
507 |       expect(mockAdapter.prepare).toHaveBeenCalled();
508 |     });
509 | 
510 |     it('should handle very long string parameters', () => {
511 |       const veryLongString = 'a'.repeat(100000); // 100KB string
512 |       
513 |       const stmt = new MockPreparedStatement('');
514 |       stmt._setMockResults([]);
515 |       mockAdapter.prepare = vi.fn().mockReturnValue(stmt);
516 |       
517 |       repository.searchTemplatesByMetadata({
518 |         category: veryLongString}, 10, 0);
519 |       
520 |       const capturedParams = stmt._getCapturedParams();
521 |       expect(capturedParams[0][0]).toContain(veryLongString);
522 |       
523 |       // Should handle without breaking
524 |       expect(mockAdapter.prepare).toHaveBeenCalled();
525 |     });
526 |   });
527 | });
```

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

```typescript
  1 | /**
  2 |  * Integration Tests: handleCreateWorkflow
  3 |  *
  4 |  * Tests workflow creation against a real n8n instance.
  5 |  * Verifies the P0 bug fix (FULL vs SHORT node type formats)
  6 |  * and covers all major workflow creation scenarios.
  7 |  */
  8 | 
  9 | import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest';
 10 | import { createTestContext, TestContext, createTestWorkflowName } from '../utils/test-context';
 11 | import { getTestN8nClient } from '../utils/n8n-client';
 12 | import { N8nApiClient } from '../../../../src/services/n8n-api-client';
 13 | import { Workflow } from '../../../../src/types/n8n-api';
 14 | import {
 15 |   SIMPLE_WEBHOOK_WORKFLOW,
 16 |   SIMPLE_HTTP_WORKFLOW,
 17 |   MULTI_NODE_WORKFLOW,
 18 |   ERROR_HANDLING_WORKFLOW,
 19 |   AI_AGENT_WORKFLOW,
 20 |   EXPRESSION_WORKFLOW,
 21 |   getFixture
 22 | } from '../utils/fixtures';
 23 | import { cleanupOrphanedWorkflows } from '../utils/cleanup-helpers';
 24 | import { createMcpContext } from '../utils/mcp-context';
 25 | import { InstanceContext } from '../../../../src/types/instance-context';
 26 | import { handleCreateWorkflow } from '../../../../src/mcp/handlers-n8n-manager';
 27 | 
 28 | describe('Integration: handleCreateWorkflow', () => {
 29 |   let context: TestContext;
 30 |   let client: N8nApiClient;
 31 |   let mcpContext: InstanceContext;
 32 | 
 33 |   beforeEach(() => {
 34 |     context = createTestContext();
 35 |     client = getTestN8nClient();
 36 |     mcpContext = createMcpContext();
 37 |   });
 38 | 
 39 |   afterEach(async () => {
 40 |     await context.cleanup();
 41 |   });
 42 | 
 43 |   // Global cleanup after all tests to catch any orphaned workflows
 44 |   // (e.g., from test retries or failures)
 45 |   // IMPORTANT: Skip cleanup in CI to preserve shared n8n instance workflows
 46 |   afterAll(async () => {
 47 |     if (!process.env.CI) {
 48 |       await cleanupOrphanedWorkflows();
 49 |     }
 50 |   });
 51 | 
 52 |   // ======================================================================
 53 |   // P0: Critical Bug Verification
 54 |   // ======================================================================
 55 | 
 56 |   describe('P0: Node Type Format Bug Fix', () => {
 57 |     it('should create workflow with webhook node using FULL node type format', async () => {
 58 |       // This test verifies the P0 bug fix where SHORT node type format
 59 |       // (e.g., "webhook") was incorrectly normalized to FULL format
 60 |       // causing workflow creation failures.
 61 |       //
 62 |       // The fix ensures FULL format (e.g., "n8n-nodes-base.webhook")
 63 |       // is preserved and passed to n8n API correctly.
 64 | 
 65 |       const workflowName = createTestWorkflowName('P0 Bug Verification - Webhook Node');
 66 |       const workflow = {
 67 |         name: workflowName,
 68 |         ...getFixture('simple-webhook')
 69 |       };
 70 | 
 71 |       // Create workflow using MCP handler
 72 |       const response = await handleCreateWorkflow({ ...workflow }, mcpContext);
 73 |       expect(response.success).toBe(true);
 74 |       const result = response.data as Workflow;
 75 | 
 76 |       // Verify workflow created successfully
 77 |       expect(result).toBeDefined();
 78 |       expect(result.id).toBeTruthy();
 79 |       if (!result.id) throw new Error('Workflow ID is missing');
 80 |       context.trackWorkflow(result.id);
 81 |       expect(result.name).toBe(workflowName);
 82 |       expect(result.nodes).toHaveLength(1);
 83 | 
 84 |       // Critical: Verify FULL node type format is preserved
 85 |       expect(result.nodes[0].type).toBe('n8n-nodes-base.webhook');
 86 |       expect(result.nodes[0].name).toBe('Webhook');
 87 |       expect(result.nodes[0].parameters).toBeDefined();
 88 |     });
 89 |   });
 90 | 
 91 |   // ======================================================================
 92 |   // P1: Base Nodes (High Priority)
 93 |   // ======================================================================
 94 | 
 95 |   describe('P1: Base n8n Nodes', () => {
 96 |     it('should create workflow with HTTP Request node', async () => {
 97 |       const workflowName = createTestWorkflowName('HTTP Request Node');
 98 |       const workflow = {
 99 |         name: workflowName,
100 |         ...getFixture('simple-http')
101 |       };
102 | 
103 |       const response = await handleCreateWorkflow({ ...workflow }, mcpContext);
104 |       expect(response.success).toBe(true);
105 |       const result = response.data as Workflow;
106 | 
107 |       expect(result).toBeDefined();
108 |       expect(result.id).toBeTruthy();
109 |       if (!result.id) throw new Error('Workflow ID is missing');
110 |       context.trackWorkflow(result.id);
111 |       expect(result.name).toBe(workflowName);
112 |       expect(result.nodes).toHaveLength(2);
113 | 
114 |       // Verify both nodes created with FULL type format
115 |       const webhookNode = result.nodes.find((n: any) => n.name === 'Webhook');
116 |       const httpNode = result.nodes.find((n: any) => n.name === 'HTTP Request');
117 | 
118 |       expect(webhookNode).toBeDefined();
119 |       expect(webhookNode!.type).toBe('n8n-nodes-base.webhook');
120 | 
121 |       expect(httpNode).toBeDefined();
122 |       expect(httpNode!.type).toBe('n8n-nodes-base.httpRequest');
123 | 
124 |       // Verify connections
125 |       expect(result.connections).toBeDefined();
126 |       expect(result.connections.Webhook).toBeDefined();
127 |     });
128 | 
129 |     it('should create workflow with langchain agent node', async () => {
130 |       const workflowName = createTestWorkflowName('Langchain Agent Node');
131 |       const workflow = {
132 |         name: workflowName,
133 |         ...getFixture('ai-agent')
134 |       };
135 | 
136 |       const response = await handleCreateWorkflow({ ...workflow }, mcpContext);
137 |       expect(response.success).toBe(true);
138 |       const result = response.data as Workflow;
139 | 
140 |       expect(result).toBeDefined();
141 |       expect(result.id).toBeTruthy();
142 |       if (!result.id) throw new Error('Workflow ID is missing');
143 |       context.trackWorkflow(result.id);
144 |       expect(result.name).toBe(workflowName);
145 |       expect(result.nodes).toHaveLength(2);
146 | 
147 |       // Verify langchain node type format
148 |       const agentNode = result.nodes.find((n: any) => n.name === 'AI Agent');
149 |       expect(agentNode).toBeDefined();
150 |       expect(agentNode!.type).toBe('@n8n/n8n-nodes-langchain.agent');
151 |     });
152 | 
153 |     it('should create complex multi-node workflow', async () => {
154 |       const workflowName = createTestWorkflowName('Multi-Node Workflow');
155 |       const workflow = {
156 |         name: workflowName,
157 |         ...getFixture('multi-node')
158 |       };
159 | 
160 |       const response = await handleCreateWorkflow({ ...workflow }, mcpContext);
161 |       expect(response.success).toBe(true);
162 |       const result = response.data as Workflow;
163 | 
164 |       expect(result).toBeDefined();
165 |       expect(result.id).toBeTruthy();
166 |       if (!result.id) throw new Error('Workflow ID is missing');
167 |       context.trackWorkflow(result.id);
168 |       expect(result.name).toBe(workflowName);
169 |       expect(result.nodes).toHaveLength(4);
170 | 
171 |       // Verify all node types preserved
172 |       const nodeTypes = result.nodes.map((n: any) => n.type);
173 |       expect(nodeTypes).toContain('n8n-nodes-base.webhook');
174 |       expect(nodeTypes).toContain('n8n-nodes-base.set');
175 |       expect(nodeTypes).toContain('n8n-nodes-base.merge');
176 | 
177 |       // Verify complex connections
178 |       expect(result.connections.Webhook.main[0]).toHaveLength(2); // Branches to 2 nodes
179 |     });
180 |   });
181 | 
182 |   // ======================================================================
183 |   // P2: Advanced Features (Medium Priority)
184 |   // ======================================================================
185 | 
186 |   describe('P2: Advanced Workflow Features', () => {
187 |     it('should create workflow with complex connections and branching', async () => {
188 |       const workflowName = createTestWorkflowName('Complex Connections');
189 |       const workflow = {
190 |         name: workflowName,
191 |         ...getFixture('multi-node')
192 |       };
193 | 
194 |       const response = await handleCreateWorkflow({ ...workflow }, mcpContext);
195 |       expect(response.success).toBe(true);
196 |       const result = response.data as Workflow;
197 | 
198 |       expect(result).toBeDefined();
199 |       expect(result.id).toBeTruthy();
200 |       if (!result.id) throw new Error('Workflow ID is missing');
201 |       context.trackWorkflow(result.id);
202 |       expect(result.connections).toBeDefined();
203 | 
204 |       // Verify branching: Webhook -> Set 1 and Set 2
205 |       const webhookConnections = result.connections.Webhook.main[0];
206 |       expect(webhookConnections).toHaveLength(2);
207 | 
208 |       // Verify merging: Set 1 -> Merge (port 0), Set 2 -> Merge (port 1)
209 |       const set1Connections = result.connections['Set 1'].main[0];
210 |       const set2Connections = result.connections['Set 2'].main[0];
211 | 
212 |       expect(set1Connections[0].node).toBe('Merge');
213 |       expect(set1Connections[0].index).toBe(0);
214 | 
215 |       expect(set2Connections[0].node).toBe('Merge');
216 |       expect(set2Connections[0].index).toBe(1);
217 |     });
218 | 
219 |     it('should create workflow with custom settings', async () => {
220 |       const workflowName = createTestWorkflowName('Custom Settings');
221 |       const workflow = {
222 |         name: workflowName,
223 |         ...getFixture('error-handling'),
224 |         settings: {
225 |           executionOrder: 'v1' as const,
226 |           timezone: 'America/New_York',
227 |           saveDataErrorExecution: 'all' as const,
228 |           saveDataSuccessExecution: 'all' as const,
229 |           saveExecutionProgress: true
230 |         }
231 |       };
232 | 
233 |       const response = await handleCreateWorkflow({ ...workflow }, mcpContext);
234 |       expect(response.success).toBe(true);
235 |       const result = response.data as Workflow;
236 | 
237 |       expect(result).toBeDefined();
238 |       expect(result.id).toBeTruthy();
239 |       if (!result.id) throw new Error('Workflow ID is missing');
240 |       context.trackWorkflow(result.id);
241 |       expect(result.settings).toBeDefined();
242 |       expect(result.settings!.executionOrder).toBe('v1');
243 |     });
244 | 
245 |     it('should create workflow with n8n expressions', async () => {
246 |       const workflowName = createTestWorkflowName('n8n Expressions');
247 |       const workflow = {
248 |         name: workflowName,
249 |         ...getFixture('expression')
250 |       };
251 | 
252 |       const response = await handleCreateWorkflow({ ...workflow }, mcpContext);
253 |       expect(response.success).toBe(true);
254 |       const result = response.data as Workflow;
255 | 
256 |       expect(result).toBeDefined();
257 |       expect(result.id).toBeTruthy();
258 |       if (!result.id) throw new Error('Workflow ID is missing');
259 |       context.trackWorkflow(result.id);
260 |       expect(result.nodes).toHaveLength(2);
261 | 
262 |       // Verify Set node with expressions
263 |       const setNode = result.nodes.find((n: any) => n.name === 'Set Variables');
264 |       expect(setNode).toBeDefined();
265 |       expect(setNode!.parameters.assignments).toBeDefined();
266 | 
267 |       // Verify expressions are preserved
268 |       const assignmentsData = setNode!.parameters.assignments as { assignments: Array<{ value: string }> };
269 |       expect(assignmentsData.assignments).toHaveLength(3);
270 |       expect(assignmentsData.assignments[0].value).toContain('$now');
271 |       expect(assignmentsData.assignments[1].value).toContain('$json');
272 |       expect(assignmentsData.assignments[2].value).toContain('$node');
273 |     });
274 | 
275 |     it('should create workflow with error handling configuration', async () => {
276 |       const workflowName = createTestWorkflowName('Error Handling');
277 |       const workflow = {
278 |         name: workflowName,
279 |         ...getFixture('error-handling')
280 |       };
281 | 
282 |       const response = await handleCreateWorkflow({ ...workflow }, mcpContext);
283 |       expect(response.success).toBe(true);
284 |       const result = response.data as Workflow;
285 | 
286 |       expect(result).toBeDefined();
287 |       expect(result.id).toBeTruthy();
288 |       if (!result.id) throw new Error('Workflow ID is missing');
289 |       context.trackWorkflow(result.id);
290 |       expect(result.nodes).toHaveLength(3);
291 | 
292 |       // Verify HTTP node with error handling
293 |       const httpNode = result.nodes.find((n: any) => n.name === 'HTTP Request');
294 |       expect(httpNode).toBeDefined();
295 |       expect(httpNode!.continueOnFail).toBe(true);
296 |       expect(httpNode!.onError).toBe('continueErrorOutput');
297 | 
298 |       // Verify error connection
299 |       expect(result.connections['HTTP Request'].error).toBeDefined();
300 |       expect(result.connections['HTTP Request'].error[0][0].node).toBe('Handle Error');
301 |     });
302 |   });
303 | 
304 |   // ======================================================================
305 |   // Error Scenarios (P1 Priority)
306 |   // ======================================================================
307 | 
308 |   describe('Error Scenarios', () => {
309 |     it('should reject workflow with invalid node type (MCP validation)', async () => {
310 |       // MCP handler correctly validates workflows before sending to n8n API.
311 |       // Invalid node types are caught during MCP validation.
312 |       //
313 |       // Note: Raw n8n API would accept this and only fail at execution time,
314 |       // but MCP handler does proper pre-validation (correct behavior).
315 | 
316 |       const workflowName = createTestWorkflowName('Invalid Node Type');
317 |       const workflow = {
318 |         name: workflowName,
319 |         nodes: [
320 |           {
321 |             id: 'invalid-1',
322 |             name: 'Invalid Node',
323 |             type: 'n8n-nodes-base.nonexistentnode',
324 |             typeVersion: 1,
325 |             position: [250, 300] as [number, number],
326 |             parameters: {}
327 |           }
328 |         ],
329 |         connections: {},
330 |         settings: { executionOrder: 'v1' as const }
331 |       };
332 | 
333 |       // MCP handler rejects invalid workflows (correct behavior)
334 |       const response = await handleCreateWorkflow({ ...workflow }, mcpContext);
335 |       expect(response.success).toBe(false);
336 |       expect(response.error).toBeDefined();
337 |       expect(response.error).toContain('validation');
338 |     });
339 | 
340 |     it('should reject workflow with missing required node parameters (MCP validation)', async () => {
341 |       // MCP handler validates required parameters before sending to n8n API.
342 |       //
343 |       // Note: Raw n8n API would accept this and only fail at execution time,
344 |       // but MCP handler does proper pre-validation (correct behavior).
345 | 
346 |       const workflowName = createTestWorkflowName('Missing Parameters');
347 |       const workflow = {
348 |         name: workflowName,
349 |         nodes: [
350 |           {
351 |             id: 'http-1',
352 |             name: 'HTTP Request',
353 |             type: 'n8n-nodes-base.httpRequest',
354 |             typeVersion: 4.2,
355 |             position: [250, 300] as [number, number],
356 |             parameters: {
357 |               // Missing required 'url' parameter
358 |               method: 'GET'
359 |             }
360 |           }
361 |         ],
362 |         connections: {},
363 |         settings: { executionOrder: 'v1' as const }
364 |       };
365 | 
366 |       // MCP handler rejects workflows with validation errors (correct behavior)
367 |       const response = await handleCreateWorkflow({ ...workflow }, mcpContext);
368 |       expect(response.success).toBe(false);
369 |       expect(response.error).toBeDefined();
370 |     });
371 | 
372 |     it('should reject workflow with duplicate node names (MCP validation)', async () => {
373 |       // MCP handler validates that node names are unique.
374 |       //
375 |       // Note: Raw n8n API might auto-rename duplicates, but MCP handler
376 |       // enforces unique names upfront (correct behavior).
377 | 
378 |       const workflowName = createTestWorkflowName('Duplicate Node Names');
379 |       const workflow = {
380 |         name: workflowName,
381 |         nodes: [
382 |           {
383 |             id: 'set-1',
384 |             name: 'Set',
385 |             type: 'n8n-nodes-base.set',
386 |             typeVersion: 3.4,
387 |             position: [250, 300] as [number, number],
388 |             parameters: {
389 |               assignments: { assignments: [] },
390 |               options: {}
391 |             }
392 |           },
393 |           {
394 |             id: 'set-2',
395 |             name: 'Set', // Duplicate name
396 |             type: 'n8n-nodes-base.set',
397 |             typeVersion: 3.4,
398 |             position: [450, 300] as [number, number],
399 |             parameters: {
400 |               assignments: { assignments: [] },
401 |               options: {}
402 |             }
403 |           }
404 |         ],
405 |         connections: {},
406 |         settings: { executionOrder: 'v1' as const }
407 |       };
408 | 
409 |       // MCP handler rejects workflows with validation errors (correct behavior)
410 |       const response = await handleCreateWorkflow({ ...workflow }, mcpContext);
411 |       expect(response.success).toBe(false);
412 |       expect(response.error).toBeDefined();
413 |     });
414 | 
415 |     it('should reject workflow with invalid connection references (MCP validation)', async () => {
416 |       // MCP handler validates that connection references point to existing nodes.
417 |       //
418 |       // Note: Raw n8n API would accept this and only fail at execution time,
419 |       // but MCP handler does proper connection validation (correct behavior).
420 | 
421 |       const workflowName = createTestWorkflowName('Invalid Connections');
422 |       const workflow = {
423 |         name: workflowName,
424 |         nodes: [
425 |           {
426 |             id: 'webhook-1',
427 |             name: 'Webhook',
428 |             type: 'n8n-nodes-base.webhook',
429 |             typeVersion: 2,
430 |             position: [250, 300] as [number, number],
431 |             parameters: {
432 |               httpMethod: 'GET',
433 |               path: 'test'
434 |             }
435 |           }
436 |         ],
437 |         connections: {
438 |           // Connection references non-existent node
439 |           Webhook: {
440 |             main: [[{ node: 'NonExistent', type: 'main', index: 0 }]]
441 |           }
442 |         },
443 |         settings: { executionOrder: 'v1' as const }
444 |       };
445 | 
446 |       // MCP handler rejects workflows with invalid connections (correct behavior)
447 |       const response = await handleCreateWorkflow({ ...workflow }, mcpContext);
448 |       expect(response.success).toBe(false);
449 |       expect(response.error).toBeDefined();
450 |       expect(response.error).toContain('validation');
451 |     });
452 |   });
453 | 
454 |   // ======================================================================
455 |   // Additional Edge Cases
456 |   // ======================================================================
457 | 
458 |   describe('Edge Cases', () => {
459 |     it('should reject single-node non-webhook workflow (MCP validation)', async () => {
460 |       // MCP handler enforces that single-node workflows are only valid for webhooks.
461 |       // This is a best practice validation.
462 | 
463 |       const workflowName = createTestWorkflowName('Minimal Single Node');
464 |       const workflow = {
465 |         name: workflowName,
466 |         nodes: [
467 |           {
468 |             id: 'manual-1',
469 |             name: 'Manual Trigger',
470 |             type: 'n8n-nodes-base.manualTrigger',
471 |             typeVersion: 1,
472 |             position: [250, 300] as [number, number],
473 |             parameters: {}
474 |           }
475 |         ],
476 |         connections: {},
477 |         settings: { executionOrder: 'v1' as const }
478 |       };
479 | 
480 |       // MCP handler rejects single-node non-webhook workflows (correct behavior)
481 |       const response = await handleCreateWorkflow({ ...workflow }, mcpContext);
482 |       expect(response.success).toBe(false);
483 |       expect(response.error).toBeDefined();
484 |       expect(response.error).toContain('validation');
485 |     });
486 | 
487 |     it('should reject single-node non-trigger workflow (MCP validation)', async () => {
488 |       // MCP handler enforces workflow best practices.
489 |       // Single isolated nodes without connections are rejected.
490 | 
491 |       const workflowName = createTestWorkflowName('Empty Connections');
492 |       const workflow = {
493 |         name: workflowName,
494 |         nodes: [
495 |           {
496 |             id: 'set-1',
497 |             name: 'Set',
498 |             type: 'n8n-nodes-base.set',
499 |             typeVersion: 3.4,
500 |             position: [250, 300] as [number, number],
501 |             parameters: {
502 |               assignments: { assignments: [] },
503 |               options: {}
504 |             }
505 |           }
506 |         ],
507 |         connections: {}, // Explicitly empty
508 |         settings: { executionOrder: 'v1' as const }
509 |       };
510 | 
511 |       // MCP handler rejects single-node workflows (correct behavior)
512 |       const response = await handleCreateWorkflow({ ...workflow }, mcpContext);
513 |       expect(response.success).toBe(false);
514 |       expect(response.error).toBeDefined();
515 |     });
516 | 
517 |     it('should reject single-node workflow without settings (MCP validation)', async () => {
518 |       // MCP handler enforces workflow best practices.
519 |       // Single-node non-webhook workflows are rejected.
520 | 
521 |       const workflowName = createTestWorkflowName('No Settings');
522 |       const workflow = {
523 |         name: workflowName,
524 |         nodes: [
525 |           {
526 |             id: 'manual-1',
527 |             name: 'Manual Trigger',
528 |             type: 'n8n-nodes-base.manualTrigger',
529 |             typeVersion: 1,
530 |             position: [250, 300] as [number, number],
531 |             parameters: {}
532 |           }
533 |         ],
534 |         connections: {}
535 |         // No settings property
536 |       };
537 | 
538 |       // MCP handler rejects single-node workflows (correct behavior)
539 |       const response = await handleCreateWorkflow({ ...workflow }, mcpContext);
540 |       expect(response.success).toBe(false);
541 |       expect(response.error).toBeDefined();
542 |     });
543 |   });
544 | });
545 | 
```

--------------------------------------------------------------------------------
/tests/integration/mcp-protocol/tool-invocation.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  2 | import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
  3 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
  4 | import { TestableN8NMCPServer } from './test-helpers';
  5 | 
  6 | describe('MCP Tool Invocation', () => {
  7 |   let mcpServer: TestableN8NMCPServer;
  8 |   let client: Client;
  9 | 
 10 |   beforeEach(async () => {
 11 |     mcpServer = new TestableN8NMCPServer();
 12 |     await mcpServer.initialize();
 13 |     
 14 |     const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
 15 |     await mcpServer.connectToTransport(serverTransport);
 16 |     
 17 |     client = new Client({
 18 |       name: 'test-client',
 19 |       version: '1.0.0'
 20 |     }, {
 21 |       capabilities: {}
 22 |     });
 23 |     
 24 |     await client.connect(clientTransport);
 25 |   });
 26 | 
 27 |   afterEach(async () => {
 28 |     await client.close();
 29 |     await mcpServer.close();
 30 |   });
 31 | 
 32 |   describe('Node Discovery Tools', () => {
 33 |     describe('list_nodes', () => {
 34 |       it('should list nodes with default parameters', async () => {
 35 |         const response = await client.callTool({ name: 'list_nodes', arguments: {} });
 36 |         
 37 |         expect((response as any).content).toHaveLength(1);
 38 |         expect((response as any).content[0].type).toBe('text');
 39 |         
 40 |         const result = JSON.parse(((response as any).content[0]).text);
 41 |         // The result is an object with nodes array and totalCount
 42 |         expect(result).toHaveProperty('nodes');
 43 |         expect(result).toHaveProperty('totalCount');
 44 |         
 45 |         const nodes = result.nodes;
 46 |         expect(Array.isArray(nodes)).toBe(true);
 47 |         expect(nodes.length).toBeGreaterThan(0);
 48 |         
 49 |         // Check node structure
 50 |         const firstNode = nodes[0];
 51 |         expect(firstNode).toHaveProperty('nodeType');
 52 |         expect(firstNode).toHaveProperty('displayName');
 53 |         expect(firstNode).toHaveProperty('category');
 54 |       });
 55 | 
 56 |       it('should filter nodes by category', async () => {
 57 |         const response = await client.callTool({ name: 'list_nodes', arguments: {
 58 |           category: 'trigger'
 59 |         }});
 60 | 
 61 |         const result = JSON.parse(((response as any).content[0]).text);
 62 |         const nodes = result.nodes;
 63 |         expect(nodes.length).toBeGreaterThan(0);
 64 |         nodes.forEach((node: any) => {
 65 |           expect(node.category).toBe('trigger');
 66 |         });
 67 |       });
 68 | 
 69 |       it('should limit results', async () => {
 70 |         const response = await client.callTool({ name: 'list_nodes', arguments: {
 71 |           limit: 5
 72 |         }});
 73 | 
 74 |         const result = JSON.parse(((response as any).content[0]).text);
 75 |         const nodes = result.nodes;
 76 |         expect(nodes).toHaveLength(5);
 77 |       });
 78 | 
 79 |       it('should filter by package', async () => {
 80 |         const response = await client.callTool({ name: 'list_nodes', arguments: {
 81 |           package: 'n8n-nodes-base'
 82 |         }});
 83 | 
 84 |         const result = JSON.parse(((response as any).content[0]).text);
 85 |         const nodes = result.nodes;
 86 |         expect(nodes.length).toBeGreaterThan(0);
 87 |         nodes.forEach((node: any) => {
 88 |           expect(node.package).toBe('n8n-nodes-base');
 89 |         });
 90 |       });
 91 |     });
 92 | 
 93 |     describe('search_nodes', () => {
 94 |       it('should search nodes by keyword', async () => {
 95 |         const response = await client.callTool({ name: 'search_nodes', arguments: {
 96 |           query: 'webhook'
 97 |         }});
 98 | 
 99 |         const result = JSON.parse(((response as any).content[0]).text);
100 |         const nodes = result.results;
101 |         expect(nodes.length).toBeGreaterThan(0);
102 |         
103 |         // Should find webhook node
104 |         const webhookNode = nodes.find((n: any) => n.displayName.toLowerCase().includes('webhook'));
105 |         expect(webhookNode).toBeDefined();
106 |       });
107 | 
108 |       it('should support different search modes', async () => {
109 |         // OR mode
110 |         const orResponse = await client.callTool({ name: 'search_nodes', arguments: {
111 |           query: 'http request',
112 |           mode: 'OR'
113 |         }});
114 |         const orResult = JSON.parse(((orResponse as any).content[0]).text);
115 |         const orNodes = orResult.results;
116 |         expect(orNodes.length).toBeGreaterThan(0);
117 | 
118 |         // AND mode
119 |         const andResponse = await client.callTool({ name: 'search_nodes', arguments: {
120 |           query: 'http request',
121 |           mode: 'AND'
122 |         }});
123 |         const andResult = JSON.parse(((andResponse as any).content[0]).text);
124 |         const andNodes = andResult.results;
125 |         expect(andNodes.length).toBeLessThanOrEqual(orNodes.length);
126 | 
127 |         // FUZZY mode - use less typo-heavy search
128 |         const fuzzyResponse = await client.callTool({ name: 'search_nodes', arguments: {
129 |           query: 'http req', // Partial match should work
130 |           mode: 'FUZZY'
131 |         }});
132 |         const fuzzyResult = JSON.parse(((fuzzyResponse as any).content[0]).text);
133 |         const fuzzyNodes = fuzzyResult.results;
134 |         expect(fuzzyNodes.length).toBeGreaterThan(0);
135 |       });
136 | 
137 |       it('should respect result limit', async () => {
138 |         const response = await client.callTool({ name: 'search_nodes', arguments: {
139 |           query: 'node',
140 |           limit: 3
141 |         }});
142 | 
143 |         const result = JSON.parse(((response as any).content[0]).text);
144 |         const nodes = result.results;
145 |         expect(nodes).toHaveLength(3);
146 |       });
147 |     });
148 | 
149 |     describe('get_node_info', () => {
150 |       it('should get complete node information', async () => {
151 |         const response = await client.callTool({ name: 'get_node_info', arguments: {
152 |           nodeType: 'nodes-base.httpRequest'
153 |         }});
154 | 
155 |         expect(((response as any).content[0]).type).toBe('text');
156 |         const nodeInfo = JSON.parse(((response as any).content[0]).text);
157 |         
158 |         expect(nodeInfo).toHaveProperty('nodeType', 'nodes-base.httpRequest');
159 |         expect(nodeInfo).toHaveProperty('displayName');
160 |         expect(nodeInfo).toHaveProperty('properties');
161 |         expect(Array.isArray(nodeInfo.properties)).toBe(true);
162 |       });
163 | 
164 |       it('should handle non-existent nodes', async () => {
165 |         try {
166 |           await client.callTool({ name: 'get_node_info', arguments: {
167 |             nodeType: 'nodes-base.nonExistent'
168 |           }});
169 |           expect.fail('Should have thrown an error');
170 |         } catch (error: any) {
171 |           expect(error.message).toContain('not found');
172 |         }
173 |       });
174 | 
175 |       it('should handle invalid node type format', async () => {
176 |         try {
177 |           await client.callTool({ name: 'get_node_info', arguments: {
178 |             nodeType: 'invalidFormat'
179 |           }});
180 |           expect.fail('Should have thrown an error');
181 |         } catch (error: any) {
182 |           expect(error.message).toContain('not found');
183 |         }
184 |       });
185 |     });
186 | 
187 |     describe('get_node_essentials', () => {
188 |       it('should return condensed node information', async () => {
189 |         const response = await client.callTool({ name: 'get_node_essentials', arguments: {
190 |           nodeType: 'nodes-base.httpRequest'
191 |         }});
192 | 
193 |         const essentials = JSON.parse(((response as any).content[0]).text);
194 |         
195 |         expect(essentials).toHaveProperty('nodeType');
196 |         expect(essentials).toHaveProperty('displayName');
197 |         expect(essentials).toHaveProperty('commonProperties');
198 |         expect(essentials).toHaveProperty('requiredProperties');
199 |         
200 |         // Should be smaller than full info
201 |         const fullResponse = await client.callTool({ name: 'get_node_info', arguments: {
202 |           nodeType: 'nodes-base.httpRequest'
203 |         }});
204 |         
205 |         expect(((response as any).content[0]).text.length).toBeLessThan(((fullResponse as any).content[0]).text.length);
206 |       });
207 |     });
208 |   });
209 | 
210 |   describe('Validation Tools', () => {
211 |     describe('validate_node_operation', () => {
212 |       it('should validate valid node configuration', async () => {
213 |         const response = await client.callTool({ name: 'validate_node_operation', arguments: {
214 |           nodeType: 'nodes-base.httpRequest',
215 |           config: {
216 |             method: 'GET',
217 |             url: 'https://api.example.com/data'
218 |           }
219 |         }});
220 | 
221 |         const validation = JSON.parse(((response as any).content[0]).text);
222 |         expect(validation).toHaveProperty('valid');
223 |         expect(validation).toHaveProperty('errors');
224 |         expect(validation).toHaveProperty('warnings');
225 |       });
226 | 
227 |       it('should detect missing required fields', async () => {
228 |         const response = await client.callTool({ name: 'validate_node_operation', arguments: {
229 |           nodeType: 'nodes-base.httpRequest',
230 |           config: {
231 |             method: 'GET'
232 |             // Missing required 'url' field
233 |           }
234 |         }});
235 | 
236 |         const validation = JSON.parse(((response as any).content[0]).text);
237 |         expect(validation.valid).toBe(false);
238 |         expect(validation.errors.length).toBeGreaterThan(0);
239 |         expect(validation.errors[0].message.toLowerCase()).toContain('url');
240 |       });
241 | 
242 |       it('should support different validation profiles', async () => {
243 |         const profiles = ['minimal', 'runtime', 'ai-friendly', 'strict'];
244 |         
245 |         for (const profile of profiles) {
246 |           const response = await client.callTool({ name: 'validate_node_operation', arguments: {
247 |             nodeType: 'nodes-base.httpRequest',
248 |             config: { method: 'GET', url: 'https://api.example.com' },
249 |             profile
250 |           }});
251 | 
252 |           const validation = JSON.parse(((response as any).content[0]).text);
253 |           expect(validation).toHaveProperty('profile', profile);
254 |         }
255 |       });
256 |     });
257 | 
258 |     describe('validate_workflow', () => {
259 |       it('should validate complete workflow', async () => {
260 |         const workflow = {
261 |           nodes: [
262 |             {
263 |               id: '1',
264 |               name: 'Start',
265 |               type: 'nodes-base.manualTrigger',
266 |               typeVersion: 1,
267 |               position: [0, 0],
268 |               parameters: {}
269 |             },
270 |             {
271 |               id: '2',
272 |               name: 'HTTP Request',
273 |               type: 'nodes-base.httpRequest',
274 |               typeVersion: 3,
275 |               position: [250, 0],
276 |               parameters: {
277 |                 method: 'GET',
278 |                 url: 'https://api.example.com/data'
279 |               }
280 |             }
281 |           ],
282 |           connections: {
283 |             'Start': {
284 |               'main': [[{ node: 'HTTP Request', type: 'main', index: 0 }]]
285 |             }
286 |           }
287 |         };
288 | 
289 |         const response = await client.callTool({ name: 'validate_workflow', arguments: {
290 |           workflow
291 |         }});
292 | 
293 |         const validation = JSON.parse(((response as any).content[0]).text);
294 |         expect(validation).toHaveProperty('valid');
295 |         expect(validation).toHaveProperty('errors');
296 |         expect(validation).toHaveProperty('warnings');
297 |       });
298 | 
299 |       it('should detect connection errors', async () => {
300 |         const workflow = {
301 |           nodes: [
302 |             {
303 |               id: '1',
304 |               name: 'Start',
305 |               type: 'nodes-base.manualTrigger',
306 |               typeVersion: 1,
307 |               position: [0, 0],
308 |               parameters: {}
309 |             }
310 |           ],
311 |           connections: {
312 |             'Start': {
313 |               'main': [[{ node: 'NonExistent', type: 'main', index: 0 }]]
314 |             }
315 |           }
316 |         };
317 | 
318 |         const response = await client.callTool({ name: 'validate_workflow', arguments: {
319 |           workflow
320 |         }});
321 | 
322 |         const validation = JSON.parse(((response as any).content[0]).text);
323 |         expect(validation.valid).toBe(false);
324 |         expect(validation.errors.length).toBeGreaterThan(0);
325 |       });
326 | 
327 |       it('should validate expressions', async () => {
328 |         const workflow = {
329 |           nodes: [
330 |             {
331 |               id: '1',
332 |               name: 'Start',
333 |               type: 'n8n-nodes-base.manualTrigger',
334 |               typeVersion: 1,
335 |               position: [0, 0],
336 |               parameters: {}
337 |             },
338 |             {
339 |               id: '2',
340 |               name: 'Set',
341 |               type: 'n8n-nodes-base.set',
342 |               typeVersion: 3.4,
343 |               position: [250, 0],
344 |               parameters: {
345 |                 mode: 'manual',
346 |                 duplicateItem: false,
347 |                 values: {
348 |                   string: [
349 |                     {
350 |                       name: 'test',
351 |                       value: '={{ $json.invalidExpression }}'
352 |                     }
353 |                   ]
354 |                 }
355 |               }
356 |             }
357 |           ],
358 |           connections: {
359 |             'Start': {
360 |               'main': [[{ node: 'Set', type: 'main', index: 0 }]]
361 |             }
362 |           }
363 |         };
364 | 
365 |         const response = await client.callTool({ name: 'validate_workflow', arguments: {
366 |           workflow,
367 |           options: {
368 |             validateExpressions: true
369 |           }
370 |         }});
371 | 
372 |         const validation = JSON.parse(((response as any).content[0]).text);
373 |         expect(validation).toHaveProperty('valid');
374 |         
375 |         // The workflow should have either errors or warnings about the expression
376 |         if (validation.errors && validation.errors.length > 0) {
377 |           expect(validation.errors.some((e: any) => 
378 |             e.message.includes('expression') || e.message.includes('$json')
379 |           )).toBe(true);
380 |         } else if (validation.warnings) {
381 |           expect(validation.warnings.length).toBeGreaterThan(0);
382 |           expect(validation.warnings.some((w: any) => 
383 |             w.message.includes('expression') || w.message.includes('$json')
384 |           )).toBe(true);
385 |         }
386 |       });
387 |     });
388 |   });
389 | 
390 |   describe('Documentation Tools', () => {
391 |     describe('tools_documentation', () => {
392 |       it('should get quick start guide', async () => {
393 |         const response = await client.callTool({ name: 'tools_documentation', arguments: {} });
394 | 
395 |         expect(((response as any).content[0]).type).toBe('text');
396 |         expect(((response as any).content[0]).text).toContain('n8n MCP Tools');
397 |       });
398 | 
399 |       it('should get specific tool documentation', async () => {
400 |         const response = await client.callTool({ name: 'tools_documentation', arguments: {
401 |           topic: 'search_nodes'
402 |         }});
403 | 
404 |         expect(((response as any).content[0]).text).toContain('search_nodes');
405 |         expect(((response as any).content[0]).text).toContain('Text search');
406 |       });
407 | 
408 |       it('should get comprehensive documentation', async () => {
409 |         const response = await client.callTool({ name: 'tools_documentation', arguments: {
410 |           depth: 'full'
411 |         }});
412 | 
413 |         expect(((response as any).content[0]).text.length).toBeGreaterThan(5000);
414 |         expect(((response as any).content[0]).text).toBeDefined();
415 |       });
416 | 
417 |       it('should handle invalid topics gracefully', async () => {
418 |         const response = await client.callTool({ name: 'tools_documentation', arguments: {
419 |           topic: 'nonexistent_tool'
420 |         }});
421 | 
422 |         expect(((response as any).content[0]).text).toContain('not found');
423 |       });
424 |     });
425 |   });
426 | 
427 |   describe('AI Tools', () => {
428 |     describe('list_ai_tools', () => {
429 |       it('should list AI-capable nodes', async () => {
430 |         const response = await client.callTool({ name: 'list_ai_tools', arguments: {} });
431 | 
432 |         const result = JSON.parse(((response as any).content[0]).text);
433 |         expect(result).toHaveProperty('tools');
434 |         const aiTools = result.tools;
435 |         expect(Array.isArray(aiTools)).toBe(true);
436 |         expect(aiTools.length).toBeGreaterThan(0);
437 |         
438 |         // All should have nodeType and displayName
439 |         aiTools.forEach((tool: any) => {
440 |           expect(tool).toHaveProperty('nodeType');
441 |           expect(tool).toHaveProperty('displayName');
442 |         });
443 |       });
444 |     });
445 | 
446 |     describe('get_node_as_tool_info', () => {
447 |       it('should provide AI tool usage information', async () => {
448 |         const response = await client.callTool({ name: 'get_node_as_tool_info', arguments: {
449 |           nodeType: 'nodes-base.slack'
450 |         }});
451 | 
452 |         const info = JSON.parse(((response as any).content[0]).text);
453 |         expect(info).toHaveProperty('nodeType');
454 |         expect(info).toHaveProperty('isMarkedAsAITool');
455 |         expect(info).toHaveProperty('aiToolCapabilities');
456 |         expect(info.aiToolCapabilities).toHaveProperty('commonUseCases');
457 |       });
458 |     });
459 |   });
460 | 
461 |   describe('Task Templates', () => {
462 |     // get_node_for_task was removed in v2.15.0
463 |     // Use search_nodes({ includeExamples: true }) instead for real-world examples
464 | 
465 |     describe('list_tasks', () => {
466 |       it('should list all available tasks', async () => {
467 |         const response = await client.callTool({ name: 'list_tasks', arguments: {} });
468 | 
469 |         const result = JSON.parse(((response as any).content[0]).text);
470 |         expect(result).toHaveProperty('totalTasks');
471 |         expect(result).toHaveProperty('categories');
472 |         expect(result.totalTasks).toBeGreaterThan(0);
473 |         
474 |         // Check categories structure
475 |         const categories = result.categories;
476 |         expect(typeof categories).toBe('object');
477 |         
478 |         // Check at least one category has tasks
479 |         const hasTasksInCategories = Object.values(categories).some((tasks: any) => 
480 |           Array.isArray(tasks) && tasks.length > 0
481 |         );
482 |         expect(hasTasksInCategories).toBe(true);
483 |       });
484 | 
485 |       it('should filter by category', async () => {
486 |         const response = await client.callTool({ name: 'list_tasks', arguments: {
487 |           category: 'HTTP/API'
488 |         }});
489 | 
490 |         const result = JSON.parse(((response as any).content[0]).text);
491 |         expect(result).toHaveProperty('category', 'HTTP/API');
492 |         expect(result).toHaveProperty('tasks');
493 |         
494 |         const httpTasks = result.tasks;
495 |         expect(Array.isArray(httpTasks)).toBe(true);
496 |         expect(httpTasks.length).toBeGreaterThan(0);
497 |         
498 |         httpTasks.forEach((task: any) => {
499 |           expect(task).toHaveProperty('task');
500 |           expect(task).toHaveProperty('description');
501 |           expect(task).toHaveProperty('nodeType');
502 |         });
503 |       });
504 |     });
505 |   });
506 | 
507 |   describe('Complex Tool Interactions', () => {
508 |     it('should handle tool chaining', async () => {
509 |       // Search for nodes
510 |       const searchResponse = await client.callTool({ name: 'search_nodes', arguments: {
511 |         query: 'slack'
512 |       }});
513 |       const searchResult = JSON.parse(((searchResponse as any).content[0]).text);
514 |       const nodes = searchResult.results;
515 |       
516 |       // Get info for first result
517 |       const firstNode = nodes[0];
518 |       const infoResponse = await client.callTool({ name: 'get_node_info', arguments: {
519 |         nodeType: firstNode.nodeType
520 |       }});
521 |       
522 |       expect(((infoResponse as any).content[0]).text).toContain(firstNode.displayName);
523 |     });
524 | 
525 |     it('should handle parallel tool calls', async () => {
526 |       const tools = [
527 |         'list_nodes',
528 |         'get_database_statistics',
529 |         'list_ai_tools',
530 |         'list_tasks'
531 |       ];
532 | 
533 |       const promises = tools.map(tool => 
534 |         client.callTool({ name: tool as any, arguments: {} })
535 |       );
536 | 
537 |       const responses = await Promise.all(promises);
538 |       
539 |       expect(responses).toHaveLength(tools.length);
540 |       responses.forEach(response => {
541 |         expect(response.content).toHaveLength(1);
542 |         expect(((response as any).content[0]).type).toBe('text');
543 |       });
544 |     });
545 | 
546 |     it('should maintain consistency across related tools', async () => {
547 |       // Get node via different methods
548 |       const nodeType = 'nodes-base.httpRequest';
549 |       
550 |       const [fullInfo, essentials, searchResult] = await Promise.all([
551 |         client.callTool({ name: 'get_node_info', arguments: { nodeType } }),
552 |         client.callTool({ name: 'get_node_essentials', arguments: { nodeType } }),
553 |         client.callTool({ name: 'search_nodes', arguments: { query: 'httpRequest' } })
554 |       ]);
555 | 
556 |       const full = JSON.parse(((fullInfo as any).content[0]).text);
557 |       const essential = JSON.parse(((essentials as any).content[0]).text);
558 |       const searchData = JSON.parse(((searchResult as any).content[0]).text);
559 |       const search = searchData.results;
560 | 
561 |       // Should all reference the same node
562 |       expect(full.nodeType).toBe('nodes-base.httpRequest');
563 |       expect(essential.displayName).toBe(full.displayName);
564 |       expect(search.find((n: any) => n.nodeType === 'nodes-base.httpRequest')).toBeDefined();
565 |     });
566 |   });
567 | });
568 | 
```

--------------------------------------------------------------------------------
/tests/integration/database/performance.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  2 | import Database from 'better-sqlite3';
  3 | import { NodeRepository } from '../../../src/database/node-repository';
  4 | import { TemplateRepository } from '../../../src/templates/template-repository';
  5 | import { DatabaseAdapter } from '../../../src/database/database-adapter';
  6 | import { TestDatabase, TestDataGenerator, PerformanceMonitor, createTestDatabaseAdapter } from './test-utils';
  7 | import { ParsedNode } from '../../../src/parsers/node-parser';
  8 | import { TemplateWorkflow, TemplateDetail } from '../../../src/templates/template-fetcher';
  9 | 
 10 | describe('Database Performance Tests', () => {
 11 |   let testDb: TestDatabase;
 12 |   let db: Database.Database;
 13 |   let nodeRepo: NodeRepository;
 14 |   let templateRepo: TemplateRepository;
 15 |   let adapter: DatabaseAdapter;
 16 |   let monitor: PerformanceMonitor;
 17 | 
 18 |   beforeEach(async () => {
 19 |     testDb = new TestDatabase({ mode: 'file', name: 'performance-test.db', enableFTS5: true });
 20 |     db = await testDb.initialize();
 21 |     adapter = createTestDatabaseAdapter(db);
 22 |     nodeRepo = new NodeRepository(adapter);
 23 |     templateRepo = new TemplateRepository(adapter);
 24 |     monitor = new PerformanceMonitor();
 25 |   });
 26 | 
 27 |   afterEach(async () => {
 28 |     monitor.clear();
 29 |     await testDb.cleanup();
 30 |   });
 31 | 
 32 |   describe('Node Repository Performance', () => {
 33 |     it('should handle bulk inserts efficiently', () => {
 34 |       const nodeCounts = [100, 1000, 5000];
 35 |       
 36 |       nodeCounts.forEach(count => {
 37 |         const nodes = generateNodes(count);
 38 |         
 39 |         const stop = monitor.start(`insert_${count}_nodes`);
 40 |         const transaction = db.transaction((nodes: ParsedNode[]) => {
 41 |           nodes.forEach(node => nodeRepo.saveNode(node));
 42 |         });
 43 |         transaction(nodes);
 44 |         stop();
 45 |       });
 46 | 
 47 |       // Check performance metrics
 48 |       const stats100 = monitor.getStats('insert_100_nodes');
 49 |       const stats1000 = monitor.getStats('insert_1000_nodes');
 50 |       const stats5000 = monitor.getStats('insert_5000_nodes');
 51 | 
 52 |       // Environment-aware thresholds
 53 |       const threshold100 = process.env.CI ? 200 : 100;
 54 |       const threshold1000 = process.env.CI ? 1000 : 500;
 55 |       const threshold5000 = process.env.CI ? 4000 : 2000;
 56 | 
 57 |       expect(stats100!.average).toBeLessThan(threshold100);
 58 |       expect(stats1000!.average).toBeLessThan(threshold1000);
 59 |       expect(stats5000!.average).toBeLessThan(threshold5000);
 60 | 
 61 |       // Performance should scale sub-linearly
 62 |       const ratio1000to100 = stats1000!.average / stats100!.average;
 63 |       const ratio5000to1000 = stats5000!.average / stats1000!.average;
 64 | 
 65 |       // Adjusted based on actual CI performance measurements + type safety overhead
 66 |       // CI environments show ratios of ~7-10 for 1000:100 and ~6-7 for 5000:1000
 67 |       expect(ratio1000to100).toBeLessThan(12); // Allow for CI variability (was 10)
 68 |       expect(ratio5000to1000).toBeLessThan(11);  // Allow for type safety overhead (was 8)
 69 |     });
 70 | 
 71 |     it('should search nodes quickly with indexes', () => {
 72 |       // Insert test data with search-friendly content
 73 |       const searchableNodes = generateSearchableNodes(10000);
 74 |       const transaction = db.transaction((nodes: ParsedNode[]) => {
 75 |         nodes.forEach(node => nodeRepo.saveNode(node));
 76 |       });
 77 |       transaction(searchableNodes);
 78 | 
 79 |       // Test different search scenarios
 80 |       const searchTests = [
 81 |         { query: 'webhook', mode: 'OR' as const },
 82 |         { query: 'http request', mode: 'AND' as const },
 83 |         { query: 'automation data', mode: 'OR' as const },
 84 |         { query: 'HTT', mode: 'FUZZY' as const }
 85 |       ];
 86 | 
 87 |       searchTests.forEach(test => {
 88 |         const stop = monitor.start(`search_${test.query}_${test.mode}`);
 89 |         const results = nodeRepo.searchNodes(test.query, test.mode, 100);
 90 |         stop();
 91 |         
 92 |         expect(results.length).toBeGreaterThan(0);
 93 |       });
 94 | 
 95 |       // All searches should be fast
 96 |       searchTests.forEach(test => {
 97 |         const stats = monitor.getStats(`search_${test.query}_${test.mode}`);
 98 |         const threshold = process.env.CI ? 100 : 50;
 99 |         expect(stats!.average).toBeLessThan(threshold);
100 |       });
101 |     });
102 | 
103 |     it('should handle concurrent reads efficiently', () => {
104 |       // Insert initial data
105 |       const nodes = generateNodes(1000);
106 |       const transaction = db.transaction((nodes: ParsedNode[]) => {
107 |         nodes.forEach(node => nodeRepo.saveNode(node));
108 |       });
109 |       transaction(nodes);
110 | 
111 |       // Simulate concurrent reads
112 |       const readOperations = 100;
113 |       const promises: Promise<any>[] = [];
114 | 
115 |       const stop = monitor.start('concurrent_reads');
116 |       
117 |       for (let i = 0; i < readOperations; i++) {
118 |         promises.push(
119 |           Promise.resolve(nodeRepo.getNode(`n8n-nodes-base.node${i % 1000}`))
120 |         );
121 |       }
122 | 
123 |       Promise.all(promises);
124 |       stop();
125 | 
126 |       const stats = monitor.getStats('concurrent_reads');
127 |       const threshold = process.env.CI ? 200 : 100;
128 |       expect(stats!.average).toBeLessThan(threshold);
129 |       
130 |       // Average per read should be very low
131 |       const avgPerRead = stats!.average / readOperations;
132 |       const perReadThreshold = process.env.CI ? 2 : 1;
133 |       expect(avgPerRead).toBeLessThan(perReadThreshold);
134 |     });
135 |   });
136 | 
137 |   describe('Template Repository Performance with FTS5', () => {
138 |     it('should perform FTS5 searches efficiently', () => {
139 |       // Insert templates with varied content
140 |       const templates = Array.from({ length: 10000 }, (_, i) => {
141 |         const workflow: TemplateWorkflow = {
142 |           id: i + 1,
143 |           name: `${['Webhook', 'HTTP', 'Automation', 'Data Processing'][i % 4]} Workflow ${i}`,
144 |           description: generateDescription(i),
145 |           totalViews: Math.floor(Math.random() * 1000),
146 |           createdAt: new Date().toISOString(),
147 |           user: {
148 |             id: 1,
149 |             name: 'Test User',
150 |             username: 'user',
151 |             verified: false
152 |           },
153 |           nodes: [
154 |             {
155 |               id: i * 10 + 1,
156 |               name: 'Start',
157 |               icon: 'webhook'
158 |             }
159 |           ]
160 |         };
161 |         
162 |         const detail: TemplateDetail = {
163 |           id: i + 1,
164 |           name: workflow.name,
165 |           description: workflow.description || '',
166 |           views: workflow.totalViews,
167 |           createdAt: workflow.createdAt,
168 |           workflow: {
169 |             nodes: workflow.nodes,
170 |             connections: {},
171 |             settings: {}
172 |           }
173 |         };
174 |         
175 |         return { workflow, detail };
176 |       });
177 | 
178 |       const stop1 = monitor.start('insert_templates_with_fts');
179 |       const transaction = db.transaction((items: any[]) => {
180 |         items.forEach(({ workflow, detail }) => {
181 |           templateRepo.saveTemplate(workflow, detail);
182 |         });
183 |       });
184 |       transaction(templates);
185 |       stop1();
186 | 
187 |       // Ensure FTS index is built
188 |       db.prepare('INSERT INTO templates_fts(templates_fts) VALUES(\'rebuild\')').run();
189 | 
190 |       // Test various FTS5 searches - use lowercase queries since FTS5 with quotes is case-sensitive
191 |       const searchTests = [
192 |         'webhook',
193 |         'data',
194 |         'automation',
195 |         'http',
196 |         'workflow',
197 |         'processing'
198 |       ];
199 | 
200 |       searchTests.forEach(query => {
201 |         const stop = monitor.start(`fts5_search_${query}`);
202 |         const results = templateRepo.searchTemplates(query, 100);
203 |         stop();
204 |         
205 |         // Debug output
206 |         if (results.length === 0) {
207 |           console.log(`No results for query: ${query}`);
208 |           // Try to understand what's in the database
209 |           const count = db.prepare('SELECT COUNT(*) as count FROM templates').get() as { count: number };
210 |           console.log(`Total templates in DB: ${count.count}`);
211 |         }
212 |         
213 |         expect(results.length).toBeGreaterThan(0);
214 |       });
215 | 
216 |       // All FTS5 searches should be very fast
217 |       searchTests.forEach(query => {
218 |         const stats = monitor.getStats(`fts5_search_${query}`);
219 |         const threshold = process.env.CI ? 50 : 30;
220 |         expect(stats!.average).toBeLessThan(threshold);
221 |       });
222 |     });
223 | 
224 |     it('should handle complex node type searches efficiently', () => {
225 |       // Insert templates with various node combinations
226 |       const nodeTypes = [
227 |         'n8n-nodes-base.webhook',
228 |         'n8n-nodes-base.httpRequest',
229 |         'n8n-nodes-base.slack',
230 |         'n8n-nodes-base.googleSheets',
231 |         'n8n-nodes-base.mongodb'
232 |       ];
233 | 
234 |       const templates = Array.from({ length: 5000 }, (_, i) => {
235 |         const workflow: TemplateWorkflow = {
236 |           id: i + 1,
237 |           name: `Template ${i}`,
238 |           description: `Template description ${i}`,
239 |           totalViews: 100,
240 |           createdAt: new Date().toISOString(),
241 |           user: {
242 |             id: 1,
243 |             name: 'Test User',
244 |             username: 'user',
245 |             verified: false
246 |           },
247 |           nodes: []
248 |         };
249 |         
250 |         const detail: TemplateDetail = {
251 |           id: i + 1,
252 |           name: `Template ${i}`,
253 |           description: `Template description ${i}`,
254 |           views: 100,
255 |           createdAt: new Date().toISOString(),
256 |           workflow: {
257 |             nodes: Array.from({ length: 3 }, (_, j) => ({
258 |               id: `node${j}`,
259 |               name: `Node ${j}`,
260 |               type: nodeTypes[(i + j) % nodeTypes.length],
261 |               typeVersion: 1,
262 |               position: [100 * j, 100],
263 |               parameters: {}
264 |             })),
265 |             connections: {},
266 |             settings: {}
267 |           }
268 |         };
269 |         
270 |         return { workflow, detail };
271 |       });
272 | 
273 |       const insertTransaction = db.transaction((items: any[]) => {
274 |         items.forEach(({ workflow, detail }) => templateRepo.saveTemplate(workflow, detail));
275 |       });
276 |       insertTransaction(templates);
277 | 
278 |       // Test searching by node types
279 |       const stop = monitor.start('search_by_node_types');
280 |       const results = templateRepo.getTemplatesByNodes([
281 |         'n8n-nodes-base.webhook',
282 |         'n8n-nodes-base.slack'
283 |       ], 100);
284 |       stop();
285 | 
286 |       expect(results.length).toBeGreaterThan(0);
287 |       
288 |       const stats = monitor.getStats('search_by_node_types');
289 |       const threshold = process.env.CI ? 100 : 50;
290 |       expect(stats!.average).toBeLessThan(threshold);
291 |     });
292 |   });
293 | 
294 |   describe('Database Optimization', () => {
295 |     it('should benefit from proper indexing', () => {
296 |       // Insert more data to make index benefits more apparent
297 |       const nodes = generateNodes(10000);
298 |       const transaction = db.transaction((nodes: ParsedNode[]) => {
299 |         nodes.forEach(node => nodeRepo.saveNode(node));
300 |       });
301 |       transaction(nodes);
302 | 
303 |       // Verify indexes exist
304 |       const indexes = db.prepare("SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='nodes'").all() as { name: string }[];
305 |       const indexNames = indexes.map(idx => idx.name);
306 |       expect(indexNames).toContain('idx_package');
307 |       expect(indexNames).toContain('idx_category');
308 |       expect(indexNames).toContain('idx_ai_tool');
309 | 
310 |       // Test queries that use indexes
311 |       const indexedQueries = [
312 |         { 
313 |           name: 'package_query',
314 |           query: () => nodeRepo.getNodesByPackage('n8n-nodes-base'),
315 |           column: 'package_name'
316 |         },
317 |         { 
318 |           name: 'category_query',
319 |           query: () => nodeRepo.getNodesByCategory('trigger'),
320 |           column: 'category'
321 |         },
322 |         { 
323 |           name: 'ai_tools_query',
324 |           query: () => nodeRepo.getAITools(),
325 |           column: 'is_ai_tool'
326 |         }
327 |       ];
328 | 
329 |       // Test indexed queries
330 |       indexedQueries.forEach(({ name, query, column }) => {
331 |         // Verify query plan uses index
332 |         const plan = db.prepare(`EXPLAIN QUERY PLAN SELECT * FROM nodes WHERE ${column} = ?`).all('test') as any[];
333 |         const usesIndex = plan.some(row => 
334 |           row.detail && (row.detail.includes('USING INDEX') || row.detail.includes('USING COVERING INDEX'))
335 |         );
336 |         
337 |         // For simple queries on small datasets, SQLite might choose full table scan
338 |         // This is expected behavior and doesn't indicate a problem
339 |         if (!usesIndex && process.env.CI) {
340 |           console.log(`Note: Query on ${column} may not use index with small dataset (SQLite optimizer decision)`);
341 |         }
342 | 
343 |         const stop = monitor.start(name);
344 |         const results = query();
345 |         stop();
346 |         
347 |         expect(Array.isArray(results)).toBe(true);
348 |       });
349 | 
350 |       // All queries should be fast regardless of index usage
351 |       // SQLite's query optimizer makes intelligent decisions
352 |       indexedQueries.forEach(({ name }) => {
353 |         const stats = monitor.getStats(name);
354 |         // Environment-aware thresholds - CI is slower
355 |         const threshold = process.env.CI ? 100 : 50;
356 |         expect(stats!.average).toBeLessThan(threshold);
357 |       });
358 | 
359 |       // Test a non-indexed query for comparison (description column has no index)
360 |       const stop = monitor.start('non_indexed_query');
361 |       const nonIndexedResults = db.prepare("SELECT * FROM nodes WHERE description LIKE ?").all('%webhook%') as any[];
362 |       stop();
363 | 
364 |       const nonIndexedStats = monitor.getStats('non_indexed_query');
365 |       
366 |       // Non-indexed queries should still complete reasonably fast with 10k rows
367 |       const nonIndexedThreshold = process.env.CI ? 200 : 100;
368 |       expect(nonIndexedStats!.average).toBeLessThan(nonIndexedThreshold);
369 |     });
370 | 
371 |     it('should handle VACUUM operation efficiently', () => {
372 |       // Insert and delete data to create fragmentation
373 |       const nodes = generateNodes(1000);
374 |       
375 |       // Insert
376 |       const insertTx = db.transaction((nodes: ParsedNode[]) => {
377 |         nodes.forEach(node => nodeRepo.saveNode(node));
378 |       });
379 |       insertTx(nodes);
380 | 
381 |       // Delete half
382 |       db.prepare('DELETE FROM nodes WHERE ROWID % 2 = 0').run();
383 | 
384 |       // Measure VACUUM performance
385 |       const stop = monitor.start('vacuum');
386 |       db.exec('VACUUM');
387 |       stop();
388 | 
389 |       const stats = monitor.getStats('vacuum');
390 |       const threshold = process.env.CI ? 2000 : 1000;
391 |       expect(stats!.average).toBeLessThan(threshold);
392 | 
393 |       // Verify database still works
394 |       const remaining = nodeRepo.getAllNodes();
395 |       expect(remaining.length).toBeGreaterThan(0);
396 |     });
397 | 
398 |     it('should maintain performance with WAL mode', () => {
399 |       // Verify WAL mode is enabled
400 |       const mode = db.prepare('PRAGMA journal_mode').get() as { journal_mode: string };
401 |       expect(mode.journal_mode).toBe('wal');
402 | 
403 |       // Perform mixed read/write operations
404 |       const operations = 1000;
405 |       
406 |       const stop = monitor.start('wal_mixed_operations');
407 |       
408 |       for (let i = 0; i < operations; i++) {
409 |         if (i % 10 === 0) {
410 |           // Write operation
411 |           const node = generateNodes(1)[0];
412 |           nodeRepo.saveNode(node);
413 |         } else {
414 |           // Read operation
415 |           nodeRepo.getAllNodes(10);
416 |         }
417 |       }
418 |       
419 |       stop();
420 | 
421 |       const stats = monitor.getStats('wal_mixed_operations');
422 |       const threshold = process.env.CI ? 1000 : 500;
423 |       expect(stats!.average).toBeLessThan(threshold);
424 |     });
425 |   });
426 | 
427 |   describe('Memory Usage', () => {
428 |     it('should handle large result sets without excessive memory', () => {
429 |       // Insert large dataset
430 |       const nodes = generateNodes(10000);
431 |       const transaction = db.transaction((nodes: ParsedNode[]) => {
432 |         nodes.forEach(node => nodeRepo.saveNode(node));
433 |       });
434 |       transaction(nodes);
435 | 
436 |       // Measure memory before
437 |       const memBefore = process.memoryUsage().heapUsed;
438 | 
439 |       // Fetch large result set
440 |       const stop = monitor.start('large_result_set');
441 |       const results = nodeRepo.getAllNodes();
442 |       stop();
443 | 
444 |       // Measure memory after
445 |       const memAfter = process.memoryUsage().heapUsed;
446 |       const memIncrease = (memAfter - memBefore) / 1024 / 1024; // MB
447 | 
448 |       expect(results).toHaveLength(10000);
449 |       expect(memIncrease).toBeLessThan(100); // Less than 100MB increase
450 | 
451 |       const stats = monitor.getStats('large_result_set');
452 |       const threshold = process.env.CI ? 400 : 200;
453 |       expect(stats!.average).toBeLessThan(threshold);
454 |     });
455 |   });
456 | 
457 |   describe('Concurrent Write Performance', () => {
458 |     it('should handle concurrent writes with transactions', () => {
459 |       const writeBatches = 10;
460 |       const nodesPerBatch = 100;
461 | 
462 |       const stop = monitor.start('concurrent_writes');
463 | 
464 |       // Simulate concurrent write batches
465 |       const promises = Array.from({ length: writeBatches }, (_, i) => {
466 |         return new Promise<void>((resolve) => {
467 |           const nodes = generateNodes(nodesPerBatch, i * nodesPerBatch);
468 |           const transaction = db.transaction((nodes: ParsedNode[]) => {
469 |             nodes.forEach(node => nodeRepo.saveNode(node));
470 |           });
471 |           transaction(nodes);
472 |           resolve();
473 |         });
474 |       });
475 | 
476 |       Promise.all(promises);
477 |       stop();
478 | 
479 |       const stats = monitor.getStats('concurrent_writes');
480 |       const threshold = process.env.CI ? 1000 : 500;
481 |       expect(stats!.average).toBeLessThan(threshold);
482 | 
483 |       // Verify all nodes were written
484 |       const count = nodeRepo.getNodeCount();
485 |       expect(count).toBe(writeBatches * nodesPerBatch);
486 |     });
487 |   });
488 | });
489 | 
490 | // Helper functions
491 | function generateNodes(count: number, startId: number = 0): ParsedNode[] {
492 |   const categories = ['trigger', 'automation', 'transform', 'output'];
493 |   const packages = ['n8n-nodes-base', '@n8n/n8n-nodes-langchain'];
494 |   
495 |   return Array.from({ length: count }, (_, i) => ({
496 |     nodeType: `n8n-nodes-base.node${startId + i}`,
497 |     packageName: packages[i % packages.length],
498 |     displayName: `Node ${startId + i}`,
499 |     description: `Description for node ${startId + i} with ${['webhook', 'http', 'automation', 'data'][i % 4]} functionality`,
500 |     category: categories[i % categories.length],
501 |     style: 'programmatic' as const,
502 |     isAITool: i % 10 === 0,
503 |     isTrigger: categories[i % categories.length] === 'trigger',
504 |     isWebhook: i % 5 === 0,
505 |     isVersioned: true,
506 |     version: '1',
507 |     documentation: i % 3 === 0 ? `Documentation for node ${i}` : undefined,
508 |     properties: Array.from({ length: 5 }, (_, j) => ({
509 |       displayName: `Property ${j}`,
510 |       name: `prop${j}`,
511 |       type: 'string',
512 |       default: ''
513 |     })),
514 |     operations: [],
515 |     credentials: i % 4 === 0 ? [{ name: 'httpAuth', required: true }] : [],
516 |     // Add fullNodeType for search compatibility
517 |     fullNodeType: `n8n-nodes-base.node${startId + i}`
518 |   }));
519 | }
520 | 
521 | function generateDescription(index: number): string {
522 |   const descriptions = [
523 |     'Automate your workflow with powerful webhook integrations',
524 |     'Process http requests and transform data efficiently',
525 |     'Connect to external APIs and sync data seamlessly',
526 |     'Build complex automation workflows with ease',
527 |     'Transform and filter data with advanced processing operations'
528 |   ];
529 |   return descriptions[index % descriptions.length] + ` - Version ${index}`;
530 | }
531 | 
532 | // Generate nodes with searchable content for search tests
533 | function generateSearchableNodes(count: number): ParsedNode[] {
534 |   const searchTerms = ['webhook', 'http', 'request', 'automation', 'data', 'HTTP'];
535 |   const categories = ['trigger', 'automation', 'transform', 'output'];
536 |   const packages = ['n8n-nodes-base', '@n8n/n8n-nodes-langchain'];
537 |   
538 |   return Array.from({ length: count }, (_, i) => {
539 |     // Ensure some nodes match our search terms
540 |     const termIndex = i % searchTerms.length;
541 |     const searchTerm = searchTerms[termIndex];
542 |     
543 |     return {
544 |       nodeType: `n8n-nodes-base.${searchTerm}Node${i}`,
545 |       packageName: packages[i % packages.length],
546 |       displayName: `${searchTerm} Node ${i}`,
547 |       description: `${searchTerm} functionality for ${searchTerms[(i + 1) % searchTerms.length]} operations`,
548 |       category: categories[i % categories.length],
549 |       style: 'programmatic' as const,
550 |       isAITool: i % 10 === 0,
551 |       isTrigger: categories[i % categories.length] === 'trigger',
552 |       isWebhook: searchTerm === 'webhook' || i % 5 === 0,
553 |       isVersioned: true,
554 |       version: '1',
555 |       documentation: i % 3 === 0 ? `Documentation for ${searchTerm} node ${i}` : undefined,
556 |       properties: Array.from({ length: 5 }, (_, j) => ({
557 |         displayName: `Property ${j}`,
558 |         name: `prop${j}`,
559 |         type: 'string',
560 |         default: ''
561 |       })),
562 |       operations: [],
563 |       credentials: i % 4 === 0 ? [{ name: 'httpAuth', required: true }] : []
564 |     };
565 |   });
566 | }
```

--------------------------------------------------------------------------------
/tests/unit/services/enhanced-config-validator-integration.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
  2 | import { EnhancedConfigValidator } from '@/services/enhanced-config-validator';
  3 | import { ResourceSimilarityService } from '@/services/resource-similarity-service';
  4 | import { OperationSimilarityService } from '@/services/operation-similarity-service';
  5 | import { NodeRepository } from '@/database/node-repository';
  6 | 
  7 | // Mock similarity services
  8 | vi.mock('@/services/resource-similarity-service');
  9 | vi.mock('@/services/operation-similarity-service');
 10 | 
 11 | describe('EnhancedConfigValidator - Integration Tests', () => {
 12 |   let mockResourceService: any;
 13 |   let mockOperationService: any;
 14 |   let mockRepository: any;
 15 | 
 16 |   beforeEach(() => {
 17 |     mockRepository = {
 18 |       getNode: vi.fn(),
 19 |       getNodeOperations: vi.fn().mockReturnValue([]),
 20 |       getNodeResources: vi.fn().mockReturnValue([]),
 21 |       getOperationsForResource: vi.fn().mockReturnValue([]),
 22 |       getDefaultOperationForResource: vi.fn().mockReturnValue(undefined),
 23 |       getNodePropertyDefaults: vi.fn().mockReturnValue({})
 24 |     };
 25 | 
 26 |     mockResourceService = {
 27 |       findSimilarResources: vi.fn().mockReturnValue([])
 28 |     };
 29 | 
 30 |     mockOperationService = {
 31 |       findSimilarOperations: vi.fn().mockReturnValue([])
 32 |     };
 33 | 
 34 |     // Mock the constructors to return our mock services
 35 |     vi.mocked(ResourceSimilarityService).mockImplementation(() => mockResourceService);
 36 |     vi.mocked(OperationSimilarityService).mockImplementation(() => mockOperationService);
 37 | 
 38 |     // Initialize the similarity services (this will create the service instances)
 39 |     EnhancedConfigValidator.initializeSimilarityServices(mockRepository);
 40 |   });
 41 | 
 42 |   afterEach(() => {
 43 |     vi.clearAllMocks();
 44 |   });
 45 | 
 46 |   describe('similarity service integration', () => {
 47 |     it('should initialize similarity services when initializeSimilarityServices is called', () => {
 48 |       // Services should be created when initializeSimilarityServices was called in beforeEach
 49 |       expect(ResourceSimilarityService).toHaveBeenCalled();
 50 |       expect(OperationSimilarityService).toHaveBeenCalled();
 51 |     });
 52 | 
 53 |     it('should use resource similarity service for invalid resource errors', () => {
 54 |       const config = {
 55 |         resource: 'invalidResource',
 56 |         operation: 'send'
 57 |       };
 58 | 
 59 |       const properties = [
 60 |         {
 61 |           name: 'resource',
 62 |           type: 'options',
 63 |           required: true,
 64 |           options: [
 65 |             { value: 'message', name: 'Message' },
 66 |             { value: 'channel', name: 'Channel' }
 67 |           ]
 68 |         },
 69 |         {
 70 |           name: 'operation',
 71 |           type: 'options',
 72 |           required: true,
 73 |           displayOptions: {
 74 |             show: {
 75 |               resource: ['message']
 76 |             }
 77 |           },
 78 |           options: [
 79 |             { value: 'send', name: 'Send Message' }
 80 |           ]
 81 |         }
 82 |       ];
 83 | 
 84 |       // Mock resource similarity suggestions
 85 |       mockResourceService.findSimilarResources.mockReturnValue([
 86 |         {
 87 |           value: 'message',
 88 |           confidence: 0.8,
 89 |           reason: 'Similar resource name',
 90 |           availableOperations: ['send', 'update']
 91 |         }
 92 |       ]);
 93 | 
 94 |       const result = EnhancedConfigValidator.validateWithMode(
 95 |         'nodes-base.slack',
 96 |         config,
 97 |         properties,
 98 |         'operation',
 99 |         'ai-friendly'
100 |       );
101 | 
102 |       expect(mockResourceService.findSimilarResources).toHaveBeenCalledWith(
103 |         'nodes-base.slack',
104 |         'invalidResource',
105 |         expect.any(Number)
106 |       );
107 | 
108 |       // Should have suggestions in the result
109 |       expect(result.suggestions).toBeDefined();
110 |       expect(result.suggestions.length).toBeGreaterThan(0);
111 |     });
112 | 
113 |     it('should use operation similarity service for invalid operation errors', () => {
114 |       const config = {
115 |         resource: 'message',
116 |         operation: 'invalidOperation'
117 |       };
118 | 
119 |       const properties = [
120 |         {
121 |           name: 'resource',
122 |           type: 'options',
123 |           required: true,
124 |           options: [
125 |             { value: 'message', name: 'Message' }
126 |           ]
127 |         },
128 |         {
129 |           name: 'operation',
130 |           type: 'options',
131 |           required: true,
132 |           displayOptions: {
133 |             show: {
134 |               resource: ['message']
135 |             }
136 |           },
137 |           options: [
138 |             { value: 'send', name: 'Send Message' },
139 |             { value: 'update', name: 'Update Message' }
140 |           ]
141 |         }
142 |       ];
143 | 
144 |       // Mock operation similarity suggestions
145 |       mockOperationService.findSimilarOperations.mockReturnValue([
146 |         {
147 |           value: 'send',
148 |           confidence: 0.9,
149 |           reason: 'Very similar - likely a typo',
150 |           resource: 'message'
151 |         }
152 |       ]);
153 | 
154 |       const result = EnhancedConfigValidator.validateWithMode(
155 |         'nodes-base.slack',
156 |         config,
157 |         properties,
158 |         'operation',
159 |         'ai-friendly'
160 |       );
161 | 
162 |       expect(mockOperationService.findSimilarOperations).toHaveBeenCalledWith(
163 |         'nodes-base.slack',
164 |         'invalidOperation',
165 |         'message',
166 |         expect.any(Number)
167 |       );
168 | 
169 |       // Should have suggestions in the result
170 |       expect(result.suggestions).toBeDefined();
171 |       expect(result.suggestions.length).toBeGreaterThan(0);
172 |     });
173 | 
174 |     it('should handle similarity service errors gracefully', () => {
175 |       const config = {
176 |         resource: 'invalidResource',
177 |         operation: 'send'
178 |       };
179 | 
180 |       const properties = [
181 |         {
182 |           name: 'resource',
183 |           type: 'options',
184 |           required: true,
185 |           options: [
186 |             { value: 'message', name: 'Message' }
187 |           ]
188 |         }
189 |       ];
190 | 
191 |       // Mock service to throw error
192 |       mockResourceService.findSimilarResources.mockImplementation(() => {
193 |         throw new Error('Service error');
194 |       });
195 | 
196 |       const result = EnhancedConfigValidator.validateWithMode(
197 |         'nodes-base.slack',
198 |         config,
199 |         properties,
200 |         'operation',
201 |         'ai-friendly'
202 |       );
203 | 
204 |       // Should not crash and still provide basic validation
205 |       expect(result).toBeDefined();
206 |       expect(result.valid).toBe(false);
207 |       expect(result.errors.length).toBeGreaterThan(0);
208 |     });
209 | 
210 |     it('should not call similarity services for valid configurations', () => {
211 |       // Mock repository to return valid resources for this test
212 |       mockRepository.getNodeResources.mockReturnValue([
213 |         { value: 'message', name: 'Message' },
214 |         { value: 'channel', name: 'Channel' }
215 |       ]);
216 |       // Mock getNodeOperations to return valid operations
217 |       mockRepository.getNodeOperations.mockReturnValue([
218 |         { value: 'send', name: 'Send Message' }
219 |       ]);
220 | 
221 |       const config = {
222 |         resource: 'message',
223 |         operation: 'send',
224 |         channel: '#general',  // Add required field for Slack send
225 |         text: 'Test message'  // Add required field for Slack send
226 |       };
227 | 
228 |       const properties = [
229 |         {
230 |           name: 'resource',
231 |           type: 'options',
232 |           required: true,
233 |           options: [
234 |             { value: 'message', name: 'Message' }
235 |           ]
236 |         },
237 |         {
238 |           name: 'operation',
239 |           type: 'options',
240 |           required: true,
241 |           displayOptions: {
242 |             show: {
243 |               resource: ['message']
244 |             }
245 |           },
246 |           options: [
247 |             { value: 'send', name: 'Send Message' }
248 |           ]
249 |         }
250 |       ];
251 | 
252 |       const result = EnhancedConfigValidator.validateWithMode(
253 |         'nodes-base.slack',
254 |         config,
255 |         properties,
256 |         'operation',
257 |         'ai-friendly'
258 |       );
259 | 
260 |       // Should not call similarity services for valid config
261 |       expect(mockResourceService.findSimilarResources).not.toHaveBeenCalled();
262 |       expect(mockOperationService.findSimilarOperations).not.toHaveBeenCalled();
263 |       expect(result.valid).toBe(true);
264 |     });
265 | 
266 |     it('should limit suggestion count when calling similarity services', () => {
267 |       const config = {
268 |         resource: 'invalidResource'
269 |       };
270 | 
271 |       const properties = [
272 |         {
273 |           name: 'resource',
274 |           type: 'options',
275 |           required: true,
276 |           options: [
277 |             { value: 'message', name: 'Message' }
278 |           ]
279 |         }
280 |       ];
281 | 
282 |       EnhancedConfigValidator.validateWithMode(
283 |         'nodes-base.slack',
284 |         config,
285 |         properties,
286 |         'operation',
287 |         'ai-friendly'
288 |       );
289 | 
290 |       expect(mockResourceService.findSimilarResources).toHaveBeenCalledWith(
291 |         'nodes-base.slack',
292 |         'invalidResource',
293 |         3 // Should limit to 3 suggestions
294 |       );
295 |     });
296 |   });
297 | 
298 |   describe('error enhancement with suggestions', () => {
299 |     it('should enhance resource validation errors with suggestions', () => {
300 |       const config = {
301 |         resource: 'msgs' // Typo for 'message'
302 |       };
303 | 
304 |       const properties = [
305 |         {
306 |           name: 'resource',
307 |           type: 'options',
308 |           required: true,
309 |           options: [
310 |             { value: 'message', name: 'Message' },
311 |             { value: 'channel', name: 'Channel' }
312 |           ]
313 |         }
314 |       ];
315 | 
316 |       // Mock high-confidence suggestion
317 |       mockResourceService.findSimilarResources.mockReturnValue([
318 |         {
319 |           value: 'message',
320 |           confidence: 0.85,
321 |           reason: 'Very similar - likely a typo',
322 |           availableOperations: ['send', 'update', 'delete']
323 |         }
324 |       ]);
325 | 
326 |       const result = EnhancedConfigValidator.validateWithMode(
327 |         'nodes-base.slack',
328 |         config,
329 |         properties,
330 |         'operation',
331 |         'ai-friendly'
332 |       );
333 | 
334 |       // Should have enhanced error with suggestion
335 |       const resourceError = result.errors.find(e => e.property === 'resource');
336 |       expect(resourceError).toBeDefined();
337 |       expect(resourceError!.suggestion).toBeDefined();
338 |       expect(resourceError!.suggestion).toContain('message');
339 |     });
340 | 
341 |     it('should enhance operation validation errors with suggestions', () => {
342 |       const config = {
343 |         resource: 'message',
344 |         operation: 'sned' // Typo for 'send'
345 |       };
346 | 
347 |       const properties = [
348 |         {
349 |           name: 'resource',
350 |           type: 'options',
351 |           required: true,
352 |           options: [
353 |             { value: 'message', name: 'Message' }
354 |           ]
355 |         },
356 |         {
357 |           name: 'operation',
358 |           type: 'options',
359 |           required: true,
360 |           displayOptions: {
361 |             show: {
362 |               resource: ['message']
363 |             }
364 |           },
365 |           options: [
366 |             { value: 'send', name: 'Send Message' },
367 |             { value: 'update', name: 'Update Message' }
368 |           ]
369 |         }
370 |       ];
371 | 
372 |       // Mock high-confidence suggestion
373 |       mockOperationService.findSimilarOperations.mockReturnValue([
374 |         {
375 |           value: 'send',
376 |           confidence: 0.9,
377 |           reason: 'Almost exact match - likely a typo',
378 |           resource: 'message',
379 |           description: 'Send Message'
380 |         }
381 |       ]);
382 | 
383 |       const result = EnhancedConfigValidator.validateWithMode(
384 |         'nodes-base.slack',
385 |         config,
386 |         properties,
387 |         'operation',
388 |         'ai-friendly'
389 |       );
390 | 
391 |       // Should have enhanced error with suggestion
392 |       const operationError = result.errors.find(e => e.property === 'operation');
393 |       expect(operationError).toBeDefined();
394 |       expect(operationError!.suggestion).toBeDefined();
395 |       expect(operationError!.suggestion).toContain('send');
396 |     });
397 | 
398 |     it('should not enhance errors when no good suggestions are available', () => {
399 |       const config = {
400 |         resource: 'completelyWrongValue'
401 |       };
402 | 
403 |       const properties = [
404 |         {
405 |           name: 'resource',
406 |           type: 'options',
407 |           required: true,
408 |           options: [
409 |             { value: 'message', name: 'Message' }
410 |           ]
411 |         }
412 |       ];
413 | 
414 |       // Mock low-confidence suggestions
415 |       mockResourceService.findSimilarResources.mockReturnValue([
416 |         {
417 |           value: 'message',
418 |           confidence: 0.2, // Too low confidence
419 |           reason: 'Possibly related resource'
420 |         }
421 |       ]);
422 | 
423 |       const result = EnhancedConfigValidator.validateWithMode(
424 |         'nodes-base.slack',
425 |         config,
426 |         properties,
427 |         'operation',
428 |         'ai-friendly'
429 |       );
430 | 
431 |       // Should not enhance error due to low confidence
432 |       const resourceError = result.errors.find(e => e.property === 'resource');
433 |       expect(resourceError).toBeDefined();
434 |       expect(resourceError!.suggestion).toBeUndefined();
435 |     });
436 | 
437 |     it('should provide multiple operation suggestions when resource is known', () => {
438 |       const config = {
439 |         resource: 'message',
440 |         operation: 'invalidOp'
441 |       };
442 | 
443 |       const properties = [
444 |         {
445 |           name: 'resource',
446 |           type: 'options',
447 |           required: true,
448 |           options: [
449 |             { value: 'message', name: 'Message' }
450 |           ]
451 |         },
452 |         {
453 |           name: 'operation',
454 |           type: 'options',
455 |           required: true,
456 |           displayOptions: {
457 |             show: {
458 |               resource: ['message']
459 |             }
460 |           },
461 |           options: [
462 |             { value: 'send', name: 'Send Message' },
463 |             { value: 'update', name: 'Update Message' },
464 |             { value: 'delete', name: 'Delete Message' }
465 |           ]
466 |         }
467 |       ];
468 | 
469 |       // Mock multiple suggestions
470 |       mockOperationService.findSimilarOperations.mockReturnValue([
471 |         { value: 'send', confidence: 0.7, reason: 'Similar operation' },
472 |         { value: 'update', confidence: 0.6, reason: 'Similar operation' },
473 |         { value: 'delete', confidence: 0.5, reason: 'Similar operation' }
474 |       ]);
475 | 
476 |       const result = EnhancedConfigValidator.validateWithMode(
477 |         'nodes-base.slack',
478 |         config,
479 |         properties,
480 |         'operation',
481 |         'ai-friendly'
482 |       );
483 | 
484 |       // Should include multiple suggestions in the result
485 |       expect(result.suggestions.length).toBeGreaterThan(2);
486 |       const operationSuggestions = result.suggestions.filter(s =>
487 |         s.includes('send') || s.includes('update') || s.includes('delete')
488 |       );
489 |       expect(operationSuggestions.length).toBeGreaterThan(0);
490 |     });
491 |   });
492 | 
493 |   describe('confidence thresholds and filtering', () => {
494 |     it('should only use high confidence resource suggestions', () => {
495 |       const config = {
496 |         resource: 'invalidResource'
497 |       };
498 | 
499 |       const properties = [
500 |         {
501 |           name: 'resource',
502 |           type: 'options',
503 |           required: true,
504 |           options: [
505 |             { value: 'message', name: 'Message' }
506 |           ]
507 |         }
508 |       ];
509 | 
510 |       // Mock mixed confidence suggestions
511 |       mockResourceService.findSimilarResources.mockReturnValue([
512 |         { value: 'message1', confidence: 0.9, reason: 'High confidence' },
513 |         { value: 'message2', confidence: 0.4, reason: 'Low confidence' },
514 |         { value: 'message3', confidence: 0.7, reason: 'Medium confidence' }
515 |       ]);
516 | 
517 |       const result = EnhancedConfigValidator.validateWithMode(
518 |         'nodes-base.slack',
519 |         config,
520 |         properties,
521 |         'operation',
522 |         'ai-friendly'
523 |       );
524 | 
525 |       // Should only use suggestions above threshold
526 |       const resourceError = result.errors.find(e => e.property === 'resource');
527 |       expect(resourceError?.suggestion).toBeDefined();
528 |       // Should prefer high confidence suggestion
529 |       expect(resourceError!.suggestion).toContain('message1');
530 |     });
531 | 
532 |     it('should only use high confidence operation suggestions', () => {
533 |       const config = {
534 |         resource: 'message',
535 |         operation: 'invalidOperation'
536 |       };
537 | 
538 |       const properties = [
539 |         {
540 |           name: 'resource',
541 |           type: 'options',
542 |           required: true,
543 |           options: [
544 |             { value: 'message', name: 'Message' }
545 |           ]
546 |         },
547 |         {
548 |           name: 'operation',
549 |           type: 'options',
550 |           required: true,
551 |           displayOptions: {
552 |             show: {
553 |               resource: ['message']
554 |             }
555 |           },
556 |           options: [
557 |             { value: 'send', name: 'Send Message' }
558 |           ]
559 |         }
560 |       ];
561 | 
562 |       // Mock mixed confidence suggestions
563 |       mockOperationService.findSimilarOperations.mockReturnValue([
564 |         { value: 'send', confidence: 0.95, reason: 'Very high confidence' },
565 |         { value: 'post', confidence: 0.3, reason: 'Low confidence' }
566 |       ]);
567 | 
568 |       const result = EnhancedConfigValidator.validateWithMode(
569 |         'nodes-base.slack',
570 |         config,
571 |         properties,
572 |         'operation',
573 |         'ai-friendly'
574 |       );
575 | 
576 |       // Should only use high confidence suggestion
577 |       const operationError = result.errors.find(e => e.property === 'operation');
578 |       expect(operationError?.suggestion).toBeDefined();
579 |       expect(operationError!.suggestion).toContain('send');
580 |       expect(operationError!.suggestion).not.toContain('post');
581 |     });
582 |   });
583 | 
584 |   describe('integration with existing validation logic', () => {
585 |     it('should work with minimal validation mode', () => {
586 |       // Mock repository to return empty resources
587 |       mockRepository.getNodeResources.mockReturnValue([]);
588 | 
589 |       const config = {
590 |         resource: 'invalidResource'
591 |       };
592 | 
593 |       const properties = [
594 |         {
595 |           name: 'resource',
596 |           type: 'options',
597 |           required: true,
598 |           options: [
599 |             { value: 'message', name: 'Message' }
600 |           ]
601 |         }
602 |       ];
603 | 
604 |       mockResourceService.findSimilarResources.mockReturnValue([
605 |         { value: 'message', confidence: 0.8, reason: 'Similar' }
606 |       ]);
607 | 
608 |       const result = EnhancedConfigValidator.validateWithMode(
609 |         'nodes-base.slack',
610 |         config,
611 |         properties,
612 |         'minimal',
613 |         'ai-friendly'
614 |       );
615 | 
616 |       // Should still enhance errors in minimal mode
617 |       expect(mockResourceService.findSimilarResources).toHaveBeenCalled();
618 |       expect(result.errors.length).toBeGreaterThan(0);
619 |     });
620 | 
621 |     it('should work with strict validation profile', () => {
622 |       // Mock repository to return valid resource but no operations
623 |       mockRepository.getNodeResources.mockReturnValue([
624 |         { value: 'message', name: 'Message' }
625 |       ]);
626 |       mockRepository.getOperationsForResource.mockReturnValue([]);
627 | 
628 |       const config = {
629 |         resource: 'message',
630 |         operation: 'invalidOp'
631 |       };
632 | 
633 |       const properties = [
634 |         {
635 |           name: 'resource',
636 |           type: 'options',
637 |           required: true,
638 |           options: [
639 |             { value: 'message', name: 'Message' }
640 |           ]
641 |         },
642 |         {
643 |           name: 'operation',
644 |           type: 'options',
645 |           required: true,
646 |           displayOptions: {
647 |             show: {
648 |               resource: ['message']
649 |             }
650 |           },
651 |           options: [
652 |             { value: 'send', name: 'Send Message' }
653 |           ]
654 |         }
655 |       ];
656 | 
657 |       mockOperationService.findSimilarOperations.mockReturnValue([
658 |         { value: 'send', confidence: 0.8, reason: 'Similar' }
659 |       ]);
660 | 
661 |       const result = EnhancedConfigValidator.validateWithMode(
662 |         'nodes-base.slack',
663 |         config,
664 |         properties,
665 |         'operation',
666 |         'strict'
667 |       );
668 | 
669 |       // Should enhance errors regardless of profile
670 |       expect(mockOperationService.findSimilarOperations).toHaveBeenCalled();
671 |       const operationError = result.errors.find(e => e.property === 'operation');
672 |       expect(operationError?.suggestion).toBeDefined();
673 |     });
674 | 
675 |     it('should preserve original error properties when enhancing', () => {
676 |       const config = {
677 |         resource: 'invalidResource'
678 |       };
679 | 
680 |       const properties = [
681 |         {
682 |           name: 'resource',
683 |           type: 'options',
684 |           required: true,
685 |           options: [
686 |             { value: 'message', name: 'Message' }
687 |           ]
688 |         }
689 |       ];
690 | 
691 |       mockResourceService.findSimilarResources.mockReturnValue([
692 |         { value: 'message', confidence: 0.8, reason: 'Similar' }
693 |       ]);
694 | 
695 |       const result = EnhancedConfigValidator.validateWithMode(
696 |         'nodes-base.slack',
697 |         config,
698 |         properties,
699 |         'operation',
700 |         'ai-friendly'
701 |       );
702 | 
703 |       const resourceError = result.errors.find(e => e.property === 'resource');
704 | 
705 |       // Should preserve original error properties
706 |       expect(resourceError?.type).toBeDefined();
707 |       expect(resourceError?.property).toBe('resource');
708 |       expect(resourceError?.message).toBeDefined();
709 | 
710 |       // Should add suggestion without overriding other properties
711 |       expect(resourceError?.suggestion).toBeDefined();
712 |     });
713 |   });
714 | });
```
Page 29/59FirstPrevNextLast