#
tokens: 49565/50000 17/614 files (page 11/45)
lines: off (toggle) GitHub
raw markdown copy
This is page 11 of 45. Use http://codebase.md/czlonkowski/n8n-mcp?lines=false&page={x} to view the full context.

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/tests/integration/msw-setup.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
import { mswTestServer, n8nApiMock, testDataBuilders, integrationTestServer } from './setup/msw-test-server';
import { http, HttpResponse } from 'msw';
import axios from 'axios';
import { server } from './setup/integration-setup';

describe('MSW Setup Verification', () => {
  const baseUrl = 'http://localhost:5678';

  describe('Global MSW Setup', () => {
    it('should intercept n8n API requests with default handlers', async () => {
      // This uses the global MSW setup from vitest.config.ts
      const response = await axios.get(`${baseUrl}/api/v1/health`);
      
      expect(response.status).toBe(200);
      expect(response.data).toEqual({
        status: 'ok',
        version: '1.103.2',
        features: {
          workflows: true,
          executions: true,
          credentials: true,
          webhooks: true,
        }
      });
    });

    it('should allow custom handlers for specific tests', async () => {
      // Add a custom handler just for this test using the global server
      server.use(
        http.get('*/api/v1/custom-endpoint', () => {
          return HttpResponse.json({ custom: true });
        })
      );

      const response = await axios.get(`${baseUrl}/api/v1/custom-endpoint`);
      
      expect(response.status).toBe(200);
      expect(response.data).toEqual({ custom: true });
    });

    it('should return mock workflows', async () => {
      const response = await axios.get(`${baseUrl}/api/v1/workflows`);
      
      expect(response.status).toBe(200);
      expect(response.data).toHaveProperty('data');
      expect(Array.isArray(response.data.data)).toBe(true);
      expect(response.data.data.length).toBeGreaterThan(0);
    });
  });

  describe('Integration Test Server', () => {
    // Use the global MSW server instance for these tests
    afterEach(() => {
      // Reset handlers after each test to ensure clean state
      server.resetHandlers();
    });

    it('should handle workflow creation with custom response', async () => {
      // Use the global server instance to add custom handler
      server.use(
        http.post('*/api/v1/workflows', async ({ request }) => {
          const body = await request.json() as any;
          return HttpResponse.json({
            data: {
              id: 'custom-workflow-123',
              name: 'Test Workflow from MSW',
              active: body.active || false,
              nodes: body.nodes,
              connections: body.connections,
              settings: body.settings || {},
              tags: body.tags || [],
              createdAt: new Date().toISOString(),
              updatedAt: new Date().toISOString(),
              versionId: '1'
            }
          }, { status: 201 });
        })
      );

      const workflowData = testDataBuilders.workflow({
        name: 'My Test Workflow'
      });

      const response = await axios.post(`${baseUrl}/api/v1/workflows`, workflowData);
      
      expect(response.status).toBe(201);
      expect(response.data.data).toMatchObject({
        id: 'custom-workflow-123',
        name: 'Test Workflow from MSW',
        nodes: workflowData.nodes,
        connections: workflowData.connections
      });
    });

    it('should handle error responses', async () => {
      server.use(
        http.get('*/api/v1/workflows/missing', () => {
          return HttpResponse.json(
            {
              message: 'Workflow not found',
              code: 'NOT_FOUND',
              timestamp: new Date().toISOString()
            },
            { status: 404 }
          );
        })
      );

      try {
        await axios.get(`${baseUrl}/api/v1/workflows/missing`);
        expect.fail('Should have thrown an error');
      } catch (error: any) {
        expect(error.response.status).toBe(404);
        expect(error.response.data).toEqual({
          message: 'Workflow not found',
          code: 'NOT_FOUND',
          timestamp: expect.any(String)
        });
      }
    });

    it('should simulate rate limiting', async () => {
      let requestCount = 0;
      const limit = 5;
      
      server.use(
        http.get('*/api/v1/rate-limited', () => {
          requestCount++;
          
          if (requestCount > limit) {
            return HttpResponse.json(
              {
                message: 'Rate limit exceeded',
                code: 'RATE_LIMIT',
                retryAfter: 60
              },
              {
                status: 429,
                headers: {
                  'X-RateLimit-Limit': String(limit),
                  'X-RateLimit-Remaining': '0',
                  'X-RateLimit-Reset': String(Date.now() + 60000)
                }
              }
            );
          }
          
          return HttpResponse.json({ success: true });
        })
      );

      // Make requests up to the limit
      for (let i = 0; i < 5; i++) {
        const response = await axios.get(`${baseUrl}/api/v1/rate-limited`);
        expect(response.status).toBe(200);
      }

      // Next request should be rate limited
      try {
        await axios.get(`${baseUrl}/api/v1/rate-limited`);
        expect.fail('Should have been rate limited');
      } catch (error: any) {
        expect(error.response.status).toBe(429);
        expect(error.response.data.code).toBe('RATE_LIMIT');
        expect(error.response.headers['x-ratelimit-remaining']).toBe('0');
      }
    });

    it('should handle webhook execution', async () => {
      server.use(
        http.post('*/webhook/test-webhook', async ({ request }) => {
          const body = await request.json();
          
          return HttpResponse.json({
            processed: true,
            result: 'success',
            webhookReceived: {
              path: 'test-webhook',
              method: 'POST',
              body,
              timestamp: new Date().toISOString()
            }
          });
        })
      );

      const webhookData = { message: 'Test webhook payload' };
      const response = await axios.post(`${baseUrl}/webhook/test-webhook`, webhookData);
      
      expect(response.status).toBe(200);
      expect(response.data).toMatchObject({
        processed: true,
        result: 'success',
        webhookReceived: {
          path: 'test-webhook',
          method: 'POST',
          body: webhookData,
          timestamp: expect.any(String)
        }
      });
    });

    it('should wait for specific requests', async () => {
      // Since the global server is already handling these endpoints,
      // we'll just make the requests and verify they succeed
      const responses = await Promise.all([
        axios.get(`${baseUrl}/api/v1/workflows`),
        axios.get(`${baseUrl}/api/v1/executions`)
      ]);

      expect(responses).toHaveLength(2);
      expect(responses[0].status).toBe(200);
      expect(responses[0].config.url).toContain('/api/v1/workflows');
      expect(responses[1].status).toBe(200);
      expect(responses[1].config.url).toContain('/api/v1/executions');
    }, { timeout: 10000 }); // Increase timeout for this specific test

    it('should work with scoped handlers', async () => {
      // First add the scoped handler
      server.use(
        http.get('*/api/v1/scoped', () => {
          return HttpResponse.json({ scoped: true });
        })
      );
      
      // Make the request while handler is active
      const response = await axios.get(`${baseUrl}/api/v1/scoped`);
      expect(response.data).toEqual({ scoped: true });
      
      // Reset handlers to remove the scoped handler
      server.resetHandlers();
      
      // Verify the scoped handler is no longer active
      // Since there's no handler for this endpoint now, it should fall through to the catch-all
      try {
        await axios.get(`${baseUrl}/api/v1/scoped`);
        expect.fail('Should have returned 501');
      } catch (error: any) {
        expect(error.response.status).toBe(501);
      }
    });
  });

  describe('Factory Functions', () => {
    it('should create workflows using factory', async () => {
      const { workflowFactory } = await import('../mocks/n8n-api/data/workflows');
      
      const simpleWorkflow = workflowFactory.simple('n8n-nodes-base.slack', {
        resource: 'message',
        operation: 'post',
        channel: '#general',
        text: 'Hello from test'
      });

      expect(simpleWorkflow).toMatchObject({
        id: expect.stringMatching(/^workflow_\d+$/),
        name: 'Test n8n-nodes-base.slack Workflow', // Factory uses nodeType in the name
        active: true,
        nodes: expect.arrayContaining([
          expect.objectContaining({ type: 'n8n-nodes-base.start' }),
          expect.objectContaining({ 
            type: 'n8n-nodes-base.slack',
            parameters: {
              resource: 'message',
              operation: 'post',
              channel: '#general',
              text: 'Hello from test'
            }
          })
        ])
      });
    });

    it('should create executions using factory', async () => {
      const { executionFactory } = await import('../mocks/n8n-api/data/executions');
      
      const successExecution = executionFactory.success('workflow_123');
      const errorExecution = executionFactory.error('workflow_456', {
        message: 'Connection timeout',
        node: 'http_request_1'
      });

      expect(successExecution).toMatchObject({
        workflowId: 'workflow_123',
        status: 'success',
        mode: 'manual'
      });

      expect(errorExecution).toMatchObject({
        workflowId: 'workflow_456',
        status: 'error',
        error: {
          message: 'Connection timeout',
          node: 'http_request_1'
        }
      });
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/integration/ai-validation/chat-trigger-validation.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Integration Tests: Chat Trigger Validation
 *
 * Tests Chat Trigger validation against real n8n instance.
 */

import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest';
import { createTestContext, TestContext, createTestWorkflowName } from '../n8n-api/utils/test-context';
import { getTestN8nClient } from '../n8n-api/utils/n8n-client';
import { N8nApiClient } from '../../../src/services/n8n-api-client';
import { cleanupOrphanedWorkflows } from '../n8n-api/utils/cleanup-helpers';
import { createMcpContext } from '../n8n-api/utils/mcp-context';
import { InstanceContext } from '../../../src/types/instance-context';
import { handleValidateWorkflow } from '../../../src/mcp/handlers-n8n-manager';
import { getNodeRepository, closeNodeRepository } from '../n8n-api/utils/node-repository';
import { NodeRepository } from '../../../src/database/node-repository';
import { ValidationResponse } from '../n8n-api/types/mcp-responses';
import {
  createChatTriggerNode,
  createAIAgentNode,
  createLanguageModelNode,
  createRespondNode,
  createAIConnection,
  createMainConnection,
  mergeConnections,
  createAIWorkflow
} from './helpers';
import { WorkflowNode } from '../../../src/types/n8n-api';

describe('Integration: Chat Trigger Validation', () => {
  let context: TestContext;
  let client: N8nApiClient;
  let mcpContext: InstanceContext;
  let repository: NodeRepository;

  beforeEach(async () => {
    context = createTestContext();
    client = getTestN8nClient();
    mcpContext = createMcpContext();
    repository = await getNodeRepository();
  });

  afterEach(async () => {
    await context.cleanup();
  });

  afterAll(async () => {
    await closeNodeRepository();
    if (!process.env.CI) {
      await cleanupOrphanedWorkflows();
    }
  });

  // ======================================================================
  // TEST 1: Streaming to Non-AI-Agent
  // ======================================================================

  it('should detect streaming to non-AI-Agent', async () => {
    const chatTrigger = createChatTriggerNode({
      name: 'Chat Trigger',
      responseMode: 'streaming'
    });

    // Regular node (not AI Agent)
    const regularNode: WorkflowNode = {
      id: 'set-1',
      name: 'Set',
      type: 'n8n-nodes-base.set',
      typeVersion: 3.4,
      position: [450, 300],
      parameters: {
        assignments: {
          assignments: []
        }
      }
    };

    const workflow = createAIWorkflow(
      [chatTrigger, regularNode],
      createMainConnection('Chat Trigger', 'Set'),
      {
        name: createTestWorkflowName('Chat Trigger - Wrong Target'),
        tags: ['mcp-integration-test', 'ai-validation']
      }
    );

    const created = await client.createWorkflow(workflow);
    context.trackWorkflow(created.id!);

    const response = await handleValidateWorkflow(
      { id: created.id },
      repository,
      mcpContext
    );

    expect(response.success).toBe(true);
    const data = response.data as ValidationResponse;

    expect(data.valid).toBe(false);
    expect(data.errors).toBeDefined();

    const errorCodes = data.errors!.map(e => e.details?.code || e.code);
    expect(errorCodes).toContain('STREAMING_WRONG_TARGET');

    const errorMessages = data.errors!.map(e => e.message).join(' ');
    expect(errorMessages).toMatch(/streaming.*AI Agent/i);
  });

  // ======================================================================
  // TEST 2: Missing Connections
  // ======================================================================

  it('should detect missing connections', async () => {
    const chatTrigger = createChatTriggerNode({
      name: 'Chat Trigger'
    });

    const workflow = createAIWorkflow(
      [chatTrigger],
      {}, // No connections
      {
        name: createTestWorkflowName('Chat Trigger - No Connections'),
        tags: ['mcp-integration-test', 'ai-validation']
      }
    );

    const created = await client.createWorkflow(workflow);
    context.trackWorkflow(created.id!);

    const response = await handleValidateWorkflow(
      { id: created.id },
      repository,
      mcpContext
    );

    expect(response.success).toBe(true);
    const data = response.data as ValidationResponse;

    expect(data.valid).toBe(false);
    expect(data.errors).toBeDefined();

    const errorCodes = data.errors!.map(e => e.details?.code || e.code);
    expect(errorCodes).toContain('MISSING_CONNECTIONS');
  });

  // ======================================================================
  // TEST 3: Valid Streaming Setup
  // ======================================================================

  it('should validate valid streaming setup', async () => {
    const chatTrigger = createChatTriggerNode({
      name: 'Chat Trigger',
      responseMode: 'streaming'
    });

    const languageModel = createLanguageModelNode('openai', {
      name: 'OpenAI Chat Model'
    });

    const agent = createAIAgentNode({
      name: 'AI Agent',
      text: 'You are a helpful assistant'
      // No main output connections - streaming mode
    });

    const workflow = createAIWorkflow(
      [chatTrigger, languageModel, agent],
      mergeConnections(
        createMainConnection('Chat Trigger', 'AI Agent'),
        createAIConnection('OpenAI Chat Model', 'AI Agent', 'ai_languageModel')
        // NO main output from AI Agent
      ),
      {
        name: createTestWorkflowName('Chat Trigger - Valid Streaming'),
        tags: ['mcp-integration-test', 'ai-validation']
      }
    );

    const created = await client.createWorkflow(workflow);
    context.trackWorkflow(created.id!);

    const response = await handleValidateWorkflow(
      { id: created.id },
      repository,
      mcpContext
    );

    expect(response.success).toBe(true);
    const data = response.data as ValidationResponse;

    expect(data.valid).toBe(true);
    expect(data.errors).toBeUndefined();
    expect(data.summary.errorCount).toBe(0);
  });

  // ======================================================================
  // TEST 4: LastNode Mode (Default)
  // ======================================================================

  it('should validate lastNode mode with AI Agent', async () => {
    const chatTrigger = createChatTriggerNode({
      name: 'Chat Trigger',
      responseMode: 'lastNode'
    });

    const languageModel = createLanguageModelNode('openai', {
      name: 'OpenAI Chat Model'
    });

    const agent = createAIAgentNode({
      name: 'AI Agent',
      text: 'You are a helpful assistant'
    });

    const respond = createRespondNode({
      name: 'Respond to Webhook'
    });

    const workflow = createAIWorkflow(
      [chatTrigger, languageModel, agent, respond],
      mergeConnections(
        createMainConnection('Chat Trigger', 'AI Agent'),
        createAIConnection('OpenAI Chat Model', 'AI Agent', 'ai_languageModel'),
        createMainConnection('AI Agent', 'Respond to Webhook')
      ),
      {
        name: createTestWorkflowName('Chat Trigger - LastNode Mode'),
        tags: ['mcp-integration-test', 'ai-validation']
      }
    );

    const created = await client.createWorkflow(workflow);
    context.trackWorkflow(created.id!);

    const response = await handleValidateWorkflow(
      { id: created.id },
      repository,
      mcpContext
    );

    expect(response.success).toBe(true);
    const data = response.data as ValidationResponse;

    // Should be valid (lastNode mode allows main output)
    expect(data.valid).toBe(true);

    // May have info suggestion about using streaming
    if (data.info) {
      const streamingSuggestion = data.info.find((i: any) =>
        i.message.toLowerCase().includes('streaming')
      );
      // This is optional - just checking the suggestion exists if present
      if (streamingSuggestion) {
        expect(streamingSuggestion.severity).toBe('info');
      }
    }
  });

  // ======================================================================
  // TEST 5: Streaming Agent with Output Connection (Error)
  // ======================================================================

  it('should detect streaming agent with output connection', async () => {
    const chatTrigger = createChatTriggerNode({
      name: 'Chat Trigger',
      responseMode: 'streaming'
    });

    const languageModel = createLanguageModelNode('openai', {
      name: 'OpenAI Chat Model'
    });

    const agent = createAIAgentNode({
      name: 'AI Agent',
      text: 'You are a helpful assistant'
    });

    const respond = createRespondNode({
      name: 'Respond to Webhook'
    });

    const workflow = createAIWorkflow(
      [chatTrigger, languageModel, agent, respond],
      mergeConnections(
        createMainConnection('Chat Trigger', 'AI Agent'),
        createAIConnection('OpenAI Chat Model', 'AI Agent', 'ai_languageModel'),
        createMainConnection('AI Agent', 'Respond to Webhook') // ERROR in streaming mode
      ),
      {
        name: createTestWorkflowName('Chat Trigger - Streaming With Output'),
        tags: ['mcp-integration-test', 'ai-validation']
      }
    );

    const created = await client.createWorkflow(workflow);
    context.trackWorkflow(created.id!);

    const response = await handleValidateWorkflow(
      { id: created.id },
      repository,
      mcpContext
    );

    expect(response.success).toBe(true);
    const data = response.data as ValidationResponse;

    expect(data.valid).toBe(false);
    expect(data.errors).toBeDefined();

    // Should detect streaming agent has output
    const streamingErrors = data.errors!.filter(e => {
      const code = e.details?.code || e.code;
      return code === 'STREAMING_AGENT_HAS_OUTPUT' ||
             e.message.toLowerCase().includes('streaming');
    });
    expect(streamingErrors.length).toBeGreaterThan(0);
  });
});

```

--------------------------------------------------------------------------------
/tests/unit/flexible-instance-security.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Unit tests for flexible instance configuration security improvements
 */

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { InstanceContext, isInstanceContext, validateInstanceContext } from '../../src/types/instance-context';
import { getN8nApiClient } from '../../src/mcp/handlers-n8n-manager';
import { createHash } from 'crypto';

describe('Flexible Instance Security', () => {
  beforeEach(() => {
    // Clear module cache to reset singleton state
    vi.resetModules();
  });

  afterEach(() => {
    vi.clearAllMocks();
  });

  describe('Input Validation', () => {
    describe('URL Validation', () => {
      it('should accept valid HTTP and HTTPS URLs', () => {
        const validContext: InstanceContext = {
          n8nApiUrl: 'https://api.n8n.cloud',
          n8nApiKey: 'valid-key'
        };
        expect(isInstanceContext(validContext)).toBe(true);

        const httpContext: InstanceContext = {
          n8nApiUrl: 'http://localhost:5678',
          n8nApiKey: 'valid-key'
        };
        expect(isInstanceContext(httpContext)).toBe(true);
      });

      it('should reject invalid URL formats', () => {
        const invalidUrls = [
          'not-a-url',
          'ftp://invalid-protocol.com',
          'javascript:alert(1)',
          '//missing-protocol.com',
          'https://',
          ''
        ];

        invalidUrls.forEach(url => {
          const context = {
            n8nApiUrl: url,
            n8nApiKey: 'key'
          };
          const validation = validateInstanceContext(context);
          expect(validation.valid).toBe(false);
          expect(validation.errors?.some(error => error.startsWith('Invalid n8nApiUrl:'))).toBe(true);
        });
      });
    });

    describe('API Key Validation', () => {
      it('should accept valid API keys', () => {
        const validKeys = [
          'abc123def456',
          'sk_live_abcdefghijklmnop',
          'token_1234567890',
          'a'.repeat(100) // Long key
        ];

        validKeys.forEach(key => {
          const context: InstanceContext = {
            n8nApiUrl: 'https://api.n8n.cloud',
            n8nApiKey: key
          };
          const validation = validateInstanceContext(context);
          expect(validation.valid).toBe(true);
        });
      });

      it('should reject placeholder or invalid API keys', () => {
        const invalidKeys = [
          'YOUR_API_KEY',
          'placeholder',
          'example',
          'YOUR_API_KEY_HERE',
          'example-key',
          'placeholder-token'
        ];

        invalidKeys.forEach(key => {
          const context: InstanceContext = {
            n8nApiUrl: 'https://api.n8n.cloud',
            n8nApiKey: key
          };
          const validation = validateInstanceContext(context);
          expect(validation.valid).toBe(false);
          expect(validation.errors?.some(error => error.startsWith('Invalid n8nApiKey:'))).toBe(true);
        });
      });
    });

    describe('Timeout and Retry Validation', () => {
      it('should validate timeout values', () => {
        const invalidTimeouts = [0, -1, -1000];

        invalidTimeouts.forEach(timeout => {
          const context: InstanceContext = {
            n8nApiUrl: 'https://api.n8n.cloud',
            n8nApiKey: 'key',
            n8nApiTimeout: timeout
          };
          const validation = validateInstanceContext(context);
          expect(validation.valid).toBe(false);
          expect(validation.errors?.some(error => error.includes('Must be positive (greater than 0)'))).toBe(true);
        });

        // NaN and Infinity are handled differently
        const nanContext: InstanceContext = {
          n8nApiUrl: 'https://api.n8n.cloud',
          n8nApiKey: 'key',
          n8nApiTimeout: NaN
        };
        const nanValidation = validateInstanceContext(nanContext);
        expect(nanValidation.valid).toBe(false);

        // Valid timeout
        const validContext: InstanceContext = {
          n8nApiUrl: 'https://api.n8n.cloud',
          n8nApiKey: 'key',
          n8nApiTimeout: 30000
        };
        const validation = validateInstanceContext(validContext);
        expect(validation.valid).toBe(true);
      });

      it('should validate retry values', () => {
        const invalidRetries = [-1, -10];

        invalidRetries.forEach(retries => {
          const context: InstanceContext = {
            n8nApiUrl: 'https://api.n8n.cloud',
            n8nApiKey: 'key',
            n8nApiMaxRetries: retries
          };
          const validation = validateInstanceContext(context);
          expect(validation.valid).toBe(false);
          expect(validation.errors?.some(error => error.includes('Must be non-negative (0 or greater)'))).toBe(true);
        });

        // Valid retries (including 0)
        [0, 1, 3, 10].forEach(retries => {
          const context: InstanceContext = {
            n8nApiUrl: 'https://api.n8n.cloud',
            n8nApiKey: 'key',
            n8nApiMaxRetries: retries
          };
          const validation = validateInstanceContext(context);
          expect(validation.valid).toBe(true);
        });
      });
    });
  });

  describe('Cache Key Security', () => {
    it('should hash cache keys instead of using raw credentials', () => {
      const context: InstanceContext = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: 'super-secret-key',
        instanceId: 'instance-1'
      };

      // Calculate expected hash
      const expectedHash = createHash('sha256')
        .update(`${context.n8nApiUrl}:${context.n8nApiKey}:${context.instanceId}`)
        .digest('hex');

      // The actual cache key should be hashed, not contain raw values
      // We can't directly test the internal cache key, but we can verify
      // that the function doesn't throw and returns a client
      const client = getN8nApiClient(context);

      // If validation passes, client could be created (or null if no env vars)
      // The important part is that raw credentials aren't exposed
      expect(() => getN8nApiClient(context)).not.toThrow();
    });

    it('should not expose API keys in any form', () => {
      const sensitiveKey = 'super-secret-api-key-12345';
      const context: InstanceContext = {
        n8nApiUrl: 'https://api.n8n.cloud',
        n8nApiKey: sensitiveKey,
        instanceId: 'test'
      };

      // Mock console methods to capture any output
      const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
      const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
      const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

      getN8nApiClient(context);

      // Verify the sensitive key is never logged
      const allLogs = [
        ...consoleSpy.mock.calls,
        ...consoleWarnSpy.mock.calls,
        ...consoleErrorSpy.mock.calls
      ].flat().join(' ');

      expect(allLogs).not.toContain(sensitiveKey);

      consoleSpy.mockRestore();
      consoleWarnSpy.mockRestore();
      consoleErrorSpy.mockRestore();
    });
  });

  describe('Error Message Sanitization', () => {
    it('should not expose sensitive data in error messages', () => {
      const context: InstanceContext = {
        n8nApiUrl: 'invalid-url',
        n8nApiKey: 'secret-key-that-should-not-appear',
        instanceId: 'test-instance'
      };

      const validation = validateInstanceContext(context);

      // Error messages should be generic, not include actual values
      expect(validation.errors).toBeDefined();
      expect(validation.errors!.join(' ')).not.toContain('secret-key');
      expect(validation.errors!.join(' ')).not.toContain(context.n8nApiKey);
    });
  });

  describe('Type Guard Security', () => {
    it('should safely handle malicious input', () => {
      // Test specific malicious inputs
      const objectAsUrl = { n8nApiUrl: { toString: () => { throw new Error('XSS'); } } };
      expect(() => isInstanceContext(objectAsUrl)).not.toThrow();
      expect(isInstanceContext(objectAsUrl)).toBe(false);

      const arrayAsKey = { n8nApiKey: ['array', 'instead', 'of', 'string'] };
      expect(() => isInstanceContext(arrayAsKey)).not.toThrow();
      expect(isInstanceContext(arrayAsKey)).toBe(false);

      // These are actually valid objects with extra properties
      const protoObj = { __proto__: { isAdmin: true } };
      expect(() => isInstanceContext(protoObj)).not.toThrow();
      // This is actually a valid object, just has __proto__ property
      expect(isInstanceContext(protoObj)).toBe(true);

      const constructorObj = { constructor: { name: 'Evil' } };
      expect(() => isInstanceContext(constructorObj)).not.toThrow();
      // This is also a valid object with constructor property
      expect(isInstanceContext(constructorObj)).toBe(true);

      // Object.create(null) creates an object without prototype
      const nullProto = Object.create(null);
      expect(() => isInstanceContext(nullProto)).not.toThrow();
      // This is actually a valid empty object, so it passes
      expect(isInstanceContext(nullProto)).toBe(true);
    });

    it('should handle circular references safely', () => {
      const circular: any = { n8nApiUrl: 'https://api.n8n.cloud' };
      circular.self = circular;

      expect(() => isInstanceContext(circular)).not.toThrow();
    });
  });

  describe('Memory Management', () => {
    it('should validate LRU cache configuration', () => {
      // This is more of a configuration test
      // In real implementation, we'd test that the cache has proper limits
      const MAX_CACHE_SIZE = 100;
      const TTL_MINUTES = 30;

      // Verify reasonable limits are in place
      expect(MAX_CACHE_SIZE).toBeLessThanOrEqual(1000); // Not too many
      expect(TTL_MINUTES).toBeLessThanOrEqual(60); // Not too long
    });
  });
});
```

--------------------------------------------------------------------------------
/src/mcp/tool-docs/workflow_management/n8n-get-execution.ts:
--------------------------------------------------------------------------------

```typescript
import { ToolDocumentation } from '../types';

export const n8nGetExecutionDoc: ToolDocumentation = {
  name: 'n8n_get_execution',
  category: 'workflow_management',
  essentials: {
    description: 'Get execution details with smart filtering to avoid token limits. Use preview mode first to assess data size, then fetch appropriately.',
    keyParameters: ['id', 'mode', 'itemsLimit', 'nodeNames'],
    example: `
// RECOMMENDED WORKFLOW:
// 1. Preview first
n8n_get_execution({id: "12345", mode: "preview"})
// Returns: structure, counts, size estimate, recommendation

// 2. Based on recommendation, fetch data:
n8n_get_execution({id: "12345", mode: "summary"}) // 2 items per node
n8n_get_execution({id: "12345", mode: "filtered", itemsLimit: 5}) // 5 items
n8n_get_execution({id: "12345", nodeNames: ["HTTP Request"]}) // Specific node
`,
    performance: 'Preview: <50ms, Summary: <200ms, Full: depends on data size',
    tips: [
      'ALWAYS use preview mode first for large datasets',
      'Preview shows structure + counts without consuming tokens for data',
      'Summary mode (2 items per node) is safe default',
      'Use nodeNames to focus on specific nodes only',
      'itemsLimit: 0 = structure only, -1 = unlimited',
      'Check recommendation.suggestedMode from preview'
    ]
  },
  full: {
    description: `Retrieves and intelligently filters execution data to enable inspection without exceeding token limits. This tool provides multiple modes for different use cases, from quick previews to complete data retrieval.

**The Problem**: Workflows processing large datasets (50+ database records) generate execution data that exceeds token/response limits, making traditional full-data fetching impossible.

**The Solution**: Four retrieval modes with smart filtering:
1. **Preview**: Structure + counts only (no actual data)
2. **Summary**: 2 sample items per node (safe default)
3. **Filtered**: Custom limits and node selection
4. **Full**: Complete data (use with caution)

**Recommended Workflow**:
1. Start with preview mode to assess size
2. Use recommendation to choose appropriate mode
3. Fetch filtered data as needed`,

    parameters: {
      id: {
        type: 'string',
        required: true,
        description: 'The execution ID to retrieve. Obtained from list_executions or webhook trigger responses'
      },
      mode: {
        type: 'string',
        required: false,
        description: `Retrieval mode (default: auto-detect from other params):
- 'preview': Structure, counts, size estimates - NO actual data (fastest)
- 'summary': Metadata + 2 sample items per node (safe default)
- 'filtered': Custom filtering with itemsLimit/nodeNames
- 'full': Complete execution data (use with caution)`
      },
      nodeNames: {
        type: 'array',
        required: false,
        description: 'Filter to specific nodes by name. Example: ["HTTP Request", "Filter"]. Useful when you only need to inspect specific nodes.'
      },
      itemsLimit: {
        type: 'number',
        required: false,
        description: `Items to return per node (default: 2):
- 0: Structure only (see data shape without values)
- 1-N: Return N items per node
- -1: Unlimited (return all items)

Note: Structure-only mode (0) shows JSON schema without actual values.`
      },
      includeInputData: {
        type: 'boolean',
        required: false,
        description: 'Include input data in addition to output data (default: false). Useful for debugging data transformations.'
      },
      includeData: {
        type: 'boolean',
        required: false,
        description: 'DEPRECATED: Legacy parameter. Use mode instead. If true, maps to mode="summary" for backward compatibility.'
      }
    },

    returns: `**Preview Mode Response**:
{
  mode: 'preview',
  preview: {
    totalNodes: number,
    executedNodes: number,
    estimatedSizeKB: number,
    nodes: {
      [nodeName]: {
        status: 'success' | 'error',
        itemCounts: { input: number, output: number },
        dataStructure: {...}, // JSON schema
        estimatedSizeKB: number
      }
    }
  },
  recommendation: {
    canFetchFull: boolean,
    suggestedMode: 'preview'|'summary'|'filtered'|'full',
    suggestedItemsLimit?: number,
    reason: string
  }
}

**Summary/Filtered/Full Mode Response**:
{
  mode: 'summary' | 'filtered' | 'full',
  summary: {
    totalNodes: number,
    executedNodes: number,
    totalItems: number,
    hasMoreData: boolean  // true if truncated
  },
  nodes: {
    [nodeName]: {
      executionTime: number,
      itemsInput: number,
      itemsOutput: number,
      status: 'success' | 'error',
      error?: string,
      data: {
        output: [...],  // Actual data items
        metadata: {
          totalItems: number,
          itemsShown: number,
          truncated: boolean
        }
      }
    }
  }
}`,

    examples: [
      `// Example 1: Preview workflow (RECOMMENDED FIRST STEP)
n8n_get_execution({id: "exec_123", mode: "preview"})
// Returns structure, counts, size, recommendation
// Use this to decide how to fetch data`,

      `// Example 2: Follow recommendation
const preview = n8n_get_execution({id: "exec_123", mode: "preview"});
if (preview.recommendation.canFetchFull) {
  n8n_get_execution({id: "exec_123", mode: "full"});
} else {
  n8n_get_execution({
    id: "exec_123",
    mode: "filtered",
    itemsLimit: preview.recommendation.suggestedItemsLimit
  });
}`,

      `// Example 3: Summary mode (safe default for unknown datasets)
n8n_get_execution({id: "exec_123", mode: "summary"})
// Gets 2 items per node - safe for most cases`,

      `// Example 4: Filter to specific node
n8n_get_execution({
  id: "exec_123",
  mode: "filtered",
  nodeNames: ["HTTP Request"],
  itemsLimit: 5
})
// Gets only HTTP Request node, 5 items`,

      `// Example 5: Structure only (see data shape)
n8n_get_execution({
  id: "exec_123",
  mode: "filtered",
  itemsLimit: 0
})
// Returns JSON schema without actual values`,

      `// Example 6: Debug with input data
n8n_get_execution({
  id: "exec_123",
  mode: "filtered",
  nodeNames: ["Transform"],
  itemsLimit: 2,
  includeInputData: true
})
// See both input and output for debugging`,

      `// Example 7: Backward compatibility (legacy)
n8n_get_execution({id: "exec_123"}) // Minimal data
n8n_get_execution({id: "exec_123", includeData: true}) // Maps to summary mode`
    ],

    useCases: [
      'Monitor status of triggered workflows',
      'Debug failed workflows by examining error messages and partial data',
      'Inspect large datasets without exceeding token limits',
      'Validate data transformations between nodes',
      'Understand execution flow and timing',
      'Track workflow performance metrics',
      'Verify successful completion before proceeding',
      'Extract specific data from execution results'
    ],

    performance: `**Response Times** (approximate):
- Preview mode: <50ms (no data, just structure)
- Summary mode: <200ms (2 items per node)
- Filtered mode: 50-500ms (depends on filters)
- Full mode: 200ms-5s (depends on data size)

**Token Consumption**:
- Preview: ~500 tokens (no data values)
- Summary (2 items): ~2-5K tokens
- Filtered (5 items): ~5-15K tokens
- Full (50+ items): 50K+ tokens (may exceed limits)

**Optimization Tips**:
- Use preview for all large datasets
- Use nodeNames to focus on relevant nodes only
- Start with small itemsLimit and increase if needed
- Use itemsLimit: 0 to see structure without data`,

    bestPractices: [
      'ALWAYS use preview mode first for unknown datasets',
      'Trust the recommendation.suggestedMode from preview',
      'Use nodeNames to filter to relevant nodes only',
      'Start with summary mode if preview indicates moderate size',
      'Use itemsLimit: 0 to understand data structure',
      'Check hasMoreData to know if results are truncated',
      'Store execution IDs from triggers for later inspection',
      'Use mode="filtered" with custom limits for large datasets',
      'Include input data only when debugging transformations',
      'Monitor summary.totalItems to understand dataset size'
    ],

    pitfalls: [
      'DON\'T fetch full mode without previewing first - may timeout',
      'DON\'T assume all data fits - always check hasMoreData',
      'DON\'T ignore the recommendation from preview mode',
      'Execution data is retained based on n8n settings - old executions may be purged',
      'Binary data (files, images) is not fully included - only metadata',
      'Status "waiting" indicates execution is still running',
      'Error executions may have partial data from successful nodes',
      'Very large individual items (>1MB) may be truncated',
      'Preview mode estimates may be off by 10-20% for complex structures',
      'Node names are case-sensitive in nodeNames filter'
    ],

    modeComparison: `**When to use each mode**:

**Preview**:
- ALWAYS use first for unknown datasets
- When you need to know if data is safe to fetch
- To see data structure without consuming tokens
- To get size estimates and recommendations

**Summary** (default):
- Safe default for most cases
- When you need representative samples
- When preview recommends it
- For quick data inspection

**Filtered**:
- When you need specific nodes only
- When you need more than 2 items but not all
- When preview recommends it with itemsLimit
- For targeted data extraction

**Full**:
- ONLY when preview says canFetchFull: true
- For small executions (< 20 items total)
- When you genuinely need all data
- When you're certain data fits in token limit`,

    relatedTools: [
      'n8n_list_executions - Find execution IDs',
      'n8n_trigger_webhook_workflow - Trigger and get execution ID',
      'n8n_delete_execution - Clean up old executions',
      'n8n_get_workflow - Get workflow structure',
      'validate_workflow - Validate before executing'
    ]
  }
};

```

--------------------------------------------------------------------------------
/docs/local/TEMPLATE_MINING_ANALYSIS.md:
--------------------------------------------------------------------------------

```markdown
# Template Mining Analysis - Alternative to P0-R3

**Date**: 2025-10-02
**Context**: Analyzing whether to fix `get_node_for_task` (28% failure rate) or replace it with template-based configuration extraction

## Executive Summary

**RECOMMENDATION**: Replace `get_node_for_task` with template-based configuration extraction. The template database contains 2,646 real-world workflows with rich node configurations that far exceed the 31 hardcoded task templates.

## Key Findings

### 1. Template Database Coverage

- **Total Templates**: 2,646 production workflows from n8n.io
- **Unique Node Types**: 543 (covers 103% of our 525 core nodes)
- **Metadata Coverage**: 100% (AI-generated structured metadata)

### 2. Node Type Coverage in Templates

Top node types by template usage:
```
3,820 templates: n8n-nodes-base.httpRequest      (144% of total templates!)
3,678 templates: n8n-nodes-base.set
2,445 templates: n8n-nodes-base.code
1,700 templates: n8n-nodes-base.googleSheets
1,471 templates: @n8n/n8n-nodes-langchain.agent
1,269 templates: @n8n/n8n-nodes-langchain.lmChatOpenAi
  792 templates: n8n-nodes-base.telegram
  702 templates: n8n-nodes-base.httpRequestTool
  596 templates: n8n-nodes-base.gmail
  466 templates: n8n-nodes-base.webhook
```

**Comparison**:
- Hardcoded task templates: 31 tasks covering 5.9% of nodes
- Real templates: 2,646 templates with 2-3k examples for common nodes

### 3. Database Structure

```sql
CREATE TABLE templates (
  id INTEGER PRIMARY KEY,
  workflow_id INTEGER UNIQUE NOT NULL,
  name TEXT NOT NULL,
  description TEXT,
  -- Node information
  nodes_used TEXT,              -- JSON array: ["n8n-nodes-base.httpRequest", ...]
  workflow_json_compressed TEXT, -- Base64 encoded gzip of full workflow
  -- Metadata (100% coverage)
  metadata_json TEXT,           -- AI-generated structured metadata
  -- Stats
  views INTEGER DEFAULT 0,
  created_at DATETIME,
  -- ...
);
```

### 4. Real Configuration Examples

#### HTTP Request Node Configurations

**Simple URL fetch**:
```json
{
  "url": "https://api.example.com/data",
  "options": {}
}
```

**With authentication**:
```json
{
  "url": "=https://api.wavespeed.ai/api/v3/predictions/{{ $json.data.id }}/result",
  "options": {},
  "authentication": "genericCredentialType",
  "genericAuthType": "httpHeaderAuth"
}
```

**Complex expressions**:
```json
{
  "url": "=https://image.pollinations.ai/prompt/{{$('Social Media Content Factory').item.json.output.description.replaceAll(' ','-').replaceAll(',','').replaceAll('.','') }}",
  "options": {}
}
```

#### Webhook Node Configurations

**Basic webhook**:
```json
{
  "path": "ytube",
  "options": {},
  "httpMethod": "POST",
  "responseMode": "responseNode"
}
```

**With binary data**:
```json
{
  "path": "your-endpoint",
  "options": {
    "binaryPropertyName": "data"
  },
  "httpMethod": "POST"
}
```

### 5. AI-Generated Metadata

Each template has structured metadata including:

```json
{
  "categories": ["automation", "integration", "data processing"],
  "complexity": "medium",
  "use_cases": [
    "Extract transaction data from Gmail",
    "Automate bookkeeping",
    "Expense tracking"
  ],
  "estimated_setup_minutes": 30,
  "required_services": ["Gmail", "Google Sheets", "Google Gemini"],
  "key_features": [
    "Fetch emails by label",
    "Extract transaction data",
    "Use LLM for structured output"
  ],
  "target_audience": ["Accountants", "Small business owners"]
}
```

## Comparison: Task Templates vs Real Templates

### Current Approach (get_node_for_task)

**Pros**:
- Curated configurations with best practices
- Predictable, stable responses
- Fast lookup (no decompression needed)

**Cons**:
- Only 31 tasks (5.9% node coverage)
- 28% failure rate (users can't find what they need)
- Requires manual maintenance
- Static configurations without real-world context
- Usage ratio 22.5:1 (search_nodes is preferred)

### Template-Based Approach

**Pros**:
- 2,646 real workflows with 2-3k examples for common nodes
- 100% metadata coverage for semantic matching
- Real-world patterns and best practices
- Covers 543 node types (103% coverage)
- Self-updating (templates fetched from n8n.io)
- Rich context (use cases, complexity, setup time)

**Cons**:
- Requires decompression for full workflow access
- May contain template-specific context (but can be filtered)
- Need ranking/filtering logic for best matches

## Proposed Implementation Strategy

### Phase 1: Extract Node Configurations from Templates

Create a new service: `TemplateConfigExtractor`

```typescript
interface ExtractedNodeConfig {
  nodeType: string;
  configuration: Record<string, any>;
  source: {
    templateId: number;
    templateName: string;
    templateViews: number;
    useCases: string[];
    complexity: 'simple' | 'medium' | 'complex';
  };
  patterns: {
    hasAuthentication: boolean;
    hasExpressions: boolean;
    hasOptionalFields: boolean;
  };
}

class TemplateConfigExtractor {
  async extractConfigsForNode(
    nodeType: string,
    options?: {
      complexity?: 'simple' | 'medium' | 'complex';
      requiresAuth?: boolean;
      limit?: number;
    }
  ): Promise<ExtractedNodeConfig[]> {
    // 1. Query templates containing nodeType
    // 2. Decompress workflow_json_compressed
    // 3. Extract node configurations
    // 4. Rank by popularity + complexity match
    // 5. Return top N configurations
  }
}
```

### Phase 2: Integrate with Existing Tools

**Option A**: Enhance `get_node_essentials`
- Add `includeExamples: boolean` parameter
- Return 2-3 real configurations from templates
- Preserve existing compact format

**Option B**: Enhance `get_node_info`
- Add `examples` section with template-sourced configs
- Include source attribution (template name, views)

**Option C**: New tool `get_node_examples`
- Dedicated tool for retrieving configuration examples
- Query by node type, complexity, use case
- Returns ranked list of real configurations

### Phase 3: Deprecate get_node_for_task

- Mark as deprecated in tool documentation
- Redirect to enhanced tools
- Remove after 2-3 version cycles

## Performance Considerations

### Decompression Cost

- Average compressed size: 6-12 KB
- Decompression time: ~5-10ms per template
- Caching strategy needed for frequently accessed templates

### Query Strategy

```sql
-- Fast: Get templates for a node type (no decompression)
SELECT id, name, views, metadata_json
FROM templates
WHERE nodes_used LIKE '%n8n-nodes-base.httpRequest%'
ORDER BY views DESC
LIMIT 10;

-- Then decompress only top matches
```

### Caching

- Cache decompressed workflows for popular templates (top 100)
- TTL: 1 hour
- Estimated memory: 100 * 50KB = 5MB

## Impact on P0-R3

**Original P0-R3 Plan**: Expand task library from 31 to 100+ tasks using fuzzy matching

**New Approach**: Mine 2,646 templates for real configurations

**Impact Assessment**:

| Metric | Original Plan | Template Mining |
|--------|--------------|-----------------|
| Configuration examples | 100 (estimated) | 2,646+ actual |
| Node coverage | ~20% | 103% |
| Maintenance | High (manual) | Low (auto-fetch) |
| Accuracy | Curated | Production-tested |
| Context richness | Limited | Rich metadata |
| Development time | 2-3 weeks | 1 week |

**Recommendation**: PIVOT to template mining approach for P0-R3

## Implementation Estimate

### Week 1: Core Infrastructure
- Day 1-2: Create `TemplateConfigExtractor` service
- Day 3: Implement caching layer
- Day 4-5: Testing and optimization

### Week 2: Integration
- Day 1-2: Enhance `get_node_essentials` with examples
- Day 3: Update tool documentation
- Day 4-5: Integration testing

**Total**: 2 weeks vs 3 weeks for original plan

## Validation Tests

```typescript
// Test: Extract HTTP Request configs
const configs = await extractor.extractConfigsForNode(
  'n8n-nodes-base.httpRequest',
  { complexity: 'simple', limit: 5 }
);

// Expected: 5 configs from top templates
// - Simple URL fetch
// - With authentication
// - With custom headers
// - With expressions
// - With error handling

// Test: Extract webhook configs
const webhookConfigs = await extractor.extractConfigsForNode(
  'n8n-nodes-base.webhook',
  { limit: 3 }
);

// Expected: 3 configs showing different patterns
// - Basic POST webhook
// - With response node
// - With binary data handling
```

## Risks and Mitigation

### Risk 1: Template Quality Varies
- **Mitigation**: Filter by views (popularity) and metadata complexity rating
- Only use templates with >1000 views for examples

### Risk 2: Decompression Performance
- **Mitigation**: Cache decompressed popular templates
- Implement lazy loading (decompress on demand)

### Risk 3: Template-Specific Context
- **Mitigation**: Extract only node configuration, strip workflow-specific context
- Provide source attribution for context

### Risk 4: Breaking Changes in Template Structure
- **Mitigation**: Robust error handling in decompression
- Fallback to cached configs if template fetch fails

## Success Metrics

**Before** (get_node_for_task):
- 392 calls, 72% success rate
- 28% failure rate
- 31 task templates
- 5.9% node coverage

**Target** (template-based):
- 90%+ success rate for configuration discovery
- 100%+ node coverage
- 2,646+ real-world examples
- Self-updating from n8n.io

## Next Steps

1. ✅ Complete template database analysis
2. ⏳ Create `TemplateConfigExtractor` service
3. ⏳ Implement caching layer
4. ⏳ Enhance `get_node_essentials` with examples
5. ⏳ Update P0 implementation plan
6. ⏳ Begin implementation

## Conclusion

The template database provides a vastly superior alternative to hardcoded task templates:

- **2,646 templates** vs 31 tasks (85x more examples)
- **103% node coverage** vs 5.9% coverage (17x improvement)
- **Real-world configurations** vs synthetic examples
- **Self-updating** vs manual maintenance
- **Rich metadata** for semantic matching

**Recommendation**: Pivot P0-R3 from "expand task library" to "mine template configurations"

```

--------------------------------------------------------------------------------
/scripts/test-essentials.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env ts-node
/**
 * Test script for validating the get_node_essentials tool
 * 
 * This script:
 * 1. Compares get_node_essentials vs get_node_info response sizes
 * 2. Validates that essential properties are correctly extracted
 * 3. Checks that examples are properly generated
 * 4. Tests the property search functionality
 */

import { N8NDocumentationMCPServer } from '../src/mcp/server';
import { readFileSync, writeFileSync } from 'fs';
import { join } from 'path';

// Color codes for terminal output
const colors = {
  reset: '\x1b[0m',
  bright: '\x1b[1m',
  green: '\x1b[32m',
  red: '\x1b[31m',
  yellow: '\x1b[33m',
  blue: '\x1b[34m',
  cyan: '\x1b[36m'
};

function log(message: string, color: string = colors.reset) {
  console.log(`${color}${message}${colors.reset}`);
}

function logSection(title: string) {
  console.log('\n' + '='.repeat(60));
  log(title, colors.bright + colors.cyan);
  console.log('='.repeat(60));
}

function formatBytes(bytes: number): string {
  if (bytes < 1024) return bytes + ' B';
  const kb = bytes / 1024;
  if (kb < 1024) return kb.toFixed(1) + ' KB';
  const mb = kb / 1024;
  return mb.toFixed(2) + ' MB';
}

async function testNodeEssentials(server: N8NDocumentationMCPServer, nodeType: string) {
  logSection(`Testing ${nodeType}`);
  
  try {
    // Get full node info
    const startFull = Date.now();
    const fullInfo = await server.executeTool('get_node_info', { nodeType });
    const fullTime = Date.now() - startFull;
    const fullSize = JSON.stringify(fullInfo).length;
    
    // Get essential info
    const startEssential = Date.now();
    const essentialInfo = await server.executeTool('get_node_essentials', { nodeType });
    const essentialTime = Date.now() - startEssential;
    const essentialSize = JSON.stringify(essentialInfo).length;
    
    // Calculate metrics
    const sizeReduction = ((fullSize - essentialSize) / fullSize * 100).toFixed(1);
    const speedImprovement = ((fullTime - essentialTime) / fullTime * 100).toFixed(1);
    
    // Display results
    log(`\n📊 Size Comparison:`, colors.bright);
    log(`   Full response:      ${formatBytes(fullSize)}`, colors.yellow);
    log(`   Essential response: ${formatBytes(essentialSize)}`, colors.green);
    log(`   Size reduction:     ${sizeReduction}% ✨`, colors.bright + colors.green);
    
    log(`\n⚡ Performance:`, colors.bright);
    log(`   Full response time:      ${fullTime}ms`);
    log(`   Essential response time: ${essentialTime}ms`);
    log(`   Speed improvement:       ${speedImprovement}%`, colors.green);
    
    log(`\n📋 Property Count:`, colors.bright);
    const fullPropCount = fullInfo.properties?.length || 0;
    const essentialPropCount = (essentialInfo.requiredProperties?.length || 0) + 
                               (essentialInfo.commonProperties?.length || 0);
    log(`   Full properties:      ${fullPropCount}`);
    log(`   Essential properties: ${essentialPropCount}`);
    log(`   Properties removed:   ${fullPropCount - essentialPropCount} (${((fullPropCount - essentialPropCount) / fullPropCount * 100).toFixed(1)}%)`, colors.green);
    
    log(`\n🔧 Essential Properties:`, colors.bright);
    log(`   Required: ${essentialInfo.requiredProperties?.map((p: any) => p.name).join(', ') || 'None'}`);
    log(`   Common:   ${essentialInfo.commonProperties?.map((p: any) => p.name).join(', ') || 'None'}`);
    
    log(`\n📚 Examples:`, colors.bright);
    const examples = Object.keys(essentialInfo.examples || {});
    log(`   Available examples: ${examples.join(', ') || 'None'}`);
    
    if (essentialInfo.examples?.minimal) {
      log(`   Minimal example properties: ${Object.keys(essentialInfo.examples.minimal).join(', ')}`);
    }
    
    log(`\n📊 Metadata:`, colors.bright);
    log(`   Total properties available: ${essentialInfo.metadata?.totalProperties || 0}`);
    log(`   Is AI Tool: ${essentialInfo.metadata?.isAITool ? 'Yes' : 'No'}`);
    log(`   Is Trigger: ${essentialInfo.metadata?.isTrigger ? 'Yes' : 'No'}`);
    log(`   Has Credentials: ${essentialInfo.metadata?.hasCredentials ? 'Yes' : 'No'}`);
    
    // Test property search
    const searchTerms = ['auth', 'header', 'body', 'json'];
    log(`\n🔍 Property Search Test:`, colors.bright);
    
    for (const term of searchTerms) {
      try {
        const searchResult = await server.executeTool('search_node_properties', {
          nodeType,
          query: term,
          maxResults: 5
        });
        log(`   "${term}": Found ${searchResult.totalMatches} properties`);
      } catch (error) {
        log(`   "${term}": Search failed`, colors.red);
      }
    }
    
    return {
      nodeType,
      fullSize,
      essentialSize,
      sizeReduction: parseFloat(sizeReduction),
      fullPropCount,
      essentialPropCount,
      success: true
    };
    
  } catch (error) {
    log(`❌ Error testing ${nodeType}: ${error}`, colors.red);
    return {
      nodeType,
      fullSize: 0,
      essentialSize: 0,
      sizeReduction: 0,
      fullPropCount: 0,
      essentialPropCount: 0,
      success: false,
      error: error instanceof Error ? error.message : String(error)
    };
  }
}

async function main() {
  logSection('n8n MCP Essentials Tool Test Suite');
  
  try {
    // Initialize server
    log('\n🚀 Initializing MCP server...', colors.cyan);
    const server = new N8NDocumentationMCPServer();
    
    // Wait for initialization
    await new Promise(resolve => setTimeout(resolve, 1000));
    
    // Test nodes
    const testNodes = [
      'nodes-base.httpRequest',
      'nodes-base.webhook',
      'nodes-base.code',
      'nodes-base.set',
      'nodes-base.if',
      'nodes-base.postgres',
      'nodes-base.openAi',
      'nodes-base.googleSheets',
      'nodes-base.slack',
      'nodes-base.merge'
    ];
    
    const results = [];
    
    for (const nodeType of testNodes) {
      const result = await testNodeEssentials(server, nodeType);
      results.push(result);
    }
    
    // Summary
    logSection('Test Summary');
    
    const successful = results.filter(r => r.success);
    const totalFullSize = successful.reduce((sum, r) => sum + r.fullSize, 0);
    const totalEssentialSize = successful.reduce((sum, r) => sum + r.essentialSize, 0);
    const avgReduction = successful.reduce((sum, r) => sum + r.sizeReduction, 0) / successful.length;
    
    log(`\n✅ Successful tests: ${successful.length}/${results.length}`, colors.green);
    
    if (successful.length > 0) {
      log(`\n📊 Overall Statistics:`, colors.bright);
      log(`   Total full size:      ${formatBytes(totalFullSize)}`);
      log(`   Total essential size: ${formatBytes(totalEssentialSize)}`);
      log(`   Average reduction:    ${avgReduction.toFixed(1)}%`, colors.bright + colors.green);
      
      log(`\n🏆 Best Performers:`, colors.bright);
      const sorted = successful.sort((a, b) => b.sizeReduction - a.sizeReduction);
      sorted.slice(0, 3).forEach((r, i) => {
        log(`   ${i + 1}. ${r.nodeType}: ${r.sizeReduction}% reduction (${formatBytes(r.fullSize)} → ${formatBytes(r.essentialSize)})`);
      });
    }
    
    const failed = results.filter(r => !r.success);
    if (failed.length > 0) {
      log(`\n❌ Failed tests:`, colors.red);
      failed.forEach(r => {
        log(`   - ${r.nodeType}: ${r.error}`, colors.red);
      });
    }
    
    // Save detailed results
    const reportPath = join(process.cwd(), 'test-results-essentials.json');
    writeFileSync(reportPath, JSON.stringify({
      timestamp: new Date().toISOString(),
      summary: {
        totalTests: results.length,
        successful: successful.length,
        failed: failed.length,
        averageReduction: avgReduction,
        totalFullSize,
        totalEssentialSize
      },
      results
    }, null, 2));
    
    log(`\n📄 Detailed results saved to: ${reportPath}`, colors.cyan);
    
    // Recommendations
    logSection('Recommendations');
    
    if (avgReduction > 90) {
      log('✨ Excellent! The essentials tool is achieving >90% size reduction.', colors.green);
    } else if (avgReduction > 80) {
      log('👍 Good! The essentials tool is achieving 80-90% size reduction.', colors.yellow);
      log('   Consider reviewing nodes with lower reduction rates.');
    } else {
      log('⚠️  The average size reduction is below 80%.', colors.yellow);
      log('   Review the essential property lists for optimization.');
    }
    
    // Test specific functionality
    logSection('Testing Advanced Features');
    
    // Test error handling
    log('\n🧪 Testing error handling...', colors.cyan);
    try {
      await server.executeTool('get_node_essentials', { nodeType: 'non-existent-node' });
      log('   ❌ Error handling failed - should have thrown error', colors.red);
    } catch (error) {
      log('   ✅ Error handling works correctly', colors.green);
    }
    
    // Test alternative node type formats
    log('\n🧪 Testing alternative node type formats...', colors.cyan);
    const alternativeFormats = [
      { input: 'httpRequest', expected: 'nodes-base.httpRequest' },
      { input: 'nodes-base.httpRequest', expected: 'nodes-base.httpRequest' },
      { input: 'HTTPREQUEST', expected: 'nodes-base.httpRequest' }
    ];
    
    for (const format of alternativeFormats) {
      try {
        const result = await server.executeTool('get_node_essentials', { nodeType: format.input });
        if (result.nodeType === format.expected) {
          log(`   ✅ "${format.input}" → "${format.expected}"`, colors.green);
        } else {
          log(`   ❌ "${format.input}" → "${result.nodeType}" (expected "${format.expected}")`, colors.red);
        }
      } catch (error) {
        log(`   ❌ "${format.input}" → Error: ${error}`, colors.red);
      }
    }
    
    log('\n✨ Test suite completed!', colors.bright + colors.green);
    
  } catch (error) {
    log(`\n❌ Fatal error: ${error}`, colors.red);
    process.exit(1);
  }
}

// Run the test
main().catch(error => {
  console.error('Unhandled error:', error);
  process.exit(1);
});
```

--------------------------------------------------------------------------------
/src/services/expression-validator.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Expression Validator for n8n expressions
 * Validates expression syntax, variable references, and context availability
 */

interface ExpressionValidationResult {
  valid: boolean;
  errors: string[];
  warnings: string[];
  usedVariables: Set<string>;
  usedNodes: Set<string>;
}

interface ExpressionContext {
  availableNodes: string[];
  currentNodeName?: string;
  isInLoop?: boolean;
  hasInputData?: boolean;
}

export class ExpressionValidator {
  // Common n8n expression patterns
  private static readonly EXPRESSION_PATTERN = /\{\{([\s\S]+?)\}\}/g;
  private static readonly VARIABLE_PATTERNS = {
    json: /\$json(\.[a-zA-Z_][\w]*|\["[^"]+"\]|\['[^']+'\]|\[\d+\])*/g,
    node: /\$node\["([^"]+)"\]\.json/g,
    input: /\$input\.item(\.[a-zA-Z_][\w]*|\["[^"]+"\]|\['[^']+'\]|\[\d+\])*/g,
    items: /\$items\("([^"]+)"(?:,\s*(-?\d+))?\)/g,
    parameter: /\$parameter\["([^"]+)"\]/g,
    env: /\$env\.([a-zA-Z_][\w]*)/g,
    workflow: /\$workflow\.(id|name|active)/g,
    execution: /\$execution\.(id|mode|resumeUrl)/g,
    prevNode: /\$prevNode\.(name|outputIndex|runIndex)/g,
    itemIndex: /\$itemIndex/g,
    runIndex: /\$runIndex/g,
    now: /\$now/g,
    today: /\$today/g,
  };

  /**
   * Validate a single expression
   */
  static validateExpression(
    expression: string,
    context: ExpressionContext
  ): ExpressionValidationResult {
    const result: ExpressionValidationResult = {
      valid: true,
      errors: [],
      warnings: [],
      usedVariables: new Set(),
      usedNodes: new Set(),
    };

    // Handle null/undefined expression
    if (!expression) {
      return result;
    }

    // Handle null/undefined context
    if (!context) {
      result.valid = false;
      result.errors.push('Validation context is required');
      return result;
    }

    // Check for basic syntax errors
    const syntaxErrors = this.checkSyntaxErrors(expression);
    result.errors.push(...syntaxErrors);

    // Extract all expressions
    const expressions = this.extractExpressions(expression);
    
    for (const expr of expressions) {
      // Validate each expression
      this.validateSingleExpression(expr, context, result);
    }

    // Check for undefined node references
    this.checkNodeReferences(result, context);

    result.valid = result.errors.length === 0;
    return result;
  }

  /**
   * Check for basic syntax errors
   */
  private static checkSyntaxErrors(expression: string): string[] {
    const errors: string[] = [];

    // Check for unmatched brackets
    const openBrackets = (expression.match(/\{\{/g) || []).length;
    const closeBrackets = (expression.match(/\}\}/g) || []).length;
    
    if (openBrackets !== closeBrackets) {
      errors.push('Unmatched expression brackets {{ }}');
    }

    // Check for nested expressions (not supported in n8n)
    if (expression.includes('{{') && expression.includes('{{', expression.indexOf('{{') + 2)) {
      const match = expression.match(/\{\{.*\{\{/);
      if (match) {
        errors.push('Nested expressions are not supported');
      }
    }

    // Check for empty expressions
    const emptyExpressionPattern = /\{\{\s*\}\}/;
    if (emptyExpressionPattern.test(expression)) {
      errors.push('Empty expression found');
    }

    return errors;
  }

  /**
   * Extract all expressions from a string
   */
  private static extractExpressions(text: string): string[] {
    const expressions: string[] = [];
    let match;
    
    while ((match = this.EXPRESSION_PATTERN.exec(text)) !== null) {
      expressions.push(match[1].trim());
    }
    
    return expressions;
  }

  /**
   * Validate a single expression content
   */
  private static validateSingleExpression(
    expr: string,
    context: ExpressionContext,
    result: ExpressionValidationResult
  ): void {
    // Check for $json usage
    let match;
    const jsonPattern = new RegExp(this.VARIABLE_PATTERNS.json.source, this.VARIABLE_PATTERNS.json.flags);
    while ((match = jsonPattern.exec(expr)) !== null) {
      result.usedVariables.add('$json');

      if (!context.hasInputData && !context.isInLoop) {
        result.warnings.push(
          'Using $json but node might not have input data'
        );
      }

      // Check for suspicious property names that might be test/invalid data
      const fullMatch = match[0];
      if (fullMatch.includes('.invalid') || fullMatch.includes('.undefined') ||
          fullMatch.includes('.null') || fullMatch.includes('.test')) {
        result.warnings.push(
          `Property access '${fullMatch}' looks suspicious - verify this property exists in your data`
        );
      }
    }

    // Check for $node references
    const nodePattern = new RegExp(this.VARIABLE_PATTERNS.node.source, this.VARIABLE_PATTERNS.node.flags);
    while ((match = nodePattern.exec(expr)) !== null) {
      const nodeName = match[1];
      result.usedNodes.add(nodeName);
      result.usedVariables.add('$node');
    }

    // Check for $input usage
    const inputPattern = new RegExp(this.VARIABLE_PATTERNS.input.source, this.VARIABLE_PATTERNS.input.flags);
    while ((match = inputPattern.exec(expr)) !== null) {
      result.usedVariables.add('$input');
      
      if (!context.hasInputData) {
        result.warnings.push(
          '$input is only available when the node has input data'
        );
      }
    }

    // Check for $items usage
    const itemsPattern = new RegExp(this.VARIABLE_PATTERNS.items.source, this.VARIABLE_PATTERNS.items.flags);
    while ((match = itemsPattern.exec(expr)) !== null) {
      const nodeName = match[1];
      result.usedNodes.add(nodeName);
      result.usedVariables.add('$items');
    }

    // Check for other variables
    for (const [varName, pattern] of Object.entries(this.VARIABLE_PATTERNS)) {
      if (['json', 'node', 'input', 'items'].includes(varName)) continue;
      
      const testPattern = new RegExp(pattern.source, pattern.flags);
      if (testPattern.test(expr)) {
        result.usedVariables.add(`$${varName}`);
      }
    }

    // Check for common mistakes
    this.checkCommonMistakes(expr, result);
  }

  /**
   * Check for common expression mistakes
   */
  private static checkCommonMistakes(
    expr: string,
    result: ExpressionValidationResult
  ): void {
    // Check for missing $ prefix - but exclude cases where $ is already present
    const missingPrefixPattern = /(?<!\$)\b(json|node|input|items|workflow|execution)\b(?!\s*:)/;
    if (expr.match(missingPrefixPattern)) {
      result.warnings.push(
        'Possible missing $ prefix for variable (e.g., use $json instead of json)'
      );
    }

    // Check for incorrect array access
    if (expr.includes('$json[') && !expr.match(/\$json\[\d+\]/)) {
      result.warnings.push(
        'Array access should use numeric index: $json[0] or property access: $json.property'
      );
    }

    // Check for Python-style property access
    if (expr.match(/\$json\['[^']+'\]/)) {
      result.warnings.push(
        "Consider using dot notation: $json.property instead of $json['property']"
      );
    }

    // Check for undefined/null access attempts
    if (expr.match(/\?\./)) {
      result.warnings.push(
        'Optional chaining (?.) is not supported in n8n expressions'
      );
    }

    // Check for template literals
    if (expr.includes('${')) {
      result.errors.push(
        'Template literals ${} are not supported. Use string concatenation instead'
      );
    }
  }

  /**
   * Check that all referenced nodes exist
   */
  private static checkNodeReferences(
    result: ExpressionValidationResult,
    context: ExpressionContext
  ): void {
    for (const nodeName of result.usedNodes) {
      if (!context.availableNodes.includes(nodeName)) {
        result.errors.push(
          `Referenced node "${nodeName}" not found in workflow`
        );
      }
    }
  }

  /**
   * Validate all expressions in a node's parameters
   */
  static validateNodeExpressions(
    parameters: any,
    context: ExpressionContext
  ): ExpressionValidationResult {
    const combinedResult: ExpressionValidationResult = {
      valid: true,
      errors: [],
      warnings: [],
      usedVariables: new Set(),
      usedNodes: new Set(),
    };

    const visited = new WeakSet();
    this.validateParametersRecursive(parameters, context, combinedResult, '', visited);
    
    combinedResult.valid = combinedResult.errors.length === 0;
    return combinedResult;
  }

  /**
   * Recursively validate expressions in parameters
   */
  private static validateParametersRecursive(
    obj: any,
    context: ExpressionContext,
    result: ExpressionValidationResult,
    path: string = '',
    visited: WeakSet<object> = new WeakSet()
  ): void {
    // Handle circular references
    if (obj && typeof obj === 'object') {
      if (visited.has(obj)) {
        return; // Skip already visited objects
      }
      visited.add(obj);
    }
    
    if (typeof obj === 'string') {
      if (obj.includes('{{')) {
        const validation = this.validateExpression(obj, context);
        
        // Add path context to errors
        validation.errors.forEach(error => {
          result.errors.push(path ? `${path}: ${error}` : error);
        });
        
        validation.warnings.forEach(warning => {
          result.warnings.push(path ? `${path}: ${warning}` : warning);
        });
        
        // Merge used variables and nodes
        validation.usedVariables.forEach(v => result.usedVariables.add(v));
        validation.usedNodes.forEach(n => result.usedNodes.add(n));
      }
    } else if (Array.isArray(obj)) {
      obj.forEach((item, index) => {
        this.validateParametersRecursive(
          item,
          context,
          result,
          `${path}[${index}]`,
          visited
        );
      });
    } else if (obj && typeof obj === 'object') {
      Object.entries(obj).forEach(([key, value]) => {
        const newPath = path ? `${path}.${key}` : key;
        this.validateParametersRecursive(value, context, result, newPath, visited);
      });
    }
  }
}
```

--------------------------------------------------------------------------------
/src/mcp/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

import { N8NDocumentationMCPServer } from './server';
import { logger } from '../utils/logger';
import { TelemetryConfigManager } from '../telemetry/config-manager';
import { EarlyErrorLogger } from '../telemetry/early-error-logger';
import { STARTUP_CHECKPOINTS, findFailedCheckpoint, StartupCheckpoint } from '../telemetry/startup-checkpoints';
import { existsSync } from 'fs';

// Add error details to stderr for Claude Desktop debugging
process.on('uncaughtException', (error) => {
  if (process.env.MCP_MODE !== 'stdio') {
    console.error('Uncaught Exception:', error);
  }
  logger.error('Uncaught Exception:', error);
  process.exit(1);
});

process.on('unhandledRejection', (reason, promise) => {
  if (process.env.MCP_MODE !== 'stdio') {
    console.error('Unhandled Rejection at:', promise, 'reason:', reason);
  }
  logger.error('Unhandled Rejection:', reason);
  process.exit(1);
});

/**
 * Detects if running in a container environment (Docker, Podman, Kubernetes, etc.)
 * Uses multiple detection methods for robustness:
 * 1. Environment variables (IS_DOCKER, IS_CONTAINER with multiple formats)
 * 2. Filesystem markers (/.dockerenv, /run/.containerenv)
 */
function isContainerEnvironment(): boolean {
  // Check environment variables with multiple truthy formats
  const dockerEnv = (process.env.IS_DOCKER || '').toLowerCase();
  const containerEnv = (process.env.IS_CONTAINER || '').toLowerCase();

  if (['true', '1', 'yes'].includes(dockerEnv)) {
    return true;
  }
  if (['true', '1', 'yes'].includes(containerEnv)) {
    return true;
  }

  // Fallback: Check filesystem markers
  // /.dockerenv exists in Docker containers
  // /run/.containerenv exists in Podman containers
  try {
    return existsSync('/.dockerenv') || existsSync('/run/.containerenv');
  } catch (error) {
    // If filesystem check fails, assume not in container
    logger.debug('Container detection filesystem check failed:', error);
    return false;
  }
}

async function main() {
  // Initialize early error logger for pre-handshake error capture (v2.18.3)
  // Now using singleton pattern with defensive initialization
  const startTime = Date.now();
  const earlyLogger = EarlyErrorLogger.getInstance();
  const checkpoints: StartupCheckpoint[] = [];

  try {
    // Checkpoint: Process started (fire-and-forget, no await)
    earlyLogger.logCheckpoint(STARTUP_CHECKPOINTS.PROCESS_STARTED);
    checkpoints.push(STARTUP_CHECKPOINTS.PROCESS_STARTED);

    // Handle telemetry CLI commands
    const args = process.argv.slice(2);
  if (args.length > 0 && args[0] === 'telemetry') {
    const telemetryConfig = TelemetryConfigManager.getInstance();
    const action = args[1];

    switch (action) {
      case 'enable':
        telemetryConfig.enable();
        process.exit(0);
        break;
      case 'disable':
        telemetryConfig.disable();
        process.exit(0);
        break;
      case 'status':
        console.log(telemetryConfig.getStatus());
        process.exit(0);
        break;
      default:
        console.log(`
Usage: n8n-mcp telemetry [command]

Commands:
  enable   Enable anonymous telemetry
  disable  Disable anonymous telemetry
  status   Show current telemetry status

Learn more: https://github.com/czlonkowski/n8n-mcp/blob/main/PRIVACY.md
`);
        process.exit(args[1] ? 1 : 0);
    }
  }

  const mode = process.env.MCP_MODE || 'stdio';

    // Checkpoint: Telemetry initializing (fire-and-forget, no await)
    earlyLogger.logCheckpoint(STARTUP_CHECKPOINTS.TELEMETRY_INITIALIZING);
    checkpoints.push(STARTUP_CHECKPOINTS.TELEMETRY_INITIALIZING);

    // Telemetry is already initialized by TelemetryConfigManager in imports
    // Mark as ready (fire-and-forget, no await)
    earlyLogger.logCheckpoint(STARTUP_CHECKPOINTS.TELEMETRY_READY);
    checkpoints.push(STARTUP_CHECKPOINTS.TELEMETRY_READY);

  try {
    // Only show debug messages in HTTP mode to avoid corrupting stdio communication
    if (mode === 'http') {
      console.error(`Starting n8n Documentation MCP Server in ${mode} mode...`);
      console.error('Current directory:', process.cwd());
      console.error('Node version:', process.version);
    }

    // Checkpoint: MCP handshake starting (fire-and-forget, no await)
    earlyLogger.logCheckpoint(STARTUP_CHECKPOINTS.MCP_HANDSHAKE_STARTING);
    checkpoints.push(STARTUP_CHECKPOINTS.MCP_HANDSHAKE_STARTING);
    
    if (mode === 'http') {
      // Check if we should use the fixed implementation
      if (process.env.USE_FIXED_HTTP === 'true') {
        // Use the fixed HTTP implementation that bypasses StreamableHTTPServerTransport issues
        const { startFixedHTTPServer } = await import('../http-server');
        await startFixedHTTPServer();
      } else {
        // HTTP mode - for remote deployment with single-session architecture
        const { SingleSessionHTTPServer } = await import('../http-server-single-session');
        const server = new SingleSessionHTTPServer();
        
        // Graceful shutdown handlers
        const shutdown = async () => {
          await server.shutdown();
          process.exit(0);
        };
        
        process.on('SIGTERM', shutdown);
        process.on('SIGINT', shutdown);
        
        await server.start();
      }
    } else {
      // Stdio mode - for local Claude Desktop
      const server = new N8NDocumentationMCPServer(undefined, earlyLogger);

      // Graceful shutdown handler (fixes Issue #277)
      let isShuttingDown = false;
      const shutdown = async (signal: string = 'UNKNOWN') => {
        if (isShuttingDown) return; // Prevent multiple shutdown calls
        isShuttingDown = true;

        try {
          logger.info(`Shutdown initiated by: ${signal}`);

          await server.shutdown();

          // Close stdin to signal we're done reading
          if (process.stdin && !process.stdin.destroyed) {
            process.stdin.pause();
            process.stdin.destroy();
          }

          // Exit with timeout to ensure we don't hang
          // Increased to 1000ms for slower systems
          setTimeout(() => {
            logger.warn('Shutdown timeout exceeded, forcing exit');
            process.exit(0);
          }, 1000).unref();

          // Let the timeout handle the exit for graceful shutdown
          // (removed immediate exit to allow cleanup to complete)
        } catch (error) {
          logger.error('Error during shutdown:', error);
          process.exit(1);
        }
      };

      // Handle termination signals (fixes Issue #277)
      // Signal handling strategy:
      // - Claude Desktop (Windows/macOS/Linux): stdin handlers + signal handlers
      //   Primary: stdin close when Claude quits | Fallback: SIGTERM/SIGINT/SIGHUP
      // - Container environments: signal handlers ONLY
      //   stdin closed in detached mode would trigger immediate shutdown
      //   Container detection via IS_DOCKER/IS_CONTAINER env vars + filesystem markers
      // - Manual execution: Both stdin and signal handlers work
      process.on('SIGTERM', () => shutdown('SIGTERM'));
      process.on('SIGINT', () => shutdown('SIGINT'));
      process.on('SIGHUP', () => shutdown('SIGHUP'));

      // Handle stdio disconnect - PRIMARY shutdown mechanism for Claude Desktop
      // Skip in container environments (Docker, Kubernetes, Podman) to prevent
      // premature shutdown when stdin is closed in detached mode.
      // Containers rely on signal handlers (SIGTERM/SIGINT/SIGHUP) for proper shutdown.
      const isContainer = isContainerEnvironment();

      if (!isContainer && process.stdin.readable && !process.stdin.destroyed) {
        try {
          process.stdin.on('end', () => shutdown('STDIN_END'));
          process.stdin.on('close', () => shutdown('STDIN_CLOSE'));
        } catch (error) {
          logger.error('Failed to register stdin handlers, using signal handlers only:', error);
          // Continue - signal handlers will still work
        }
      }

      await server.run();
    }

    // Checkpoint: MCP handshake complete (fire-and-forget, no await)
    earlyLogger.logCheckpoint(STARTUP_CHECKPOINTS.MCP_HANDSHAKE_COMPLETE);
    checkpoints.push(STARTUP_CHECKPOINTS.MCP_HANDSHAKE_COMPLETE);

    // Checkpoint: Server ready (fire-and-forget, no await)
    earlyLogger.logCheckpoint(STARTUP_CHECKPOINTS.SERVER_READY);
    checkpoints.push(STARTUP_CHECKPOINTS.SERVER_READY);

    // Log successful startup (fire-and-forget, no await)
    const startupDuration = Date.now() - startTime;
    earlyLogger.logStartupSuccess(checkpoints, startupDuration);

    logger.info(`Server startup completed in ${startupDuration}ms (${checkpoints.length} checkpoints passed)`);

  } catch (error) {
    // Log startup error with checkpoint context (fire-and-forget, no await)
    const failedCheckpoint = findFailedCheckpoint(checkpoints);
    earlyLogger.logStartupError(failedCheckpoint, error);

    // In stdio mode, we cannot output to console at all
    if (mode !== 'stdio') {
      console.error('Failed to start MCP server:', error);
      logger.error('Failed to start MCP server', error);

      // Provide helpful error messages
      if (error instanceof Error && error.message.includes('nodes.db not found')) {
        console.error('\nTo fix this issue:');
        console.error('1. cd to the n8n-mcp directory');
        console.error('2. Run: npm run build');
        console.error('3. Run: npm run rebuild');
      } else if (error instanceof Error && error.message.includes('NODE_MODULE_VERSION')) {
        console.error('\nTo fix this Node.js version mismatch:');
        console.error('1. cd to the n8n-mcp directory');
        console.error('2. Run: npm rebuild better-sqlite3');
        console.error('3. If that doesn\'t work, try: rm -rf node_modules && npm install');
      }
    }

    process.exit(1);
  }
  } catch (outerError) {
    // Outer error catch for early initialization failures
    logger.error('Critical startup error:', outerError);
    process.exit(1);
  }
}

// Run if called directly
if (require.main === module) {
  main().catch(console.error);
}
```

--------------------------------------------------------------------------------
/docs/DOCKER_TROUBLESHOOTING.md:
--------------------------------------------------------------------------------

```markdown
# Docker Troubleshooting Guide

This guide helps resolve common issues when running n8n-mcp with Docker, especially when connecting to n8n instances.

## Table of Contents
- [Common Issues](#common-issues)
  - [502 Bad Gateway Errors](#502-bad-gateway-errors)
  - [Custom Database Path Not Working](#custom-database-path-not-working-v27160)
  - [Container Name Conflicts](#container-name-conflicts)
  - [n8n API Connection Issues](#n8n-api-connection-issues)
- [Docker Networking](#docker-networking)
- [Quick Solutions](#quick-solutions)
- [Debugging Steps](#debugging-steps)

## Common Issues

### Docker Configuration File Not Working (v2.8.2+)

**Symptoms:**
- Config file mounted but environment variables not set
- Container starts but ignores configuration
- Getting "permission denied" errors

**Solutions:**

1. **Ensure file is mounted correctly:**
```bash
# Correct - mount as read-only
docker run -v $(pwd)/config.json:/app/config.json:ro ...

# Check if file is accessible
docker exec n8n-mcp cat /app/config.json
```

2. **Verify JSON syntax:**
```bash
# Validate JSON file
cat config.json | jq .
```

3. **Check Docker logs for parsing errors:**
```bash
docker logs n8n-mcp | grep -i config
```

4. **Common issues:**
- Invalid JSON syntax (use a JSON validator)
- File permissions (should be readable)
- Wrong mount path (must be `/app/config.json`)
- Dangerous variables blocked (PATH, LD_PRELOAD, etc.)

### Custom Database Path Not Working (v2.7.16+)

**Symptoms:**
- `NODE_DB_PATH` environment variable is set but ignored
- Database always created at `/app/data/nodes.db`
- Custom path setting has no effect

**Root Cause:** Fixed in v2.7.16. Earlier versions had hardcoded paths in docker-entrypoint.sh.

**Solutions:**

1. **Update to v2.7.16 or later:**
```bash
docker pull ghcr.io/czlonkowski/n8n-mcp:latest
```

2. **Ensure path ends with .db:**
```bash
# Correct
NODE_DB_PATH=/app/data/custom/my-nodes.db

# Incorrect (will be rejected)
NODE_DB_PATH=/app/data/custom/my-nodes
```

3. **Use path within mounted volume for persistence:**
```yaml
services:
  n8n-mcp:
    environment:
      NODE_DB_PATH: /app/data/custom/nodes.db
    volumes:
      - n8n-mcp-data:/app/data  # Ensure parent directory is mounted
```

### 502 Bad Gateway Errors

**Symptoms:**
- `n8n_health_check` returns 502 error
- All n8n management API calls fail
- n8n web UI is accessible but API is not

**Root Cause:** Network connectivity issues between n8n-mcp container and n8n instance.

**Solutions:**

#### 1. When n8n runs in Docker on same machine

Use Docker's special hostnames instead of `localhost`:

```json
{
  "mcpServers": {
    "n8n-mcp": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "N8N_API_URL=http://host.docker.internal:5678",
        "-e", "N8N_API_KEY=your-api-key",
        "ghcr.io/czlonkowski/n8n-mcp:latest"
      ]
    }
  }
}
```

**Alternative hostnames to try:**
- `host.docker.internal` (Docker Desktop on macOS/Windows)
- `172.17.0.1` (Default Docker bridge IP on Linux)
- Your machine's actual IP address (e.g., `192.168.1.100`)

#### 2. When both containers are in same Docker network

```bash
# Create a shared network
docker network create n8n-network

# Run n8n in the network
docker run -d --name n8n --network n8n-network -p 5678:5678 n8nio/n8n

# Configure n8n-mcp to use container name
```

```json
{
  "N8N_API_URL": "http://n8n:5678"
}
```

#### 3. For Docker Compose setups

```yaml
# docker-compose.yml
services:
  n8n:
    image: n8nio/n8n
    container_name: n8n
    networks:
      - n8n-net
    ports:
      - "5678:5678"
  
  n8n-mcp:
    image: ghcr.io/czlonkowski/n8n-mcp:latest
    environment:
      N8N_API_URL: http://n8n:5678
      N8N_API_KEY: ${N8N_API_KEY}
    networks:
      - n8n-net

networks:
  n8n-net:
    driver: bridge
```

### Container Cleanup Issues (Fixed in v2.7.20+)

**Symptoms:**
- Containers accumulate after Claude Desktop restarts
- Containers show as "unhealthy" but don't clean up
- `--rm` flag doesn't work as expected

**Root Cause:** Fixed in v2.7.20 - containers weren't handling termination signals properly.

**Solutions:**

1. **Update to v2.7.20+ and use --init flag (Recommended):**
```json
{
  "command": "docker",
  "args": [
    "run", "-i", "--rm", "--init",
    "ghcr.io/czlonkowski/n8n-mcp:latest"
  ]
}
```

2. **Manual cleanup of old containers:**
```bash
# Remove all stopped n8n-mcp containers
docker ps -a | grep n8n-mcp | grep Exited | awk '{print $1}' | xargs -r docker rm
```

3. **For versions before 2.7.20:**
- Manually clean up containers periodically
- Consider using HTTP mode instead

### Webhooks to Local n8n Fail (v2.16.3+)

**Symptoms:**
- `n8n_trigger_webhook_workflow` fails with "SSRF protection" error
- Error message: "SSRF protection: Localhost access is blocked"
- Webhooks work from n8n UI but not from n8n-MCP

**Root Cause:** Default strict SSRF protection blocks localhost access to prevent attacks.

**Solution:** Use moderate security mode for local development

```bash
# For Docker run
docker run -d \
  --name n8n-mcp \
  -e MCP_MODE=http \
  -e AUTH_TOKEN=your-token \
  -e WEBHOOK_SECURITY_MODE=moderate \
  -p 3000:3000 \
  ghcr.io/czlonkowski/n8n-mcp:latest

# For Docker Compose - add to environment:
services:
  n8n-mcp:
    environment:
      WEBHOOK_SECURITY_MODE: moderate
```

**Security Modes Explained:**
- `strict` (default): Blocks localhost + private IPs + cloud metadata (production)
- `moderate`: Allows localhost, blocks private IPs + cloud metadata (local development)
- `permissive`: Allows localhost + private IPs, blocks cloud metadata (testing only)

**Important:** Always use `strict` mode in production. Cloud metadata is blocked in all modes.

### n8n API Connection Issues

**Symptoms:**
- API calls fail but n8n web UI works
- Authentication errors
- API endpoints return 404

**Solutions:**

1. **Verify n8n API is enabled:**
   - Check n8n settings → REST API is enabled
   - Ensure API key is valid and not expired

2. **Test API directly:**
```bash
# From host machine
curl -H "X-N8N-API-KEY: your-key" http://localhost:5678/api/v1/workflows

# From inside Docker container
docker run --rm curlimages/curl \
  -H "X-N8N-API-KEY: your-key" \
  http://host.docker.internal:5678/api/v1/workflows
```

3. **Check n8n environment variables:**
```yaml
environment:
  - N8N_BASIC_AUTH_ACTIVE=true
  - N8N_BASIC_AUTH_USER=user
  - N8N_BASIC_AUTH_PASSWORD=password
```

## Docker Networking

### Understanding Docker Network Modes

| Scenario | Use This URL | Why |
|----------|--------------|-----|
| n8n on host, n8n-mcp in Docker | `http://host.docker.internal:5678` | Docker can't reach host's localhost |
| Both in same Docker network | `http://container-name:5678` | Direct container-to-container |
| n8n behind reverse proxy | `http://your-domain.com` | Use public URL |
| Local development | `http://YOUR_LOCAL_IP:5678` | Use machine's IP address |

### Finding Your Configuration

```bash
# Check if n8n is running in Docker
docker ps | grep n8n

# Find Docker network
docker network ls

# Get container details
docker inspect n8n | grep NetworkMode

# Find your local IP
# macOS/Linux
ifconfig | grep "inet " | grep -v 127.0.0.1

# Windows
ipconfig | findstr IPv4
```

## Quick Solutions

### Solution 1: Use Host Network (Linux only)
```json
{
  "command": "docker",
  "args": [
    "run", "-i", "--rm",
    "--network", "host",
    "-e", "N8N_API_URL=http://localhost:5678",
    "ghcr.io/czlonkowski/n8n-mcp:latest"
  ]
}
```

### Solution 2: Use Your Machine's IP
```json
{
  "N8N_API_URL": "http://192.168.1.100:5678"  // Replace with your IP
}
```

### Solution 3: HTTP Mode Deployment
Deploy n8n-mcp as HTTP server to avoid stdio/Docker issues:

```bash
# Start HTTP server
docker run -d \
  -p 3000:3000 \
  -e MCP_MODE=http \
  -e AUTH_TOKEN=your-token \
  -e N8N_API_URL=http://host.docker.internal:5678 \
  -e N8N_API_KEY=your-n8n-key \
  ghcr.io/czlonkowski/n8n-mcp:latest

# Configure Claude with mcp-remote
```

## Debugging Steps

### 1. Enable Debug Logging
```json
{
  "env": {
    "LOG_LEVEL": "debug",
    "DEBUG_MCP": "true"
  }
}
```

### 2. Test Connectivity
```bash
# Test from n8n-mcp container
docker run --rm ghcr.io/czlonkowski/n8n-mcp:latest \
  sh -c "apk add curl && curl -v http://host.docker.internal:5678/api/v1/workflows"
```

### 3. Check Docker Logs
```bash
# View n8n-mcp logs
docker logs $(docker ps -q -f ancestor=ghcr.io/czlonkowski/n8n-mcp:latest)

# View n8n logs
docker logs n8n
```

### 4. Validate Environment
```bash
# Check what n8n-mcp sees
docker run --rm ghcr.io/czlonkowski/n8n-mcp:latest \
  sh -c "env | grep N8N"
```

### 5. Network Diagnostics
```bash
# Check Docker networks
docker network inspect bridge

# Test DNS resolution
docker run --rm busybox nslookup host.docker.internal
```

## Platform-Specific Notes

### Docker Desktop (macOS/Windows)
- `host.docker.internal` works out of the box
- Ensure Docker Desktop is running
- Check Docker Desktop settings → Resources → Network

### Linux
- `host.docker.internal` requires Docker 20.10+
- Alternative: Use `--add-host=host.docker.internal:host-gateway`
- Or use the Docker bridge IP: `172.17.0.1`

### Windows with WSL2
- Use `host.docker.internal` or WSL2 IP
- Check firewall rules for port 5678
- Ensure n8n binds to `0.0.0.0` not `127.0.0.1`

## Still Having Issues?

1. **Check n8n logs** for API-related errors
2. **Verify firewall/security** isn't blocking connections
3. **Try simpler setup** - Run n8n-mcp on host instead of Docker
4. **Report issue** with debug logs at [GitHub Issues](https://github.com/czlonkowski/n8n-mcp/issues)

## Useful Commands

```bash
# Remove all n8n-mcp containers
docker rm -f $(docker ps -aq -f ancestor=ghcr.io/czlonkowski/n8n-mcp:latest)

# Test n8n API with curl
curl -H "X-N8N-API-KEY: your-key" http://localhost:5678/api/v1/workflows

# Run interactive debug session
docker run -it --rm \
  -e LOG_LEVEL=debug \
  -e N8N_API_URL=http://host.docker.internal:5678 \
  -e N8N_API_KEY=your-key \
  ghcr.io/czlonkowski/n8n-mcp:latest \
  sh

# Check container networking
docker run --rm alpine ping -c 4 host.docker.internal
```
```

--------------------------------------------------------------------------------
/tests/fixtures/factories/parser-node.factory.ts:
--------------------------------------------------------------------------------

```typescript
import { Factory } from 'fishery';
import { faker } from '@faker-js/faker';

// Declarative node definition
export interface DeclarativeNodeDefinition {
  name: string;
  displayName: string;
  description: string;
  version?: number | number[];
  group?: string[];
  categories?: string[];
  routing: {
    request?: {
      resource?: {
        options: Array<{ name: string; value: string }>;
      };
      operation?: {
        options: Record<string, Array<{ name: string; value: string; action?: string }>>;
      };
    };
  };
  properties?: any[];
  credentials?: any[];
  usableAsTool?: boolean;
  webhooks?: any[];
  polling?: boolean;
}

// Programmatic node definition
export interface ProgrammaticNodeDefinition {
  name: string;
  displayName: string;
  description: string;
  version?: number | number[];
  group?: string[];
  categories?: string[];
  properties: any[];
  credentials?: any[];
  usableAsTool?: boolean;
  webhooks?: any[];
  polling?: boolean;
  trigger?: boolean;
  eventTrigger?: boolean;
}

// Versioned node class structure
export interface VersionedNodeClass {
  baseDescription?: {
    name: string;
    displayName: string;
    description: string;
    defaultVersion: number;
  };
  nodeVersions?: Record<number, { description: any }>;
}

// Property definition
export interface PropertyDefinition {
  displayName: string;
  name: string;
  type: string;
  default?: any;
  description?: string;
  options?: Array<{ name: string; value: string; description?: string; action?: string; displayName?: string }> | any[];
  required?: boolean;
  displayOptions?: {
    show?: Record<string, any[]>;
    hide?: Record<string, any[]>;
  };
  typeOptions?: any;
  noDataExpression?: boolean;
}

// Base property factory
export const propertyFactory = Factory.define<PropertyDefinition>(() => ({
  displayName: faker.helpers.arrayElement(['Resource', 'Operation', 'Field', 'Option']),
  name: faker.helpers.slugify(faker.word.noun()).toLowerCase(),
  type: faker.helpers.arrayElement(['string', 'number', 'boolean', 'options', 'json', 'collection']),
  default: '',
  description: faker.lorem.sentence(),
  required: faker.datatype.boolean(),
  noDataExpression: faker.datatype.boolean()
}));

// String property factory
export const stringPropertyFactory = propertyFactory.params({
  type: 'string',
  default: faker.lorem.word()
});

// Number property factory
export const numberPropertyFactory = propertyFactory.params({
  type: 'number',
  default: faker.number.int({ min: 0, max: 100 })
});

// Boolean property factory
export const booleanPropertyFactory = propertyFactory.params({
  type: 'boolean',
  default: faker.datatype.boolean()
});

// Options property factory
export const optionsPropertyFactory = propertyFactory.params({
  type: 'options',
  options: [
    { name: 'Option A', value: 'a', description: 'First option' },
    { name: 'Option B', value: 'b', description: 'Second option' },
    { name: 'Option C', value: 'c', description: 'Third option' }
  ],
  default: 'a'
});

// Resource property for programmatic nodes
export const resourcePropertyFactory = optionsPropertyFactory.params({
  displayName: 'Resource',
  name: 'resource',
  options: [
    { name: 'User', value: 'user' },
    { name: 'Post', value: 'post' },
    { name: 'Comment', value: 'comment' }
  ]
});

// Operation property for programmatic nodes
export const operationPropertyFactory = optionsPropertyFactory.params({
  displayName: 'Operation',
  name: 'operation',
  displayOptions: {
    show: {
      resource: ['user']
    }
  },
  options: [
    { name: 'Create', value: 'create', action: 'Create a user' } as any,
    { name: 'Get', value: 'get', action: 'Get a user' } as any,
    { name: 'Update', value: 'update', action: 'Update a user' } as any,
    { name: 'Delete', value: 'delete', action: 'Delete a user' } as any
  ]
});

// Collection property factory
export const collectionPropertyFactory = propertyFactory.params({
  type: 'collection',
  default: {},
  options: [
    stringPropertyFactory.build({ name: 'field1', displayName: 'Field 1' }) as any,
    numberPropertyFactory.build({ name: 'field2', displayName: 'Field 2' }) as any
  ]
});

// Declarative node factory
export const declarativeNodeFactory = Factory.define<DeclarativeNodeDefinition>(() => ({
  name: faker.helpers.slugify(faker.company.name()).toLowerCase(),
  displayName: faker.company.name(),
  description: faker.lorem.sentence(),
  version: faker.number.int({ min: 1, max: 3 }),
  group: [faker.helpers.arrayElement(['transform', 'output'])],
  routing: {
    request: {
      resource: {
        options: [
          { name: 'User', value: 'user' },
          { name: 'Post', value: 'post' }
        ]
      },
      operation: {
        options: {
          user: [
            { name: 'Create', value: 'create', action: 'Create a user' },
            { name: 'Get', value: 'get', action: 'Get a user' }
          ],
          post: [
            { name: 'Create', value: 'create', action: 'Create a post' },
            { name: 'List', value: 'list', action: 'List posts' }
          ]
        }
      }
    }
  },
  properties: [
    stringPropertyFactory.build({ name: 'apiKey', displayName: 'API Key' })
  ],
  credentials: [
    { name: 'apiCredentials', required: true }
  ]
}));

// Programmatic node factory
export const programmaticNodeFactory = Factory.define<ProgrammaticNodeDefinition>(() => ({
  name: faker.helpers.slugify(faker.company.name()).toLowerCase(),
  displayName: faker.company.name(),
  description: faker.lorem.sentence(),
  version: faker.number.int({ min: 1, max: 3 }),
  group: [faker.helpers.arrayElement(['transform', 'output'])],
  properties: [
    resourcePropertyFactory.build(),
    operationPropertyFactory.build(),
    stringPropertyFactory.build({ 
      name: 'field',
      displayName: 'Field',
      displayOptions: {
        show: {
          resource: ['user'],
          operation: ['create', 'update']
        }
      }
    })
  ],
  credentials: []
}));

// Trigger node factory
export const triggerNodeFactory = programmaticNodeFactory.params({
  group: ['trigger'],
  trigger: true,
  properties: [
    {
      displayName: 'Event',
      name: 'event',
      type: 'options',
      default: 'created',
      options: [
        { name: 'Created', value: 'created' },
        { name: 'Updated', value: 'updated' },
        { name: 'Deleted', value: 'deleted' }
      ]
    }
  ]
});

// Webhook node factory
export const webhookNodeFactory = programmaticNodeFactory.params({
  group: ['trigger'],
  webhooks: [
    {
      name: 'default',
      httpMethod: 'POST',
      responseMode: 'onReceived',
      path: 'webhook'
    }
  ],
  properties: [
    {
      displayName: 'Path',
      name: 'path',
      type: 'string',
      default: 'webhook',
      required: true
    }
  ]
});

// AI tool node factory
export const aiToolNodeFactory = declarativeNodeFactory.params({
  usableAsTool: true,
  name: 'openai',
  displayName: 'OpenAI',
  description: 'Use OpenAI models'
});

// Versioned node class factory
export const versionedNodeClassFactory = Factory.define<VersionedNodeClass>(() => ({
  baseDescription: {
    name: faker.helpers.slugify(faker.company.name()).toLowerCase(),
    displayName: faker.company.name(),
    description: faker.lorem.sentence(),
    defaultVersion: 2
  },
  nodeVersions: {
    1: {
      description: {
        properties: [
          stringPropertyFactory.build({ name: 'oldField', displayName: 'Old Field' })
        ]
      }
    },
    2: {
      description: {
        properties: [
          stringPropertyFactory.build({ name: 'newField', displayName: 'New Field' }),
          numberPropertyFactory.build({ name: 'version', displayName: 'Version' })
        ]
      }
    }
  }
}));

// Malformed node factory (for error testing)
export const malformedNodeFactory = Factory.define<any>(() => ({
  // Missing required 'name' property
  displayName: faker.company.name(),
  description: faker.lorem.sentence()
}));

// Complex nested property factory
export const nestedPropertyFactory = Factory.define<PropertyDefinition>(() => ({
  displayName: 'Advanced Options',
  name: 'advancedOptions',
  type: 'collection',
  default: {},
  options: [
    {
      displayName: 'Headers',
      name: 'headers',
      type: 'fixedCollection',
      typeOptions: {
        multipleValues: true
      },
      options: [
        {
          name: 'header',
          displayName: 'Header',
          values: [
            stringPropertyFactory.build({ name: 'name', displayName: 'Name' }),
            stringPropertyFactory.build({ name: 'value', displayName: 'Value' })
          ]
        }
      ]
    } as any,
    {
      displayName: 'Query Parameters',
      name: 'queryParams',
      type: 'collection',
      options: [
        stringPropertyFactory.build({ name: 'key', displayName: 'Key' }),
        stringPropertyFactory.build({ name: 'value', displayName: 'Value' })
      ] as any[]
    } as any
  ]
}));

// Node class mock factory
export const nodeClassFactory = Factory.define<any>(({ params }) => {
  const description = params.description || programmaticNodeFactory.build();
  
  return class MockNode {
    description = description;
    
    constructor() {
      // Constructor logic if needed
    }
  };
});

// Versioned node type class mock
export const versionedNodeTypeClassFactory = Factory.define<any>(({ params }) => {
  const baseDescription = params.baseDescription || {
    name: 'versionedNode',
    displayName: 'Versioned Node',
    description: 'A versioned node',
    defaultVersion: 2
  };
  
  const nodeVersions = params.nodeVersions || {
    1: {
      description: {
        properties: [propertyFactory.build()]
      }
    },
    2: {
      description: {
        properties: [propertyFactory.build(), propertyFactory.build()]
      }
    }
  };
  
  return class VersionedNodeType {
    baseDescription = baseDescription;
    nodeVersions = nodeVersions;
    currentVersion = baseDescription.defaultVersion;
    
    constructor() {
      Object.defineProperty(this.constructor, 'name', {
        value: 'VersionedNodeType',
        writable: false,
        configurable: true
      });
    }
  };
});
```

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

```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { TestableN8NMCPServer } from './test-helpers';

describe('MCP Protocol Compliance', () => {
  let mcpServer: TestableN8NMCPServer;
  let transport: InMemoryTransport;
  let client: Client;

  beforeEach(async () => {
    mcpServer = new TestableN8NMCPServer();
    await mcpServer.initialize();
    
    const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
    transport = serverTransport;
    
    // Connect MCP server to transport
    await mcpServer.connectToTransport(transport);
    
    // Create client
    client = new Client({
      name: 'test-client',
      version: '1.0.0'
    }, {
      capabilities: {}
    });
    
    await client.connect(clientTransport);
  });

  afterEach(async () => {
    await client.close();
    await mcpServer.close();
  });

  describe('JSON-RPC 2.0 Compliance', () => {
    it('should return proper JSON-RPC 2.0 response format', async () => {
      const response = await client.listTools();

      // Response should have tools array
      expect(response).toHaveProperty('tools');
      expect(Array.isArray((response as any).tools)).toBe(true);
    });

    it('should handle request with id correctly', async () => {
      const response = await client.listTools();

      expect(response).toBeDefined();
      expect(typeof response).toBe('object');
    });

    it('should handle batch requests', async () => {
      // Send multiple requests concurrently
      const promises = [
        client.listTools(),
        client.listTools(),
        client.listTools()
      ];

      const responses = await Promise.all(promises);
      
      expect(responses).toHaveLength(3);
      responses.forEach(response => {
        expect(response).toHaveProperty('tools');
      });
    });

    it('should preserve request order in responses', async () => {
      const requests = [];
      const expectedOrder = [];

      // Create requests with different tools to track order
      for (let i = 0; i < 5; i++) {
        expectedOrder.push(i);
        requests.push(
          client.callTool({ name: 'get_database_statistics', arguments: {} })
            .then(() => i)
        );
      }

      const results = await Promise.all(requests);
      expect(results).toEqual(expectedOrder);
    });
  });

  describe('Protocol Version Negotiation', () => {
    it('should negotiate protocol capabilities', async () => {
      const serverInfo = await client.getServerVersion();
      
      expect(serverInfo).toHaveProperty('name');
      expect(serverInfo).toHaveProperty('version');
      expect(serverInfo!.name).toBe('n8n-documentation-mcp');
    });

    it('should expose supported capabilities', async () => {
      const serverCapabilities = client.getServerCapabilities();
      
      expect(serverCapabilities).toBeDefined();
      
      // Should support tools
      expect(serverCapabilities).toHaveProperty('tools');
    });
  });

  describe('Message Format Validation', () => {
    it('should reject messages without method', async () => {
      // Test by sending raw message through transport
      const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
      const testClient = new Client({ name: 'test', version: '1.0.0' }, {});
      
      await mcpServer.connectToTransport(serverTransport);
      await testClient.connect(clientTransport);

      try {
        // This should fail as MCP SDK validates method
        await (testClient as any).request({ method: '', params: {} });
        expect.fail('Should have thrown an error');
      } catch (error) {
        expect(error).toBeDefined();
      } finally {
        await testClient.close();
      }
    });

    it('should handle missing params gracefully', async () => {
      // Most tools should work without params
      const response = await client.callTool({ name: 'list_nodes', arguments: {} });
      expect(response).toBeDefined();
    });

    it('should validate params schema', async () => {
      try {
        // Invalid nodeType format (missing prefix)
        const response = await client.callTool({ name: 'get_node_info', arguments: {
          nodeType: 'httpRequest' // Should be 'nodes-base.httpRequest'
        } });
        // Check if the response indicates an error
        const text = (response as any).content[0].text;
        expect(text).toContain('not found');
      } catch (error: any) {
        // If it throws, that's also acceptable
        expect(error.message).toContain('not found');
      }
    });
  });

  describe('Content Types', () => {
    it('should handle text content in tool responses', async () => {
      const response = await client.callTool({ name: 'get_database_statistics', arguments: {} });
      
      expect((response as any).content).toHaveLength(1);
      expect((response as any).content[0]).toHaveProperty('type', 'text');
      expect((response as any).content[0]).toHaveProperty('text');
      expect(typeof (response as any).content[0].text).toBe('string');
    });

    it('should handle large text responses', async () => {
      // Get a large node info response
      const response = await client.callTool({ name: 'get_node_info', arguments: {
        nodeType: 'nodes-base.httpRequest'
      } });

      expect((response as any).content).toHaveLength(1);
      expect((response as any).content[0].type).toBe('text');
      expect((response as any).content[0].text.length).toBeGreaterThan(1000);
    });

    it('should handle JSON content properly', async () => {
      const response = await client.callTool({ name: 'list_nodes', arguments: {
        limit: 5
      } });

      expect((response as any).content).toHaveLength(1);
      const content = JSON.parse((response as any).content[0].text);
      expect(content).toHaveProperty('nodes');
      expect(Array.isArray(content.nodes)).toBe(true);
    });
  });

  describe('Request/Response Correlation', () => {
    it('should correlate concurrent requests correctly', async () => {
      const requests = [
        client.callTool({ name: 'get_node_essentials', arguments: { nodeType: 'nodes-base.httpRequest' } }),
        client.callTool({ name: 'get_node_essentials', arguments: { nodeType: 'nodes-base.webhook' } }),
        client.callTool({ name: 'get_node_essentials', arguments: { nodeType: 'nodes-base.slack' } })
      ];

      const responses = await Promise.all(requests);

      expect((responses[0] as any).content[0].text).toContain('httpRequest');
      expect((responses[1] as any).content[0].text).toContain('webhook');
      expect((responses[2] as any).content[0].text).toContain('slack');
    });

    it('should handle interleaved requests', async () => {
      const results: string[] = [];

      // Start multiple requests with different delays
      const p1 = client.callTool({ name: 'get_database_statistics', arguments: {} })
        .then(() => { results.push('stats'); return 'stats'; });

      const p2 = client.callTool({ name: 'list_nodes', arguments: { limit: 1 } })
        .then(() => { results.push('nodes'); return 'nodes'; });

      const p3 = client.callTool({ name: 'search_nodes', arguments: { query: 'http' } })
        .then(() => { results.push('search'); return 'search'; });

      const resolved = await Promise.all([p1, p2, p3]);

      // All should complete
      expect(resolved).toHaveLength(3);
      expect(results).toHaveLength(3);
    });
  });

  describe('Protocol Extensions', () => {
    it('should handle tool-specific extensions', async () => {
      // Test tool with complex params
      const response = await client.callTool({ name: 'validate_node_operation', arguments: {
        nodeType: 'nodes-base.httpRequest',
        config: {
          method: 'GET',
          url: 'https://api.example.com'
        },
        profile: 'runtime'
      } });

      expect((response as any).content).toHaveLength(1);
      expect((response as any).content[0].type).toBe('text');
    });

    it('should support optional parameters', async () => {
      // Call with minimal params
      const response1 = await client.callTool({ name: 'list_nodes', arguments: {} });
      
      // Call with all params
      const response2 = await client.callTool({ name: 'list_nodes', arguments: {
        limit: 10,
        category: 'trigger',
        package: 'n8n-nodes-base'
      } });

      expect(response1).toBeDefined();
      expect(response2).toBeDefined();
    });
  });

  describe('Transport Layer', () => {
    it('should handle transport disconnection gracefully', async () => {
      const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
      const testClient = new Client({ name: 'test', version: '1.0.0' }, {});

      await mcpServer.connectToTransport(serverTransport);
      await testClient.connect(clientTransport);

      // Make a request
      const response = await testClient.callTool({ name: 'get_database_statistics', arguments: {} });
      expect(response).toBeDefined();

      // Close client
      await testClient.close();

      // Further requests should fail
      try {
        await testClient.callTool({ name: 'get_database_statistics', arguments: {} });
        expect.fail('Should have thrown an error');
      } catch (error) {
        expect(error).toBeDefined();
      }
    });

    it('should handle multiple sequential connections', async () => {
      // Close existing connection
      await client.close();
      await mcpServer.close();

      // Create new connections
      for (let i = 0; i < 3; i++) {
        const engine = new TestableN8NMCPServer();
        await engine.initialize();

        const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
        await engine.connectToTransport(serverTransport);

        const testClient = new Client({ name: 'test', version: '1.0.0' }, {});
        await testClient.connect(clientTransport);

        const response = await testClient.callTool({ name: 'get_database_statistics', arguments: {} });
        expect(response).toBeDefined();

        await testClient.close();
        await engine.close();
      }
    });
  });
});
```

--------------------------------------------------------------------------------
/tests/integration/ai-validation/llm-chain-validation.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Integration Tests: Basic LLM Chain Validation
 *
 * Tests Basic LLM Chain validation against real n8n instance.
 */

import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest';
import { createTestContext, TestContext, createTestWorkflowName } from '../n8n-api/utils/test-context';
import { getTestN8nClient } from '../n8n-api/utils/n8n-client';
import { N8nApiClient } from '../../../src/services/n8n-api-client';
import { cleanupOrphanedWorkflows } from '../n8n-api/utils/cleanup-helpers';
import { createMcpContext } from '../n8n-api/utils/mcp-context';
import { InstanceContext } from '../../../src/types/instance-context';
import { handleValidateWorkflow } from '../../../src/mcp/handlers-n8n-manager';
import { getNodeRepository, closeNodeRepository } from '../n8n-api/utils/node-repository';
import { NodeRepository } from '../../../src/database/node-repository';
import { ValidationResponse } from '../n8n-api/types/mcp-responses';
import {
  createBasicLLMChainNode,
  createLanguageModelNode,
  createMemoryNode,
  createAIConnection,
  mergeConnections,
  createAIWorkflow
} from './helpers';
import { WorkflowNode } from '../../../src/types/n8n-api';

describe('Integration: Basic LLM Chain Validation', () => {
  let context: TestContext;
  let client: N8nApiClient;
  let mcpContext: InstanceContext;
  let repository: NodeRepository;

  beforeEach(async () => {
    context = createTestContext();
    client = getTestN8nClient();
    mcpContext = createMcpContext();
    repository = await getNodeRepository();
  });

  afterEach(async () => {
    await context.cleanup();
  });

  afterAll(async () => {
    await closeNodeRepository();
    if (!process.env.CI) {
      await cleanupOrphanedWorkflows();
    }
  });

  // ======================================================================
  // TEST 1: Missing Language Model
  // ======================================================================

  it('should detect missing language model', async () => {
    const llmChain = createBasicLLMChainNode({
      name: 'Basic LLM Chain',
      promptType: 'define',
      text: 'Test prompt'
    });

    const workflow = createAIWorkflow(
      [llmChain],
      {}, // No connections
      {
        name: createTestWorkflowName('LLM Chain - Missing Model'),
        tags: ['mcp-integration-test', 'ai-validation']
      }
    );

    const created = await client.createWorkflow(workflow);
    context.trackWorkflow(created.id!);

    const response = await handleValidateWorkflow(
      { id: created.id },
      repository,
      mcpContext
    );

    expect(response.success).toBe(true);
    const data = response.data as ValidationResponse;

    expect(data.valid).toBe(false);
    expect(data.errors).toBeDefined();

    const errorCodes = data.errors!.map(e => e.details?.code || e.code);
    expect(errorCodes).toContain('MISSING_LANGUAGE_MODEL');
  });

  // ======================================================================
  // TEST 2: Missing Prompt Text (promptType=define)
  // ======================================================================

  it('should detect missing prompt text', async () => {
    const languageModel = createLanguageModelNode('openai', {
      name: 'OpenAI Chat Model'
    });

    const llmChain = createBasicLLMChainNode({
      name: 'Basic LLM Chain',
      promptType: 'define',
      text: '' // Empty prompt text
    });

    const workflow = createAIWorkflow(
      [languageModel, llmChain],
      createAIConnection('OpenAI Chat Model', 'Basic LLM Chain', 'ai_languageModel'),
      {
        name: createTestWorkflowName('LLM Chain - Missing Prompt'),
        tags: ['mcp-integration-test', 'ai-validation']
      }
    );

    const created = await client.createWorkflow(workflow);
    context.trackWorkflow(created.id!);

    const response = await handleValidateWorkflow(
      { id: created.id },
      repository,
      mcpContext
    );

    expect(response.success).toBe(true);
    const data = response.data as ValidationResponse;

    expect(data.valid).toBe(false);
    expect(data.errors).toBeDefined();

    const errorCodes = data.errors!.map(e => e.details?.code || e.code);
    expect(errorCodes).toContain('MISSING_PROMPT_TEXT');
  });

  // ======================================================================
  // TEST 3: Valid Complete LLM Chain
  // ======================================================================

  it('should validate complete LLM Chain', async () => {
    const languageModel = createLanguageModelNode('openai', {
      name: 'OpenAI Chat Model'
    });

    const llmChain = createBasicLLMChainNode({
      name: 'Basic LLM Chain',
      promptType: 'define',
      text: 'You are a helpful assistant. Answer the following: {{ $json.question }}'
    });

    const workflow = createAIWorkflow(
      [languageModel, llmChain],
      createAIConnection('OpenAI Chat Model', 'Basic LLM Chain', 'ai_languageModel'),
      {
        name: createTestWorkflowName('LLM Chain - Valid'),
        tags: ['mcp-integration-test', 'ai-validation']
      }
    );

    const created = await client.createWorkflow(workflow);
    context.trackWorkflow(created.id!);

    const response = await handleValidateWorkflow(
      { id: created.id },
      repository,
      mcpContext
    );

    expect(response.success).toBe(true);
    const data = response.data as ValidationResponse;

    expect(data.valid).toBe(true);
    expect(data.errors).toBeUndefined();
    expect(data.summary.errorCount).toBe(0);
  });

  // ======================================================================
  // TEST 4: LLM Chain with Memory
  // ======================================================================

  it('should validate LLM Chain with memory', async () => {
    const languageModel = createLanguageModelNode('anthropic', {
      name: 'Anthropic Chat Model'
    });

    const memory = createMemoryNode({
      name: 'Window Buffer Memory',
      contextWindowLength: 10
    });

    const llmChain = createBasicLLMChainNode({
      name: 'Basic LLM Chain',
      promptType: 'auto'
    });

    const workflow = createAIWorkflow(
      [languageModel, memory, llmChain],
      mergeConnections(
        createAIConnection('Anthropic Chat Model', 'Basic LLM Chain', 'ai_languageModel'),
        createAIConnection('Window Buffer Memory', 'Basic LLM Chain', 'ai_memory')
      ),
      {
        name: createTestWorkflowName('LLM Chain - With Memory'),
        tags: ['mcp-integration-test', 'ai-validation']
      }
    );

    const created = await client.createWorkflow(workflow);
    context.trackWorkflow(created.id!);

    const response = await handleValidateWorkflow(
      { id: created.id },
      repository,
      mcpContext
    );

    expect(response.success).toBe(true);
    const data = response.data as ValidationResponse;

    expect(data.valid).toBe(true);
    expect(data.errors).toBeUndefined();
  });

  // ======================================================================
  // TEST 5: LLM Chain with Multiple Language Models (Error)
  // ======================================================================

  it('should detect multiple language models', async () => {
    const languageModel1 = createLanguageModelNode('openai', {
      id: 'model-1',
      name: 'OpenAI Chat Model 1'
    });

    const languageModel2 = createLanguageModelNode('anthropic', {
      id: 'model-2',
      name: 'Anthropic Chat Model'
    });

    const llmChain = createBasicLLMChainNode({
      name: 'Basic LLM Chain',
      promptType: 'define',
      text: 'Test prompt'
    });

    const workflow = createAIWorkflow(
      [languageModel1, languageModel2, llmChain],
      mergeConnections(
        createAIConnection('OpenAI Chat Model 1', 'Basic LLM Chain', 'ai_languageModel'),
        createAIConnection('Anthropic Chat Model', 'Basic LLM Chain', 'ai_languageModel') // ERROR: multiple models
      ),
      {
        name: createTestWorkflowName('LLM Chain - Multiple Models'),
        tags: ['mcp-integration-test', 'ai-validation']
      }
    );

    const created = await client.createWorkflow(workflow);
    context.trackWorkflow(created.id!);

    const response = await handleValidateWorkflow(
      { id: created.id },
      repository,
      mcpContext
    );

    expect(response.success).toBe(true);
    const data = response.data as ValidationResponse;

    expect(data.valid).toBe(false);
    expect(data.errors).toBeDefined();

    const errorCodes = data.errors!.map(e => e.details?.code || e.code);
    expect(errorCodes).toContain('MULTIPLE_LANGUAGE_MODELS');
  });

  // ======================================================================
  // TEST 6: LLM Chain with Tools (Error - not supported)
  // ======================================================================

  it('should detect tools connection (not supported)', async () => {
    const languageModel = createLanguageModelNode('openai', {
      name: 'OpenAI Chat Model'
    });

    // Manually create a tool node
    const toolNode: WorkflowNode = {
      id: 'tool-1',
      name: 'Calculator',
      type: '@n8n/n8n-nodes-langchain.toolCalculator',
      typeVersion: 1,
      position: [250, 400],
      parameters: {}
    };

    const llmChain = createBasicLLMChainNode({
      name: 'Basic LLM Chain',
      promptType: 'define',
      text: 'Calculate something'
    });

    const workflow = createAIWorkflow(
      [languageModel, toolNode, llmChain],
      mergeConnections(
        createAIConnection('OpenAI Chat Model', 'Basic LLM Chain', 'ai_languageModel'),
        createAIConnection('Calculator', 'Basic LLM Chain', 'ai_tool') // ERROR: tools not supported
      ),
      {
        name: createTestWorkflowName('LLM Chain - With Tools'),
        tags: ['mcp-integration-test', 'ai-validation']
      }
    );

    const created = await client.createWorkflow(workflow);
    context.trackWorkflow(created.id!);

    const response = await handleValidateWorkflow(
      { id: created.id },
      repository,
      mcpContext
    );

    expect(response.success).toBe(true);
    const data = response.data as ValidationResponse;

    expect(data.valid).toBe(false);
    expect(data.errors).toBeDefined();

    const errorCodes = data.errors!.map(e => e.details?.code || e.code);
    expect(errorCodes).toContain('TOOLS_NOT_SUPPORTED');

    const errorMessages = data.errors!.map(e => e.message).join(' ');
    expect(errorMessages).toMatch(/AI Agent/i); // Should suggest using AI Agent
  });
});

```

--------------------------------------------------------------------------------
/src/utils/cache-utils.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Cache utilities for flexible instance configuration
 * Provides hash creation, metrics tracking, and cache configuration
 */

import { createHash } from 'crypto';
import { LRUCache } from 'lru-cache';
import { logger } from './logger';

/**
 * Cache metrics for monitoring and optimization
 */
export interface CacheMetrics {
  hits: number;
  misses: number;
  evictions: number;
  sets: number;
  deletes: number;
  clears: number;
  size: number;
  maxSize: number;
  avgHitRate: number;
  createdAt: Date;
  lastResetAt: Date;
}

/**
 * Cache configuration options
 */
export interface CacheConfig {
  max: number;
  ttlMinutes: number;
}

/**
 * Simple memoization cache for hash results
 * Limited size to prevent memory growth
 */
const hashMemoCache = new Map<string, string>();
const MAX_MEMO_SIZE = 1000;

/**
 * Metrics tracking for cache operations
 */
class CacheMetricsTracker {
  private metrics!: CacheMetrics;
  private startTime: Date;

  constructor() {
    this.startTime = new Date();
    this.reset();
  }

  /**
   * Reset all metrics to initial state
   */
  reset(): void {
    this.metrics = {
      hits: 0,
      misses: 0,
      evictions: 0,
      sets: 0,
      deletes: 0,
      clears: 0,
      size: 0,
      maxSize: 0,
      avgHitRate: 0,
      createdAt: this.startTime,
      lastResetAt: new Date()
    };
  }

  /**
   * Record a cache hit
   */
  recordHit(): void {
    this.metrics.hits++;
    this.updateHitRate();
  }

  /**
   * Record a cache miss
   */
  recordMiss(): void {
    this.metrics.misses++;
    this.updateHitRate();
  }

  /**
   * Record a cache eviction
   */
  recordEviction(): void {
    this.metrics.evictions++;
  }

  /**
   * Record a cache set operation
   */
  recordSet(): void {
    this.metrics.sets++;
  }

  /**
   * Record a cache delete operation
   */
  recordDelete(): void {
    this.metrics.deletes++;
  }

  /**
   * Record a cache clear operation
   */
  recordClear(): void {
    this.metrics.clears++;
  }

  /**
   * Update cache size metrics
   */
  updateSize(current: number, max: number): void {
    this.metrics.size = current;
    this.metrics.maxSize = max;
  }

  /**
   * Update average hit rate
   */
  private updateHitRate(): void {
    const total = this.metrics.hits + this.metrics.misses;
    if (total > 0) {
      this.metrics.avgHitRate = this.metrics.hits / total;
    }
  }

  /**
   * Get current metrics snapshot
   */
  getMetrics(): CacheMetrics {
    return { ...this.metrics };
  }

  /**
   * Get formatted metrics for logging
   */
  getFormattedMetrics(): string {
    const { hits, misses, evictions, avgHitRate, size, maxSize } = this.metrics;
    return `Cache Metrics: Hits=${hits}, Misses=${misses}, HitRate=${(avgHitRate * 100).toFixed(2)}%, Size=${size}/${maxSize}, Evictions=${evictions}`;
  }
}

// Global metrics tracker instance
export const cacheMetrics = new CacheMetricsTracker();

/**
 * Get cache configuration from environment variables or defaults
 * @returns Cache configuration with max size and TTL
 */
export function getCacheConfig(): CacheConfig {
  const max = parseInt(process.env.INSTANCE_CACHE_MAX || '100', 10);
  const ttlMinutes = parseInt(process.env.INSTANCE_CACHE_TTL_MINUTES || '30', 10);

  // Validate configuration bounds
  const validatedMax = Math.max(1, Math.min(10000, max)) || 100;
  const validatedTtl = Math.max(1, Math.min(1440, ttlMinutes)) || 30; // Max 24 hours

  if (validatedMax !== max || validatedTtl !== ttlMinutes) {
    logger.warn('Cache configuration adjusted to valid bounds', {
      requestedMax: max,
      requestedTtl: ttlMinutes,
      actualMax: validatedMax,
      actualTtl: validatedTtl
    });
  }

  return {
    max: validatedMax,
    ttlMinutes: validatedTtl
  };
}

/**
 * Create a secure hash for cache key with memoization
 * @param input - The input string to hash
 * @returns SHA-256 hash as hex string
 */
export function createCacheKey(input: string): string {
  // Check memoization cache first
  if (hashMemoCache.has(input)) {
    return hashMemoCache.get(input)!;
  }

  // Create hash
  const hash = createHash('sha256').update(input).digest('hex');

  // Add to memoization cache with size limit
  if (hashMemoCache.size >= MAX_MEMO_SIZE) {
    // Remove oldest entries (simple FIFO)
    const firstKey = hashMemoCache.keys().next().value;
    if (firstKey) {
      hashMemoCache.delete(firstKey);
    }
  }
  hashMemoCache.set(input, hash);

  return hash;
}

/**
 * Create LRU cache with metrics tracking
 * @param onDispose - Optional callback for when items are evicted
 * @returns Configured LRU cache instance
 */
export function createInstanceCache<T extends {}>(
  onDispose?: (value: T, key: string) => void
): LRUCache<string, T> {
  const config = getCacheConfig();

  return new LRUCache<string, T>({
    max: config.max,
    ttl: config.ttlMinutes * 60 * 1000, // Convert to milliseconds
    updateAgeOnGet: true,
    dispose: (value, key) => {
      cacheMetrics.recordEviction();
      if (onDispose) {
        onDispose(value, key);
      }
      logger.debug('Cache eviction', {
        cacheKey: key.substring(0, 8) + '...',
        metrics: cacheMetrics.getFormattedMetrics()
      });
    }
  });
}

/**
 * Mutex implementation for cache operations
 * Prevents race conditions during concurrent access
 */
export class CacheMutex {
  private locks: Map<string, Promise<void>> = new Map();
  private lockTimeouts: Map<string, NodeJS.Timeout> = new Map();
  private readonly timeout: number = 5000; // 5 second timeout

  /**
   * Acquire a lock for the given key
   * @param key - The cache key to lock
   * @returns Promise that resolves when lock is acquired
   */
  async acquire(key: string): Promise<() => void> {
    while (this.locks.has(key)) {
      try {
        await this.locks.get(key);
      } catch {
        // Previous lock failed, we can proceed
      }
    }

    let releaseLock: () => void;
    const lockPromise = new Promise<void>((resolve) => {
      releaseLock = () => {
        resolve();
        this.locks.delete(key);
        const timeout = this.lockTimeouts.get(key);
        if (timeout) {
          clearTimeout(timeout);
          this.lockTimeouts.delete(key);
        }
      };
    });

    this.locks.set(key, lockPromise);

    // Set timeout to prevent stuck locks
    const timeout = setTimeout(() => {
      logger.warn('Cache lock timeout, forcefully releasing', { key: key.substring(0, 8) + '...' });
      releaseLock!();
    }, this.timeout);
    this.lockTimeouts.set(key, timeout);

    return releaseLock!;
  }

  /**
   * Check if a key is currently locked
   * @param key - The cache key to check
   * @returns True if the key is locked
   */
  isLocked(key: string): boolean {
    return this.locks.has(key);
  }

  /**
   * Clear all locks (use with caution)
   */
  clearAll(): void {
    this.lockTimeouts.forEach(timeout => clearTimeout(timeout));
    this.locks.clear();
    this.lockTimeouts.clear();
  }
}

/**
 * Retry configuration for API operations
 */
export interface RetryConfig {
  maxAttempts: number;
  baseDelayMs: number;
  maxDelayMs: number;
  jitterFactor: number;
}

/**
 * Default retry configuration
 */
export const DEFAULT_RETRY_CONFIG: RetryConfig = {
  maxAttempts: 3,
  baseDelayMs: 1000,
  maxDelayMs: 10000,
  jitterFactor: 0.3
};

/**
 * Calculate exponential backoff delay with jitter
 * @param attempt - Current attempt number (0-based)
 * @param config - Retry configuration
 * @returns Delay in milliseconds
 */
export function calculateBackoffDelay(attempt: number, config: RetryConfig = DEFAULT_RETRY_CONFIG): number {
  const exponentialDelay = Math.min(
    config.baseDelayMs * Math.pow(2, attempt),
    config.maxDelayMs
  );

  // Add jitter to prevent thundering herd
  const jitter = exponentialDelay * config.jitterFactor * Math.random();

  return Math.floor(exponentialDelay + jitter);
}

/**
 * Execute function with retry logic
 * @param fn - Function to execute
 * @param config - Retry configuration
 * @param context - Optional context for logging
 * @returns Result of the function
 */
export async function withRetry<T>(
  fn: () => Promise<T>,
  config: RetryConfig = DEFAULT_RETRY_CONFIG,
  context?: string
): Promise<T> {
  let lastError: Error;

  for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;

      // Check if error is retryable
      if (!isRetryableError(error)) {
        throw error;
      }

      if (attempt < config.maxAttempts - 1) {
        const delay = calculateBackoffDelay(attempt, config);
        logger.debug('Retrying operation after delay', {
          context,
          attempt: attempt + 1,
          maxAttempts: config.maxAttempts,
          delayMs: delay,
          error: lastError.message
        });
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }

  logger.error('All retry attempts exhausted', {
    context,
    attempts: config.maxAttempts,
    lastError: lastError!.message
  });

  throw lastError!;
}

/**
 * Check if an error is retryable
 * @param error - The error to check
 * @returns True if the error is retryable
 */
function isRetryableError(error: any): boolean {
  // Network errors
  if (error.code === 'ECONNREFUSED' ||
      error.code === 'ECONNRESET' ||
      error.code === 'ETIMEDOUT' ||
      error.code === 'ENOTFOUND') {
    return true;
  }

  // HTTP status codes that are retryable
  if (error.response?.status) {
    const status = error.response.status;
    return status === 429 || // Too Many Requests
           status === 503 || // Service Unavailable
           status === 504 || // Gateway Timeout
           (status >= 500 && status < 600); // Server errors
  }

  // Timeout errors
  if (error.message && error.message.toLowerCase().includes('timeout')) {
    return true;
  }

  return false;
}

/**
 * Format cache statistics for logging or display
 * @returns Formatted statistics string
 */
export function getCacheStatistics(): string {
  const metrics = cacheMetrics.getMetrics();
  const runtime = Date.now() - metrics.createdAt.getTime();
  const runtimeMinutes = Math.floor(runtime / 60000);

  return `
Cache Statistics:
  Runtime: ${runtimeMinutes} minutes
  Total Operations: ${metrics.hits + metrics.misses}
  Hit Rate: ${(metrics.avgHitRate * 100).toFixed(2)}%
  Current Size: ${metrics.size}/${metrics.maxSize}
  Total Evictions: ${metrics.evictions}
  Sets: ${metrics.sets}, Deletes: ${metrics.deletes}, Clears: ${metrics.clears}
  `.trim();
}
```

--------------------------------------------------------------------------------
/tests/setup/test-env.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Test Environment Configuration Loader
 * 
 * This module handles loading and validating test environment variables
 * with type safety and default values.
 */

import * as dotenv from 'dotenv';
import * as path from 'path';
import { existsSync } from 'fs';

// Load test environment variables
export function loadTestEnvironment(): void {
  // CI Debug logging
  const isCI = process.env.CI === 'true';

  // First, load the main .env file (for integration tests that need real credentials)
  const mainEnvPath = path.resolve(process.cwd(), '.env');
  if (existsSync(mainEnvPath)) {
    dotenv.config({ path: mainEnvPath });
    if (isCI) {
      console.log('[CI-DEBUG] Loaded .env file from:', mainEnvPath);
    }
  }

  // Load base test environment
  const testEnvPath = path.resolve(process.cwd(), '.env.test');

  if (isCI) {
    console.log('[CI-DEBUG] Looking for .env.test at:', testEnvPath);
    console.log('[CI-DEBUG] File exists?', existsSync(testEnvPath));
  }

  if (existsSync(testEnvPath)) {
    // Don't override values from .env
    const result = dotenv.config({ path: testEnvPath, override: false });
    if (isCI && result.error) {
      console.error('[CI-DEBUG] Failed to load .env.test:', result.error);
    } else if (isCI && result.parsed) {
      console.log('[CI-DEBUG] Successfully loaded', Object.keys(result.parsed).length, 'env vars from .env.test');
    }
  } else if (isCI) {
    console.warn('[CI-DEBUG] .env.test file not found, will use defaults only');
  }

  // Load local test overrides (for sensitive values)
  const localEnvPath = path.resolve(process.cwd(), '.env.test.local');
  if (existsSync(localEnvPath)) {
    dotenv.config({ path: localEnvPath, override: true });
  }

  // Set test-specific defaults (only if not already set)
  setTestDefaults();

  // Validate required environment variables
  validateTestEnvironment();
}

/**
 * Set default values for test environment variables
 */
function setTestDefaults(): void {
  // Ensure we're in test mode
  process.env.NODE_ENV = 'test';
  process.env.TEST_ENVIRONMENT = 'true';
  
  // Set defaults if not already set
  const defaults: Record<string, string> = {
    // Database
    NODE_DB_PATH: ':memory:',
    REBUILD_ON_START: 'false',
    
    // API
    N8N_API_URL: 'http://localhost:3001/mock-api',
    N8N_API_KEY: 'test-api-key-12345',
    
    // Server
    PORT: '3001',
    HOST: '127.0.0.1',
    
    // Logging
    LOG_LEVEL: 'error',
    DEBUG: 'false',
    TEST_LOG_VERBOSE: 'false',
    
    // Timeouts
    TEST_TIMEOUT_UNIT: '5000',
    TEST_TIMEOUT_INTEGRATION: '15000',
    TEST_TIMEOUT_E2E: '30000',
    TEST_TIMEOUT_GLOBAL: '30000', // Reduced from 60s to 30s to catch hangs faster
    
    // Test execution
    TEST_RETRY_ATTEMPTS: '2',
    TEST_RETRY_DELAY: '1000',
    TEST_PARALLEL: 'true',
    TEST_MAX_WORKERS: '4',
    
    // Features
    FEATURE_MOCK_EXTERNAL_APIS: 'true',
    FEATURE_USE_TEST_CONTAINERS: 'false',
    MSW_ENABLED: 'true',
    MSW_API_DELAY: '0',
    
    // Paths
    TEST_FIXTURES_PATH: './tests/fixtures',
    TEST_DATA_PATH: './tests/data',
    TEST_SNAPSHOTS_PATH: './tests/__snapshots__',
    
    // Performance
    PERF_THRESHOLD_API_RESPONSE: '100',
    PERF_THRESHOLD_DB_QUERY: '50',
    PERF_THRESHOLD_NODE_PARSE: '200',
    
    // Caching
    CACHE_TTL: '0',
    CACHE_ENABLED: 'false',
    
    // Rate limiting
    RATE_LIMIT_MAX: '0',
    RATE_LIMIT_WINDOW: '0',
    
    // Error handling
    ERROR_SHOW_STACK: 'true',
    ERROR_SHOW_DETAILS: 'true',
    
    // Cleanup
    TEST_CLEANUP_ENABLED: 'true',
    TEST_CLEANUP_ON_FAILURE: 'false',
    
    // Database seeding
    TEST_SEED_DATABASE: 'true',
    TEST_SEED_TEMPLATES: 'true',
    
    // Network
    NETWORK_TIMEOUT: '5000',
    NETWORK_RETRY_COUNT: '0',
    
    // Memory
    TEST_MEMORY_LIMIT: '512',
    
    // Coverage
    COVERAGE_DIR: './coverage',
    COVERAGE_REPORTER: 'lcov,html,text-summary'
  };

  for (const [key, value] of Object.entries(defaults)) {
    if (!process.env[key]) {
      process.env[key] = value;
    }
  }
}

/**
 * Validate that required environment variables are set
 */
function validateTestEnvironment(): void {
  const required = [
    'NODE_ENV',
    'NODE_DB_PATH',
    'N8N_API_URL',
    'N8N_API_KEY'
  ];

  const missing = required.filter(key => !process.env[key]);
  
  if (missing.length > 0) {
    throw new Error(
      `Missing required test environment variables: ${missing.join(', ')}\n` +
      'Please ensure .env.test is properly configured.'
    );
  }

  // Validate NODE_ENV is set to test
  if (process.env.NODE_ENV !== 'test') {
    throw new Error(
      'NODE_ENV must be set to "test" when running tests.\n' +
      'This prevents accidental execution against production systems.'
    );
  }
}

/**
 * Get typed test environment configuration
 */
export function getTestConfig() {
  // Ensure defaults are set before accessing
  if (!process.env.N8N_API_URL) {
    setTestDefaults();
  }
  
  return {
    // Environment
    nodeEnv: process.env.NODE_ENV || 'test',
    isTest: process.env.TEST_ENVIRONMENT === 'true',
    
    // Database
    database: {
      path: process.env.NODE_DB_PATH || ':memory:',
      rebuildOnStart: process.env.REBUILD_ON_START === 'true',
      seedData: process.env.TEST_SEED_DATABASE === 'true',
      seedTemplates: process.env.TEST_SEED_TEMPLATES === 'true'
    },
    
    // API
    api: {
      url: process.env.N8N_API_URL || 'http://localhost:3001/mock-api',
      key: process.env.N8N_API_KEY || 'test-api-key-12345',
      webhookBaseUrl: process.env.N8N_WEBHOOK_BASE_URL,
      webhookTestUrl: process.env.N8N_WEBHOOK_TEST_URL
    },
    
    // Server
    server: {
      port: parseInt(process.env.PORT || '3001', 10),
      host: process.env.HOST || '127.0.0.1',
      corsOrigin: process.env.CORS_ORIGIN?.split(',') || []
    },
    
    // Authentication
    auth: {
      token: process.env.AUTH_TOKEN,
      mcpToken: process.env.MCP_AUTH_TOKEN
    },
    
    // Logging
    logging: {
      level: process.env.LOG_LEVEL || 'error',
      debug: process.env.DEBUG === 'true',
      verbose: process.env.TEST_LOG_VERBOSE === 'true',
      showStack: process.env.ERROR_SHOW_STACK === 'true',
      showDetails: process.env.ERROR_SHOW_DETAILS === 'true'
    },
    
    // Test execution
    execution: {
      timeouts: {
        unit: parseInt(process.env.TEST_TIMEOUT_UNIT || '5000', 10),
        integration: parseInt(process.env.TEST_TIMEOUT_INTEGRATION || '15000', 10),
        e2e: parseInt(process.env.TEST_TIMEOUT_E2E || '30000', 10),
        global: parseInt(process.env.TEST_TIMEOUT_GLOBAL || '60000', 10)
      },
      retry: {
        attempts: parseInt(process.env.TEST_RETRY_ATTEMPTS || '2', 10),
        delay: parseInt(process.env.TEST_RETRY_DELAY || '1000', 10)
      },
      parallel: process.env.TEST_PARALLEL === 'true',
      maxWorkers: parseInt(process.env.TEST_MAX_WORKERS || '4', 10)
    },
    
    // Features
    features: {
      coverage: process.env.FEATURE_TEST_COVERAGE === 'true',
      screenshots: process.env.FEATURE_TEST_SCREENSHOTS === 'true',
      videos: process.env.FEATURE_TEST_VIDEOS === 'true',
      trace: process.env.FEATURE_TEST_TRACE === 'true',
      mockExternalApis: process.env.FEATURE_MOCK_EXTERNAL_APIS === 'true',
      useTestContainers: process.env.FEATURE_USE_TEST_CONTAINERS === 'true'
    },
    
    // Mocking
    mocking: {
      msw: {
        enabled: process.env.MSW_ENABLED === 'true',
        apiDelay: parseInt(process.env.MSW_API_DELAY || '0', 10)
      },
      redis: {
        enabled: process.env.REDIS_MOCK_ENABLED === 'true',
        port: parseInt(process.env.REDIS_MOCK_PORT || '6380', 10)
      },
      elasticsearch: {
        enabled: process.env.ELASTICSEARCH_MOCK_ENABLED === 'true',
        port: parseInt(process.env.ELASTICSEARCH_MOCK_PORT || '9201', 10)
      }
    },
    
    // Paths
    paths: {
      fixtures: process.env.TEST_FIXTURES_PATH || './tests/fixtures',
      data: process.env.TEST_DATA_PATH || './tests/data',
      snapshots: process.env.TEST_SNAPSHOTS_PATH || './tests/__snapshots__'
    },
    
    // Performance
    performance: {
      thresholds: {
        apiResponse: parseInt(process.env.PERF_THRESHOLD_API_RESPONSE || '100', 10),
        dbQuery: parseInt(process.env.PERF_THRESHOLD_DB_QUERY || '50', 10),
        nodeParse: parseInt(process.env.PERF_THRESHOLD_NODE_PARSE || '200', 10)
      }
    },
    
    // Rate limiting
    rateLimiting: {
      max: parseInt(process.env.RATE_LIMIT_MAX || '0', 10),
      window: parseInt(process.env.RATE_LIMIT_WINDOW || '0', 10)
    },
    
    // Caching
    cache: {
      enabled: process.env.CACHE_ENABLED === 'true',
      ttl: parseInt(process.env.CACHE_TTL || '0', 10)
    },
    
    // Cleanup
    cleanup: {
      enabled: process.env.TEST_CLEANUP_ENABLED === 'true',
      onFailure: process.env.TEST_CLEANUP_ON_FAILURE === 'true'
    },
    
    // Network
    network: {
      timeout: parseInt(process.env.NETWORK_TIMEOUT || '5000', 10),
      retryCount: parseInt(process.env.NETWORK_RETRY_COUNT || '0', 10)
    },
    
    // Memory
    memory: {
      limit: parseInt(process.env.TEST_MEMORY_LIMIT || '512', 10)
    },
    
    // Coverage
    coverage: {
      dir: process.env.COVERAGE_DIR || './coverage',
      reporters: (process.env.COVERAGE_REPORTER || 'lcov,html,text-summary').split(',')
    }
  };
}

// Export type for the test configuration
export type TestConfig = ReturnType<typeof getTestConfig>;

/**
 * Helper to check if we're in test mode
 */
export function isTestMode(): boolean {
  return process.env.NODE_ENV === 'test' || process.env.TEST_ENVIRONMENT === 'true';
}

/**
 * Helper to get timeout for specific test type
 */
export function getTestTimeout(type: 'unit' | 'integration' | 'e2e' | 'global' = 'unit'): number {
  const config = getTestConfig();
  return config.execution.timeouts[type];
}

/**
 * Helper to check if a feature is enabled
 */
export function isFeatureEnabled(feature: keyof TestConfig['features']): boolean {
  const config = getTestConfig();
  return config.features[feature];
}

/**
 * Reset environment to defaults (useful for test isolation)
 */
export function resetTestEnvironment(): void {
  // Clear all test-specific environment variables
  const testKeys = Object.keys(process.env).filter(key => 
    key.startsWith('TEST_') || 
    key.startsWith('FEATURE_') ||
    key.startsWith('MSW_') ||
    key.startsWith('PERF_')
  );
  
  testKeys.forEach(key => {
    delete process.env[key];
  });
  
  // Reload defaults
  loadTestEnvironment();
}
```

--------------------------------------------------------------------------------
/docs/RAILWAY_DEPLOYMENT.md:
--------------------------------------------------------------------------------

```markdown
# Railway Deployment Guide for n8n-MCP

Deploy n8n-MCP to Railway's cloud platform with zero configuration and connect it to Claude Desktop from anywhere.

## 🚀 Quick Deploy

Deploy n8n-MCP with one click:

[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/VY6UOG?referralCode=n8n-mcp)

## 📋 Overview

Railway deployment provides:
- ☁️ **Instant cloud hosting** - No server setup required
- 🔒 **Secure by default** - HTTPS included, auth token warnings
- 🌐 **Global access** - Connect from any Claude Desktop
- ⚡ **Auto-scaling** - Railway handles the infrastructure
- 📊 **Built-in monitoring** - Logs and metrics included

## 🎯 Step-by-Step Deployment

### 1. Deploy to Railway

1. **Click the Deploy button** above
2. **Sign in to Railway** (or create account)
3. **Configure your deployment**:
   - Project name (optional)
   - Environment (leave as "production")
   - Region (choose closest to you)
4. **Click "Deploy"** and wait ~2-3 minutes

### 2. Configure Security

**IMPORTANT**: The deployment includes a default AUTH_TOKEN for instant functionality, but you MUST change it:

![Railway Dashboard - Variables Tab](./img/railway-variables.png)

1. **Go to your Railway dashboard**
2. **Click on your n8n-mcp service**
3. **Navigate to "Variables" tab**
4. **Find `AUTH_TOKEN`** 
5. **Replace with secure token**:
   ```bash
   # Generate secure token locally:
   openssl rand -base64 32
   ```
6. **Railway will automatically redeploy** with the new token

> ⚠️ **Security Warning**: The server displays warnings every 5 minutes until you change the default token!

### 3. Get Your Service URL

![Railway Dashboard - Domain Settings](./img/railway-domain.png)

1. In Railway dashboard, click on your service
2. Go to **"Settings"** tab
3. Under **"Domains"**, you'll see your URL:
   ```
   https://your-app-name.up.railway.app
   ```
4. Copy this URL for Claude Desktop configuration and add /mcp at the end

### 4. Connect Claude Desktop

Add to your Claude Desktop configuration:

```json
{
  "mcpServers": {
    "n8n-railway": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://your-app-name.up.railway.app/mcp",
        "--header",
        "Authorization: Bearer YOUR_SECURE_TOKEN_HERE"
      ]
    }
  }
}
```

**Configuration file locations:**
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
- **Linux**: `~/.config/Claude/claude_desktop_config.json`

**Restart Claude Desktop** after saving the configuration.

## 🔧 Environment Variables

### Default Variables (Pre-configured)

These are automatically set by the Railway template:

| Variable | Default Value | Description |
|----------|--------------|-------------|
| `AUTH_TOKEN` | `REPLACE_THIS...` | **⚠️ CHANGE IMMEDIATELY** |
| `MCP_MODE` | `http` | Required for cloud deployment |
| `USE_FIXED_HTTP` | `true` | Stable HTTP implementation |
| `NODE_ENV` | `production` | Production optimizations |
| `LOG_LEVEL` | `info` | Balanced logging |
| `TRUST_PROXY` | `1` | Railway runs behind proxy |
| `CORS_ORIGIN` | `*` | Allow any origin |
| `HOST` | `0.0.0.0` | Listen on all interfaces |
| `PORT` | (Railway provides) | Don't set manually |
| `AUTH_RATE_LIMIT_WINDOW` | `900000` (15 min) | Rate limit window (v2.16.3+) |
| `AUTH_RATE_LIMIT_MAX` | `20` | Max auth attempts (v2.16.3+) |
| `WEBHOOK_SECURITY_MODE` | `strict` | SSRF protection mode (v2.16.3+) |

### Optional Variables

| Variable | Default Value | Description |
|----------|--------------|-------------|
| `N8N_MODE` | `false` | Enable n8n integration mode for MCP Client Tool |
| `N8N_API_URL` | - | URL of your n8n instance (for workflow management) |
| `N8N_API_KEY` | - | API key from n8n Settings → API |

### Optional: n8n Integration

#### For n8n MCP Client Tool Integration

To use n8n-MCP with n8n's MCP Client Tool node:

1. **Go to Railway dashboard** → Your service → **Variables**
2. **Add this variable**:
   - `N8N_MODE`: Set to `true` to enable n8n integration mode
3. **Save changes** - Railway will redeploy automatically

#### For n8n API Integration (Workflow Management)

To enable workflow management features:

1. **Go to Railway dashboard** → Your service → **Variables**
2. **Add these variables**:
   - `N8N_API_URL`: Your n8n instance URL (e.g., `https://n8n.example.com`)
   - `N8N_API_KEY`: API key from n8n Settings → API
3. **Save changes** - Railway will redeploy automatically

## 🏗️ Architecture Details

### How It Works

```
Claude Desktop → mcp-remote → Railway (HTTPS) → n8n-MCP Server
```

1. **Claude Desktop** uses `mcp-remote` as a bridge
2. **mcp-remote** converts stdio to HTTP requests
3. **Railway** provides HTTPS endpoint and infrastructure
4. **n8n-MCP** runs in HTTP mode on Railway

### Single-Instance Design

**Important**: The n8n-MCP HTTP server is designed for single n8n instance deployment:
- n8n API credentials are configured server-side via environment variables
- All clients connecting to the server share the same n8n instance
- For multi-tenant usage, deploy separate Railway instances

### Security Model

- **Bearer Token Authentication**: All requests require the AUTH_TOKEN
- **HTTPS by Default**: Railway provides SSL certificates
- **Environment Isolation**: Each deployment is isolated
- **No State Storage**: Server is stateless (database is read-only)

## 🚨 Troubleshooting

### Connection Issues

**"Invalid URL" error in Claude Desktop:**
- Ensure you're using the exact configuration format shown above
- Don't add "connect" or other arguments before the URL
- The URL should end with `/mcp`

**"Unauthorized" error:**
- Check that your AUTH_TOKEN matches exactly (no extra spaces)
- Ensure the Authorization header format is correct: `Authorization: Bearer TOKEN`

**"Cannot connect to server":**
- Verify your Railway deployment is running (check Railway dashboard)
- Ensure the URL is correct and includes `https://`
- Check Railway logs for any errors

**Windows: "The filename, directory name, or volume label syntax is incorrect" or npx command not found:**

This is a common Windows issue with spaces in Node.js installation paths. The error occurs because Claude Desktop can't properly execute npx.

**Solution 1: Use node directly (Recommended)**
```json
{
  "mcpServers": {
    "n8n-railway": {
      "command": "node",
      "args": [
        "C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npx-cli.js",
        "-y",
        "mcp-remote",
        "https://your-app-name.up.railway.app/mcp",
        "--header",
        "Authorization: Bearer YOUR_SECURE_TOKEN_HERE"
      ]
    }
  }
}
```

**Solution 2: Use cmd wrapper**
```json
{
  "mcpServers": {
    "n8n-railway": {
      "command": "cmd",
      "args": [
        "/C",
        "\"C:\\Program Files\\nodejs\\npx\" -y mcp-remote https://your-app-name.up.railway.app/mcp --header \"Authorization: Bearer YOUR_SECURE_TOKEN_HERE\""
      ]
    }
  }
}
```

To find your exact npx path, open Command Prompt and run: `where npx`

### Railway-Specific Issues

**Build failures:**
- Railway uses AMD64 architecture - the template is configured for this
- Check build logs in Railway dashboard for specific errors

**Environment variable issues:**
- Variables are case-sensitive
- Don't include quotes in the Railway dashboard (only in JSON config)
- Railway automatically restarts when you change variables

**Domain not working:**
- It may take 1-2 minutes for the domain to become active
- Check the "Deployments" tab to ensure the latest deployment succeeded

## 📊 Monitoring & Logs

### View Logs

1. Go to Railway dashboard
2. Click on your n8n-mcp service
3. Click on **"Logs"** tab
4. You'll see real-time logs including:
   - Server startup messages
   - Authentication attempts
   - API requests (without sensitive data)
   - Any errors or warnings

### Monitor Usage

Railway provides metrics for:
- **Memory usage** (typically ~100-200MB)
- **CPU usage** (minimal when idle)
- **Network traffic**
- **Response times**

## 💰 Pricing & Limits

### Railway Free Tier
- **$5 free credit** monthly
- **500 hours** of runtime
- **Sufficient for personal use** of n8n-MCP

### Estimated Costs
- **n8n-MCP typically uses**: ~0.1 GB RAM
- **Monthly cost**: ~$2-3 for 24/7 operation
- **Well within free tier** for most users

## 🔄 Updates & Maintenance

### Manual Updates

Since the Railway template uses a specific Docker image tag, updates are manual:

1. **Check for updates** on [GitHub](https://github.com/czlonkowski/n8n-mcp)
2. **Update image tag** in Railway:
   - Go to Settings → Deploy → Docker Image
   - Change tag from current to new version
   - Click "Redeploy"

### Automatic Updates (Not Recommended)

You could use the `latest` tag, but this may cause unexpected breaking changes.

## 🔒 Security Features (v2.16.3+)

Railway deployments include enhanced security features:

### Rate Limiting
- **Automatic brute force protection** - 20 attempts per 15 minutes per IP
- **Configurable limits** via `AUTH_RATE_LIMIT_WINDOW` and `AUTH_RATE_LIMIT_MAX`
- **Standard rate limit headers** for client awareness

### SSRF Protection
- **Default strict mode** blocks localhost, private IPs, and cloud metadata
- **Cloud metadata always blocked** (169.254.169.254, metadata.google.internal, etc.)
- **Use `moderate` mode only if** connecting to local n8n instance

**Security Configuration:**
```bash
# In Railway Variables tab:
WEBHOOK_SECURITY_MODE=strict          # Production (recommended)
# or
WEBHOOK_SECURITY_MODE=moderate        # If using local n8n with port forwarding

# Rate limiting (defaults are good for most use cases)
AUTH_RATE_LIMIT_WINDOW=900000         # 15 minutes
AUTH_RATE_LIMIT_MAX=20                # 20 attempts per IP
```

## 📝 Best Practices

1. **Always change the default AUTH_TOKEN immediately**
2. **Use strong, unique tokens** (32+ characters)
3. **Monitor logs** for unauthorized access attempts
4. **Keep credentials secure** - never commit them to git
5. **Use environment variables** for all sensitive data
6. **Regular updates** - check for new versions monthly

## 🆘 Getting Help

- **Railway Documentation**: [docs.railway.app](https://docs.railway.app)
- **n8n-MCP Issues**: [GitHub Issues](https://github.com/czlonkowski/n8n-mcp/issues)
- **Railway Community**: [Discord](https://discord.gg/railway)

## 🎉 Success!

Once connected, you can use all n8n-MCP features from Claude Desktop:
- Search and explore 500+ n8n nodes
- Get node configurations and examples
- Validate workflows before deployment
- Manage n8n workflows (if API configured)

The cloud deployment means you can access your n8n knowledge base from any computer with Claude Desktop installed!
```

--------------------------------------------------------------------------------
/docs/MCP_QUICK_START_GUIDE.md:
--------------------------------------------------------------------------------

```markdown
# MCP Implementation Quick Start Guide

## Immediate Actions (Day 1)

### 1. Create Essential Properties Configuration

Create `src/data/essential-properties.json`:
```json
{
  "nodes-base.httpRequest": {
    "required": ["url"],
    "common": ["method", "authentication", "sendBody", "contentType", "sendHeaders"],
    "examples": {
      "minimal": {
        "url": "https://api.example.com/data"
      },
      "getWithAuth": {
        "method": "GET",
        "url": "https://api.example.com/protected",
        "authentication": "genericCredentialType",
        "genericAuthType": "headerAuth"
      },
      "postJson": {
        "method": "POST",
        "url": "https://api.example.com/create",
        "sendBody": true,
        "contentType": "json",
        "jsonBody": "{ \"name\": \"example\" }"
      }
    }
  },
  "nodes-base.webhook": {
    "required": [],
    "common": ["path", "method", "responseMode", "responseData"],
    "examples": {
      "minimal": {
        "path": "webhook",
        "method": "POST"
      }
    }
  }
}
```

### 2. Implement get_node_essentials Tool

Add to `src/mcp/server.ts`:

```typescript
// Add to tool implementations
case "get_node_essentials": {
  const { nodeType } = request.params.arguments as { nodeType: string };
  
  // Load essential properties config
  const essentialsConfig = require('../data/essential-properties.json');
  const nodeConfig = essentialsConfig[nodeType];
  
  if (!nodeConfig) {
    // Fallback: extract from existing data
    const node = await service.getNodeByType(nodeType);
    if (!node) {
      return { error: `Node type ${nodeType} not found` };
    }
    
    // Parse properties to find required ones
    const properties = JSON.parse(node.properties_schema || '[]');
    const required = properties.filter((p: any) => p.required);
    const common = properties.slice(0, 5); // Top 5 as fallback
    
    return {
      nodeType,
      displayName: node.display_name,
      description: node.description,
      requiredProperties: required.map(simplifyProperty),
      commonProperties: common.map(simplifyProperty),
      examples: {
        minimal: {},
        common: {}
      }
    };
  }
  
  // Use configured essentials
  const node = await service.getNodeByType(nodeType);
  const properties = JSON.parse(node.properties_schema || '[]');
  
  const requiredProps = nodeConfig.required.map((name: string) => {
    const prop = findPropertyByName(properties, name);
    return prop ? simplifyProperty(prop) : null;
  }).filter(Boolean);
  
  const commonProps = nodeConfig.common.map((name: string) => {
    const prop = findPropertyByName(properties, name);
    return prop ? simplifyProperty(prop) : null;
  }).filter(Boolean);
  
  return {
    nodeType,
    displayName: node.display_name,
    description: node.description,
    requiredProperties: requiredProps,
    commonProperties: commonProps,
    examples: nodeConfig.examples || {}
  };
}

// Helper functions
function simplifyProperty(prop: any) {
  return {
    name: prop.name,
    type: prop.type,
    description: prop.description || prop.displayName || '',
    default: prop.default,
    options: prop.options?.map((opt: any) => 
      typeof opt === 'string' ? opt : opt.value
    ),
    placeholder: prop.placeholder
  };
}

function findPropertyByName(properties: any[], name: string): any {
  for (const prop of properties) {
    if (prop.name === name) return prop;
    // Check in nested collections
    if (prop.type === 'collection' && prop.options) {
      const found = findPropertyByName(prop.options, name);
      if (found) return found;
    }
  }
  return null;
}
```

### 3. Add Tool Definition

Add to tool definitions:

```typescript
{
  name: "get_node_essentials",
  description: "Get only essential and commonly-used properties for a node - perfect for quick configuration",
  inputSchema: {
    type: "object",
    properties: {
      nodeType: {
        type: "string",
        description: "The node type (e.g., 'nodes-base.httpRequest')"
      }
    },
    required: ["nodeType"]
  }
}
```

### 4. Create Property Parser Service

Create `src/services/property-parser.ts`:

```typescript
export class PropertyParser {
  /**
   * Parse nested properties and flatten to searchable format
   */
  static parseProperties(properties: any[], path = ''): ParsedProperty[] {
    const results: ParsedProperty[] = [];
    
    for (const prop of properties) {
      const currentPath = path ? `${path}.${prop.name}` : prop.name;
      
      // Add current property
      results.push({
        name: prop.name,
        path: currentPath,
        type: prop.type,
        description: prop.description || prop.displayName || '',
        required: prop.required || false,
        displayConditions: prop.displayOptions,
        default: prop.default,
        options: prop.options?.filter((opt: any) => typeof opt === 'string' || opt.value)
      });
      
      // Recursively parse nested properties
      if (prop.type === 'collection' && prop.options) {
        results.push(...this.parseProperties(prop.options, currentPath));
      } else if (prop.type === 'fixedCollection' && prop.options) {
        for (const option of prop.options) {
          if (option.values) {
            results.push(...this.parseProperties(option.values, `${currentPath}.${option.name}`));
          }
        }
      }
    }
    
    return results;
  }
  
  /**
   * Find properties matching a search query
   */
  static searchProperties(properties: ParsedProperty[], query: string): ParsedProperty[] {
    const lowerQuery = query.toLowerCase();
    return properties.filter(prop => 
      prop.name.toLowerCase().includes(lowerQuery) ||
      prop.description.toLowerCase().includes(lowerQuery) ||
      prop.path.toLowerCase().includes(lowerQuery)
    );
  }
  
  /**
   * Categorize properties
   */
  static categorizeProperties(properties: ParsedProperty[]): CategorizedProperties {
    const categories: CategorizedProperties = {
      authentication: [],
      request: [],
      response: [],
      advanced: [],
      other: []
    };
    
    for (const prop of properties) {
      if (prop.name.includes('auth') || prop.name.includes('credential')) {
        categories.authentication.push(prop);
      } else if (prop.name.includes('body') || prop.name.includes('header') || 
                 prop.name.includes('query') || prop.name.includes('url')) {
        categories.request.push(prop);
      } else if (prop.name.includes('response') || prop.name.includes('output')) {
        categories.response.push(prop);
      } else if (prop.path.includes('options.')) {
        categories.advanced.push(prop);
      } else {
        categories.other.push(prop);
      }
    }
    
    return categories;
  }
}

interface ParsedProperty {
  name: string;
  path: string;
  type: string;
  description: string;
  required: boolean;
  displayConditions?: any;
  default?: any;
  options?: any[];
}

interface CategorizedProperties {
  authentication: ParsedProperty[];
  request: ParsedProperty[];
  response: ParsedProperty[];
  advanced: ParsedProperty[];
  other: ParsedProperty[];
}
```

### 5. Quick Test Script

Create `scripts/test-essentials.ts`:

```typescript
import { MCPClient } from '../src/mcp/client';

async function testEssentials() {
  const client = new MCPClient();
  
  console.log('Testing get_node_essentials...\n');
  
  // Test HTTP Request node
  const httpEssentials = await client.call('get_node_essentials', {
    nodeType: 'nodes-base.httpRequest'
  });
  
  console.log('HTTP Request Essentials:');
  console.log(`- Required: ${httpEssentials.requiredProperties.map(p => p.name).join(', ')}`);
  console.log(`- Common: ${httpEssentials.commonProperties.map(p => p.name).join(', ')}`);
  console.log(`- Total properties: ${httpEssentials.requiredProperties.length + httpEssentials.commonProperties.length}`);
  
  // Compare with full response
  const fullInfo = await client.call('get_node_info', {
    nodeType: 'nodes-base.httpRequest'
  });
  
  const fullSize = JSON.stringify(fullInfo).length;
  const essentialSize = JSON.stringify(httpEssentials).length;
  
  console.log(`\nSize comparison:`);
  console.log(`- Full response: ${(fullSize / 1024).toFixed(1)}KB`);
  console.log(`- Essential response: ${(essentialSize / 1024).toFixed(1)}KB`);
  console.log(`- Reduction: ${((1 - essentialSize / fullSize) * 100).toFixed(1)}%`);
}

testEssentials().catch(console.error);
```

## Day 2-3: Implement search_node_properties

```typescript
case "search_node_properties": {
  const { nodeType, query } = request.params.arguments as { 
    nodeType: string; 
    query: string;
  };
  
  const node = await service.getNodeByType(nodeType);
  if (!node) {
    return { error: `Node type ${nodeType} not found` };
  }
  
  const properties = JSON.parse(node.properties_schema || '[]');
  const parsed = PropertyParser.parseProperties(properties);
  const matches = PropertyParser.searchProperties(parsed, query);
  
  return {
    query,
    matches: matches.map(prop => ({
      name: prop.name,
      type: prop.type,
      path: prop.path,
      description: prop.description,
      visibleWhen: prop.displayConditions?.show
    })),
    totalMatches: matches.length
  };
}
```

## Day 4-5: Implement get_node_for_task

Create `src/data/task-templates.json`:

```json
{
  "post_json_request": {
    "description": "Make a POST request with JSON data",
    "nodeType": "nodes-base.httpRequest",
    "configuration": {
      "method": "POST",
      "url": "",
      "sendBody": true,
      "contentType": "json",
      "specifyBody": "json",
      "jsonBody": ""
    },
    "userMustProvide": [
      { "property": "url", "description": "API endpoint URL" },
      { "property": "jsonBody", "description": "JSON data to send" }
    ],
    "optionalEnhancements": [
      { "property": "authentication", "description": "Add authentication if required" },
      { "property": "sendHeaders", "description": "Add custom headers" }
    ]
  }
}
```

## Testing Checklist

- [ ] Test get_node_essentials with HTTP Request node
- [ ] Verify size reduction is >90%
- [ ] Test with Webhook, Agent, and Code nodes
- [ ] Validate examples work correctly
- [ ] Test property search functionality
- [ ] Verify task templates are valid
- [ ] Check backward compatibility
- [ ] Measure response times (<100ms)

## Success Indicators

1. **Immediate (Day 1)**:
   - get_node_essentials returns <5KB for HTTP Request
   - Response includes working examples
   - No errors with top 10 nodes

2. **Week 1**:
   - 90% reduction in response size
   - Property search working
   - 5+ task templates created
   - Positive AI agent feedback

3. **Month 1**:
   - All tools implemented
   - 50+ nodes optimized
   - Configuration time <1 minute
   - Error rate <10%
```

--------------------------------------------------------------------------------
/tests/integration/n8n-api/executions/trigger-webhook.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Integration Tests: handleTriggerWebhookWorkflow
 *
 * Tests webhook triggering against a real n8n instance.
 * Covers all HTTP methods, request data, headers, and error handling.
 */

import { describe, it, expect, beforeEach } from 'vitest';
import { createMcpContext } from '../utils/mcp-context';
import { InstanceContext } from '../../../../src/types/instance-context';
import { handleTriggerWebhookWorkflow } from '../../../../src/mcp/handlers-n8n-manager';
import { getN8nCredentials } from '../utils/credentials';

describe('Integration: handleTriggerWebhookWorkflow', () => {
  let mcpContext: InstanceContext;
  let webhookUrls: {
    get: string;
    post: string;
    put: string;
    delete: string;
  };

  beforeEach(() => {
    mcpContext = createMcpContext();
    const creds = getN8nCredentials();
    webhookUrls = creds.webhookUrls;
  });

  // ======================================================================
  // GET Method Tests
  // ======================================================================

  describe('GET Method', () => {
    it('should trigger GET webhook without data', async () => {
      const response = await handleTriggerWebhookWorkflow(
        {
          webhookUrl: webhookUrls.get,
          httpMethod: 'GET'
        },
        mcpContext
      );

      expect(response.success).toBe(true);
      expect(response.data).toBeDefined();
      expect(response.message).toContain('Webhook triggered successfully');
    });

    it('should trigger GET webhook with query parameters', async () => {
      // GET method uses query parameters in URL
      const urlWithParams = `${webhookUrls.get}?testParam=value&number=42`;

      const response = await handleTriggerWebhookWorkflow(
        {
          webhookUrl: urlWithParams,
          httpMethod: 'GET'
        },
        mcpContext
      );

      expect(response.success).toBe(true);
      expect(response.data).toBeDefined();
    });

    it('should trigger GET webhook with custom headers', async () => {
      const response = await handleTriggerWebhookWorkflow(
        {
          webhookUrl: webhookUrls.get,
          httpMethod: 'GET',
          headers: {
            'X-Custom-Header': 'test-value',
            'X-Request-Id': '12345'
          }
        },
        mcpContext
      );

      expect(response.success).toBe(true);
      expect(response.data).toBeDefined();
    });

    it('should trigger GET webhook and wait for response', async () => {
      const response = await handleTriggerWebhookWorkflow(
        {
          webhookUrl: webhookUrls.get,
          httpMethod: 'GET',
          waitForResponse: true
        },
        mcpContext
      );

      expect(response.success).toBe(true);
      expect(response.data).toBeDefined();
      // Response should contain workflow execution data
    });
  });

  // ======================================================================
  // POST Method Tests
  // ======================================================================

  describe('POST Method', () => {
    it('should trigger POST webhook with JSON data', async () => {
      const response = await handleTriggerWebhookWorkflow(
        {
          webhookUrl: webhookUrls.post,
          httpMethod: 'POST',
          data: {
            message: 'Test webhook trigger',
            timestamp: Date.now(),
            nested: {
              value: 'nested data'
            }
          }
        },
        mcpContext
      );

      expect(response.success).toBe(true);
      expect(response.data).toBeDefined();
    });

    it('should trigger POST webhook without data', async () => {
      const response = await handleTriggerWebhookWorkflow(
        {
          webhookUrl: webhookUrls.post,
          httpMethod: 'POST'
        },
        mcpContext
      );

      expect(response.success).toBe(true);
      expect(response.data).toBeDefined();
    });

    it('should trigger POST webhook with custom headers', async () => {
      const response = await handleTriggerWebhookWorkflow(
        {
          webhookUrl: webhookUrls.post,
          httpMethod: 'POST',
          data: { test: 'data' },
          headers: {
            'Content-Type': 'application/json',
            'X-Api-Key': 'test-key'
          }
        },
        mcpContext
      );

      expect(response.success).toBe(true);
      expect(response.data).toBeDefined();
    });

    it('should trigger POST webhook without waiting for response', async () => {
      const response = await handleTriggerWebhookWorkflow(
        {
          webhookUrl: webhookUrls.post,
          httpMethod: 'POST',
          data: { async: true },
          waitForResponse: false
        },
        mcpContext
      );

      expect(response.success).toBe(true);
      // With waitForResponse: false, may return immediately
    });
  });

  // ======================================================================
  // PUT Method Tests
  // ======================================================================

  describe('PUT Method', () => {
    it('should trigger PUT webhook with update data', async () => {
      const response = await handleTriggerWebhookWorkflow(
        {
          webhookUrl: webhookUrls.put,
          httpMethod: 'PUT',
          data: {
            id: '123',
            updatedField: 'new value',
            timestamp: Date.now()
          }
        },
        mcpContext
      );

      expect(response.success).toBe(true);
      expect(response.data).toBeDefined();
    });

    it('should trigger PUT webhook with custom headers', async () => {
      const response = await handleTriggerWebhookWorkflow(
        {
          webhookUrl: webhookUrls.put,
          httpMethod: 'PUT',
          data: { update: true },
          headers: {
            'X-Update-Operation': 'modify',
            'If-Match': 'etag-value'
          }
        },
        mcpContext
      );

      expect(response.success).toBe(true);
      expect(response.data).toBeDefined();
    });

    it('should trigger PUT webhook without data', async () => {
      const response = await handleTriggerWebhookWorkflow(
        {
          webhookUrl: webhookUrls.put,
          httpMethod: 'PUT'
        },
        mcpContext
      );

      expect(response.success).toBe(true);
      expect(response.data).toBeDefined();
    });
  });

  // ======================================================================
  // DELETE Method Tests
  // ======================================================================

  describe('DELETE Method', () => {
    it('should trigger DELETE webhook with query parameters', async () => {
      const urlWithParams = `${webhookUrls.delete}?id=123&reason=test`;

      const response = await handleTriggerWebhookWorkflow(
        {
          webhookUrl: urlWithParams,
          httpMethod: 'DELETE'
        },
        mcpContext
      );

      expect(response.success).toBe(true);
      expect(response.data).toBeDefined();
    });

    it('should trigger DELETE webhook with custom headers', async () => {
      const response = await handleTriggerWebhookWorkflow(
        {
          webhookUrl: webhookUrls.delete,
          httpMethod: 'DELETE',
          headers: {
            'X-Delete-Reason': 'cleanup',
            'Authorization': 'Bearer token'
          }
        },
        mcpContext
      );

      expect(response.success).toBe(true);
      expect(response.data).toBeDefined();
    });

    it('should trigger DELETE webhook without parameters', async () => {
      const response = await handleTriggerWebhookWorkflow(
        {
          webhookUrl: webhookUrls.delete,
          httpMethod: 'DELETE'
        },
        mcpContext
      );

      expect(response.success).toBe(true);
      expect(response.data).toBeDefined();
    });
  });

  // ======================================================================
  // Error Handling
  // ======================================================================

  describe('Error Handling', () => {
    it('should handle invalid webhook URL', async () => {
      const response = await handleTriggerWebhookWorkflow(
        {
          webhookUrl: 'https://invalid-url.example.com/webhook/nonexistent',
          httpMethod: 'GET'
        },
        mcpContext
      );

      expect(response.success).toBe(false);
      expect(response.error).toBeDefined();
    });

    it('should handle malformed webhook URL', async () => {
      const response = await handleTriggerWebhookWorkflow(
        {
          webhookUrl: 'not-a-valid-url',
          httpMethod: 'GET'
        },
        mcpContext
      );

      expect(response.success).toBe(false);
      expect(response.error).toBeDefined();
    });

    it('should handle missing webhook URL', async () => {
      const response = await handleTriggerWebhookWorkflow(
        {
          httpMethod: 'GET'
        } as any,
        mcpContext
      );

      expect(response.success).toBe(false);
      expect(response.error).toBeDefined();
    });

    it('should handle invalid HTTP method', async () => {
      const response = await handleTriggerWebhookWorkflow(
        {
          webhookUrl: webhookUrls.get,
          httpMethod: 'INVALID' as any
        },
        mcpContext
      );

      expect(response.success).toBe(false);
      expect(response.error).toBeDefined();
    });
  });

  // ======================================================================
  // Default Method (POST)
  // ======================================================================

  describe('Default Method Behavior', () => {
    it('should default to POST method when not specified', async () => {
      const response = await handleTriggerWebhookWorkflow(
        {
          webhookUrl: webhookUrls.post,
          data: { defaultMethod: true }
        },
        mcpContext
      );

      expect(response.success).toBe(true);
      expect(response.data).toBeDefined();
    });
  });

  // ======================================================================
  // Response Format Verification
  // ======================================================================

  describe('Response Format', () => {
    it('should return complete webhook response structure', async () => {
      const response = await handleTriggerWebhookWorkflow(
        {
          webhookUrl: webhookUrls.get,
          httpMethod: 'GET',
          waitForResponse: true
        },
        mcpContext
      );

      expect(response.success).toBe(true);
      expect(response.data).toBeDefined();
      expect(response.message).toBeDefined();
      expect(response.message).toContain('Webhook triggered successfully');

      // Response data should be defined (either workflow output or execution info)
      expect(typeof response.data).not.toBe('undefined');
    });
  });
});

```
Page 11/45FirstPrevNextLast