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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/src/mcp/server.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
   2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
   3 | import { 
   4 |   CallToolRequestSchema, 
   5 |   ListToolsRequestSchema,
   6 |   InitializeRequestSchema,
   7 | } from '@modelcontextprotocol/sdk/types.js';
   8 | import { existsSync, promises as fs } from 'fs';
   9 | import path from 'path';
  10 | import { n8nDocumentationToolsFinal } from './tools';
  11 | import { n8nManagementTools } from './tools-n8n-manager';
  12 | import { makeToolsN8nFriendly } from './tools-n8n-friendly';
  13 | import { getWorkflowExampleString } from './workflow-examples';
  14 | import { logger } from '../utils/logger';
  15 | import { NodeRepository } from '../database/node-repository';
  16 | import { DatabaseAdapter, createDatabaseAdapter } from '../database/database-adapter';
  17 | import { PropertyFilter } from '../services/property-filter';
  18 | import { TaskTemplates } from '../services/task-templates';
  19 | import { ConfigValidator } from '../services/config-validator';
  20 | import { EnhancedConfigValidator, ValidationMode, ValidationProfile } from '../services/enhanced-config-validator';
  21 | import { PropertyDependencies } from '../services/property-dependencies';
  22 | import { SimpleCache } from '../utils/simple-cache';
  23 | import { TemplateService } from '../templates/template-service';
  24 | import { WorkflowValidator } from '../services/workflow-validator';
  25 | import { isN8nApiConfigured } from '../config/n8n-api';
  26 | import * as n8nHandlers from './handlers-n8n-manager';
  27 | import { handleUpdatePartialWorkflow } from './handlers-workflow-diff';
  28 | import { getToolDocumentation, getToolsOverview } from './tools-documentation';
  29 | import { PROJECT_VERSION } from '../utils/version';
  30 | import { getNodeTypeAlternatives, getWorkflowNodeType } from '../utils/node-utils';
  31 | import { NodeTypeNormalizer } from '../utils/node-type-normalizer';
  32 | import { ToolValidation, Validator, ValidationError } from '../utils/validation-schemas';
  33 | import {
  34 |   negotiateProtocolVersion,
  35 |   logProtocolNegotiation,
  36 |   STANDARD_PROTOCOL_VERSION
  37 | } from '../utils/protocol-version';
  38 | import { InstanceContext } from '../types/instance-context';
  39 | import { telemetry } from '../telemetry';
  40 | import { EarlyErrorLogger } from '../telemetry/early-error-logger';
  41 | import { STARTUP_CHECKPOINTS } from '../telemetry/startup-checkpoints';
  42 | 
  43 | interface NodeRow {
  44 |   node_type: string;
  45 |   package_name: string;
  46 |   display_name: string;
  47 |   description?: string;
  48 |   category?: string;
  49 |   development_style?: string;
  50 |   is_ai_tool: number;
  51 |   is_trigger: number;
  52 |   is_webhook: number;
  53 |   is_versioned: number;
  54 |   version?: string;
  55 |   documentation?: string;
  56 |   properties_schema?: string;
  57 |   operations?: string;
  58 |   credentials_required?: string;
  59 | }
  60 | 
  61 | export class N8NDocumentationMCPServer {
  62 |   private server: Server;
  63 |   private db: DatabaseAdapter | null = null;
  64 |   private repository: NodeRepository | null = null;
  65 |   private templateService: TemplateService | null = null;
  66 |   private initialized: Promise<void>;
  67 |   private cache = new SimpleCache();
  68 |   private clientInfo: any = null;
  69 |   private instanceContext?: InstanceContext;
  70 |   private previousTool: string | null = null;
  71 |   private previousToolTimestamp: number = Date.now();
  72 |   private earlyLogger: EarlyErrorLogger | null = null;
  73 | 
  74 |   constructor(instanceContext?: InstanceContext, earlyLogger?: EarlyErrorLogger) {
  75 |     this.instanceContext = instanceContext;
  76 |     this.earlyLogger = earlyLogger || null;
  77 |     // Check for test environment first
  78 |     const envDbPath = process.env.NODE_DB_PATH;
  79 |     let dbPath: string | null = null;
  80 |     
  81 |     let possiblePaths: string[] = [];
  82 |     
  83 |     if (envDbPath && (envDbPath === ':memory:' || existsSync(envDbPath))) {
  84 |       dbPath = envDbPath;
  85 |     } else {
  86 |       // Try multiple database paths
  87 |       possiblePaths = [
  88 |         path.join(process.cwd(), 'data', 'nodes.db'),
  89 |         path.join(__dirname, '../../data', 'nodes.db'),
  90 |         './data/nodes.db'
  91 |       ];
  92 |       
  93 |       for (const p of possiblePaths) {
  94 |         if (existsSync(p)) {
  95 |           dbPath = p;
  96 |           break;
  97 |         }
  98 |       }
  99 |     }
 100 |     
 101 |     if (!dbPath) {
 102 |       logger.error('Database not found in any of the expected locations:', possiblePaths);
 103 |       throw new Error('Database nodes.db not found. Please run npm run rebuild first.');
 104 |     }
 105 |     
 106 |     // Initialize database asynchronously
 107 |     this.initialized = this.initializeDatabase(dbPath).then(() => {
 108 |       // After database is ready, check n8n API configuration (v2.18.3)
 109 |       if (this.earlyLogger) {
 110 |         this.earlyLogger.logCheckpoint(STARTUP_CHECKPOINTS.N8N_API_CHECKING);
 111 |       }
 112 | 
 113 |       // Log n8n API configuration status at startup
 114 |       const apiConfigured = isN8nApiConfigured();
 115 |       const totalTools = apiConfigured ?
 116 |         n8nDocumentationToolsFinal.length + n8nManagementTools.length :
 117 |         n8nDocumentationToolsFinal.length;
 118 | 
 119 |       logger.info(`MCP server initialized with ${totalTools} tools (n8n API: ${apiConfigured ? 'configured' : 'not configured'})`);
 120 | 
 121 |       if (this.earlyLogger) {
 122 |         this.earlyLogger.logCheckpoint(STARTUP_CHECKPOINTS.N8N_API_READY);
 123 |       }
 124 |     });
 125 | 
 126 |     logger.info('Initializing n8n Documentation MCP server');
 127 |     
 128 |     this.server = new Server(
 129 |       {
 130 |         name: 'n8n-documentation-mcp',
 131 |         version: '1.0.0',
 132 |       },
 133 |       {
 134 |         capabilities: {
 135 |           tools: {},
 136 |         },
 137 |       }
 138 |     );
 139 | 
 140 |     this.setupHandlers();
 141 |   }
 142 |   
 143 |   private async initializeDatabase(dbPath: string): Promise<void> {
 144 |     try {
 145 |       // Checkpoint: Database connecting (v2.18.3)
 146 |       if (this.earlyLogger) {
 147 |         this.earlyLogger.logCheckpoint(STARTUP_CHECKPOINTS.DATABASE_CONNECTING);
 148 |       }
 149 | 
 150 |       logger.debug('Database initialization starting...', { dbPath });
 151 | 
 152 |       this.db = await createDatabaseAdapter(dbPath);
 153 |       logger.debug('Database adapter created');
 154 | 
 155 |       // If using in-memory database for tests, initialize schema
 156 |       if (dbPath === ':memory:') {
 157 |         await this.initializeInMemorySchema();
 158 |         logger.debug('In-memory schema initialized');
 159 |       }
 160 | 
 161 |       this.repository = new NodeRepository(this.db);
 162 |       logger.debug('Node repository initialized');
 163 | 
 164 |       this.templateService = new TemplateService(this.db);
 165 |       logger.debug('Template service initialized');
 166 | 
 167 |       // Initialize similarity services for enhanced validation
 168 |       EnhancedConfigValidator.initializeSimilarityServices(this.repository);
 169 |       logger.debug('Similarity services initialized');
 170 | 
 171 |       // Checkpoint: Database connected (v2.18.3)
 172 |       if (this.earlyLogger) {
 173 |         this.earlyLogger.logCheckpoint(STARTUP_CHECKPOINTS.DATABASE_CONNECTED);
 174 |       }
 175 | 
 176 |       logger.info(`Database initialized successfully from: ${dbPath}`);
 177 |     } catch (error) {
 178 |       logger.error('Failed to initialize database:', error);
 179 |       throw new Error(`Failed to open database: ${error instanceof Error ? error.message : 'Unknown error'}`);
 180 |     }
 181 |   }
 182 |   
 183 |   private async initializeInMemorySchema(): Promise<void> {
 184 |     if (!this.db) return;
 185 | 
 186 |     // Read and execute schema
 187 |     const schemaPath = path.join(__dirname, '../../src/database/schema.sql');
 188 |     const schema = await fs.readFile(schemaPath, 'utf-8');
 189 | 
 190 |     // Parse SQL statements properly (handles BEGIN...END blocks in triggers)
 191 |     const statements = this.parseSQLStatements(schema);
 192 | 
 193 |     for (const statement of statements) {
 194 |       if (statement.trim()) {
 195 |         try {
 196 |           this.db.exec(statement);
 197 |         } catch (error) {
 198 |           logger.error(`Failed to execute SQL statement: ${statement.substring(0, 100)}...`, error);
 199 |           throw error;
 200 |         }
 201 |       }
 202 |     }
 203 |   }
 204 | 
 205 |   /**
 206 |    * Parse SQL statements from schema file, properly handling multi-line statements
 207 |    * including triggers with BEGIN...END blocks
 208 |    */
 209 |   private parseSQLStatements(sql: string): string[] {
 210 |     const statements: string[] = [];
 211 |     let current = '';
 212 |     let inBlock = false;
 213 | 
 214 |     const lines = sql.split('\n');
 215 | 
 216 |     for (const line of lines) {
 217 |       const trimmed = line.trim().toUpperCase();
 218 | 
 219 |       // Skip comments and empty lines
 220 |       if (trimmed.startsWith('--') || trimmed === '') {
 221 |         continue;
 222 |       }
 223 | 
 224 |       // Track BEGIN...END blocks (triggers, procedures)
 225 |       if (trimmed.includes('BEGIN')) {
 226 |         inBlock = true;
 227 |       }
 228 | 
 229 |       current += line + '\n';
 230 | 
 231 |       // End of block (trigger/procedure)
 232 |       if (inBlock && trimmed === 'END;') {
 233 |         statements.push(current.trim());
 234 |         current = '';
 235 |         inBlock = false;
 236 |         continue;
 237 |       }
 238 | 
 239 |       // Regular statement end (not in block)
 240 |       if (!inBlock && trimmed.endsWith(';')) {
 241 |         statements.push(current.trim());
 242 |         current = '';
 243 |       }
 244 |     }
 245 | 
 246 |     // Add any remaining content
 247 |     if (current.trim()) {
 248 |       statements.push(current.trim());
 249 |     }
 250 | 
 251 |     return statements.filter(s => s.length > 0);
 252 |   }
 253 |   
 254 |   private async ensureInitialized(): Promise<void> {
 255 |     await this.initialized;
 256 |     if (!this.db || !this.repository) {
 257 |       throw new Error('Database not initialized');
 258 |     }
 259 | 
 260 |     // Validate database health on first access
 261 |     if (!this.dbHealthChecked) {
 262 |       await this.validateDatabaseHealth();
 263 |       this.dbHealthChecked = true;
 264 |     }
 265 |   }
 266 | 
 267 |   private dbHealthChecked: boolean = false;
 268 | 
 269 |   private async validateDatabaseHealth(): Promise<void> {
 270 |     if (!this.db) return;
 271 | 
 272 |     try {
 273 |       // Check if nodes table has data
 274 |       const nodeCount = this.db.prepare('SELECT COUNT(*) as count FROM nodes').get() as { count: number };
 275 | 
 276 |       if (nodeCount.count === 0) {
 277 |         logger.error('CRITICAL: Database is empty - no nodes found! Please run: npm run rebuild');
 278 |         throw new Error('Database is empty. Run "npm run rebuild" to populate node data.');
 279 |       }
 280 | 
 281 |       // Check if FTS5 table exists
 282 |       const ftsExists = this.db.prepare(`
 283 |         SELECT name FROM sqlite_master
 284 |         WHERE type='table' AND name='nodes_fts'
 285 |       `).get();
 286 | 
 287 |       if (!ftsExists) {
 288 |         logger.warn('FTS5 table missing - search performance will be degraded. Please run: npm run rebuild');
 289 |       } else {
 290 |         const ftsCount = this.db.prepare('SELECT COUNT(*) as count FROM nodes_fts').get() as { count: number };
 291 |         if (ftsCount.count === 0) {
 292 |           logger.warn('FTS5 index is empty - search will not work properly. Please run: npm run rebuild');
 293 |         }
 294 |       }
 295 | 
 296 |       logger.info(`Database health check passed: ${nodeCount.count} nodes loaded`);
 297 |     } catch (error) {
 298 |       logger.error('Database health check failed:', error);
 299 |       throw error;
 300 |     }
 301 |   }
 302 | 
 303 |   private setupHandlers(): void {
 304 |     // Handle initialization
 305 |     this.server.setRequestHandler(InitializeRequestSchema, async (request) => {
 306 |       const clientVersion = request.params.protocolVersion;
 307 |       const clientCapabilities = request.params.capabilities;
 308 |       const clientInfo = request.params.clientInfo;
 309 |       
 310 |       logger.info('MCP Initialize request received', {
 311 |         clientVersion,
 312 |         clientCapabilities,
 313 |         clientInfo
 314 |       });
 315 | 
 316 |       // Track session start
 317 |       telemetry.trackSessionStart();
 318 | 
 319 |       // Store client info for later use
 320 |       this.clientInfo = clientInfo;
 321 |       
 322 |       // Negotiate protocol version based on client information
 323 |       const negotiationResult = negotiateProtocolVersion(
 324 |         clientVersion,
 325 |         clientInfo,
 326 |         undefined, // no user agent in MCP protocol
 327 |         undefined  // no headers in MCP protocol
 328 |       );
 329 |       
 330 |       logProtocolNegotiation(negotiationResult, logger, 'MCP_INITIALIZE');
 331 |       
 332 |       // Warn if there's a version mismatch (for debugging)
 333 |       if (clientVersion && clientVersion !== negotiationResult.version) {
 334 |         logger.warn(`Protocol version negotiated: client requested ${clientVersion}, server will use ${negotiationResult.version}`, {
 335 |           reasoning: negotiationResult.reasoning
 336 |         });
 337 |       }
 338 |       
 339 |       const response = {
 340 |         protocolVersion: negotiationResult.version,
 341 |         capabilities: {
 342 |           tools: {},
 343 |         },
 344 |         serverInfo: {
 345 |           name: 'n8n-documentation-mcp',
 346 |           version: PROJECT_VERSION,
 347 |         },
 348 |       };
 349 |       
 350 |       logger.info('MCP Initialize response', { response });
 351 |       return response;
 352 |     });
 353 | 
 354 |     // Handle tool listing
 355 |     this.server.setRequestHandler(ListToolsRequestSchema, async (request) => {
 356 |       // Combine documentation tools with management tools if API is configured
 357 |       let tools = [...n8nDocumentationToolsFinal];
 358 | 
 359 |       // Check if n8n API tools should be available
 360 |       // 1. Environment variables (backward compatibility)
 361 |       // 2. Instance context (multi-tenant support)
 362 |       // 3. Multi-tenant mode enabled (always show tools, runtime checks will handle auth)
 363 |       const hasEnvConfig = isN8nApiConfigured();
 364 |       const hasInstanceConfig = !!(this.instanceContext?.n8nApiUrl && this.instanceContext?.n8nApiKey);
 365 |       const isMultiTenantEnabled = process.env.ENABLE_MULTI_TENANT === 'true';
 366 | 
 367 |       const shouldIncludeManagementTools = hasEnvConfig || hasInstanceConfig || isMultiTenantEnabled;
 368 | 
 369 |       if (shouldIncludeManagementTools) {
 370 |         tools.push(...n8nManagementTools);
 371 |         logger.debug(`Tool listing: ${tools.length} tools available (${n8nDocumentationToolsFinal.length} documentation + ${n8nManagementTools.length} management)`, {
 372 |           hasEnvConfig,
 373 |           hasInstanceConfig,
 374 |           isMultiTenantEnabled
 375 |         });
 376 |       } else {
 377 |         logger.debug(`Tool listing: ${tools.length} tools available (documentation only)`, {
 378 |           hasEnvConfig,
 379 |           hasInstanceConfig,
 380 |           isMultiTenantEnabled
 381 |         });
 382 |       }
 383 |       
 384 |       // Check if client is n8n (from initialization)
 385 |       const clientInfo = this.clientInfo;
 386 |       const isN8nClient = clientInfo?.name?.includes('n8n') || 
 387 |                          clientInfo?.name?.includes('langchain');
 388 |       
 389 |       if (isN8nClient) {
 390 |         logger.info('Detected n8n client, using n8n-friendly tool descriptions');
 391 |         tools = makeToolsN8nFriendly(tools);
 392 |       }
 393 |       
 394 |       // Log validation tools' input schemas for debugging
 395 |       const validationTools = tools.filter(t => t.name.startsWith('validate_'));
 396 |       validationTools.forEach(tool => {
 397 |         logger.info('Validation tool schema', {
 398 |           toolName: tool.name,
 399 |           inputSchema: JSON.stringify(tool.inputSchema, null, 2),
 400 |           hasOutputSchema: !!tool.outputSchema,
 401 |           description: tool.description
 402 |         });
 403 |       });
 404 |       
 405 |       return { tools };
 406 |     });
 407 | 
 408 |     // Handle tool execution
 409 |     this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
 410 |       const { name, arguments: args } = request.params;
 411 |       
 412 |       // Enhanced logging for debugging tool calls
 413 |       logger.info('Tool call received - DETAILED DEBUG', {
 414 |         toolName: name,
 415 |         arguments: JSON.stringify(args, null, 2),
 416 |         argumentsType: typeof args,
 417 |         argumentsKeys: args ? Object.keys(args) : [],
 418 |         hasNodeType: args && 'nodeType' in args,
 419 |         hasConfig: args && 'config' in args,
 420 |         configType: args && args.config ? typeof args.config : 'N/A',
 421 |         rawRequest: JSON.stringify(request.params)
 422 |       });
 423 |       
 424 |       // Workaround for n8n's nested output bug
 425 |       // Check if args contains nested 'output' structure from n8n's memory corruption
 426 |       let processedArgs = args;
 427 |       if (args && typeof args === 'object' && 'output' in args) {
 428 |         try {
 429 |           const possibleNestedData = args.output;
 430 |           // If output is a string that looks like JSON, try to parse it
 431 |           if (typeof possibleNestedData === 'string' && possibleNestedData.trim().startsWith('{')) {
 432 |             const parsed = JSON.parse(possibleNestedData);
 433 |             if (parsed && typeof parsed === 'object') {
 434 |               logger.warn('Detected n8n nested output bug, attempting to extract actual arguments', {
 435 |                 originalArgs: args,
 436 |                 extractedArgs: parsed
 437 |               });
 438 |               
 439 |               // Validate the extracted arguments match expected tool schema
 440 |               if (this.validateExtractedArgs(name, parsed)) {
 441 |                 // Use the extracted data as args
 442 |                 processedArgs = parsed;
 443 |               } else {
 444 |                 logger.warn('Extracted arguments failed validation, using original args', {
 445 |                   toolName: name,
 446 |                   extractedArgs: parsed
 447 |                 });
 448 |               }
 449 |             }
 450 |           }
 451 |         } catch (parseError) {
 452 |           logger.debug('Failed to parse nested output, continuing with original args', { 
 453 |             error: parseError instanceof Error ? parseError.message : String(parseError) 
 454 |           });
 455 |         }
 456 |       }
 457 |       
 458 |       try {
 459 |         logger.debug(`Executing tool: ${name}`, { args: processedArgs });
 460 |         const startTime = Date.now();
 461 |         const result = await this.executeTool(name, processedArgs);
 462 |         const duration = Date.now() - startTime;
 463 |         logger.debug(`Tool ${name} executed successfully`);
 464 | 
 465 |         // Track tool usage and sequence
 466 |         telemetry.trackToolUsage(name, true, duration);
 467 | 
 468 |         // Track tool sequence if there was a previous tool
 469 |         if (this.previousTool) {
 470 |           const timeDelta = Date.now() - this.previousToolTimestamp;
 471 |           telemetry.trackToolSequence(this.previousTool, name, timeDelta);
 472 |         }
 473 | 
 474 |         // Update previous tool tracking
 475 |         this.previousTool = name;
 476 |         this.previousToolTimestamp = Date.now();
 477 |         
 478 |         // Ensure the result is properly formatted for MCP
 479 |         let responseText: string;
 480 |         let structuredContent: any = null;
 481 |         
 482 |         try {
 483 |           // For validation tools, check if we should use structured content
 484 |           if (name.startsWith('validate_') && typeof result === 'object' && result !== null) {
 485 |             // Clean up the result to ensure it matches the outputSchema
 486 |             const cleanResult = this.sanitizeValidationResult(result, name);
 487 |             structuredContent = cleanResult;
 488 |             responseText = JSON.stringify(cleanResult, null, 2);
 489 |           } else {
 490 |             responseText = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
 491 |           }
 492 |         } catch (jsonError) {
 493 |           logger.warn(`Failed to stringify tool result for ${name}:`, jsonError);
 494 |           responseText = String(result);
 495 |         }
 496 |         
 497 |         // Validate response size (n8n might have limits)
 498 |         if (responseText.length > 1000000) { // 1MB limit
 499 |           logger.warn(`Tool ${name} response is very large (${responseText.length} chars), truncating`);
 500 |           responseText = responseText.substring(0, 999000) + '\n\n[Response truncated due to size limits]';
 501 |           structuredContent = null; // Don't use structured content for truncated responses
 502 |         }
 503 |         
 504 |         // Build MCP response with strict schema compliance
 505 |         const mcpResponse: any = {
 506 |           content: [
 507 |             {
 508 |               type: 'text' as const,
 509 |               text: responseText,
 510 |             },
 511 |           ],
 512 |         };
 513 |         
 514 |         // For tools with outputSchema, structuredContent is REQUIRED by MCP spec
 515 |         if (name.startsWith('validate_') && structuredContent !== null) {
 516 |           mcpResponse.structuredContent = structuredContent;
 517 |         }
 518 |         
 519 |         return mcpResponse;
 520 |       } catch (error) {
 521 |         logger.error(`Error executing tool ${name}`, error);
 522 |         const errorMessage = error instanceof Error ? error.message : 'Unknown error';
 523 | 
 524 |         // Track tool error
 525 |         telemetry.trackToolUsage(name, false);
 526 |         telemetry.trackError(
 527 |           error instanceof Error ? error.constructor.name : 'UnknownError',
 528 |           `tool_execution`,
 529 |           name,
 530 |           errorMessage
 531 |         );
 532 | 
 533 |         // Track tool sequence even for errors
 534 |         if (this.previousTool) {
 535 |           const timeDelta = Date.now() - this.previousToolTimestamp;
 536 |           telemetry.trackToolSequence(this.previousTool, name, timeDelta);
 537 |         }
 538 | 
 539 |         // Update previous tool tracking (even for failed tools)
 540 |         this.previousTool = name;
 541 |         this.previousToolTimestamp = Date.now();
 542 | 
 543 |         // Provide more helpful error messages for common n8n issues
 544 |         let helpfulMessage = `Error executing tool ${name}: ${errorMessage}`;
 545 |         
 546 |         if (errorMessage.includes('required') || errorMessage.includes('missing')) {
 547 |           helpfulMessage += '\n\nNote: This error often occurs when the AI agent sends incomplete or incorrectly formatted parameters. Please ensure all required fields are provided with the correct types.';
 548 |         } else if (errorMessage.includes('type') || errorMessage.includes('expected')) {
 549 |           helpfulMessage += '\n\nNote: This error indicates a type mismatch. The AI agent may be sending data in the wrong format (e.g., string instead of object).';
 550 |         } else if (errorMessage.includes('Unknown category') || errorMessage.includes('not found')) {
 551 |           helpfulMessage += '\n\nNote: The requested resource or category was not found. Please check the available options.';
 552 |         }
 553 |         
 554 |         // For n8n schema errors, add specific guidance
 555 |         if (name.startsWith('validate_') && (errorMessage.includes('config') || errorMessage.includes('nodeType'))) {
 556 |           helpfulMessage += '\n\nFor validation tools:\n- nodeType should be a string (e.g., "nodes-base.webhook")\n- config should be an object (e.g., {})';
 557 |         }
 558 |         
 559 |         return {
 560 |           content: [
 561 |             {
 562 |               type: 'text',
 563 |               text: helpfulMessage,
 564 |             },
 565 |           ],
 566 |           isError: true,
 567 |         };
 568 |       }
 569 |     });
 570 |   }
 571 | 
 572 |   /**
 573 |    * Sanitize validation result to match outputSchema
 574 |    */
 575 |   private sanitizeValidationResult(result: any, toolName: string): any {
 576 |     if (!result || typeof result !== 'object') {
 577 |       return result;
 578 |     }
 579 | 
 580 |     const sanitized = { ...result };
 581 | 
 582 |     // Ensure required fields exist with proper types and filter to schema-defined fields only
 583 |     if (toolName === 'validate_node_minimal') {
 584 |       // Filter to only schema-defined fields
 585 |       const filtered = {
 586 |         nodeType: String(sanitized.nodeType || ''),
 587 |         displayName: String(sanitized.displayName || ''),
 588 |         valid: Boolean(sanitized.valid),
 589 |         missingRequiredFields: Array.isArray(sanitized.missingRequiredFields) 
 590 |           ? sanitized.missingRequiredFields.map(String) 
 591 |           : []
 592 |       };
 593 |       return filtered;
 594 |     } else if (toolName === 'validate_node_operation') {
 595 |       // Ensure summary exists
 596 |       let summary = sanitized.summary;
 597 |       if (!summary || typeof summary !== 'object') {
 598 |         summary = {
 599 |           hasErrors: Array.isArray(sanitized.errors) ? sanitized.errors.length > 0 : false,
 600 |           errorCount: Array.isArray(sanitized.errors) ? sanitized.errors.length : 0,
 601 |           warningCount: Array.isArray(sanitized.warnings) ? sanitized.warnings.length : 0,
 602 |           suggestionCount: Array.isArray(sanitized.suggestions) ? sanitized.suggestions.length : 0
 603 |         };
 604 |       }
 605 |       
 606 |       // Filter to only schema-defined fields
 607 |       const filtered = {
 608 |         nodeType: String(sanitized.nodeType || ''),
 609 |         workflowNodeType: String(sanitized.workflowNodeType || sanitized.nodeType || ''),
 610 |         displayName: String(sanitized.displayName || ''),
 611 |         valid: Boolean(sanitized.valid),
 612 |         errors: Array.isArray(sanitized.errors) ? sanitized.errors : [],
 613 |         warnings: Array.isArray(sanitized.warnings) ? sanitized.warnings : [],
 614 |         suggestions: Array.isArray(sanitized.suggestions) ? sanitized.suggestions : [],
 615 |         summary: summary
 616 |       };
 617 |       return filtered;
 618 |     } else if (toolName.startsWith('validate_workflow')) {
 619 |       sanitized.valid = Boolean(sanitized.valid);
 620 |       
 621 |       // Ensure arrays exist
 622 |       sanitized.errors = Array.isArray(sanitized.errors) ? sanitized.errors : [];
 623 |       sanitized.warnings = Array.isArray(sanitized.warnings) ? sanitized.warnings : [];
 624 |       
 625 |       // Ensure statistics/summary exists
 626 |       if (toolName === 'validate_workflow') {
 627 |         if (!sanitized.summary || typeof sanitized.summary !== 'object') {
 628 |           sanitized.summary = {
 629 |             totalNodes: 0,
 630 |             enabledNodes: 0,
 631 |             triggerNodes: 0,
 632 |             validConnections: 0,
 633 |             invalidConnections: 0,
 634 |             expressionsValidated: 0,
 635 |             errorCount: sanitized.errors.length,
 636 |             warningCount: sanitized.warnings.length
 637 |           };
 638 |         }
 639 |       } else {
 640 |         if (!sanitized.statistics || typeof sanitized.statistics !== 'object') {
 641 |           sanitized.statistics = {
 642 |             totalNodes: 0,
 643 |             triggerNodes: 0,
 644 |             validConnections: 0,
 645 |             invalidConnections: 0,
 646 |             expressionsValidated: 0
 647 |           };
 648 |         }
 649 |       }
 650 |     }
 651 | 
 652 |     // Remove undefined values to ensure clean JSON
 653 |     return JSON.parse(JSON.stringify(sanitized));
 654 |   }
 655 | 
 656 |   /**
 657 |    * Enhanced parameter validation using schemas
 658 |    */
 659 |   private validateToolParams(toolName: string, args: any, legacyRequiredParams?: string[]): void {
 660 |     try {
 661 |       // If legacy required params are provided, use the new validation but fall back to basic if needed
 662 |       let validationResult;
 663 |       
 664 |       switch (toolName) {
 665 |         case 'validate_node_operation':
 666 |           validationResult = ToolValidation.validateNodeOperation(args);
 667 |           break;
 668 |         case 'validate_node_minimal':
 669 |           validationResult = ToolValidation.validateNodeMinimal(args);
 670 |           break;
 671 |         case 'validate_workflow':
 672 |         case 'validate_workflow_connections':
 673 |         case 'validate_workflow_expressions':
 674 |           validationResult = ToolValidation.validateWorkflow(args);
 675 |           break;
 676 |       case 'search_nodes':
 677 |         validationResult = ToolValidation.validateSearchNodes(args);
 678 |         break;
 679 |       case 'list_node_templates':
 680 |         validationResult = ToolValidation.validateListNodeTemplates(args);
 681 |         break;
 682 |       case 'n8n_create_workflow':
 683 |         validationResult = ToolValidation.validateCreateWorkflow(args);
 684 |         break;
 685 |       case 'n8n_get_workflow':
 686 |       case 'n8n_get_workflow_details':
 687 |       case 'n8n_get_workflow_structure':
 688 |       case 'n8n_get_workflow_minimal':
 689 |       case 'n8n_update_full_workflow':
 690 |       case 'n8n_delete_workflow':
 691 |       case 'n8n_validate_workflow':
 692 |       case 'n8n_autofix_workflow':
 693 |       case 'n8n_get_execution':
 694 |       case 'n8n_delete_execution':
 695 |         validationResult = ToolValidation.validateWorkflowId(args);
 696 |         break;
 697 |       default:
 698 |         // For tools not yet migrated to schema validation, use basic validation
 699 |         return this.validateToolParamsBasic(toolName, args, legacyRequiredParams || []);
 700 |       }
 701 |       
 702 |       if (!validationResult.valid) {
 703 |         const errorMessage = Validator.formatErrors(validationResult, toolName);
 704 |         logger.error(`Parameter validation failed for ${toolName}:`, errorMessage);
 705 |         throw new ValidationError(errorMessage);
 706 |       }
 707 |     } catch (error) {
 708 |       // Handle validation errors properly
 709 |       if (error instanceof ValidationError) {
 710 |         throw error; // Re-throw validation errors as-is
 711 |       }
 712 |       
 713 |       // Handle unexpected errors from validation system
 714 |       logger.error(`Validation system error for ${toolName}:`, error);
 715 |       
 716 |       // Provide a user-friendly error message
 717 |       const errorMessage = error instanceof Error 
 718 |         ? `Internal validation error: ${error.message}`
 719 |         : `Internal validation error while processing ${toolName}`;
 720 |       
 721 |       throw new Error(errorMessage);
 722 |     }
 723 |   }
 724 |   
 725 |   /**
 726 |    * Legacy parameter validation (fallback)
 727 |    */
 728 |   private validateToolParamsBasic(toolName: string, args: any, requiredParams: string[]): void {
 729 |     const missing: string[] = [];
 730 |     const invalid: string[] = [];
 731 | 
 732 |     for (const param of requiredParams) {
 733 |       if (!(param in args) || args[param] === undefined || args[param] === null) {
 734 |         missing.push(param);
 735 |       } else if (typeof args[param] === 'string' && args[param].trim() === '') {
 736 |         invalid.push(`${param} (empty string)`);
 737 |       }
 738 |     }
 739 | 
 740 |     if (missing.length > 0) {
 741 |       throw new Error(`Missing required parameters for ${toolName}: ${missing.join(', ')}. Please provide the required parameters to use this tool.`);
 742 |     }
 743 | 
 744 |     if (invalid.length > 0) {
 745 |       throw new Error(`Invalid parameters for ${toolName}: ${invalid.join(', ')}. String parameters cannot be empty.`);
 746 |     }
 747 |   }
 748 | 
 749 |   /**
 750 |    * Validate extracted arguments match expected tool schema
 751 |    */
 752 |   private validateExtractedArgs(toolName: string, args: any): boolean {
 753 |     if (!args || typeof args !== 'object') {
 754 |       return false;
 755 |     }
 756 | 
 757 |     // Get all available tools
 758 |     const allTools = [...n8nDocumentationToolsFinal, ...n8nManagementTools];
 759 |     const tool = allTools.find(t => t.name === toolName);
 760 |     if (!tool || !tool.inputSchema) {
 761 |       return true; // If no schema, assume valid
 762 |     }
 763 | 
 764 |     const schema = tool.inputSchema;
 765 |     const required = schema.required || [];
 766 |     const properties = schema.properties || {};
 767 | 
 768 |     // Check all required fields are present
 769 |     for (const requiredField of required) {
 770 |       if (!(requiredField in args)) {
 771 |         logger.debug(`Extracted args missing required field: ${requiredField}`, {
 772 |           toolName,
 773 |           extractedArgs: args,
 774 |           required
 775 |         });
 776 |         return false;
 777 |       }
 778 |     }
 779 | 
 780 |     // Check field types match schema
 781 |     for (const [fieldName, fieldValue] of Object.entries(args)) {
 782 |       if (properties[fieldName]) {
 783 |         const expectedType = properties[fieldName].type;
 784 |         const actualType = Array.isArray(fieldValue) ? 'array' : typeof fieldValue;
 785 | 
 786 |         // Basic type validation
 787 |         if (expectedType && expectedType !== actualType) {
 788 |           // Special case: number can be coerced from string
 789 |           if (expectedType === 'number' && actualType === 'string' && !isNaN(Number(fieldValue))) {
 790 |             continue;
 791 |           }
 792 |           
 793 |           logger.debug(`Extracted args field type mismatch: ${fieldName}`, {
 794 |             toolName,
 795 |             expectedType,
 796 |             actualType,
 797 |             fieldValue
 798 |           });
 799 |           return false;
 800 |         }
 801 |       }
 802 |     }
 803 | 
 804 |     // Check for extraneous fields if additionalProperties is false
 805 |     if (schema.additionalProperties === false) {
 806 |       const allowedFields = Object.keys(properties);
 807 |       const extraFields = Object.keys(args).filter(field => !allowedFields.includes(field));
 808 |       
 809 |       if (extraFields.length > 0) {
 810 |         logger.debug(`Extracted args have extra fields`, {
 811 |           toolName,
 812 |           extraFields,
 813 |           allowedFields
 814 |         });
 815 |         // For n8n compatibility, we'll still consider this valid but log it
 816 |       }
 817 |     }
 818 | 
 819 |     return true;
 820 |   }
 821 | 
 822 |   async executeTool(name: string, args: any): Promise<any> {
 823 |     // Ensure args is an object and validate it
 824 |     args = args || {};
 825 |     
 826 |     // Log the tool call for debugging n8n issues
 827 |     logger.info(`Tool execution: ${name}`, { 
 828 |       args: typeof args === 'object' ? JSON.stringify(args) : args,
 829 |       argsType: typeof args,
 830 |       argsKeys: typeof args === 'object' ? Object.keys(args) : 'not-object'
 831 |     });
 832 |     
 833 |     // Validate that args is actually an object
 834 |     if (typeof args !== 'object' || args === null) {
 835 |       throw new Error(`Invalid arguments for tool ${name}: expected object, got ${typeof args}`);
 836 |     }
 837 |     
 838 |     switch (name) {
 839 |       case 'tools_documentation':
 840 |         // No required parameters
 841 |         return this.getToolsDocumentation(args.topic, args.depth);
 842 |       case 'list_nodes':
 843 |         // No required parameters
 844 |         return this.listNodes(args);
 845 |       case 'get_node_info':
 846 |         this.validateToolParams(name, args, ['nodeType']);
 847 |         return this.getNodeInfo(args.nodeType);
 848 |       case 'search_nodes':
 849 |         this.validateToolParams(name, args, ['query']);
 850 |         // Convert limit to number if provided, otherwise use default
 851 |         const limit = args.limit !== undefined ? Number(args.limit) || 20 : 20;
 852 |         return this.searchNodes(args.query, limit, { mode: args.mode, includeExamples: args.includeExamples });
 853 |       case 'list_ai_tools':
 854 |         // No required parameters
 855 |         return this.listAITools();
 856 |       case 'get_node_documentation':
 857 |         this.validateToolParams(name, args, ['nodeType']);
 858 |         return this.getNodeDocumentation(args.nodeType);
 859 |       case 'get_database_statistics':
 860 |         // No required parameters
 861 |         return this.getDatabaseStatistics();
 862 |       case 'get_node_essentials':
 863 |         this.validateToolParams(name, args, ['nodeType']);
 864 |         return this.getNodeEssentials(args.nodeType, args.includeExamples);
 865 |       case 'search_node_properties':
 866 |         this.validateToolParams(name, args, ['nodeType', 'query']);
 867 |         const maxResults = args.maxResults !== undefined ? Number(args.maxResults) || 20 : 20;
 868 |         return this.searchNodeProperties(args.nodeType, args.query, maxResults);
 869 |       case 'list_tasks':
 870 |         // No required parameters
 871 |         return this.listTasks(args.category);
 872 |       case 'validate_node_operation':
 873 |         this.validateToolParams(name, args, ['nodeType', 'config']);
 874 |         // Ensure config is an object
 875 |         if (typeof args.config !== 'object' || args.config === null) {
 876 |           logger.warn(`validate_node_operation called with invalid config type: ${typeof args.config}`);
 877 |           return {
 878 |             nodeType: args.nodeType || 'unknown',
 879 |             workflowNodeType: args.nodeType || 'unknown',
 880 |             displayName: 'Unknown Node',
 881 |             valid: false,
 882 |             errors: [{
 883 |               type: 'config',
 884 |               property: 'config',
 885 |               message: 'Invalid config format - expected object',
 886 |               fix: 'Provide config as an object with node properties'
 887 |             }],
 888 |             warnings: [],
 889 |             suggestions: [
 890 |               '🔧 RECOVERY: Invalid config detected. Fix with:',
 891 |               '   • Ensure config is an object: { "resource": "...", "operation": "..." }',
 892 |               '   • Use get_node_essentials to see required fields for this node type',
 893 |               '   • Check if the node type is correct before configuring it'
 894 |             ],
 895 |             summary: {
 896 |               hasErrors: true,
 897 |               errorCount: 1,
 898 |               warningCount: 0,
 899 |               suggestionCount: 3
 900 |             }
 901 |           };
 902 |         }
 903 |         return this.validateNodeConfig(args.nodeType, args.config, 'operation', args.profile);
 904 |       case 'validate_node_minimal':
 905 |         this.validateToolParams(name, args, ['nodeType', 'config']);
 906 |         // Ensure config is an object
 907 |         if (typeof args.config !== 'object' || args.config === null) {
 908 |           logger.warn(`validate_node_minimal called with invalid config type: ${typeof args.config}`);
 909 |           return {
 910 |             nodeType: args.nodeType || 'unknown',
 911 |             displayName: 'Unknown Node',
 912 |             valid: false,
 913 |             missingRequiredFields: [
 914 |               'Invalid config format - expected object',
 915 |               '🔧 RECOVERY: Use format { "resource": "...", "operation": "..." } or {} for empty config'
 916 |             ]
 917 |           };
 918 |         }
 919 |         return this.validateNodeMinimal(args.nodeType, args.config);
 920 |       case 'get_property_dependencies':
 921 |         this.validateToolParams(name, args, ['nodeType']);
 922 |         return this.getPropertyDependencies(args.nodeType, args.config);
 923 |       case 'get_node_as_tool_info':
 924 |         this.validateToolParams(name, args, ['nodeType']);
 925 |         return this.getNodeAsToolInfo(args.nodeType);
 926 |       case 'list_templates':
 927 |         // No required params
 928 |         const listLimit = Math.min(Math.max(Number(args.limit) || 10, 1), 100);
 929 |         const listOffset = Math.max(Number(args.offset) || 0, 0);
 930 |         const sortBy = args.sortBy || 'views';
 931 |         const includeMetadata = Boolean(args.includeMetadata);
 932 |         return this.listTemplates(listLimit, listOffset, sortBy, includeMetadata);
 933 |       case 'list_node_templates':
 934 |         this.validateToolParams(name, args, ['nodeTypes']);
 935 |         const templateLimit = Math.min(Math.max(Number(args.limit) || 10, 1), 100);
 936 |         const templateOffset = Math.max(Number(args.offset) || 0, 0);
 937 |         return this.listNodeTemplates(args.nodeTypes, templateLimit, templateOffset);
 938 |       case 'get_template':
 939 |         this.validateToolParams(name, args, ['templateId']);
 940 |         const templateId = Number(args.templateId);
 941 |         const mode = args.mode || 'full';
 942 |         return this.getTemplate(templateId, mode);
 943 |       case 'search_templates':
 944 |         this.validateToolParams(name, args, ['query']);
 945 |         const searchLimit = Math.min(Math.max(Number(args.limit) || 20, 1), 100);
 946 |         const searchOffset = Math.max(Number(args.offset) || 0, 0);
 947 |         const searchFields = args.fields as string[] | undefined;
 948 |         return this.searchTemplates(args.query, searchLimit, searchOffset, searchFields);
 949 |       case 'get_templates_for_task':
 950 |         this.validateToolParams(name, args, ['task']);
 951 |         const taskLimit = Math.min(Math.max(Number(args.limit) || 10, 1), 100);
 952 |         const taskOffset = Math.max(Number(args.offset) || 0, 0);
 953 |         return this.getTemplatesForTask(args.task, taskLimit, taskOffset);
 954 |       case 'search_templates_by_metadata':
 955 |         // No required params - all filters are optional
 956 |         const metadataLimit = Math.min(Math.max(Number(args.limit) || 20, 1), 100);
 957 |         const metadataOffset = Math.max(Number(args.offset) || 0, 0);
 958 |         return this.searchTemplatesByMetadata({
 959 |           category: args.category,
 960 |           complexity: args.complexity,
 961 |           maxSetupMinutes: args.maxSetupMinutes ? Number(args.maxSetupMinutes) : undefined,
 962 |           minSetupMinutes: args.minSetupMinutes ? Number(args.minSetupMinutes) : undefined,
 963 |           requiredService: args.requiredService,
 964 |           targetAudience: args.targetAudience
 965 |         }, metadataLimit, metadataOffset);
 966 |       case 'validate_workflow':
 967 |         this.validateToolParams(name, args, ['workflow']);
 968 |         return this.validateWorkflow(args.workflow, args.options);
 969 |       case 'validate_workflow_connections':
 970 |         this.validateToolParams(name, args, ['workflow']);
 971 |         return this.validateWorkflowConnections(args.workflow);
 972 |       case 'validate_workflow_expressions':
 973 |         this.validateToolParams(name, args, ['workflow']);
 974 |         return this.validateWorkflowExpressions(args.workflow);
 975 |       
 976 |       // n8n Management Tools (if API is configured)
 977 |       case 'n8n_create_workflow':
 978 |         this.validateToolParams(name, args, ['name', 'nodes', 'connections']);
 979 |         return n8nHandlers.handleCreateWorkflow(args, this.instanceContext);
 980 |       case 'n8n_get_workflow':
 981 |         this.validateToolParams(name, args, ['id']);
 982 |         return n8nHandlers.handleGetWorkflow(args, this.instanceContext);
 983 |       case 'n8n_get_workflow_details':
 984 |         this.validateToolParams(name, args, ['id']);
 985 |         return n8nHandlers.handleGetWorkflowDetails(args, this.instanceContext);
 986 |       case 'n8n_get_workflow_structure':
 987 |         this.validateToolParams(name, args, ['id']);
 988 |         return n8nHandlers.handleGetWorkflowStructure(args, this.instanceContext);
 989 |       case 'n8n_get_workflow_minimal':
 990 |         this.validateToolParams(name, args, ['id']);
 991 |         return n8nHandlers.handleGetWorkflowMinimal(args, this.instanceContext);
 992 |       case 'n8n_update_full_workflow':
 993 |         this.validateToolParams(name, args, ['id']);
 994 |         return n8nHandlers.handleUpdateWorkflow(args, this.instanceContext);
 995 |       case 'n8n_update_partial_workflow':
 996 |         this.validateToolParams(name, args, ['id', 'operations']);
 997 |         return handleUpdatePartialWorkflow(args, this.instanceContext);
 998 |       case 'n8n_delete_workflow':
 999 |         this.validateToolParams(name, args, ['id']);
1000 |         return n8nHandlers.handleDeleteWorkflow(args, this.instanceContext);
1001 |       case 'n8n_list_workflows':
1002 |         // No required parameters
1003 |         return n8nHandlers.handleListWorkflows(args, this.instanceContext);
1004 |       case 'n8n_validate_workflow':
1005 |         this.validateToolParams(name, args, ['id']);
1006 |         await this.ensureInitialized();
1007 |         if (!this.repository) throw new Error('Repository not initialized');
1008 |         return n8nHandlers.handleValidateWorkflow(args, this.repository, this.instanceContext);
1009 |       case 'n8n_autofix_workflow':
1010 |         this.validateToolParams(name, args, ['id']);
1011 |         await this.ensureInitialized();
1012 |         if (!this.repository) throw new Error('Repository not initialized');
1013 |         return n8nHandlers.handleAutofixWorkflow(args, this.repository, this.instanceContext);
1014 |       case 'n8n_trigger_webhook_workflow':
1015 |         this.validateToolParams(name, args, ['webhookUrl']);
1016 |         return n8nHandlers.handleTriggerWebhookWorkflow(args, this.instanceContext);
1017 |       case 'n8n_get_execution':
1018 |         this.validateToolParams(name, args, ['id']);
1019 |         return n8nHandlers.handleGetExecution(args, this.instanceContext);
1020 |       case 'n8n_list_executions':
1021 |         // No required parameters
1022 |         return n8nHandlers.handleListExecutions(args, this.instanceContext);
1023 |       case 'n8n_delete_execution':
1024 |         this.validateToolParams(name, args, ['id']);
1025 |         return n8nHandlers.handleDeleteExecution(args, this.instanceContext);
1026 |       case 'n8n_health_check':
1027 |         // No required parameters
1028 |         return n8nHandlers.handleHealthCheck(this.instanceContext);
1029 |       case 'n8n_list_available_tools':
1030 |         // No required parameters
1031 |         return n8nHandlers.handleListAvailableTools(this.instanceContext);
1032 |       case 'n8n_diagnostic':
1033 |         // No required parameters
1034 |         return n8nHandlers.handleDiagnostic({ params: { arguments: args } }, this.instanceContext);
1035 |         
1036 |       default:
1037 |         throw new Error(`Unknown tool: ${name}`);
1038 |     }
1039 |   }
1040 | 
1041 |   private async listNodes(filters: any = {}): Promise<any> {
1042 |     await this.ensureInitialized();
1043 |     
1044 |     let query = 'SELECT * FROM nodes WHERE 1=1';
1045 |     const params: any[] = [];
1046 |     
1047 |     // console.log('DEBUG list_nodes:', { filters, query, params }); // Removed to prevent stdout interference
1048 | 
1049 |     if (filters.package) {
1050 |       // Handle both formats
1051 |       const packageVariants = [
1052 |         filters.package,
1053 |         `@n8n/${filters.package}`,
1054 |         filters.package.replace('@n8n/', '')
1055 |       ];
1056 |       query += ' AND package_name IN (' + packageVariants.map(() => '?').join(',') + ')';
1057 |       params.push(...packageVariants);
1058 |     }
1059 | 
1060 |     if (filters.category) {
1061 |       query += ' AND category = ?';
1062 |       params.push(filters.category);
1063 |     }
1064 | 
1065 |     if (filters.developmentStyle) {
1066 |       query += ' AND development_style = ?';
1067 |       params.push(filters.developmentStyle);
1068 |     }
1069 | 
1070 |     if (filters.isAITool !== undefined) {
1071 |       query += ' AND is_ai_tool = ?';
1072 |       params.push(filters.isAITool ? 1 : 0);
1073 |     }
1074 | 
1075 |     query += ' ORDER BY display_name';
1076 | 
1077 |     if (filters.limit) {
1078 |       query += ' LIMIT ?';
1079 |       params.push(filters.limit);
1080 |     }
1081 | 
1082 |     const nodes = this.db!.prepare(query).all(...params) as NodeRow[];
1083 |     
1084 |     return {
1085 |       nodes: nodes.map(node => ({
1086 |         nodeType: node.node_type,
1087 |         displayName: node.display_name,
1088 |         description: node.description,
1089 |         category: node.category,
1090 |         package: node.package_name,
1091 |         developmentStyle: node.development_style,
1092 |         isAITool: Number(node.is_ai_tool) === 1,
1093 |         isTrigger: Number(node.is_trigger) === 1,
1094 |         isVersioned: Number(node.is_versioned) === 1,
1095 |       })),
1096 |       totalCount: nodes.length,
1097 |     };
1098 |   }
1099 | 
1100 |   private async getNodeInfo(nodeType: string): Promise<any> {
1101 |     await this.ensureInitialized();
1102 |     if (!this.repository) throw new Error('Repository not initialized');
1103 | 
1104 |     // First try with normalized type (repository will also normalize internally)
1105 |     const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType);
1106 |     let node = this.repository.getNode(normalizedType);
1107 |     
1108 |     if (!node && normalizedType !== nodeType) {
1109 |       // Try original if normalization changed it
1110 |       node = this.repository.getNode(nodeType);
1111 |     }
1112 |     
1113 |     if (!node) {
1114 |       // Fallback to other alternatives for edge cases
1115 |       const alternatives = getNodeTypeAlternatives(normalizedType);
1116 |       
1117 |       for (const alt of alternatives) {
1118 |         const found = this.repository!.getNode(alt);
1119 |         if (found) {
1120 |           node = found;
1121 |           break;
1122 |         }
1123 |       }
1124 |     }
1125 |     
1126 |     if (!node) {
1127 |       throw new Error(`Node ${nodeType} not found`);
1128 |     }
1129 |     
1130 |     // Add AI tool capabilities information with null safety
1131 |     const aiToolCapabilities = {
1132 |       canBeUsedAsTool: true, // Any node can be used as a tool in n8n
1133 |       hasUsableAsToolProperty: node.isAITool ?? false,
1134 |       requiresEnvironmentVariable: !(node.isAITool ?? false) && node.package !== 'n8n-nodes-base',
1135 |       toolConnectionType: 'ai_tool',
1136 |       commonToolUseCases: this.getCommonAIToolUseCases(node.nodeType),
1137 |       environmentRequirement: node.package && node.package !== 'n8n-nodes-base' ?
1138 |         'N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true' :
1139 |         null
1140 |     };
1141 | 
1142 |     // Process outputs to provide clear mapping with null safety
1143 |     let outputs = undefined;
1144 |     if (node.outputNames && Array.isArray(node.outputNames) && node.outputNames.length > 0) {
1145 |       outputs = node.outputNames.map((name: string, index: number) => {
1146 |         // Special handling for loop nodes like SplitInBatches
1147 |         const descriptions = this.getOutputDescriptions(node.nodeType, name, index);
1148 |         return {
1149 |           index,
1150 |           name,
1151 |           description: descriptions?.description ?? '',
1152 |           connectionGuidance: descriptions?.connectionGuidance ?? ''
1153 |         };
1154 |       });
1155 |     }
1156 | 
1157 |     return {
1158 |       ...node,
1159 |       workflowNodeType: getWorkflowNodeType(node.package ?? 'n8n-nodes-base', node.nodeType),
1160 |       aiToolCapabilities,
1161 |       outputs
1162 |     };
1163 |   }
1164 | 
1165 |   /**
1166 |    * Primary search method used by ALL MCP search tools.
1167 |    *
1168 |    * This method automatically detects and uses FTS5 full-text search when available
1169 |    * (lines 1189-1203), falling back to LIKE queries only if FTS5 table doesn't exist.
1170 |    *
1171 |    * NOTE: This is separate from NodeRepository.searchNodes() which is legacy LIKE-based.
1172 |    * All MCP tool invocations route through this method to leverage FTS5 performance.
1173 |    */
1174 |   private async searchNodes(
1175 |     query: string,
1176 |     limit: number = 20,
1177 |     options?: {
1178 |       mode?: 'OR' | 'AND' | 'FUZZY';
1179 |       includeSource?: boolean;
1180 |       includeExamples?: boolean;
1181 |     }
1182 |   ): Promise<any> {
1183 |     await this.ensureInitialized();
1184 |     if (!this.db) throw new Error('Database not initialized');
1185 | 
1186 |     // Normalize the query if it looks like a full node type
1187 |     let normalizedQuery = query;
1188 |     
1189 |     // Check if query contains node type patterns and normalize them
1190 |     if (query.includes('n8n-nodes-base.') || query.includes('@n8n/n8n-nodes-langchain.')) {
1191 |       normalizedQuery = query
1192 |         .replace(/n8n-nodes-base\./g, 'nodes-base.')
1193 |         .replace(/@n8n\/n8n-nodes-langchain\./g, 'nodes-langchain.');
1194 |     }
1195 |     
1196 |     const searchMode = options?.mode || 'OR';
1197 |     
1198 |     // Check if FTS5 table exists
1199 |     const ftsExists = this.db.prepare(`
1200 |       SELECT name FROM sqlite_master 
1201 |       WHERE type='table' AND name='nodes_fts'
1202 |     `).get();
1203 |     
1204 |     if (ftsExists) {
1205 |       // Use FTS5 search with normalized query
1206 |       logger.debug(`Using FTS5 search with includeExamples=${options?.includeExamples}`);
1207 |       return this.searchNodesFTS(normalizedQuery, limit, searchMode, options);
1208 |     } else {
1209 |       // Fallback to LIKE search with normalized query
1210 |       logger.debug('Using LIKE search (no FTS5)');
1211 |       return this.searchNodesLIKE(normalizedQuery, limit, options);
1212 |     }
1213 |   }
1214 | 
1215 |   private async searchNodesFTS(
1216 |     query: string,
1217 |     limit: number,
1218 |     mode: 'OR' | 'AND' | 'FUZZY',
1219 |     options?: { includeSource?: boolean; includeExamples?: boolean; }
1220 |   ): Promise<any> {
1221 |     if (!this.db) throw new Error('Database not initialized');
1222 | 
1223 |     // Clean and prepare the query
1224 |     const cleanedQuery = query.trim();
1225 |     if (!cleanedQuery) {
1226 |       return { query, results: [], totalCount: 0 };
1227 |     }
1228 |     
1229 |     // For FUZZY mode, use LIKE search with typo patterns
1230 |     if (mode === 'FUZZY') {
1231 |       return this.searchNodesFuzzy(cleanedQuery, limit);
1232 |     }
1233 |     
1234 |     let ftsQuery: string;
1235 |     
1236 |     // Handle exact phrase searches with quotes
1237 |     if (cleanedQuery.startsWith('"') && cleanedQuery.endsWith('"')) {
1238 |       // Keep exact phrase as is for FTS5
1239 |       ftsQuery = cleanedQuery;
1240 |     } else {
1241 |       // Split into words and handle based on mode
1242 |       const words = cleanedQuery.split(/\s+/).filter(w => w.length > 0);
1243 |       
1244 |       switch (mode) {
1245 |         case 'AND':
1246 |           // All words must be present
1247 |           ftsQuery = words.join(' AND ');
1248 |           break;
1249 |           
1250 |         case 'OR':
1251 |         default:
1252 |           // Any word can match (default)
1253 |           ftsQuery = words.join(' OR ');
1254 |           break;
1255 |       }
1256 |     }
1257 |     
1258 |     try {
1259 |       // Use FTS5 with ranking
1260 |       const nodes = this.db.prepare(`
1261 |         SELECT 
1262 |           n.*,
1263 |           rank
1264 |         FROM nodes n
1265 |         JOIN nodes_fts ON n.rowid = nodes_fts.rowid
1266 |         WHERE nodes_fts MATCH ?
1267 |         ORDER BY 
1268 |           rank,
1269 |           CASE 
1270 |             WHEN n.display_name = ? THEN 0
1271 |             WHEN n.display_name LIKE ? THEN 1
1272 |             WHEN n.node_type LIKE ? THEN 2
1273 |             ELSE 3
1274 |           END,
1275 |           n.display_name
1276 |         LIMIT ?
1277 |       `).all(ftsQuery, cleanedQuery, `%${cleanedQuery}%`, `%${cleanedQuery}%`, limit) as (NodeRow & { rank: number })[];
1278 |       
1279 |       // Apply additional relevance scoring for better results
1280 |       const scoredNodes = nodes.map(node => {
1281 |         const relevanceScore = this.calculateRelevanceScore(node, cleanedQuery);
1282 |         return { ...node, relevanceScore };
1283 |       });
1284 |       
1285 |       // Sort by combined score (FTS rank + relevance score)
1286 |       scoredNodes.sort((a, b) => {
1287 |         // Prioritize exact matches
1288 |         if (a.display_name.toLowerCase() === cleanedQuery.toLowerCase()) return -1;
1289 |         if (b.display_name.toLowerCase() === cleanedQuery.toLowerCase()) return 1;
1290 |         
1291 |         // Then by relevance score
1292 |         if (a.relevanceScore !== b.relevanceScore) {
1293 |           return b.relevanceScore - a.relevanceScore;
1294 |         }
1295 |         
1296 |         // Then by FTS rank
1297 |         return a.rank - b.rank;
1298 |       });
1299 |       
1300 |       // If FTS didn't find key primary nodes, augment with LIKE search
1301 |       const hasHttpRequest = scoredNodes.some(n => n.node_type === 'nodes-base.httpRequest');
1302 |       if (cleanedQuery.toLowerCase().includes('http') && !hasHttpRequest) {
1303 |         // FTS missed HTTP Request, fall back to LIKE search
1304 |         logger.debug('FTS missed HTTP Request node, augmenting with LIKE search');
1305 |         return this.searchNodesLIKE(query, limit);
1306 |       }
1307 |       
1308 |       const result: any = {
1309 |         query,
1310 |         results: scoredNodes.map(node => ({
1311 |           nodeType: node.node_type,
1312 |           workflowNodeType: getWorkflowNodeType(node.package_name, node.node_type),
1313 |           displayName: node.display_name,
1314 |           description: node.description,
1315 |           category: node.category,
1316 |           package: node.package_name,
1317 |           relevance: this.calculateRelevance(node, cleanedQuery)
1318 |         })),
1319 |         totalCount: scoredNodes.length
1320 |       };
1321 | 
1322 |       // Only include mode if it's not the default
1323 |       if (mode !== 'OR') {
1324 |         result.mode = mode;
1325 |       }
1326 | 
1327 |       // Add examples if requested
1328 |       if (options && options.includeExamples) {
1329 |         try {
1330 |           for (const nodeResult of result.results) {
1331 |             const examples = this.db!.prepare(`
1332 |               SELECT
1333 |                 parameters_json,
1334 |                 template_name,
1335 |                 template_views
1336 |               FROM template_node_configs
1337 |               WHERE node_type = ?
1338 |               ORDER BY rank
1339 |               LIMIT 2
1340 |             `).all(nodeResult.workflowNodeType) as any[];
1341 | 
1342 |             if (examples.length > 0) {
1343 |               nodeResult.examples = examples.map((ex: any) => ({
1344 |                 configuration: JSON.parse(ex.parameters_json),
1345 |                 template: ex.template_name,
1346 |                 views: ex.template_views
1347 |               }));
1348 |             }
1349 |           }
1350 |         } catch (error: any) {
1351 |           logger.error(`Failed to add examples:`, error);
1352 |         }
1353 |       }
1354 | 
1355 |       // Track search query telemetry
1356 |       telemetry.trackSearchQuery(query, scoredNodes.length, mode ?? 'OR');
1357 | 
1358 |       return result;
1359 |       
1360 |     } catch (error: any) {
1361 |       // If FTS5 query fails, fallback to LIKE search
1362 |       logger.warn('FTS5 search failed, falling back to LIKE search:', error.message);
1363 |       
1364 |       // Special handling for syntax errors
1365 |       if (error.message.includes('syntax error') || error.message.includes('fts5')) {
1366 |         logger.warn(`FTS5 syntax error for query "${query}" in mode ${mode}`);
1367 |         
1368 |         // For problematic queries, use LIKE search with mode info
1369 |         const likeResult = await this.searchNodesLIKE(query, limit);
1370 | 
1371 |         // Track search query telemetry for fallback
1372 |         telemetry.trackSearchQuery(query, likeResult.results?.length ?? 0, `${mode}_LIKE_FALLBACK`);
1373 | 
1374 |         return {
1375 |           ...likeResult,
1376 |           mode
1377 |         };
1378 |       }
1379 |       
1380 |       return this.searchNodesLIKE(query, limit);
1381 |     }
1382 |   }
1383 |   
1384 |   private async searchNodesFuzzy(query: string, limit: number): Promise<any> {
1385 |     if (!this.db) throw new Error('Database not initialized');
1386 |     
1387 |     // Split into words for fuzzy matching
1388 |     const words = query.toLowerCase().split(/\s+/).filter(w => w.length > 0);
1389 |     
1390 |     if (words.length === 0) {
1391 |       return { query, results: [], totalCount: 0, mode: 'FUZZY' };
1392 |     }
1393 |     
1394 |     // For fuzzy search, get ALL nodes to ensure we don't miss potential matches
1395 |     // We'll limit results after scoring
1396 |     const candidateNodes = this.db!.prepare(`
1397 |       SELECT * FROM nodes
1398 |     `).all() as NodeRow[];
1399 |     
1400 |     // Calculate fuzzy scores for candidate nodes
1401 |     const scoredNodes = candidateNodes.map(node => {
1402 |       const score = this.calculateFuzzyScore(node, query);
1403 |       return { node, score };
1404 |     });
1405 |     
1406 |     // Filter and sort by score
1407 |     const matchingNodes = scoredNodes
1408 |       .filter(item => item.score >= 200) // Lower threshold for better typo tolerance
1409 |       .sort((a, b) => b.score - a.score)
1410 |       .slice(0, limit)
1411 |       .map(item => item.node);
1412 |     
1413 |     // Debug logging
1414 |     if (matchingNodes.length === 0) {
1415 |       const topScores = scoredNodes
1416 |         .sort((a, b) => b.score - a.score)
1417 |         .slice(0, 5);
1418 |       logger.debug(`FUZZY search for "${query}" - no matches above 400. Top scores:`, 
1419 |         topScores.map(s => ({ name: s.node.display_name, score: s.score })));
1420 |     }
1421 |     
1422 |     return {
1423 |       query,
1424 |       mode: 'FUZZY',
1425 |       results: matchingNodes.map(node => ({
1426 |         nodeType: node.node_type,
1427 |         workflowNodeType: getWorkflowNodeType(node.package_name, node.node_type),
1428 |         displayName: node.display_name,
1429 |         description: node.description,
1430 |         category: node.category,
1431 |         package: node.package_name
1432 |       })),
1433 |       totalCount: matchingNodes.length
1434 |     };
1435 |   }
1436 |   
1437 |   private calculateFuzzyScore(node: NodeRow, query: string): number {
1438 |     const queryLower = query.toLowerCase();
1439 |     const displayNameLower = node.display_name.toLowerCase();
1440 |     const nodeTypeLower = node.node_type.toLowerCase();
1441 |     const nodeTypeClean = nodeTypeLower.replace(/^nodes-base\./, '').replace(/^nodes-langchain\./, '');
1442 |     
1443 |     // Exact match gets highest score
1444 |     if (displayNameLower === queryLower || nodeTypeClean === queryLower) {
1445 |       return 1000;
1446 |     }
1447 |     
1448 |     // Calculate edit distances for different parts
1449 |     const nameDistance = this.getEditDistance(queryLower, displayNameLower);
1450 |     const typeDistance = this.getEditDistance(queryLower, nodeTypeClean);
1451 |     
1452 |     // Also check individual words in the display name
1453 |     const nameWords = displayNameLower.split(/\s+/);
1454 |     let minWordDistance = Infinity;
1455 |     for (const word of nameWords) {
1456 |       const distance = this.getEditDistance(queryLower, word);
1457 |       if (distance < minWordDistance) {
1458 |         minWordDistance = distance;
1459 |       }
1460 |     }
1461 |     
1462 |     // Calculate best match score
1463 |     const bestDistance = Math.min(nameDistance, typeDistance, minWordDistance);
1464 |     
1465 |     // Use the length of the matched word for similarity calculation
1466 |     let matchedLen = queryLower.length;
1467 |     if (minWordDistance === bestDistance) {
1468 |       // Find which word matched best
1469 |       for (const word of nameWords) {
1470 |         if (this.getEditDistance(queryLower, word) === minWordDistance) {
1471 |           matchedLen = Math.max(queryLower.length, word.length);
1472 |           break;
1473 |         }
1474 |       }
1475 |     } else if (typeDistance === bestDistance) {
1476 |       matchedLen = Math.max(queryLower.length, nodeTypeClean.length);
1477 |     } else {
1478 |       matchedLen = Math.max(queryLower.length, displayNameLower.length);
1479 |     }
1480 |     
1481 |     const similarity = 1 - (bestDistance / matchedLen);
1482 |     
1483 |     // Boost if query is a substring
1484 |     if (displayNameLower.includes(queryLower) || nodeTypeClean.includes(queryLower)) {
1485 |       return 800 + (similarity * 100);
1486 |     }
1487 |     
1488 |     // Check if it's a prefix match
1489 |     if (displayNameLower.startsWith(queryLower) || 
1490 |         nodeTypeClean.startsWith(queryLower) ||
1491 |         nameWords.some(w => w.startsWith(queryLower))) {
1492 |       return 700 + (similarity * 100);
1493 |     }
1494 |     
1495 |     // Allow up to 1-2 character differences for typos
1496 |     if (bestDistance <= 2) {
1497 |       return 500 + ((2 - bestDistance) * 100) + (similarity * 50);
1498 |     }
1499 |     
1500 |     // Allow up to 3 character differences for longer words
1501 |     if (bestDistance <= 3 && queryLower.length >= 4) {
1502 |       return 400 + ((3 - bestDistance) * 50) + (similarity * 50);
1503 |     }
1504 |     
1505 |     // Base score on similarity
1506 |     return similarity * 300;
1507 |   }
1508 |   
1509 |   private getEditDistance(s1: string, s2: string): number {
1510 |     // Simple Levenshtein distance implementation
1511 |     const m = s1.length;
1512 |     const n = s2.length;
1513 |     const dp: number[][] = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
1514 |     
1515 |     for (let i = 0; i <= m; i++) dp[i][0] = i;
1516 |     for (let j = 0; j <= n; j++) dp[0][j] = j;
1517 |     
1518 |     for (let i = 1; i <= m; i++) {
1519 |       for (let j = 1; j <= n; j++) {
1520 |         if (s1[i - 1] === s2[j - 1]) {
1521 |           dp[i][j] = dp[i - 1][j - 1];
1522 |         } else {
1523 |           dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
1524 |         }
1525 |       }
1526 |     }
1527 |     
1528 |     return dp[m][n];
1529 |   }
1530 |   
1531 |   private async searchNodesLIKE(
1532 |     query: string,
1533 |     limit: number,
1534 |     options?: { includeSource?: boolean; includeExamples?: boolean; }
1535 |   ): Promise<any> {
1536 |     if (!this.db) throw new Error('Database not initialized');
1537 | 
1538 |     // This is the existing LIKE-based implementation
1539 |     // Handle exact phrase searches with quotes
1540 |     if (query.startsWith('"') && query.endsWith('"')) {
1541 |       const exactPhrase = query.slice(1, -1);
1542 |       const nodes = this.db!.prepare(`
1543 |         SELECT * FROM nodes
1544 |         WHERE node_type LIKE ? OR display_name LIKE ? OR description LIKE ?
1545 |         LIMIT ?
1546 |       `).all(`%${exactPhrase}%`, `%${exactPhrase}%`, `%${exactPhrase}%`, limit * 3) as NodeRow[];
1547 | 
1548 |       // Apply relevance ranking for exact phrase search
1549 |       const rankedNodes = this.rankSearchResults(nodes, exactPhrase, limit);
1550 | 
1551 |       const result: any = {
1552 |         query,
1553 |         results: rankedNodes.map(node => ({
1554 |           nodeType: node.node_type,
1555 |           workflowNodeType: getWorkflowNodeType(node.package_name, node.node_type),
1556 |           displayName: node.display_name,
1557 |           description: node.description,
1558 |           category: node.category,
1559 |           package: node.package_name
1560 |         })),
1561 |         totalCount: rankedNodes.length
1562 |       };
1563 | 
1564 |       // Add examples if requested
1565 |       if (options?.includeExamples) {
1566 |         for (const nodeResult of result.results) {
1567 |           try {
1568 |             const examples = this.db!.prepare(`
1569 |               SELECT
1570 |                 parameters_json,
1571 |                 template_name,
1572 |                 template_views
1573 |               FROM template_node_configs
1574 |               WHERE node_type = ?
1575 |               ORDER BY rank
1576 |               LIMIT 2
1577 |             `).all(nodeResult.workflowNodeType) as any[];
1578 | 
1579 |             if (examples.length > 0) {
1580 |               nodeResult.examples = examples.map((ex: any) => ({
1581 |                 configuration: JSON.parse(ex.parameters_json),
1582 |                 template: ex.template_name,
1583 |                 views: ex.template_views
1584 |               }));
1585 |             }
1586 |           } catch (error: any) {
1587 |             logger.warn(`Failed to fetch examples for ${nodeResult.nodeType}:`, error.message);
1588 |           }
1589 |         }
1590 |       }
1591 | 
1592 |       return result;
1593 |     }
1594 |     
1595 |     // Split into words for normal search
1596 |     const words = query.toLowerCase().split(/\s+/).filter(w => w.length > 0);
1597 |     
1598 |     if (words.length === 0) {
1599 |       return { query, results: [], totalCount: 0 };
1600 |     }
1601 |     
1602 |     // Build conditions for each word
1603 |     const conditions = words.map(() => 
1604 |       '(node_type LIKE ? OR display_name LIKE ? OR description LIKE ?)'
1605 |     ).join(' OR ');
1606 |     
1607 |     const params: any[] = words.flatMap(w => [`%${w}%`, `%${w}%`, `%${w}%`]);
1608 |     // Fetch more results initially to ensure we get the best matches after ranking
1609 |     params.push(limit * 3);
1610 |     
1611 |     const nodes = this.db!.prepare(`
1612 |       SELECT DISTINCT * FROM nodes 
1613 |       WHERE ${conditions}
1614 |       LIMIT ?
1615 |     `).all(...params) as NodeRow[];
1616 |     
1617 |     // Apply relevance ranking
1618 |     const rankedNodes = this.rankSearchResults(nodes, query, limit);
1619 | 
1620 |     const result: any = {
1621 |       query,
1622 |       results: rankedNodes.map(node => ({
1623 |         nodeType: node.node_type,
1624 |         workflowNodeType: getWorkflowNodeType(node.package_name, node.node_type),
1625 |         displayName: node.display_name,
1626 |         description: node.description,
1627 |         category: node.category,
1628 |         package: node.package_name
1629 |       })),
1630 |       totalCount: rankedNodes.length
1631 |     };
1632 | 
1633 |     // Add examples if requested
1634 |     if (options?.includeExamples) {
1635 |       for (const nodeResult of result.results) {
1636 |         try {
1637 |           const examples = this.db!.prepare(`
1638 |             SELECT
1639 |               parameters_json,
1640 |               template_name,
1641 |               template_views
1642 |             FROM template_node_configs
1643 |             WHERE node_type = ?
1644 |             ORDER BY rank
1645 |             LIMIT 2
1646 |           `).all(nodeResult.workflowNodeType) as any[];
1647 | 
1648 |           if (examples.length > 0) {
1649 |             nodeResult.examples = examples.map((ex: any) => ({
1650 |               configuration: JSON.parse(ex.parameters_json),
1651 |               template: ex.template_name,
1652 |               views: ex.template_views
1653 |             }));
1654 |           }
1655 |         } catch (error: any) {
1656 |           logger.warn(`Failed to fetch examples for ${nodeResult.nodeType}:`, error.message);
1657 |         }
1658 |       }
1659 |     }
1660 | 
1661 |     return result;
1662 |   }
1663 | 
1664 |   private calculateRelevance(node: NodeRow, query: string): string {
1665 |     const lowerQuery = query.toLowerCase();
1666 |     if (node.node_type.toLowerCase().includes(lowerQuery)) return 'high';
1667 |     if (node.display_name.toLowerCase().includes(lowerQuery)) return 'high';
1668 |     if (node.description?.toLowerCase().includes(lowerQuery)) return 'medium';
1669 |     return 'low';
1670 |   }
1671 |   
1672 |   private calculateRelevanceScore(node: NodeRow, query: string): number {
1673 |     const query_lower = query.toLowerCase();
1674 |     const name_lower = node.display_name.toLowerCase();
1675 |     const type_lower = node.node_type.toLowerCase();
1676 |     const type_without_prefix = type_lower.replace(/^nodes-base\./, '').replace(/^nodes-langchain\./, '');
1677 |     
1678 |     let score = 0;
1679 |     
1680 |     // Exact match in display name (highest priority)
1681 |     if (name_lower === query_lower) {
1682 |       score = 1000;
1683 |     }
1684 |     // Exact match in node type (without prefix)
1685 |     else if (type_without_prefix === query_lower) {
1686 |       score = 950;
1687 |     }
1688 |     // Special boost for common primary nodes
1689 |     else if (query_lower === 'webhook' && node.node_type === 'nodes-base.webhook') {
1690 |       score = 900;
1691 |     }
1692 |     else if ((query_lower === 'http' || query_lower === 'http request' || query_lower === 'http call') && node.node_type === 'nodes-base.httpRequest') {
1693 |       score = 900;
1694 |     }
1695 |     // Additional boost for multi-word queries matching primary nodes
1696 |     else if (query_lower.includes('http') && query_lower.includes('call') && node.node_type === 'nodes-base.httpRequest') {
1697 |       score = 890;
1698 |     }
1699 |     else if (query_lower.includes('http') && node.node_type === 'nodes-base.httpRequest') {
1700 |       score = 850;
1701 |     }
1702 |     // Boost for webhook queries
1703 |     else if (query_lower.includes('webhook') && node.node_type === 'nodes-base.webhook') {
1704 |       score = 850;
1705 |     }
1706 |     // Display name starts with query
1707 |     else if (name_lower.startsWith(query_lower)) {
1708 |       score = 800;
1709 |     }
1710 |     // Word boundary match in display name
1711 |     else if (new RegExp(`\\b${query_lower}\\b`, 'i').test(node.display_name)) {
1712 |       score = 700;
1713 |     }
1714 |     // Contains in display name
1715 |     else if (name_lower.includes(query_lower)) {
1716 |       score = 600;
1717 |     }
1718 |     // Type contains query (without prefix)
1719 |     else if (type_without_prefix.includes(query_lower)) {
1720 |       score = 500;
1721 |     }
1722 |     // Contains in description
1723 |     else if (node.description?.toLowerCase().includes(query_lower)) {
1724 |       score = 400;
1725 |     }
1726 |     
1727 |     return score;
1728 |   }
1729 | 
1730 |   private rankSearchResults(nodes: NodeRow[], query: string, limit: number): NodeRow[] {
1731 |     const query_lower = query.toLowerCase();
1732 |     
1733 |     // Calculate relevance scores for each node
1734 |     const scoredNodes = nodes.map(node => {
1735 |       const name_lower = node.display_name.toLowerCase();
1736 |       const type_lower = node.node_type.toLowerCase();
1737 |       const type_without_prefix = type_lower.replace(/^nodes-base\./, '').replace(/^nodes-langchain\./, '');
1738 |       
1739 |       let score = 0;
1740 |       
1741 |       // Exact match in display name (highest priority)
1742 |       if (name_lower === query_lower) {
1743 |         score = 1000;
1744 |       }
1745 |       // Exact match in node type (without prefix)
1746 |       else if (type_without_prefix === query_lower) {
1747 |         score = 950;
1748 |       }
1749 |       // Special boost for common primary nodes
1750 |       else if (query_lower === 'webhook' && node.node_type === 'nodes-base.webhook') {
1751 |         score = 900;
1752 |       }
1753 |       else if ((query_lower === 'http' || query_lower === 'http request' || query_lower === 'http call') && node.node_type === 'nodes-base.httpRequest') {
1754 |         score = 900;
1755 |       }
1756 |       // Boost for webhook queries
1757 |       else if (query_lower.includes('webhook') && node.node_type === 'nodes-base.webhook') {
1758 |         score = 850;
1759 |       }
1760 |       // Additional boost for http queries
1761 |       else if (query_lower.includes('http') && node.node_type === 'nodes-base.httpRequest') {
1762 |         score = 850;
1763 |       }
1764 |       // Display name starts with query
1765 |       else if (name_lower.startsWith(query_lower)) {
1766 |         score = 800;
1767 |       }
1768 |       // Word boundary match in display name
1769 |       else if (new RegExp(`\\b${query_lower}\\b`, 'i').test(node.display_name)) {
1770 |         score = 700;
1771 |       }
1772 |       // Contains in display name
1773 |       else if (name_lower.includes(query_lower)) {
1774 |         score = 600;
1775 |       }
1776 |       // Type contains query (without prefix)
1777 |       else if (type_without_prefix.includes(query_lower)) {
1778 |         score = 500;
1779 |       }
1780 |       // Contains in description
1781 |       else if (node.description?.toLowerCase().includes(query_lower)) {
1782 |         score = 400;
1783 |       }
1784 |       
1785 |       // For multi-word queries, check if all words are present
1786 |       const words = query_lower.split(/\s+/).filter(w => w.length > 0);
1787 |       if (words.length > 1) {
1788 |         const allWordsInName = words.every(word => name_lower.includes(word));
1789 |         const allWordsInDesc = words.every(word => node.description?.toLowerCase().includes(word));
1790 |         
1791 |         if (allWordsInName) score += 200;
1792 |         else if (allWordsInDesc) score += 100;
1793 |         
1794 |         // Special handling for common multi-word queries
1795 |         if (query_lower === 'http call' && name_lower === 'http request') {
1796 |           score = 920; // Boost HTTP Request for "http call" query
1797 |         }
1798 |       }
1799 |       
1800 |       return { node, score };
1801 |     });
1802 |     
1803 |     // Sort by score (descending) and then by display name (ascending)
1804 |     scoredNodes.sort((a, b) => {
1805 |       if (a.score !== b.score) {
1806 |         return b.score - a.score;
1807 |       }
1808 |       return a.node.display_name.localeCompare(b.node.display_name);
1809 |     });
1810 |     
1811 |     // Return only the requested number of results
1812 |     return scoredNodes.slice(0, limit).map(item => item.node);
1813 |   }
1814 | 
1815 |   private async listAITools(): Promise<any> {
1816 |     await this.ensureInitialized();
1817 |     if (!this.repository) throw new Error('Repository not initialized');
1818 |     const tools = this.repository.getAITools();
1819 |     
1820 |     // Debug: Check if is_ai_tool column is populated
1821 |     const aiCount = this.db!.prepare('SELECT COUNT(*) as ai_count FROM nodes WHERE is_ai_tool = 1').get() as any;
1822 |     // console.log('DEBUG list_ai_tools:', { 
1823 |     //   toolsLength: tools.length, 
1824 |     //   aiCountInDB: aiCount.ai_count,
1825 |     //   sampleTools: tools.slice(0, 3)
1826 |     // }); // Removed to prevent stdout interference
1827 |     
1828 |     return {
1829 |       tools,
1830 |       totalCount: tools.length,
1831 |       requirements: {
1832 |         environmentVariable: 'N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true',
1833 |         nodeProperty: 'usableAsTool: true',
1834 |       },
1835 |       usage: {
1836 |         description: 'These nodes have the usableAsTool property set to true, making them optimized for AI agent usage.',
1837 |         note: 'ANY node in n8n can be used as an AI tool by connecting it to the ai_tool port of an AI Agent node.',
1838 |         examples: [
1839 |           'Regular nodes like Slack, Google Sheets, or HTTP Request can be used as tools',
1840 |           'Connect any node to an AI Agent\'s tool port to make it available for AI-driven automation',
1841 |           'Community nodes require the environment variable to be set'
1842 |         ]
1843 |       }
1844 |     };
1845 |   }
1846 | 
1847 |   private async getNodeDocumentation(nodeType: string): Promise<any> {
1848 |     await this.ensureInitialized();
1849 |     if (!this.db) throw new Error('Database not initialized');
1850 | 
1851 |     // First try with normalized type
1852 |     const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType);
1853 |     let node = this.db!.prepare(`
1854 |       SELECT node_type, display_name, documentation, description 
1855 |       FROM nodes 
1856 |       WHERE node_type = ?
1857 |     `).get(normalizedType) as NodeRow | undefined;
1858 |     
1859 |     // If not found and normalization changed the type, try original
1860 |     if (!node && normalizedType !== nodeType) {
1861 |       node = this.db!.prepare(`
1862 |         SELECT node_type, display_name, documentation, description 
1863 |         FROM nodes 
1864 |         WHERE node_type = ?
1865 |       `).get(nodeType) as NodeRow | undefined;
1866 |     }
1867 |     
1868 |     // If still not found, try alternatives
1869 |     if (!node) {
1870 |       const alternatives = getNodeTypeAlternatives(normalizedType);
1871 |       
1872 |       for (const alt of alternatives) {
1873 |         node = this.db!.prepare(`
1874 |           SELECT node_type, display_name, documentation, description 
1875 |           FROM nodes 
1876 |           WHERE node_type = ?
1877 |         `).get(alt) as NodeRow | undefined;
1878 |         
1879 |         if (node) break;
1880 |       }
1881 |     }
1882 |     
1883 |     if (!node) {
1884 |       throw new Error(`Node ${nodeType} not found`);
1885 |     }
1886 |     
1887 |     // If no documentation, generate fallback with null safety
1888 |     if (!node.documentation) {
1889 |       const essentials = await this.getNodeEssentials(nodeType);
1890 | 
1891 |       return {
1892 |         nodeType: node.node_type,
1893 |         displayName: node.display_name || 'Unknown Node',
1894 |         documentation: `
1895 | # ${node.display_name || 'Unknown Node'}
1896 | 
1897 | ${node.description || 'No description available.'}
1898 | 
1899 | ## Common Properties
1900 | 
1901 | ${essentials?.commonProperties?.length > 0 ?
1902 |   essentials.commonProperties.map((p: any) =>
1903 |     `### ${p.displayName || 'Property'}\n${p.description || `Type: ${p.type || 'unknown'}`}`
1904 |   ).join('\n\n') :
1905 |   'No common properties available.'}
1906 | 
1907 | ## Note
1908 | Full documentation is being prepared. For now, use get_node_essentials for configuration help.
1909 | `,
1910 |         hasDocumentation: false
1911 |       };
1912 |     }
1913 | 
1914 |     return {
1915 |       nodeType: node.node_type,
1916 |       displayName: node.display_name || 'Unknown Node',
1917 |       documentation: node.documentation,
1918 |       hasDocumentation: true,
1919 |     };
1920 |   }
1921 | 
1922 |   private async getDatabaseStatistics(): Promise<any> {
1923 |     await this.ensureInitialized();
1924 |     if (!this.db) throw new Error('Database not initialized');
1925 |     const stats = this.db!.prepare(`
1926 |       SELECT 
1927 |         COUNT(*) as total,
1928 |         SUM(is_ai_tool) as ai_tools,
1929 |         SUM(is_trigger) as triggers,
1930 |         SUM(is_versioned) as versioned,
1931 |         SUM(CASE WHEN documentation IS NOT NULL THEN 1 ELSE 0 END) as with_docs,
1932 |         COUNT(DISTINCT package_name) as packages,
1933 |         COUNT(DISTINCT category) as categories
1934 |       FROM nodes
1935 |     `).get() as any;
1936 |     
1937 |     const packages = this.db!.prepare(`
1938 |       SELECT package_name, COUNT(*) as count 
1939 |       FROM nodes 
1940 |       GROUP BY package_name
1941 |     `).all() as any[];
1942 |     
1943 |     // Get template statistics
1944 |     const templateStats = this.db!.prepare(`
1945 |       SELECT 
1946 |         COUNT(*) as total_templates,
1947 |         AVG(views) as avg_views,
1948 |         MIN(views) as min_views,
1949 |         MAX(views) as max_views
1950 |       FROM templates
1951 |     `).get() as any;
1952 |     
1953 |     return {
1954 |       totalNodes: stats.total,
1955 |       totalTemplates: templateStats.total_templates || 0,
1956 |       statistics: {
1957 |         aiTools: stats.ai_tools,
1958 |         triggers: stats.triggers,
1959 |         versionedNodes: stats.versioned,
1960 |         nodesWithDocumentation: stats.with_docs,
1961 |         documentationCoverage: Math.round((stats.with_docs / stats.total) * 100) + '%',
1962 |         uniquePackages: stats.packages,
1963 |         uniqueCategories: stats.categories,
1964 |         templates: {
1965 |           total: templateStats.total_templates || 0,
1966 |           avgViews: Math.round(templateStats.avg_views || 0),
1967 |           minViews: templateStats.min_views || 0,
1968 |           maxViews: templateStats.max_views || 0
1969 |         }
1970 |       },
1971 |       packageBreakdown: packages.map(pkg => ({
1972 |         package: pkg.package_name,
1973 |         nodeCount: pkg.count,
1974 |       })),
1975 |     };
1976 |   }
1977 | 
1978 |   private async getNodeEssentials(nodeType: string, includeExamples?: boolean): Promise<any> {
1979 |     await this.ensureInitialized();
1980 |     if (!this.repository) throw new Error('Repository not initialized');
1981 | 
1982 |     // Check cache first (cache key includes includeExamples)
1983 |     const cacheKey = `essentials:${nodeType}:${includeExamples ? 'withExamples' : 'basic'}`;
1984 |     const cached = this.cache.get(cacheKey);
1985 |     if (cached) return cached;
1986 |     
1987 |     // Get the full node information
1988 |     // First try with normalized type
1989 |     const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType);
1990 |     let node = this.repository.getNode(normalizedType);
1991 |     
1992 |     if (!node && normalizedType !== nodeType) {
1993 |       // Try original if normalization changed it
1994 |       node = this.repository.getNode(nodeType);
1995 |     }
1996 |     
1997 |     if (!node) {
1998 |       // Fallback to other alternatives for edge cases
1999 |       const alternatives = getNodeTypeAlternatives(normalizedType);
2000 |       
2001 |       for (const alt of alternatives) {
2002 |         const found = this.repository!.getNode(alt);
2003 |         if (found) {
2004 |           node = found;
2005 |           break;
2006 |         }
2007 |       }
2008 |     }
2009 |     
2010 |     if (!node) {
2011 |       throw new Error(`Node ${nodeType} not found`);
2012 |     }
2013 |     
2014 |     // Get properties (already parsed by repository)
2015 |     const allProperties = node.properties || [];
2016 |     
2017 |     // Get essential properties
2018 |     const essentials = PropertyFilter.getEssentials(allProperties, node.nodeType);
2019 |     
2020 |     // Get operations (already parsed by repository)
2021 |     const operations = node.operations || [];
2022 |     
2023 |     const result = {
2024 |       nodeType: node.nodeType,
2025 |       workflowNodeType: getWorkflowNodeType(node.package ?? 'n8n-nodes-base', node.nodeType),
2026 |       displayName: node.displayName,
2027 |       description: node.description,
2028 |       category: node.category,
2029 |       version: node.version ?? '1',
2030 |       isVersioned: node.isVersioned ?? false,
2031 |       requiredProperties: essentials.required,
2032 |       commonProperties: essentials.common,
2033 |       operations: operations.map((op: any) => ({
2034 |         name: op.name || op.operation,
2035 |         description: op.description,
2036 |         action: op.action,
2037 |         resource: op.resource
2038 |       })),
2039 |       // Examples removed - use validate_node_operation for working configurations
2040 |       metadata: {
2041 |         totalProperties: allProperties.length,
2042 |         isAITool: node.isAITool ?? false,
2043 |         isTrigger: node.isTrigger ?? false,
2044 |         isWebhook: node.isWebhook ?? false,
2045 |         hasCredentials: node.credentials ? true : false,
2046 |         package: node.package ?? 'n8n-nodes-base',
2047 |         developmentStyle: node.developmentStyle ?? 'programmatic'
2048 |       }
2049 |     };
2050 | 
2051 |     // Add examples from templates if requested
2052 |     if (includeExamples) {
2053 |       try {
2054 |         // Use the already-computed workflowNodeType from result (line 1888)
2055 |         // This ensures consistency with search_nodes behavior (line 1203)
2056 |         const examples = this.db!.prepare(`
2057 |           SELECT
2058 |             parameters_json,
2059 |             template_name,
2060 |             template_views,
2061 |             complexity,
2062 |             use_cases,
2063 |             has_credentials,
2064 |             has_expressions
2065 |           FROM template_node_configs
2066 |           WHERE node_type = ?
2067 |           ORDER BY rank
2068 |           LIMIT 3
2069 |         `).all(result.workflowNodeType) as any[];
2070 | 
2071 |         if (examples.length > 0) {
2072 |           (result as any).examples = examples.map((ex: any) => ({
2073 |             configuration: JSON.parse(ex.parameters_json),
2074 |             source: {
2075 |               template: ex.template_name,
2076 |               views: ex.template_views,
2077 |               complexity: ex.complexity
2078 |             },
2079 |             useCases: ex.use_cases ? JSON.parse(ex.use_cases).slice(0, 2) : [],
2080 |             metadata: {
2081 |               hasCredentials: ex.has_credentials === 1,
2082 |               hasExpressions: ex.has_expressions === 1
2083 |             }
2084 |           }));
2085 | 
2086 |           (result as any).examplesCount = examples.length;
2087 |         } else {
2088 |           (result as any).examples = [];
2089 |           (result as any).examplesCount = 0;
2090 |         }
2091 |       } catch (error: any) {
2092 |         logger.warn(`Failed to fetch examples for ${nodeType}:`, error.message);
2093 |         (result as any).examples = [];
2094 |         (result as any).examplesCount = 0;
2095 |       }
2096 |     }
2097 | 
2098 |     // Cache for 1 hour
2099 |     this.cache.set(cacheKey, result, 3600);
2100 | 
2101 |     return result;
2102 |   }
2103 | 
2104 |   private async searchNodeProperties(nodeType: string, query: string, maxResults: number = 20): Promise<any> {
2105 |     await this.ensureInitialized();
2106 |     if (!this.repository) throw new Error('Repository not initialized');
2107 | 
2108 |     // Get the node
2109 |     // First try with normalized type
2110 |     const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType);
2111 |     let node = this.repository.getNode(normalizedType);
2112 |     
2113 |     if (!node && normalizedType !== nodeType) {
2114 |       // Try original if normalization changed it
2115 |       node = this.repository.getNode(nodeType);
2116 |     }
2117 |     
2118 |     if (!node) {
2119 |       // Fallback to other alternatives for edge cases
2120 |       const alternatives = getNodeTypeAlternatives(normalizedType);
2121 |       
2122 |       for (const alt of alternatives) {
2123 |         const found = this.repository!.getNode(alt);
2124 |         if (found) {
2125 |           node = found;
2126 |           break;
2127 |         }
2128 |       }
2129 |     }
2130 |     
2131 |     if (!node) {
2132 |       throw new Error(`Node ${nodeType} not found`);
2133 |     }
2134 |     
2135 |     // Get properties and search (already parsed by repository)
2136 |     const allProperties = node.properties || [];
2137 |     const matches = PropertyFilter.searchProperties(allProperties, query, maxResults);
2138 |     
2139 |     return {
2140 |       nodeType: node.nodeType,
2141 |       query,
2142 |       matches: matches.map((match: any) => ({
2143 |         name: match.name,
2144 |         displayName: match.displayName,
2145 |         type: match.type,
2146 |         description: match.description,
2147 |         path: match.path || match.name,
2148 |         required: match.required,
2149 |         default: match.default,
2150 |         options: match.options,
2151 |         showWhen: match.showWhen
2152 |       })),
2153 |       totalMatches: matches.length,
2154 |       searchedIn: allProperties.length + ' properties'
2155 |     };
2156 |   }
2157 | 
2158 |   private getPropertyValue(config: any, path: string): any {
2159 |     const parts = path.split('.');
2160 |     let value = config;
2161 |     
2162 |     for (const part of parts) {
2163 |       // Handle array notation like parameters[0]
2164 |       const arrayMatch = part.match(/^(\w+)\[(\d+)\]$/);
2165 |       if (arrayMatch) {
2166 |         value = value?.[arrayMatch[1]]?.[parseInt(arrayMatch[2])];
2167 |       } else {
2168 |         value = value?.[part];
2169 |       }
2170 |     }
2171 |     
2172 |     return value;
2173 |   }
2174 |   
2175 |   private async listTasks(category?: string): Promise<any> {
2176 |     if (category) {
2177 |       const categories = TaskTemplates.getTaskCategories();
2178 |       const tasks = categories[category];
2179 |       
2180 |       if (!tasks) {
2181 |         throw new Error(
2182 |           `Unknown category: ${category}. Available categories: ${Object.keys(categories).join(', ')}`
2183 |         );
2184 |       }
2185 |       
2186 |       return {
2187 |         category,
2188 |         tasks: tasks.map(task => {
2189 |           const template = TaskTemplates.getTaskTemplate(task);
2190 |           return {
2191 |             task,
2192 |             description: template?.description || '',
2193 |             nodeType: template?.nodeType || ''
2194 |           };
2195 |         })
2196 |       };
2197 |     }
2198 |     
2199 |     // Return all tasks grouped by category
2200 |     const categories = TaskTemplates.getTaskCategories();
2201 |     const result: any = {
2202 |       totalTasks: TaskTemplates.getAllTasks().length,
2203 |       categories: {}
2204 |     };
2205 |     
2206 |     for (const [cat, tasks] of Object.entries(categories)) {
2207 |       result.categories[cat] = tasks.map(task => {
2208 |         const template = TaskTemplates.getTaskTemplate(task);
2209 |         return {
2210 |           task,
2211 |           description: template?.description || '',
2212 |           nodeType: template?.nodeType || ''
2213 |         };
2214 |       });
2215 |     }
2216 |     
2217 |     return result;
2218 |   }
2219 |   
2220 |   private async validateNodeConfig(
2221 |     nodeType: string, 
2222 |     config: Record<string, any>, 
2223 |     mode: ValidationMode = 'operation',
2224 |     profile: ValidationProfile = 'ai-friendly'
2225 |   ): Promise<any> {
2226 |     await this.ensureInitialized();
2227 |     if (!this.repository) throw new Error('Repository not initialized');
2228 | 
2229 |     // Get node info to access properties
2230 |     // First try with normalized type
2231 |     const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType);
2232 |     let node = this.repository.getNode(normalizedType);
2233 | 
2234 |     if (!node && normalizedType !== nodeType) {
2235 |       // Try original if normalization changed it
2236 |       node = this.repository.getNode(nodeType);
2237 |     }
2238 | 
2239 |     if (!node) {
2240 |       // Fallback to other alternatives for edge cases
2241 |       const alternatives = getNodeTypeAlternatives(normalizedType);
2242 |       
2243 |       for (const alt of alternatives) {
2244 |         const found = this.repository!.getNode(alt);
2245 |         if (found) {
2246 |           node = found;
2247 |           break;
2248 |         }
2249 |       }
2250 |     }
2251 |     
2252 |     if (!node) {
2253 |       throw new Error(`Node ${nodeType} not found`);
2254 |     }
2255 |     
2256 |     // Get properties
2257 |     const properties = node.properties || [];
2258 |     
2259 |     // Use enhanced validator with operation mode by default
2260 |     const validationResult = EnhancedConfigValidator.validateWithMode(
2261 |       node.nodeType, 
2262 |       config, 
2263 |       properties, 
2264 |       mode,
2265 |       profile
2266 |     );
2267 |     
2268 |     // Add node context to result
2269 |     return {
2270 |       nodeType: node.nodeType,
2271 |       workflowNodeType: getWorkflowNodeType(node.package, node.nodeType),
2272 |       displayName: node.displayName,
2273 |       ...validationResult,
2274 |       summary: {
2275 |         hasErrors: !validationResult.valid,
2276 |         errorCount: validationResult.errors.length,
2277 |         warningCount: validationResult.warnings.length,
2278 |         suggestionCount: validationResult.suggestions.length
2279 |       }
2280 |     };
2281 |   }
2282 |   
2283 |   private async getPropertyDependencies(nodeType: string, config?: Record<string, any>): Promise<any> {
2284 |     await this.ensureInitialized();
2285 |     if (!this.repository) throw new Error('Repository not initialized');
2286 | 
2287 |     // Get node info to access properties
2288 |     // First try with normalized type
2289 |     const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType);
2290 |     let node = this.repository.getNode(normalizedType);
2291 |     
2292 |     if (!node && normalizedType !== nodeType) {
2293 |       // Try original if normalization changed it
2294 |       node = this.repository.getNode(nodeType);
2295 |     }
2296 |     
2297 |     if (!node) {
2298 |       // Fallback to other alternatives for edge cases
2299 |       const alternatives = getNodeTypeAlternatives(normalizedType);
2300 |       
2301 |       for (const alt of alternatives) {
2302 |         const found = this.repository!.getNode(alt);
2303 |         if (found) {
2304 |           node = found;
2305 |           break;
2306 |         }
2307 |       }
2308 |     }
2309 |     
2310 |     if (!node) {
2311 |       throw new Error(`Node ${nodeType} not found`);
2312 |     }
2313 |     
2314 |     // Get properties
2315 |     const properties = node.properties || [];
2316 |     
2317 |     // Analyze dependencies
2318 |     const analysis = PropertyDependencies.analyze(properties);
2319 |     
2320 |     // If config provided, check visibility impact
2321 |     let visibilityImpact = null;
2322 |     if (config) {
2323 |       visibilityImpact = PropertyDependencies.getVisibilityImpact(properties, config);
2324 |     }
2325 |     
2326 |     return {
2327 |       nodeType: node.nodeType,
2328 |       displayName: node.displayName,
2329 |       ...analysis,
2330 |       currentConfig: config ? {
2331 |         providedValues: config,
2332 |         visibilityImpact
2333 |       } : undefined
2334 |     };
2335 |   }
2336 |   
2337 |   private async getNodeAsToolInfo(nodeType: string): Promise<any> {
2338 |     await this.ensureInitialized();
2339 |     if (!this.repository) throw new Error('Repository not initialized');
2340 | 
2341 |     // Get node info
2342 |     // First try with normalized type
2343 |     const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType);
2344 |     let node = this.repository.getNode(normalizedType);
2345 |     
2346 |     if (!node && normalizedType !== nodeType) {
2347 |       // Try original if normalization changed it
2348 |       node = this.repository.getNode(nodeType);
2349 |     }
2350 |     
2351 |     if (!node) {
2352 |       // Fallback to other alternatives for edge cases
2353 |       const alternatives = getNodeTypeAlternatives(normalizedType);
2354 |       
2355 |       for (const alt of alternatives) {
2356 |         const found = this.repository!.getNode(alt);
2357 |         if (found) {
2358 |           node = found;
2359 |           break;
2360 |         }
2361 |       }
2362 |     }
2363 |     
2364 |     if (!node) {
2365 |       throw new Error(`Node ${nodeType} not found`);
2366 |     }
2367 |     
2368 |     // Determine common AI tool use cases based on node type
2369 |     const commonUseCases = this.getCommonAIToolUseCases(node.nodeType);
2370 |     
2371 |     // Build AI tool capabilities info
2372 |     const aiToolCapabilities = {
2373 |       canBeUsedAsTool: true, // In n8n, ANY node can be used as a tool when connected to AI Agent
2374 |       hasUsableAsToolProperty: node.isAITool,
2375 |       requiresEnvironmentVariable: !node.isAITool && node.package !== 'n8n-nodes-base',
2376 |       connectionType: 'ai_tool',
2377 |       commonUseCases,
2378 |       requirements: {
2379 |         connection: 'Connect to the "ai_tool" port of an AI Agent node',
2380 |         environment: node.package !== 'n8n-nodes-base' ? 
2381 |           'Set N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true for community nodes' : 
2382 |           'No special environment variables needed for built-in nodes'
2383 |       },
2384 |       examples: this.getAIToolExamples(node.nodeType),
2385 |       tips: [
2386 |         'Give the tool a clear, descriptive name in the AI Agent settings',
2387 |         'Write a detailed tool description to help the AI understand when to use it',
2388 |         'Test the node independently before connecting it as a tool',
2389 |         node.isAITool ? 
2390 |           'This node is optimized for AI tool usage' : 
2391 |           'This is a regular node that can be used as an AI tool'
2392 |       ]
2393 |     };
2394 |     
2395 |     return {
2396 |       nodeType: node.nodeType,
2397 |       workflowNodeType: getWorkflowNodeType(node.package, node.nodeType),
2398 |       displayName: node.displayName,
2399 |       description: node.description,
2400 |       package: node.package,
2401 |       isMarkedAsAITool: node.isAITool,
2402 |       aiToolCapabilities
2403 |     };
2404 |   }
2405 |   
2406 |   private getOutputDescriptions(nodeType: string, outputName: string, index: number): { description: string, connectionGuidance: string } {
2407 |     // Special handling for loop nodes
2408 |     if (nodeType === 'nodes-base.splitInBatches') {
2409 |       if (outputName === 'done' && index === 0) {
2410 |         return {
2411 |           description: 'Final processed data after all iterations complete',
2412 |           connectionGuidance: 'Connect to nodes that should run AFTER the loop completes'
2413 |         };
2414 |       } else if (outputName === 'loop' && index === 1) {
2415 |         return {
2416 |           description: 'Current batch data for this iteration',
2417 |           connectionGuidance: 'Connect to nodes that process items INSIDE the loop (and connect their output back to this node)'
2418 |         };
2419 |       }
2420 |     }
2421 |     
2422 |     // Special handling for IF node
2423 |     if (nodeType === 'nodes-base.if') {
2424 |       if (outputName === 'true' && index === 0) {
2425 |         return {
2426 |           description: 'Items that match the condition',
2427 |           connectionGuidance: 'Connect to nodes that handle the TRUE case'
2428 |         };
2429 |       } else if (outputName === 'false' && index === 1) {
2430 |         return {
2431 |           description: 'Items that do not match the condition',
2432 |           connectionGuidance: 'Connect to nodes that handle the FALSE case'
2433 |         };
2434 |       }
2435 |     }
2436 |     
2437 |     // Special handling for Switch node
2438 |     if (nodeType === 'nodes-base.switch') {
2439 |       return {
2440 |         description: `Output ${index}: ${outputName || 'Route ' + index}`,
2441 |         connectionGuidance: `Connect to nodes for the "${outputName || 'route ' + index}" case`
2442 |       };
2443 |     }
2444 |     
2445 |     // Default handling
2446 |     return {
2447 |       description: outputName || `Output ${index}`,
2448 |       connectionGuidance: `Connect to downstream nodes`
2449 |     };
2450 |   }
2451 | 
2452 |   private getCommonAIToolUseCases(nodeType: string): string[] {
2453 |     const useCaseMap: Record<string, string[]> = {
2454 |       'nodes-base.slack': [
2455 |         'Send notifications about task completion',
2456 |         'Post updates to channels',
2457 |         'Send direct messages',
2458 |         'Create alerts and reminders'
2459 |       ],
2460 |       'nodes-base.googleSheets': [
2461 |         'Read data for analysis',
2462 |         'Log results and outputs',
2463 |         'Update spreadsheet records',
2464 |         'Create reports'
2465 |       ],
2466 |       'nodes-base.gmail': [
2467 |         'Send email notifications',
2468 |         'Read and process emails',
2469 |         'Send reports and summaries',
2470 |         'Handle email-based workflows'
2471 |       ],
2472 |       'nodes-base.httpRequest': [
2473 |         'Call external APIs',
2474 |         'Fetch data from web services',
2475 |         'Send webhooks',
2476 |         'Integrate with any REST API'
2477 |       ],
2478 |       'nodes-base.postgres': [
2479 |         'Query database for information',
2480 |         'Store analysis results',
2481 |         'Update records based on AI decisions',
2482 |         'Generate reports from data'
2483 |       ],
2484 |       'nodes-base.webhook': [
2485 |         'Receive external triggers',
2486 |         'Create callback endpoints',
2487 |         'Handle incoming data',
2488 |         'Integrate with external systems'
2489 |       ]
2490 |     };
2491 |     
2492 |     // Check for partial matches
2493 |     for (const [key, useCases] of Object.entries(useCaseMap)) {
2494 |       if (nodeType.includes(key)) {
2495 |         return useCases;
2496 |       }
2497 |     }
2498 |     
2499 |     // Generic use cases for unknown nodes
2500 |     return [
2501 |       'Perform automated actions',
2502 |       'Integrate with external services',
2503 |       'Process and transform data',
2504 |       'Extend AI agent capabilities'
2505 |     ];
2506 |   }
2507 |   
2508 |   private getAIToolExamples(nodeType: string): any {
2509 |     const exampleMap: Record<string, any> = {
2510 |       'nodes-base.slack': {
2511 |         toolName: 'Send Slack Message',
2512 |         toolDescription: 'Sends a message to a specified Slack channel or user. Use this to notify team members about important events or results.',
2513 |         nodeConfig: {
2514 |           resource: 'message',
2515 |           operation: 'post',
2516 |           channel: '={{ $fromAI("channel", "The Slack channel to send to, e.g. #general") }}',
2517 |           text: '={{ $fromAI("message", "The message content to send") }}'
2518 |         }
2519 |       },
2520 |       'nodes-base.googleSheets': {
2521 |         toolName: 'Update Google Sheet',
2522 |         toolDescription: 'Reads or updates data in a Google Sheets spreadsheet. Use this to log information, retrieve data, or update records.',
2523 |         nodeConfig: {
2524 |           operation: 'append',
2525 |           sheetId: 'your-sheet-id',
2526 |           range: 'A:Z',
2527 |           dataMode: 'autoMap'
2528 |         }
2529 |       },
2530 |       'nodes-base.httpRequest': {
2531 |         toolName: 'Call API',
2532 |         toolDescription: 'Makes HTTP requests to external APIs. Use this to fetch data, trigger webhooks, or integrate with any web service.',
2533 |         nodeConfig: {
2534 |           method: '={{ $fromAI("method", "HTTP method: GET, POST, PUT, DELETE") }}',
2535 |           url: '={{ $fromAI("url", "The complete API endpoint URL") }}',
2536 |           sendBody: true,
2537 |           bodyContentType: 'json',
2538 |           jsonBody: '={{ $fromAI("body", "Request body as JSON object") }}'
2539 |         }
2540 |       }
2541 |     };
2542 |     
2543 |     // Check for exact match or partial match
2544 |     for (const [key, example] of Object.entries(exampleMap)) {
2545 |       if (nodeType.includes(key)) {
2546 |         return example;
2547 |       }
2548 |     }
2549 |     
2550 |     // Generic example
2551 |     return {
2552 |       toolName: 'Custom Tool',
2553 |       toolDescription: 'Performs specific operations. Describe what this tool does and when to use it.',
2554 |       nodeConfig: {
2555 |         note: 'Configure the node based on its specific requirements'
2556 |       }
2557 |     };
2558 |   }
2559 |   
2560 |   private async validateNodeMinimal(nodeType: string, config: Record<string, any>): Promise<any> {
2561 |     await this.ensureInitialized();
2562 |     if (!this.repository) throw new Error('Repository not initialized');
2563 | 
2564 |     // Get node info
2565 |     // First try with normalized type
2566 |     const normalizedType = NodeTypeNormalizer.normalizeToFullForm(nodeType);
2567 |     let node = this.repository.getNode(normalizedType);
2568 |     
2569 |     if (!node && normalizedType !== nodeType) {
2570 |       // Try original if normalization changed it
2571 |       node = this.repository.getNode(nodeType);
2572 |     }
2573 |     
2574 |     if (!node) {
2575 |       // Fallback to other alternatives for edge cases
2576 |       const alternatives = getNodeTypeAlternatives(normalizedType);
2577 |       
2578 |       for (const alt of alternatives) {
2579 |         const found = this.repository!.getNode(alt);
2580 |         if (found) {
2581 |           node = found;
2582 |           break;
2583 |         }
2584 |       }
2585 |     }
2586 |     
2587 |     if (!node) {
2588 |       throw new Error(`Node ${nodeType} not found`);
2589 |     }
2590 |     
2591 |     // Get properties  
2592 |     const properties = node.properties || [];
2593 |     
2594 |     // Extract operation context (safely handle undefined config properties)
2595 |     const operationContext = {
2596 |       resource: config?.resource,
2597 |       operation: config?.operation,
2598 |       action: config?.action,
2599 |       mode: config?.mode
2600 |     };
2601 |     
2602 |     // Find missing required fields
2603 |     const missingFields: string[] = [];
2604 |     
2605 |     for (const prop of properties) {
2606 |       // Skip if not required
2607 |       if (!prop.required) continue;
2608 |       
2609 |       // Skip if not visible based on current config
2610 |       if (prop.displayOptions) {
2611 |         let isVisible = true;
2612 |         
2613 |         // Check show conditions
2614 |         if (prop.displayOptions.show) {
2615 |           for (const [key, values] of Object.entries(prop.displayOptions.show)) {
2616 |             const configValue = config?.[key];
2617 |             const expectedValues = Array.isArray(values) ? values : [values];
2618 |             
2619 |             if (!expectedValues.includes(configValue)) {
2620 |               isVisible = false;
2621 |               break;
2622 |             }
2623 |           }
2624 |         }
2625 |         
2626 |         // Check hide conditions
2627 |         if (isVisible && prop.displayOptions.hide) {
2628 |           for (const [key, values] of Object.entries(prop.displayOptions.hide)) {
2629 |             const configValue = config?.[key];
2630 |             const expectedValues = Array.isArray(values) ? values : [values];
2631 |             
2632 |             if (expectedValues.includes(configValue)) {
2633 |               isVisible = false;
2634 |               break;
2635 |             }
2636 |           }
2637 |         }
2638 |         
2639 |         if (!isVisible) continue;
2640 |       }
2641 |       
2642 |       // Check if field is missing (safely handle null/undefined config)
2643 |       if (!config || !(prop.name in config)) {
2644 |         missingFields.push(prop.displayName || prop.name);
2645 |       }
2646 |     }
2647 |     
2648 |     return {
2649 |       nodeType: node.nodeType,
2650 |       displayName: node.displayName,
2651 |       valid: missingFields.length === 0,
2652 |       missingRequiredFields: missingFields
2653 |     };
2654 |   }
2655 | 
2656 |   // Method removed - replaced by getToolsDocumentation
2657 | 
2658 |   private async getToolsDocumentation(topic?: string, depth: 'essentials' | 'full' = 'essentials'): Promise<string> {
2659 |     if (!topic || topic === 'overview') {
2660 |       return getToolsOverview(depth);
2661 |     }
2662 |     
2663 |     return getToolDocumentation(topic, depth);
2664 |   }
2665 | 
2666 |   // Add connect method to accept any transport
2667 |   async connect(transport: any): Promise<void> {
2668 |     await this.ensureInitialized();
2669 |     await this.server.connect(transport);
2670 |     logger.info('MCP Server connected', { 
2671 |       transportType: transport.constructor.name 
2672 |     });
2673 |   }
2674 |   
2675 |   // Template-related methods
2676 |   private async listTemplates(limit: number = 10, offset: number = 0, sortBy: 'views' | 'created_at' | 'name' = 'views', includeMetadata: boolean = false): Promise<any> {
2677 |     await this.ensureInitialized();
2678 |     if (!this.templateService) throw new Error('Template service not initialized');
2679 |     
2680 |     const result = await this.templateService.listTemplates(limit, offset, sortBy, includeMetadata);
2681 |     
2682 |     return {
2683 |       ...result,
2684 |       tip: result.items.length > 0 ? 
2685 |         `Use get_template(templateId) to get full workflow details. Total: ${result.total} templates available.` :
2686 |         "No templates found. Run 'npm run fetch:templates' to update template database"
2687 |     };
2688 |   }
2689 |   
2690 |   private async listNodeTemplates(nodeTypes: string[], limit: number = 10, offset: number = 0): Promise<any> {
2691 |     await this.ensureInitialized();
2692 |     if (!this.templateService) throw new Error('Template service not initialized');
2693 |     
2694 |     const result = await this.templateService.listNodeTemplates(nodeTypes, limit, offset);
2695 |     
2696 |     if (result.items.length === 0 && offset === 0) {
2697 |       return {
2698 |         ...result,
2699 |         message: `No templates found using nodes: ${nodeTypes.join(', ')}`,
2700 |         tip: "Try searching with more common nodes or run 'npm run fetch:templates' to update template database"
2701 |       };
2702 |     }
2703 |     
2704 |     return {
2705 |       ...result,
2706 |       tip: `Showing ${result.items.length} of ${result.total} templates. Use offset for pagination.`
2707 |     };
2708 |   }
2709 |   
2710 |   private async getTemplate(templateId: number, mode: 'nodes_only' | 'structure' | 'full' = 'full'): Promise<any> {
2711 |     await this.ensureInitialized();
2712 |     if (!this.templateService) throw new Error('Template service not initialized');
2713 |     
2714 |     const template = await this.templateService.getTemplate(templateId, mode);
2715 |     
2716 |     if (!template) {
2717 |       return {
2718 |         error: `Template ${templateId} not found`,
2719 |         tip: "Use list_templates, list_node_templates or search_templates to find available templates"
2720 |       };
2721 |     }
2722 |     
2723 |     const usage = mode === 'nodes_only' ? "Node list for quick overview" :
2724 |                   mode === 'structure' ? "Workflow structure without full details" :
2725 |                   "Complete workflow JSON ready to import into n8n";
2726 |     
2727 |     return {
2728 |       mode,
2729 |       template,
2730 |       usage
2731 |     };
2732 |   }
2733 |   
2734 |   private async searchTemplates(query: string, limit: number = 20, offset: number = 0, fields?: string[]): Promise<any> {
2735 |     await this.ensureInitialized();
2736 |     if (!this.templateService) throw new Error('Template service not initialized');
2737 |     
2738 |     const result = await this.templateService.searchTemplates(query, limit, offset, fields);
2739 |     
2740 |     if (result.items.length === 0 && offset === 0) {
2741 |       return {
2742 |         ...result,
2743 |         message: `No templates found matching: "${query}"`,
2744 |         tip: "Try different keywords or run 'npm run fetch:templates' to update template database"
2745 |       };
2746 |     }
2747 |     
2748 |     return {
2749 |       ...result,
2750 |       query,
2751 |       tip: `Found ${result.total} templates matching "${query}". Showing ${result.items.length}.`
2752 |     };
2753 |   }
2754 |   
2755 |   private async getTemplatesForTask(task: string, limit: number = 10, offset: number = 0): Promise<any> {
2756 |     await this.ensureInitialized();
2757 |     if (!this.templateService) throw new Error('Template service not initialized');
2758 |     
2759 |     const result = await this.templateService.getTemplatesForTask(task, limit, offset);
2760 |     const availableTasks = this.templateService.listAvailableTasks();
2761 |     
2762 |     if (result.items.length === 0 && offset === 0) {
2763 |       return {
2764 |         ...result,
2765 |         message: `No templates found for task: ${task}`,
2766 |         availableTasks,
2767 |         tip: "Try a different task or use search_templates for custom searches"
2768 |       };
2769 |     }
2770 |     
2771 |     return {
2772 |       ...result,
2773 |       task,
2774 |       description: this.getTaskDescription(task),
2775 |       tip: `${result.total} templates available for ${task}. Showing ${result.items.length}.`
2776 |     };
2777 |   }
2778 |   
2779 |   private async searchTemplatesByMetadata(filters: {
2780 |     category?: string;
2781 |     complexity?: 'simple' | 'medium' | 'complex';
2782 |     maxSetupMinutes?: number;
2783 |     minSetupMinutes?: number;
2784 |     requiredService?: string;
2785 |     targetAudience?: string;
2786 |   }, limit: number = 20, offset: number = 0): Promise<any> {
2787 |     await this.ensureInitialized();
2788 |     if (!this.templateService) throw new Error('Template service not initialized');
2789 |     
2790 |     const result = await this.templateService.searchTemplatesByMetadata(filters, limit, offset);
2791 |     
2792 |     // Build filter summary for feedback
2793 |     const filterSummary: string[] = [];
2794 |     if (filters.category) filterSummary.push(`category: ${filters.category}`);
2795 |     if (filters.complexity) filterSummary.push(`complexity: ${filters.complexity}`);
2796 |     if (filters.maxSetupMinutes) filterSummary.push(`max setup: ${filters.maxSetupMinutes} min`);
2797 |     if (filters.minSetupMinutes) filterSummary.push(`min setup: ${filters.minSetupMinutes} min`);
2798 |     if (filters.requiredService) filterSummary.push(`service: ${filters.requiredService}`);
2799 |     if (filters.targetAudience) filterSummary.push(`audience: ${filters.targetAudience}`);
2800 |     
2801 |     if (result.items.length === 0 && offset === 0) {
2802 |       // Get available categories and audiences for suggestions
2803 |       const availableCategories = await this.templateService.getAvailableCategories();
2804 |       const availableAudiences = await this.templateService.getAvailableTargetAudiences();
2805 |       
2806 |       return {
2807 |         ...result,
2808 |         message: `No templates found with filters: ${filterSummary.join(', ')}`,
2809 |         availableCategories: availableCategories.slice(0, 10),
2810 |         availableAudiences: availableAudiences.slice(0, 5),
2811 |         tip: "Try broader filters or different categories. Use list_templates to see all templates."
2812 |       };
2813 |     }
2814 |     
2815 |     return {
2816 |       ...result,
2817 |       filters,
2818 |       filterSummary: filterSummary.join(', '),
2819 |       tip: `Found ${result.total} templates matching filters. Showing ${result.items.length}. Each includes AI-generated metadata.`
2820 |     };
2821 |   }
2822 |   
2823 |   private getTaskDescription(task: string): string {
2824 |     const descriptions: Record<string, string> = {
2825 |       'ai_automation': 'AI-powered workflows using OpenAI, LangChain, and other AI tools',
2826 |       'data_sync': 'Synchronize data between databases, spreadsheets, and APIs',
2827 |       'webhook_processing': 'Process incoming webhooks and trigger automated actions',
2828 |       'email_automation': 'Send, receive, and process emails automatically',
2829 |       'slack_integration': 'Integrate with Slack for notifications and bot interactions',
2830 |       'data_transformation': 'Transform, clean, and manipulate data',
2831 |       'file_processing': 'Handle file uploads, downloads, and transformations',
2832 |       'scheduling': 'Schedule recurring tasks and time-based automations',
2833 |       'api_integration': 'Connect to external APIs and web services',
2834 |       'database_operations': 'Query, insert, update, and manage database records'
2835 |     };
2836 |     
2837 |     return descriptions[task] || 'Workflow templates for this task';
2838 |   }
2839 | 
2840 |   private async validateWorkflow(workflow: any, options?: any): Promise<any> {
2841 |     await this.ensureInitialized();
2842 |     if (!this.repository) throw new Error('Repository not initialized');
2843 |     
2844 |     // Enhanced logging for workflow validation
2845 |     logger.info('Workflow validation requested', {
2846 |       hasWorkflow: !!workflow,
2847 |       workflowType: typeof workflow,
2848 |       hasNodes: workflow?.nodes !== undefined,
2849 |       nodesType: workflow?.nodes ? typeof workflow.nodes : 'undefined',
2850 |       nodesIsArray: Array.isArray(workflow?.nodes),
2851 |       nodesCount: Array.isArray(workflow?.nodes) ? workflow.nodes.length : 0,
2852 |       hasConnections: workflow?.connections !== undefined,
2853 |       connectionsType: workflow?.connections ? typeof workflow.connections : 'undefined',
2854 |       options: options
2855 |     });
2856 |     
2857 |     // Help n8n AI agents with common mistakes
2858 |     if (!workflow || typeof workflow !== 'object') {
2859 |       return {
2860 |         valid: false,
2861 |         errors: [{
2862 |           node: 'workflow',
2863 |           message: 'Workflow must be an object with nodes and connections',
2864 |           details: 'Expected format: ' + getWorkflowExampleString()
2865 |         }],
2866 |         summary: { errorCount: 1 }
2867 |       };
2868 |     }
2869 |     
2870 |     if (!workflow.nodes || !Array.isArray(workflow.nodes)) {
2871 |       return {
2872 |         valid: false,
2873 |         errors: [{
2874 |           node: 'workflow',
2875 |           message: 'Workflow must have a nodes array',
2876 |           details: 'Expected: workflow.nodes = [array of node objects]. ' + getWorkflowExampleString()
2877 |         }],
2878 |         summary: { errorCount: 1 }
2879 |       };
2880 |     }
2881 |     
2882 |     if (!workflow.connections || typeof workflow.connections !== 'object') {
2883 |       return {
2884 |         valid: false,
2885 |         errors: [{
2886 |           node: 'workflow',
2887 |           message: 'Workflow must have a connections object',
2888 |           details: 'Expected: workflow.connections = {} (can be empty object). ' + getWorkflowExampleString()
2889 |         }],
2890 |         summary: { errorCount: 1 }
2891 |       };
2892 |     }
2893 |     
2894 |     // Create workflow validator instance
2895 |     const validator = new WorkflowValidator(
2896 |       this.repository,
2897 |       EnhancedConfigValidator
2898 |     );
2899 |     
2900 |     try {
2901 |       const result = await validator.validateWorkflow(workflow, options);
2902 |       
2903 |       // Format the response for better readability
2904 |       const response: any = {
2905 |         valid: result.valid,
2906 |         summary: {
2907 |           totalNodes: result.statistics.totalNodes,
2908 |           enabledNodes: result.statistics.enabledNodes,
2909 |           triggerNodes: result.statistics.triggerNodes,
2910 |           validConnections: result.statistics.validConnections,
2911 |           invalidConnections: result.statistics.invalidConnections,
2912 |           expressionsValidated: result.statistics.expressionsValidated,
2913 |           errorCount: result.errors.length,
2914 |           warningCount: result.warnings.length
2915 |         },
2916 |         // Always include errors and warnings arrays for consistent API response
2917 |         errors: result.errors.map(e => ({
2918 |           node: e.nodeName || 'workflow',
2919 |           message: e.message,
2920 |           details: e.details
2921 |         })),
2922 |         warnings: result.warnings.map(w => ({
2923 |           node: w.nodeName || 'workflow',
2924 |           message: w.message,
2925 |           details: w.details
2926 |         }))
2927 |       };
2928 |       
2929 |       if (result.suggestions.length > 0) {
2930 |         response.suggestions = result.suggestions;
2931 |       }
2932 | 
2933 |       // Track validation details in telemetry
2934 |       if (!result.valid && result.errors.length > 0) {
2935 |         // Track each validation error for analysis
2936 |         result.errors.forEach(error => {
2937 |           telemetry.trackValidationDetails(
2938 |             error.nodeName || 'workflow',
2939 |             error.type || 'validation_error',
2940 |             {
2941 |               message: error.message,
2942 |               nodeCount: workflow.nodes?.length ?? 0,
2943 |               hasConnections: Object.keys(workflow.connections || {}).length > 0
2944 |             }
2945 |           );
2946 |         });
2947 |       }
2948 | 
2949 |       // Track successfully validated workflows in telemetry
2950 |       if (result.valid) {
2951 |         telemetry.trackWorkflowCreation(workflow, true);
2952 |       }
2953 | 
2954 |       return response;
2955 |     } catch (error) {
2956 |       logger.error('Error validating workflow:', error);
2957 |       return {
2958 |         valid: false,
2959 |         error: error instanceof Error ? error.message : 'Unknown error validating workflow',
2960 |         tip: 'Ensure the workflow JSON includes nodes array and connections object'
2961 |       };
2962 |     }
2963 |   }
2964 | 
2965 |   private async validateWorkflowConnections(workflow: any): Promise<any> {
2966 |     await this.ensureInitialized();
2967 |     if (!this.repository) throw new Error('Repository not initialized');
2968 |     
2969 |     // Create workflow validator instance
2970 |     const validator = new WorkflowValidator(
2971 |       this.repository,
2972 |       EnhancedConfigValidator
2973 |     );
2974 |     
2975 |     try {
2976 |       // Validate only connections
2977 |       const result = await validator.validateWorkflow(workflow, {
2978 |         validateNodes: false,
2979 |         validateConnections: true,
2980 |         validateExpressions: false
2981 |       });
2982 |       
2983 |       const response: any = {
2984 |         valid: result.errors.length === 0,
2985 |         statistics: {
2986 |           totalNodes: result.statistics.totalNodes,
2987 |           triggerNodes: result.statistics.triggerNodes,
2988 |           validConnections: result.statistics.validConnections,
2989 |           invalidConnections: result.statistics.invalidConnections
2990 |         }
2991 |       };
2992 |       
2993 |       // Filter to only connection-related issues
2994 |       const connectionErrors = result.errors.filter(e => 
2995 |         e.message.includes('connection') || 
2996 |         e.message.includes('cycle') ||
2997 |         e.message.includes('orphaned')
2998 |       );
2999 |       
3000 |       const connectionWarnings = result.warnings.filter(w => 
3001 |         w.message.includes('connection') || 
3002 |         w.message.includes('orphaned') ||
3003 |         w.message.includes('trigger')
3004 |       );
3005 |       
3006 |       if (connectionErrors.length > 0) {
3007 |         response.errors = connectionErrors.map(e => ({
3008 |           node: e.nodeName || 'workflow',
3009 |           message: e.message
3010 |         }));
3011 |       }
3012 |       
3013 |       if (connectionWarnings.length > 0) {
3014 |         response.warnings = connectionWarnings.map(w => ({
3015 |           node: w.nodeName || 'workflow',
3016 |           message: w.message
3017 |         }));
3018 |       }
3019 |       
3020 |       return response;
3021 |     } catch (error) {
3022 |       logger.error('Error validating workflow connections:', error);
3023 |       return {
3024 |         valid: false,
3025 |         error: error instanceof Error ? error.message : 'Unknown error validating connections'
3026 |       };
3027 |     }
3028 |   }
3029 | 
3030 |   private async validateWorkflowExpressions(workflow: any): Promise<any> {
3031 |     await this.ensureInitialized();
3032 |     if (!this.repository) throw new Error('Repository not initialized');
3033 |     
3034 |     // Create workflow validator instance
3035 |     const validator = new WorkflowValidator(
3036 |       this.repository,
3037 |       EnhancedConfigValidator
3038 |     );
3039 |     
3040 |     try {
3041 |       // Validate only expressions
3042 |       const result = await validator.validateWorkflow(workflow, {
3043 |         validateNodes: false,
3044 |         validateConnections: false,
3045 |         validateExpressions: true
3046 |       });
3047 |       
3048 |       const response: any = {
3049 |         valid: result.errors.length === 0,
3050 |         statistics: {
3051 |           totalNodes: result.statistics.totalNodes,
3052 |           expressionsValidated: result.statistics.expressionsValidated
3053 |         }
3054 |       };
3055 |       
3056 |       // Filter to only expression-related issues
3057 |       const expressionErrors = result.errors.filter(e => 
3058 |         e.message.includes('Expression') || 
3059 |         e.message.includes('$') ||
3060 |         e.message.includes('{{')
3061 |       );
3062 |       
3063 |       const expressionWarnings = result.warnings.filter(w => 
3064 |         w.message.includes('Expression') || 
3065 |         w.message.includes('$') ||
3066 |         w.message.includes('{{')
3067 |       );
3068 |       
3069 |       if (expressionErrors.length > 0) {
3070 |         response.errors = expressionErrors.map(e => ({
3071 |           node: e.nodeName || 'workflow',
3072 |           message: e.message
3073 |         }));
3074 |       }
3075 |       
3076 |       if (expressionWarnings.length > 0) {
3077 |         response.warnings = expressionWarnings.map(w => ({
3078 |           node: w.nodeName || 'workflow',
3079 |           message: w.message
3080 |         }));
3081 |       }
3082 |       
3083 |       // Add tips for common expression issues
3084 |       if (expressionErrors.length > 0 || expressionWarnings.length > 0) {
3085 |         response.tips = [
3086 |           'Use {{ }} to wrap expressions',
3087 |           'Reference data with $json.propertyName',
3088 |           'Reference other nodes with $node["Node Name"].json',
3089 |           'Use $input.item for input data in loops'
3090 |         ];
3091 |       }
3092 |       
3093 |       return response;
3094 |     } catch (error) {
3095 |       logger.error('Error validating workflow expressions:', error);
3096 |       return {
3097 |         valid: false,
3098 |         error: error instanceof Error ? error.message : 'Unknown error validating expressions'
3099 |       };
3100 |     }
3101 |   }
3102 | 
3103 |   async run(): Promise<void> {
3104 |     // Ensure database is initialized before starting server
3105 |     await this.ensureInitialized();
3106 |     
3107 |     const transport = new StdioServerTransport();
3108 |     await this.server.connect(transport);
3109 |     
3110 |     // Force flush stdout for Docker environments
3111 |     // Docker uses block buffering which can delay MCP responses
3112 |     if (!process.stdout.isTTY || process.env.IS_DOCKER) {
3113 |       // Override write to auto-flush
3114 |       const originalWrite = process.stdout.write.bind(process.stdout);
3115 |       process.stdout.write = function(chunk: any, encoding?: any, callback?: any) {
3116 |         const result = originalWrite(chunk, encoding, callback);
3117 |         // Force immediate flush
3118 |         process.stdout.emit('drain');
3119 |         return result;
3120 |       };
3121 |     }
3122 |     
3123 |     logger.info('n8n Documentation MCP Server running on stdio transport');
3124 |     
3125 |     // Keep the process alive and listening
3126 |     process.stdin.resume();
3127 |   }
3128 |   
3129 |   async shutdown(): Promise<void> {
3130 |     logger.info('Shutting down MCP server...');
3131 |     
3132 |     // Clean up cache timers to prevent memory leaks
3133 |     if (this.cache) {
3134 |       try {
3135 |         this.cache.destroy();
3136 |         logger.info('Cache timers cleaned up');
3137 |       } catch (error) {
3138 |         logger.error('Error cleaning up cache:', error);
3139 |       }
3140 |     }
3141 |     
3142 |     // Close database connection if it exists
3143 |     if (this.db) {
3144 |       try {
3145 |         await this.db.close();
3146 |         logger.info('Database connection closed');
3147 |       } catch (error) {
3148 |         logger.error('Error closing database:', error);
3149 |       }
3150 |     }
3151 |   }
3152 | }
```
Page 54/59FirstPrevNextLast