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