This is page 5 of 67. Use http://codebase.md/czlonkowski/n8n-mcp?lines=true&page={x} to view the full context.
# Directory Structure
```
├── _config.yml
├── .claude
│ └── agents
│ ├── code-reviewer.md
│ ├── context-manager.md
│ ├── debugger.md
│ ├── deployment-engineer.md
│ ├── mcp-backend-engineer.md
│ ├── n8n-mcp-tester.md
│ ├── technical-researcher.md
│ └── test-automator.md
├── .dockerignore
├── .env.docker
├── .env.example
├── .env.n8n.example
├── .env.test
├── .env.test.example
├── .github
│ ├── ABOUT.md
│ ├── BENCHMARK_THRESHOLDS.md
│ ├── FUNDING.yml
│ ├── gh-pages.yml
│ ├── secret_scanning.yml
│ └── workflows
│ ├── benchmark-pr.yml
│ ├── benchmark.yml
│ ├── docker-build-fast.yml
│ ├── docker-build-n8n.yml
│ ├── docker-build.yml
│ ├── release.yml
│ ├── test.yml
│ └── update-n8n-deps.yml
├── .gitignore
├── .npmignore
├── ANALYSIS_QUICK_REFERENCE.md
├── ATTRIBUTION.md
├── CHANGELOG.md
├── CLAUDE.md
├── codecov.yml
├── coverage.json
├── data
│ ├── .gitkeep
│ ├── nodes.db
│ ├── nodes.db-shm
│ ├── nodes.db-wal
│ └── templates.db
├── deploy
│ └── quick-deploy-n8n.sh
├── docker
│ ├── docker-entrypoint.sh
│ ├── n8n-mcp
│ ├── parse-config.js
│ └── README.md
├── docker-compose.buildkit.yml
├── docker-compose.extract.yml
├── docker-compose.n8n.yml
├── docker-compose.override.yml.example
├── docker-compose.test-n8n.yml
├── docker-compose.yml
├── Dockerfile
├── Dockerfile.railway
├── Dockerfile.test
├── docs
│ ├── AUTOMATED_RELEASES.md
│ ├── BENCHMARKS.md
│ ├── CHANGELOG.md
│ ├── CI_TEST_INFRASTRUCTURE.md
│ ├── CLAUDE_CODE_SETUP.md
│ ├── CLAUDE_INTERVIEW.md
│ ├── CODECOV_SETUP.md
│ ├── CODEX_SETUP.md
│ ├── CURSOR_SETUP.md
│ ├── DEPENDENCY_UPDATES.md
│ ├── DOCKER_README.md
│ ├── DOCKER_TROUBLESHOOTING.md
│ ├── FINAL_AI_VALIDATION_SPEC.md
│ ├── FLEXIBLE_INSTANCE_CONFIGURATION.md
│ ├── HTTP_DEPLOYMENT.md
│ ├── img
│ │ ├── cc_command.png
│ │ ├── cc_connected.png
│ │ ├── codex_connected.png
│ │ ├── cursor_tut.png
│ │ ├── Railway_api.png
│ │ ├── Railway_server_address.png
│ │ ├── skills.png
│ │ ├── vsc_ghcp_chat_agent_mode.png
│ │ ├── vsc_ghcp_chat_instruction_files.png
│ │ ├── vsc_ghcp_chat_thinking_tool.png
│ │ └── windsurf_tut.png
│ ├── INSTALLATION.md
│ ├── LIBRARY_USAGE.md
│ ├── local
│ │ ├── DEEP_DIVE_ANALYSIS_2025-10-02.md
│ │ ├── DEEP_DIVE_ANALYSIS_README.md
│ │ ├── Deep_dive_p1_p2.md
│ │ ├── integration-testing-plan.md
│ │ ├── integration-tests-phase1-summary.md
│ │ ├── N8N_AI_WORKFLOW_BUILDER_ANALYSIS.md
│ │ ├── P0_IMPLEMENTATION_PLAN.md
│ │ └── TEMPLATE_MINING_ANALYSIS.md
│ ├── MCP_ESSENTIALS_README.md
│ ├── MCP_QUICK_START_GUIDE.md
│ ├── N8N_DEPLOYMENT.md
│ ├── RAILWAY_DEPLOYMENT.md
│ ├── README_CLAUDE_SETUP.md
│ ├── README.md
│ ├── SESSION_PERSISTENCE.md
│ ├── tools-documentation-usage.md
│ ├── TYPE_STRUCTURE_VALIDATION.md
│ ├── VS_CODE_PROJECT_SETUP.md
│ ├── WINDSURF_SETUP.md
│ └── workflow-diff-examples.md
├── examples
│ └── enhanced-documentation-demo.js
├── fetch_log.txt
├── LICENSE
├── MEMORY_N8N_UPDATE.md
├── MEMORY_TEMPLATE_UPDATE.md
├── monitor_fetch.sh
├── N8N_HTTP_STREAMABLE_SETUP.md
├── n8n-nodes.db
├── P0-R3-TEST-PLAN.md
├── package-lock.json
├── package.json
├── package.runtime.json
├── PRIVACY.md
├── railway.json
├── README_ANALYSIS.md
├── README.md
├── renovate.json
├── scripts
│ ├── analyze-optimization.sh
│ ├── audit-schema-coverage.ts
│ ├── backfill-mutation-hashes.ts
│ ├── build-optimized.sh
│ ├── compare-benchmarks.js
│ ├── demo-optimization.sh
│ ├── deploy-http.sh
│ ├── deploy-to-vm.sh
│ ├── export-webhook-workflows.ts
│ ├── extract-changelog.js
│ ├── extract-from-docker.js
│ ├── extract-nodes-docker.sh
│ ├── extract-nodes-simple.sh
│ ├── format-benchmark-results.js
│ ├── generate-benchmark-stub.js
│ ├── generate-detailed-reports.js
│ ├── generate-initial-release-notes.js
│ ├── generate-release-notes.js
│ ├── generate-test-summary.js
│ ├── http-bridge.js
│ ├── mcp-http-client.js
│ ├── migrate-nodes-fts.ts
│ ├── migrate-tool-docs.ts
│ ├── n8n-docs-mcp.service
│ ├── nginx-n8n-mcp.conf
│ ├── prebuild-fts5.ts
│ ├── prepare-release.js
│ ├── process-batch-metadata.ts
│ ├── publish-npm-quick.sh
│ ├── publish-npm.sh
│ ├── quick-test.ts
│ ├── run-benchmarks-ci.js
│ ├── sync-runtime-version.js
│ ├── test-ai-validation-debug.ts
│ ├── test-code-node-enhancements.ts
│ ├── test-code-node-fixes.ts
│ ├── test-docker-config.sh
│ ├── test-docker-fingerprint.ts
│ ├── test-docker-optimization.sh
│ ├── test-docker.sh
│ ├── test-empty-connection-validation.ts
│ ├── test-error-message-tracking.ts
│ ├── test-error-output-validation.ts
│ ├── test-error-validation.js
│ ├── test-essentials.ts
│ ├── test-expression-code-validation.ts
│ ├── test-expression-format-validation.js
│ ├── test-fts5-search.ts
│ ├── test-fuzzy-fix.ts
│ ├── test-fuzzy-simple.ts
│ ├── test-helpers-validation.ts
│ ├── test-http-search.ts
│ ├── test-http.sh
│ ├── test-jmespath-validation.ts
│ ├── test-multi-tenant-simple.ts
│ ├── test-multi-tenant.ts
│ ├── test-n8n-integration.sh
│ ├── test-node-info.js
│ ├── test-node-type-validation.ts
│ ├── test-nodes-base-prefix.ts
│ ├── test-operation-validation.ts
│ ├── test-optimized-docker.sh
│ ├── test-release-automation.js
│ ├── test-search-improvements.ts
│ ├── test-security.ts
│ ├── test-single-session.sh
│ ├── test-sqljs-triggers.ts
│ ├── test-structure-validation.ts
│ ├── test-telemetry-debug.ts
│ ├── test-telemetry-direct.ts
│ ├── test-telemetry-env.ts
│ ├── test-telemetry-integration.ts
│ ├── test-telemetry-no-select.ts
│ ├── test-telemetry-security.ts
│ ├── test-telemetry-simple.ts
│ ├── test-typeversion-validation.ts
│ ├── test-url-configuration.ts
│ ├── test-user-id-persistence.ts
│ ├── test-webhook-validation.ts
│ ├── test-workflow-insert.ts
│ ├── test-workflow-sanitizer.ts
│ ├── test-workflow-tracking-debug.ts
│ ├── test-workflow-versioning.ts
│ ├── update-and-publish-prep.sh
│ ├── update-n8n-deps.js
│ ├── update-readme-version.js
│ ├── vitest-benchmark-json-reporter.js
│ └── vitest-benchmark-reporter.ts
├── SECURITY.md
├── src
│ ├── config
│ │ └── n8n-api.ts
│ ├── constants
│ │ └── type-structures.ts
│ ├── data
│ │ └── canonical-ai-tool-examples.json
│ ├── database
│ │ ├── database-adapter.ts
│ │ ├── migrations
│ │ │ └── add-template-node-configs.sql
│ │ ├── node-repository.ts
│ │ ├── nodes.db
│ │ ├── schema-optimized.sql
│ │ └── schema.sql
│ ├── errors
│ │ └── validation-service-error.ts
│ ├── http-server-single-session.ts
│ ├── http-server.ts
│ ├── index.ts
│ ├── loaders
│ │ └── node-loader.ts
│ ├── mappers
│ │ └── docs-mapper.ts
│ ├── mcp
│ │ ├── handlers-n8n-manager.ts
│ │ ├── handlers-workflow-diff.ts
│ │ ├── index.ts
│ │ ├── server.ts
│ │ ├── stdio-wrapper.ts
│ │ ├── tool-docs
│ │ │ ├── configuration
│ │ │ │ ├── get-node.ts
│ │ │ │ └── index.ts
│ │ │ ├── discovery
│ │ │ │ ├── index.ts
│ │ │ │ └── search-nodes.ts
│ │ │ ├── guides
│ │ │ │ ├── ai-agents-guide.ts
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── system
│ │ │ │ ├── index.ts
│ │ │ │ ├── n8n-diagnostic.ts
│ │ │ │ ├── n8n-health-check.ts
│ │ │ │ ├── n8n-list-available-tools.ts
│ │ │ │ └── tools-documentation.ts
│ │ │ ├── templates
│ │ │ │ ├── get-template.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── search-templates.ts
│ │ │ ├── types.ts
│ │ │ ├── validation
│ │ │ │ ├── index.ts
│ │ │ │ ├── validate-node.ts
│ │ │ │ └── validate-workflow.ts
│ │ │ └── workflow_management
│ │ │ ├── index.ts
│ │ │ ├── n8n-autofix-workflow.ts
│ │ │ ├── n8n-create-workflow.ts
│ │ │ ├── n8n-delete-workflow.ts
│ │ │ ├── n8n-executions.ts
│ │ │ ├── n8n-get-workflow.ts
│ │ │ ├── n8n-list-workflows.ts
│ │ │ ├── n8n-trigger-webhook-workflow.ts
│ │ │ ├── n8n-update-full-workflow.ts
│ │ │ ├── n8n-update-partial-workflow.ts
│ │ │ ├── n8n-validate-workflow.ts
│ │ │ └── n8n-workflow-versions.ts
│ │ ├── tools-documentation.ts
│ │ ├── tools-n8n-friendly.ts
│ │ ├── tools-n8n-manager.ts
│ │ ├── tools.ts
│ │ └── workflow-examples.ts
│ ├── mcp-engine.ts
│ ├── mcp-tools-engine.ts
│ ├── n8n
│ │ ├── MCPApi.credentials.ts
│ │ └── MCPNode.node.ts
│ ├── parsers
│ │ ├── node-parser.ts
│ │ ├── property-extractor.ts
│ │ └── simple-parser.ts
│ ├── scripts
│ │ ├── debug-http-search.ts
│ │ ├── extract-from-docker.ts
│ │ ├── fetch-templates-robust.ts
│ │ ├── fetch-templates.ts
│ │ ├── rebuild-database.ts
│ │ ├── rebuild-optimized.ts
│ │ ├── rebuild.ts
│ │ ├── sanitize-templates.ts
│ │ ├── seed-canonical-ai-examples.ts
│ │ ├── test-autofix-documentation.ts
│ │ ├── test-autofix-workflow.ts
│ │ ├── test-execution-filtering.ts
│ │ ├── test-node-suggestions.ts
│ │ ├── test-protocol-negotiation.ts
│ │ ├── test-summary.ts
│ │ ├── test-telemetry-mutations-verbose.ts
│ │ ├── test-telemetry-mutations.ts
│ │ ├── test-webhook-autofix.ts
│ │ ├── validate.ts
│ │ └── validation-summary.ts
│ ├── services
│ │ ├── ai-node-validator.ts
│ │ ├── ai-tool-validators.ts
│ │ ├── breaking-change-detector.ts
│ │ ├── breaking-changes-registry.ts
│ │ ├── confidence-scorer.ts
│ │ ├── config-validator.ts
│ │ ├── enhanced-config-validator.ts
│ │ ├── example-generator.ts
│ │ ├── execution-processor.ts
│ │ ├── expression-format-validator.ts
│ │ ├── expression-validator.ts
│ │ ├── n8n-api-client.ts
│ │ ├── n8n-validation.ts
│ │ ├── node-documentation-service.ts
│ │ ├── node-migration-service.ts
│ │ ├── node-sanitizer.ts
│ │ ├── node-similarity-service.ts
│ │ ├── node-specific-validators.ts
│ │ ├── node-version-service.ts
│ │ ├── operation-similarity-service.ts
│ │ ├── post-update-validator.ts
│ │ ├── property-dependencies.ts
│ │ ├── property-filter.ts
│ │ ├── resource-similarity-service.ts
│ │ ├── sqlite-storage-service.ts
│ │ ├── task-templates.ts
│ │ ├── type-structure-service.ts
│ │ ├── universal-expression-validator.ts
│ │ ├── workflow-auto-fixer.ts
│ │ ├── workflow-diff-engine.ts
│ │ ├── workflow-validator.ts
│ │ └── workflow-versioning-service.ts
│ ├── telemetry
│ │ ├── batch-processor.ts
│ │ ├── config-manager.ts
│ │ ├── early-error-logger.ts
│ │ ├── error-sanitization-utils.ts
│ │ ├── error-sanitizer.ts
│ │ ├── event-tracker.ts
│ │ ├── event-validator.ts
│ │ ├── index.ts
│ │ ├── intent-classifier.ts
│ │ ├── intent-sanitizer.ts
│ │ ├── mutation-tracker.ts
│ │ ├── mutation-types.ts
│ │ ├── mutation-validator.ts
│ │ ├── performance-monitor.ts
│ │ ├── rate-limiter.ts
│ │ ├── startup-checkpoints.ts
│ │ ├── telemetry-error.ts
│ │ ├── telemetry-manager.ts
│ │ ├── telemetry-types.ts
│ │ └── workflow-sanitizer.ts
│ ├── templates
│ │ ├── batch-processor.ts
│ │ ├── metadata-generator.ts
│ │ ├── README.md
│ │ ├── template-fetcher.ts
│ │ ├── template-repository.ts
│ │ └── template-service.ts
│ ├── types
│ │ ├── index.ts
│ │ ├── instance-context.ts
│ │ ├── n8n-api.ts
│ │ ├── node-types.ts
│ │ ├── session-state.ts
│ │ ├── type-structures.ts
│ │ └── workflow-diff.ts
│ └── utils
│ ├── auth.ts
│ ├── bridge.ts
│ ├── cache-utils.ts
│ ├── console-manager.ts
│ ├── documentation-fetcher.ts
│ ├── enhanced-documentation-fetcher.ts
│ ├── error-handler.ts
│ ├── example-generator.ts
│ ├── expression-utils.ts
│ ├── fixed-collection-validator.ts
│ ├── logger.ts
│ ├── mcp-client.ts
│ ├── n8n-errors.ts
│ ├── node-classification.ts
│ ├── node-source-extractor.ts
│ ├── node-type-normalizer.ts
│ ├── node-type-utils.ts
│ ├── node-utils.ts
│ ├── npm-version-checker.ts
│ ├── protocol-version.ts
│ ├── simple-cache.ts
│ ├── ssrf-protection.ts
│ ├── template-node-resolver.ts
│ ├── template-sanitizer.ts
│ ├── url-detector.ts
│ ├── validation-schemas.ts
│ └── version.ts
├── test-output.txt
├── test-reinit-fix.sh
├── tests
│ ├── __snapshots__
│ │ └── .gitkeep
│ ├── auth.test.ts
│ ├── benchmarks
│ │ ├── database-queries.bench.ts
│ │ ├── index.ts
│ │ ├── mcp-tools.bench.ts
│ │ ├── mcp-tools.bench.ts.disabled
│ │ ├── mcp-tools.bench.ts.skip
│ │ ├── node-loading.bench.ts.disabled
│ │ ├── README.md
│ │ ├── search-operations.bench.ts.disabled
│ │ └── validation-performance.bench.ts.disabled
│ ├── bridge.test.ts
│ ├── comprehensive-extraction-test.js
│ ├── data
│ │ └── .gitkeep
│ ├── debug-slack-doc.js
│ ├── demo-enhanced-documentation.js
│ ├── docker-tests-README.md
│ ├── error-handler.test.ts
│ ├── examples
│ │ └── using-database-utils.test.ts
│ ├── extracted-nodes-db
│ │ ├── database-import.json
│ │ ├── extraction-report.json
│ │ ├── insert-nodes.sql
│ │ ├── n8n-nodes-base__Airtable.json
│ │ ├── n8n-nodes-base__Discord.json
│ │ ├── n8n-nodes-base__Function.json
│ │ ├── n8n-nodes-base__HttpRequest.json
│ │ ├── n8n-nodes-base__If.json
│ │ ├── n8n-nodes-base__Slack.json
│ │ ├── n8n-nodes-base__SplitInBatches.json
│ │ └── n8n-nodes-base__Webhook.json
│ ├── factories
│ │ ├── node-factory.ts
│ │ └── property-definition-factory.ts
│ ├── fixtures
│ │ ├── .gitkeep
│ │ ├── database
│ │ │ └── test-nodes.json
│ │ ├── factories
│ │ │ ├── node.factory.ts
│ │ │ └── parser-node.factory.ts
│ │ └── template-configs.ts
│ ├── helpers
│ │ └── env-helpers.ts
│ ├── http-server-auth.test.ts
│ ├── integration
│ │ ├── ai-validation
│ │ │ ├── ai-agent-validation.test.ts
│ │ │ ├── ai-tool-validation.test.ts
│ │ │ ├── chat-trigger-validation.test.ts
│ │ │ ├── e2e-validation.test.ts
│ │ │ ├── helpers.ts
│ │ │ ├── llm-chain-validation.test.ts
│ │ │ ├── README.md
│ │ │ └── TEST_REPORT.md
│ │ ├── ci
│ │ │ └── database-population.test.ts
│ │ ├── database
│ │ │ ├── connection-management.test.ts
│ │ │ ├── empty-database.test.ts
│ │ │ ├── fts5-search.test.ts
│ │ │ ├── node-fts5-search.test.ts
│ │ │ ├── node-repository.test.ts
│ │ │ ├── performance.test.ts
│ │ │ ├── sqljs-memory-leak.test.ts
│ │ │ ├── template-node-configs.test.ts
│ │ │ ├── template-repository.test.ts
│ │ │ ├── test-utils.ts
│ │ │ └── transactions.test.ts
│ │ ├── database-integration.test.ts
│ │ ├── docker
│ │ │ ├── docker-config.test.ts
│ │ │ ├── docker-entrypoint.test.ts
│ │ │ └── test-helpers.ts
│ │ ├── flexible-instance-config.test.ts
│ │ ├── mcp
│ │ │ └── template-examples-e2e.test.ts
│ │ ├── mcp-protocol
│ │ │ ├── basic-connection.test.ts
│ │ │ ├── error-handling.test.ts
│ │ │ ├── performance.test.ts
│ │ │ ├── protocol-compliance.test.ts
│ │ │ ├── README.md
│ │ │ ├── session-management.test.ts
│ │ │ ├── test-helpers.ts
│ │ │ ├── tool-invocation.test.ts
│ │ │ └── workflow-error-validation.test.ts
│ │ ├── msw-setup.test.ts
│ │ ├── n8n-api
│ │ │ ├── executions
│ │ │ │ ├── delete-execution.test.ts
│ │ │ │ ├── get-execution.test.ts
│ │ │ │ ├── list-executions.test.ts
│ │ │ │ └── trigger-webhook.test.ts
│ │ │ ├── scripts
│ │ │ │ └── cleanup-orphans.ts
│ │ │ ├── system
│ │ │ │ ├── diagnostic.test.ts
│ │ │ │ └── health-check.test.ts
│ │ │ ├── test-connection.ts
│ │ │ ├── types
│ │ │ │ └── mcp-responses.ts
│ │ │ ├── utils
│ │ │ │ ├── cleanup-helpers.ts
│ │ │ │ ├── credentials.ts
│ │ │ │ ├── factories.ts
│ │ │ │ ├── fixtures.ts
│ │ │ │ ├── mcp-context.ts
│ │ │ │ ├── n8n-client.ts
│ │ │ │ ├── node-repository.ts
│ │ │ │ ├── response-types.ts
│ │ │ │ ├── test-context.ts
│ │ │ │ └── webhook-workflows.ts
│ │ │ └── workflows
│ │ │ ├── autofix-workflow.test.ts
│ │ │ ├── create-workflow.test.ts
│ │ │ ├── delete-workflow.test.ts
│ │ │ ├── get-workflow-details.test.ts
│ │ │ ├── get-workflow-minimal.test.ts
│ │ │ ├── get-workflow-structure.test.ts
│ │ │ ├── get-workflow.test.ts
│ │ │ ├── list-workflows.test.ts
│ │ │ ├── smart-parameters.test.ts
│ │ │ ├── update-partial-workflow.test.ts
│ │ │ ├── update-workflow.test.ts
│ │ │ └── validate-workflow.test.ts
│ │ ├── security
│ │ │ ├── command-injection-prevention.test.ts
│ │ │ └── rate-limiting.test.ts
│ │ ├── setup
│ │ │ ├── integration-setup.ts
│ │ │ └── msw-test-server.ts
│ │ ├── telemetry
│ │ │ ├── docker-user-id-stability.test.ts
│ │ │ └── mcp-telemetry.test.ts
│ │ ├── templates
│ │ │ └── metadata-operations.test.ts
│ │ ├── validation
│ │ │ └── real-world-structure-validation.test.ts
│ │ ├── workflow-creation-node-type-format.test.ts
│ │ └── workflow-diff
│ │ ├── ai-node-connection-validation.test.ts
│ │ └── node-rename-integration.test.ts
│ ├── logger.test.ts
│ ├── MOCKING_STRATEGY.md
│ ├── mocks
│ │ ├── n8n-api
│ │ │ ├── data
│ │ │ │ ├── credentials.ts
│ │ │ │ ├── executions.ts
│ │ │ │ └── workflows.ts
│ │ │ ├── handlers.ts
│ │ │ └── index.ts
│ │ └── README.md
│ ├── node-storage-export.json
│ ├── setup
│ │ ├── global-setup.ts
│ │ ├── msw-setup.ts
│ │ ├── TEST_ENV_DOCUMENTATION.md
│ │ └── test-env.ts
│ ├── test-database-extraction.js
│ ├── test-direct-extraction.js
│ ├── test-enhanced-documentation.js
│ ├── test-enhanced-integration.js
│ ├── test-mcp-extraction.js
│ ├── test-mcp-server-extraction.js
│ ├── test-mcp-tools-integration.js
│ ├── test-node-documentation-service.js
│ ├── test-node-list.js
│ ├── test-package-info.js
│ ├── test-parsing-operations.js
│ ├── test-slack-node-complete.js
│ ├── test-small-rebuild.js
│ ├── test-sqlite-search.js
│ ├── test-storage-system.js
│ ├── unit
│ │ ├── __mocks__
│ │ │ ├── n8n-nodes-base.test.ts
│ │ │ ├── n8n-nodes-base.ts
│ │ │ └── README.md
│ │ ├── constants
│ │ │ └── type-structures.test.ts
│ │ ├── database
│ │ │ ├── __mocks__
│ │ │ │ └── better-sqlite3.ts
│ │ │ ├── database-adapter-unit.test.ts
│ │ │ ├── node-repository-core.test.ts
│ │ │ ├── node-repository-operations.test.ts
│ │ │ ├── node-repository-outputs.test.ts
│ │ │ ├── README.md
│ │ │ └── template-repository-core.test.ts
│ │ ├── docker
│ │ │ ├── config-security.test.ts
│ │ │ ├── edge-cases.test.ts
│ │ │ ├── parse-config.test.ts
│ │ │ └── serve-command.test.ts
│ │ ├── errors
│ │ │ └── validation-service-error.test.ts
│ │ ├── examples
│ │ │ └── using-n8n-nodes-base-mock.test.ts
│ │ ├── flexible-instance-security-advanced.test.ts
│ │ ├── flexible-instance-security.test.ts
│ │ ├── http-server
│ │ │ ├── multi-tenant-support.test.ts
│ │ │ └── session-persistence.test.ts
│ │ ├── http-server-n8n-mode.test.ts
│ │ ├── http-server-n8n-reinit.test.ts
│ │ ├── http-server-session-management.test.ts
│ │ ├── loaders
│ │ │ └── node-loader.test.ts
│ │ ├── mappers
│ │ │ └── docs-mapper.test.ts
│ │ ├── mcp
│ │ │ ├── disabled-tools-additional.test.ts
│ │ │ ├── disabled-tools.test.ts
│ │ │ ├── get-node-essentials-examples.test.ts
│ │ │ ├── get-node-unified.test.ts
│ │ │ ├── handlers-n8n-manager-simple.test.ts
│ │ │ ├── handlers-n8n-manager.test.ts
│ │ │ ├── handlers-workflow-diff.test.ts
│ │ │ ├── lru-cache-behavior.test.ts
│ │ │ ├── multi-tenant-tool-listing.test.ts.disabled
│ │ │ ├── parameter-validation.test.ts
│ │ │ ├── search-nodes-examples.test.ts
│ │ │ ├── tools-documentation.test.ts
│ │ │ └── tools.test.ts
│ │ ├── mcp-engine
│ │ │ └── session-persistence.test.ts
│ │ ├── monitoring
│ │ │ └── cache-metrics.test.ts
│ │ ├── MULTI_TENANT_TEST_COVERAGE.md
│ │ ├── multi-tenant-integration.test.ts
│ │ ├── parsers
│ │ │ ├── node-parser-outputs.test.ts
│ │ │ ├── node-parser.test.ts
│ │ │ ├── property-extractor.test.ts
│ │ │ └── simple-parser.test.ts
│ │ ├── scripts
│ │ │ └── fetch-templates-extraction.test.ts
│ │ ├── services
│ │ │ ├── ai-node-validator.test.ts
│ │ │ ├── ai-tool-validators.test.ts
│ │ │ ├── breaking-change-detector.test.ts
│ │ │ ├── confidence-scorer.test.ts
│ │ │ ├── config-validator-basic.test.ts
│ │ │ ├── config-validator-edge-cases.test.ts
│ │ │ ├── config-validator-node-specific.test.ts
│ │ │ ├── config-validator-security.test.ts
│ │ │ ├── debug-validator.test.ts
│ │ │ ├── enhanced-config-validator-integration.test.ts
│ │ │ ├── enhanced-config-validator-operations.test.ts
│ │ │ ├── enhanced-config-validator-type-structures.test.ts
│ │ │ ├── enhanced-config-validator.test.ts
│ │ │ ├── example-generator.test.ts
│ │ │ ├── execution-processor.test.ts
│ │ │ ├── expression-format-validator.test.ts
│ │ │ ├── expression-validator-edge-cases.test.ts
│ │ │ ├── expression-validator.test.ts
│ │ │ ├── fixed-collection-validation.test.ts
│ │ │ ├── loop-output-edge-cases.test.ts
│ │ │ ├── n8n-api-client.test.ts
│ │ │ ├── n8n-validation-sticky-notes.test.ts
│ │ │ ├── n8n-validation.test.ts
│ │ │ ├── node-migration-service.test.ts
│ │ │ ├── node-sanitizer.test.ts
│ │ │ ├── node-similarity-service.test.ts
│ │ │ ├── node-specific-validators.test.ts
│ │ │ ├── node-version-service.test.ts
│ │ │ ├── operation-similarity-service-comprehensive.test.ts
│ │ │ ├── operation-similarity-service.test.ts
│ │ │ ├── post-update-validator.test.ts
│ │ │ ├── property-dependencies.test.ts
│ │ │ ├── property-filter-edge-cases.test.ts
│ │ │ ├── property-filter.test.ts
│ │ │ ├── resource-similarity-service-comprehensive.test.ts
│ │ │ ├── resource-similarity-service.test.ts
│ │ │ ├── task-templates.test.ts
│ │ │ ├── template-service.test.ts
│ │ │ ├── type-structure-service.test.ts
│ │ │ ├── universal-expression-validator.test.ts
│ │ │ ├── validation-fixes.test.ts
│ │ │ ├── workflow-auto-fixer.test.ts
│ │ │ ├── workflow-diff-engine.test.ts
│ │ │ ├── workflow-diff-node-rename.test.ts
│ │ │ ├── workflow-fixed-collection-validation.test.ts
│ │ │ ├── workflow-validator-comprehensive.test.ts
│ │ │ ├── workflow-validator-edge-cases.test.ts
│ │ │ ├── workflow-validator-error-outputs.test.ts
│ │ │ ├── workflow-validator-expression-format.test.ts
│ │ │ ├── workflow-validator-loops-simple.test.ts
│ │ │ ├── workflow-validator-loops.test.ts
│ │ │ ├── workflow-validator-mocks.test.ts
│ │ │ ├── workflow-validator-performance.test.ts
│ │ │ ├── workflow-validator-with-mocks.test.ts
│ │ │ ├── workflow-validator.test.ts
│ │ │ └── workflow-versioning-service.test.ts
│ │ ├── telemetry
│ │ │ ├── batch-processor.test.ts
│ │ │ ├── config-manager.test.ts
│ │ │ ├── event-tracker.test.ts
│ │ │ ├── event-validator.test.ts
│ │ │ ├── mutation-tracker.test.ts
│ │ │ ├── mutation-validator.test.ts
│ │ │ ├── rate-limiter.test.ts
│ │ │ ├── telemetry-error.test.ts
│ │ │ ├── telemetry-manager.test.ts
│ │ │ ├── v2.18.3-fixes-verification.test.ts
│ │ │ └── workflow-sanitizer.test.ts
│ │ ├── templates
│ │ │ ├── batch-processor.test.ts
│ │ │ ├── metadata-generator.test.ts
│ │ │ ├── template-repository-metadata.test.ts
│ │ │ └── template-repository-security.test.ts
│ │ ├── test-env-example.test.ts
│ │ ├── test-infrastructure.test.ts
│ │ ├── types
│ │ │ ├── instance-context-coverage.test.ts
│ │ │ ├── instance-context-multi-tenant.test.ts
│ │ │ └── type-structures.test.ts
│ │ ├── utils
│ │ │ ├── auth-timing-safe.test.ts
│ │ │ ├── cache-utils.test.ts
│ │ │ ├── console-manager.test.ts
│ │ │ ├── database-utils.test.ts
│ │ │ ├── expression-utils.test.ts
│ │ │ ├── fixed-collection-validator.test.ts
│ │ │ ├── n8n-errors.test.ts
│ │ │ ├── node-classification.test.ts
│ │ │ ├── node-type-normalizer.test.ts
│ │ │ ├── node-type-utils.test.ts
│ │ │ ├── node-utils.test.ts
│ │ │ ├── simple-cache-memory-leak-fix.test.ts
│ │ │ ├── ssrf-protection.test.ts
│ │ │ └── template-node-resolver.test.ts
│ │ └── validation-fixes.test.ts
│ └── utils
│ ├── assertions.ts
│ ├── builders
│ │ └── workflow.builder.ts
│ ├── data-generators.ts
│ ├── database-utils.ts
│ ├── README.md
│ └── test-helpers.ts
├── thumbnail.png
├── tsconfig.build.json
├── tsconfig.json
├── types
│ ├── mcp.d.ts
│ └── test-env.d.ts
├── versioned-nodes.md
├── vitest.config.benchmark.ts
├── vitest.config.integration.ts
└── vitest.config.ts
```
# Files
--------------------------------------------------------------------------------
/scripts/migrate-tool-docs.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env tsx
2 | import * as fs from 'fs';
3 | import * as path from 'path';
4 |
5 | // This is a helper script to migrate tool documentation to the new structure
6 | // It creates a template file for each tool that needs to be migrated
7 |
8 | const toolsByCategory = {
9 | discovery: [
10 | 'search_nodes',
11 | 'list_nodes',
12 | 'list_ai_tools',
13 | 'get_database_statistics'
14 | ],
15 | configuration: [
16 | 'get_node_info',
17 | 'get_node_essentials',
18 | 'get_node_documentation',
19 | 'search_node_properties',
20 | 'get_node_as_tool_info',
21 | 'get_property_dependencies'
22 | ],
23 | validation: [
24 | 'validate_node_minimal',
25 | 'validate_node_operation',
26 | 'validate_workflow',
27 | 'validate_workflow_connections',
28 | 'validate_workflow_expressions'
29 | ],
30 | templates: [
31 | 'get_node_for_task',
32 | 'list_tasks',
33 | 'list_node_templates',
34 | 'get_template',
35 | 'search_templates',
36 | 'get_templates_for_task'
37 | ],
38 | workflow_management: [
39 | 'n8n_create_workflow',
40 | 'n8n_get_workflow',
41 | 'n8n_get_workflow_details',
42 | 'n8n_get_workflow_structure',
43 | 'n8n_get_workflow_minimal',
44 | 'n8n_update_full_workflow',
45 | 'n8n_update_partial_workflow',
46 | 'n8n_delete_workflow',
47 | 'n8n_list_workflows',
48 | 'n8n_validate_workflow',
49 | 'n8n_trigger_webhook_workflow',
50 | 'n8n_get_execution',
51 | 'n8n_list_executions',
52 | 'n8n_delete_execution'
53 | ],
54 | system: [
55 | 'tools_documentation',
56 | 'n8n_diagnostic',
57 | 'n8n_health_check',
58 | 'n8n_list_available_tools'
59 | ],
60 | special: [
61 | 'code_node_guide'
62 | ]
63 | };
64 |
65 | const template = (toolName: string, category: string) => `import { ToolDocumentation } from '../types';
66 |
67 | export const ${toCamelCase(toolName)}Doc: ToolDocumentation = {
68 | name: '${toolName}',
69 | category: '${category}',
70 | essentials: {
71 | description: 'TODO: Add description from old file',
72 | keyParameters: ['TODO'],
73 | example: '${toolName}({TODO})',
74 | performance: 'TODO',
75 | tips: [
76 | 'TODO: Add tips'
77 | ]
78 | },
79 | full: {
80 | description: 'TODO: Add full description',
81 | parameters: {
82 | // TODO: Add parameters
83 | },
84 | returns: 'TODO: Add return description',
85 | examples: [
86 | '${toolName}({TODO}) - TODO'
87 | ],
88 | useCases: [
89 | 'TODO: Add use cases'
90 | ],
91 | performance: 'TODO: Add performance description',
92 | bestPractices: [
93 | 'TODO: Add best practices'
94 | ],
95 | pitfalls: [
96 | 'TODO: Add pitfalls'
97 | ],
98 | relatedTools: ['TODO']
99 | }
100 | };`;
101 |
102 | function toCamelCase(str: string): string {
103 | return str.split('_').map((part, index) =>
104 | index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)
105 | ).join('');
106 | }
107 |
108 | function toKebabCase(str: string): string {
109 | return str.replace(/_/g, '-');
110 | }
111 |
112 | // Create template files for tools that don't exist yet
113 | Object.entries(toolsByCategory).forEach(([category, tools]) => {
114 | tools.forEach(toolName => {
115 | const fileName = toKebabCase(toolName) + '.ts';
116 | const filePath = path.join('src/mcp/tool-docs', category, fileName);
117 |
118 | // Skip if file already exists
119 | if (fs.existsSync(filePath)) {
120 | console.log(`✓ ${filePath} already exists`);
121 | return;
122 | }
123 |
124 | // Create the file with template
125 | fs.writeFileSync(filePath, template(toolName, category));
126 | console.log(`✨ Created ${filePath}`);
127 | });
128 |
129 | // Create index file for the category
130 | const indexPath = path.join('src/mcp/tool-docs', category, 'index.ts');
131 | if (!fs.existsSync(indexPath)) {
132 | const indexContent = tools.map(toolName =>
133 | `export { ${toCamelCase(toolName)}Doc } from './${toKebabCase(toolName)}';`
134 | ).join('\n');
135 |
136 | fs.writeFileSync(indexPath, indexContent);
137 | console.log(`✨ Created ${indexPath}`);
138 | }
139 | });
140 |
141 | console.log('\n📝 Migration templates created!');
142 | console.log('Next steps:');
143 | console.log('1. Copy documentation from the old tools-documentation.ts file');
144 | console.log('2. Update each template file with the actual documentation');
145 | console.log('3. Update src/mcp/tool-docs/index.ts to import all tools');
146 | console.log('4. Replace the old tools-documentation.ts with the new one');
```
--------------------------------------------------------------------------------
/tests/docker-tests-README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Docker Config File Support Tests
2 |
3 | This directory contains comprehensive tests for the Docker config file support feature added to n8n-mcp.
4 |
5 | ## Test Structure
6 |
7 | ### Unit Tests (`tests/unit/docker/`)
8 |
9 | 1. **parse-config.test.ts** - Tests for the JSON config parser
10 | - Basic JSON parsing functionality
11 | - Environment variable precedence
12 | - Shell escaping and quoting
13 | - Nested object flattening
14 | - Error handling for invalid JSON
15 |
16 | 2. **serve-command.test.ts** - Tests for "n8n-mcp serve" command
17 | - Command transformation logic
18 | - Argument preservation
19 | - Integration with config loading
20 | - Backwards compatibility
21 |
22 | 3. **config-security.test.ts** - Security-focused tests
23 | - Command injection prevention
24 | - Shell metacharacter handling
25 | - Path traversal protection
26 | - Polyglot payload defense
27 | - Real-world attack scenarios
28 |
29 | 4. **edge-cases.test.ts** - Edge case and stress tests
30 | - JavaScript number edge cases
31 | - Unicode handling
32 | - Deep nesting performance
33 | - Large config files
34 | - Invalid data types
35 |
36 | ### Integration Tests (`tests/integration/docker/`)
37 |
38 | 1. **docker-config.test.ts** - Full Docker container tests with config files
39 | - Config file loading and parsing
40 | - Environment variable precedence
41 | - Security in container context
42 | - Complex configuration scenarios
43 |
44 | 2. **docker-entrypoint.test.ts** - Docker entrypoint script tests
45 | - MCP mode handling
46 | - Database initialization
47 | - Permission management
48 | - Signal handling
49 | - Authentication validation
50 |
51 | ## Running the Tests
52 |
53 | ### Prerequisites
54 | - Node.js and npm installed
55 | - Docker installed (for integration tests)
56 | - Build the project first: `npm run build`
57 |
58 | ### Commands
59 |
60 | ```bash
61 | # Run all Docker config tests
62 | npm run test:docker
63 |
64 | # Run only unit tests (no Docker required)
65 | npm run test:docker:unit
66 |
67 | # Run only integration tests (requires Docker)
68 | npm run test:docker:integration
69 |
70 | # Run security-focused tests
71 | npm run test:docker:security
72 |
73 | # Run with coverage
74 | ./scripts/test-docker-config.sh coverage
75 | ```
76 |
77 | ### Individual test files
78 | ```bash
79 | # Run a specific test file
80 | npm test -- tests/unit/docker/parse-config.test.ts
81 |
82 | # Run with watch mode
83 | npm run test:watch -- tests/unit/docker/
84 |
85 | # Run with coverage
86 | npm run test:coverage -- tests/unit/docker/config-security.test.ts
87 | ```
88 |
89 | ## Test Coverage
90 |
91 | The tests cover:
92 |
93 | 1. **Functionality**
94 | - JSON parsing and environment variable conversion
95 | - Nested object flattening with underscore separation
96 | - Environment variable precedence (env vars override config)
97 | - "n8n-mcp serve" command auto-enables HTTP mode
98 |
99 | 2. **Security**
100 | - Command injection prevention through proper shell escaping
101 | - Protection against malicious config values
102 | - Safe handling of special characters and Unicode
103 | - Prevention of path traversal attacks
104 |
105 | 3. **Edge Cases**
106 | - Invalid JSON handling
107 | - Missing config files
108 | - Permission errors
109 | - Very large config files
110 | - Deep nesting performance
111 |
112 | 4. **Integration**
113 | - Full Docker container behavior
114 | - Database initialization with file locking
115 | - Permission handling (root vs nodejs user)
116 | - Signal propagation and process management
117 |
118 | ## CI/CD Considerations
119 |
120 | Integration tests are skipped by default unless:
121 | - Running in CI (CI=true environment variable)
122 | - Explicitly enabled (RUN_DOCKER_TESTS=true)
123 |
124 | This prevents test failures on developer machines without Docker.
125 |
126 | ## Security Notes
127 |
128 | The config parser implements defense in depth:
129 | 1. All values are wrapped in single quotes for shell safety
130 | 2. Single quotes within values are escaped as '"'"'
131 | 3. No variable expansion occurs within single quotes
132 | 4. Arrays and null values are ignored (not exported)
133 | 5. The parser exits silently on any error to prevent container startup issues
134 |
135 | ## Troubleshooting
136 |
137 | If tests fail:
138 | 1. Ensure Docker is running (for integration tests)
139 | 2. Check that the project is built (`npm run build`)
140 | 3. Verify no containers are left running: `docker ps -a | grep n8n-mcp-test`
141 | 4. Clean up test containers: `docker rm $(docker ps -aq -f name=n8n-mcp-test)`
```
--------------------------------------------------------------------------------
/src/telemetry/mutation-types.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Types and interfaces for workflow mutation tracking
3 | * Purpose: Track workflow transformations to improve partial updates tooling
4 | */
5 |
6 | import { DiffOperation } from '../types/workflow-diff.js';
7 |
8 | /**
9 | * Intent classification for workflow mutations
10 | */
11 | export enum IntentClassification {
12 | ADD_FUNCTIONALITY = 'add_functionality',
13 | MODIFY_CONFIGURATION = 'modify_configuration',
14 | REWIRE_LOGIC = 'rewire_logic',
15 | FIX_VALIDATION = 'fix_validation',
16 | CLEANUP = 'cleanup',
17 | UNKNOWN = 'unknown',
18 | }
19 |
20 | /**
21 | * Tool names that perform workflow mutations
22 | */
23 | export enum MutationToolName {
24 | UPDATE_PARTIAL = 'n8n_update_partial_workflow',
25 | UPDATE_FULL = 'n8n_update_full_workflow',
26 | }
27 |
28 | /**
29 | * Validation result structure
30 | */
31 | export interface ValidationResult {
32 | valid: boolean;
33 | errors: Array<{
34 | type: string;
35 | message: string;
36 | severity?: string;
37 | location?: string;
38 | }>;
39 | warnings?: Array<{
40 | type: string;
41 | message: string;
42 | }>;
43 | }
44 |
45 | /**
46 | * Change metrics calculated from workflow mutation
47 | */
48 | export interface MutationChangeMetrics {
49 | nodesAdded: number;
50 | nodesRemoved: number;
51 | nodesModified: number;
52 | connectionsAdded: number;
53 | connectionsRemoved: number;
54 | propertiesChanged: number;
55 | }
56 |
57 | /**
58 | * Validation improvement metrics
59 | */
60 | export interface MutationValidationMetrics {
61 | validationImproved: boolean | null;
62 | errorsResolved: number;
63 | errorsIntroduced: number;
64 | }
65 |
66 | /**
67 | * Input data for tracking a workflow mutation
68 | */
69 | export interface WorkflowMutationData {
70 | sessionId: string;
71 | toolName: MutationToolName;
72 | userIntent: string;
73 | operations: DiffOperation[];
74 | workflowBefore: any;
75 | workflowAfter: any;
76 | validationBefore?: ValidationResult;
77 | validationAfter?: ValidationResult;
78 | mutationSuccess: boolean;
79 | mutationError?: string;
80 | durationMs: number;
81 | }
82 |
83 | /**
84 | * Complete mutation record for database storage
85 | */
86 | export interface WorkflowMutationRecord {
87 | id?: string;
88 | userId: string;
89 | sessionId: string;
90 | workflowBefore: any;
91 | workflowAfter: any;
92 | workflowHashBefore: string;
93 | workflowHashAfter: string;
94 | /** Structural hash (nodeTypes + connections) for cross-referencing with telemetry_workflows */
95 | workflowStructureHashBefore?: string;
96 | /** Structural hash (nodeTypes + connections) for cross-referencing with telemetry_workflows */
97 | workflowStructureHashAfter?: string;
98 | /** Computed field: true if mutation executed successfully, improved validation, and has known intent */
99 | isTrulySuccessful?: boolean;
100 | userIntent: string;
101 | intentClassification: IntentClassification;
102 | toolName: MutationToolName;
103 | operations: DiffOperation[];
104 | operationCount: number;
105 | operationTypes: string[];
106 | validationBefore?: ValidationResult;
107 | validationAfter?: ValidationResult;
108 | validationImproved: boolean | null;
109 | errorsResolved: number;
110 | errorsIntroduced: number;
111 | nodesAdded: number;
112 | nodesRemoved: number;
113 | nodesModified: number;
114 | connectionsAdded: number;
115 | connectionsRemoved: number;
116 | propertiesChanged: number;
117 | mutationSuccess: boolean;
118 | mutationError?: string;
119 | durationMs: number;
120 | createdAt?: Date;
121 | }
122 |
123 | /**
124 | * Options for mutation tracking
125 | */
126 | export interface MutationTrackingOptions {
127 | /** Whether to track this mutation (default: true) */
128 | enabled?: boolean;
129 |
130 | /** Maximum workflow size in KB to track (default: 500) */
131 | maxWorkflowSizeKb?: number;
132 |
133 | /** Whether to validate data quality before tracking (default: true) */
134 | validateQuality?: boolean;
135 |
136 | /** Whether to sanitize workflows for PII (default: true) */
137 | sanitize?: boolean;
138 | }
139 |
140 | /**
141 | * Mutation tracking statistics for monitoring
142 | */
143 | export interface MutationTrackingStats {
144 | totalMutationsTracked: number;
145 | successfulMutations: number;
146 | failedMutations: number;
147 | mutationsWithValidationImprovement: number;
148 | averageDurationMs: number;
149 | intentClassificationBreakdown: Record<IntentClassification, number>;
150 | operationTypeBreakdown: Record<string, number>;
151 | }
152 |
153 | /**
154 | * Data quality validation result
155 | */
156 | export interface MutationDataQualityResult {
157 | valid: boolean;
158 | errors: string[];
159 | warnings: string[];
160 | }
161 |
```
--------------------------------------------------------------------------------
/tests/integration/n8n-api/workflows/get-workflow.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Integration Tests: handleGetWorkflow
3 | *
4 | * Tests workflow retrieval against a real n8n instance.
5 | * Covers successful retrieval and error handling.
6 | */
7 |
8 | import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest';
9 | import { createTestContext, TestContext, createTestWorkflowName } from '../utils/test-context';
10 | import { getTestN8nClient } from '../utils/n8n-client';
11 | import { N8nApiClient } from '../../../../src/services/n8n-api-client';
12 | import { Workflow } from '../../../../src/types/n8n-api';
13 | import { SIMPLE_WEBHOOK_WORKFLOW } from '../utils/fixtures';
14 | import { cleanupOrphanedWorkflows } from '../utils/cleanup-helpers';
15 | import { createMcpContext } from '../utils/mcp-context';
16 | import { InstanceContext } from '../../../../src/types/instance-context';
17 | import { handleGetWorkflow } from '../../../../src/mcp/handlers-n8n-manager';
18 |
19 | describe('Integration: handleGetWorkflow', () => {
20 | let context: TestContext;
21 | let client: N8nApiClient;
22 | let mcpContext: InstanceContext;
23 |
24 | beforeEach(() => {
25 | context = createTestContext();
26 | client = getTestN8nClient();
27 | mcpContext = createMcpContext();
28 | });
29 |
30 | afterEach(async () => {
31 | await context.cleanup();
32 | });
33 |
34 | afterAll(async () => {
35 | if (!process.env.CI) {
36 | await cleanupOrphanedWorkflows();
37 | }
38 | });
39 |
40 | // ======================================================================
41 | // Successful Retrieval
42 | // ======================================================================
43 |
44 | describe('Successful Retrieval', () => {
45 | it('should retrieve complete workflow data', async () => {
46 | // Create a workflow first
47 | const workflow = {
48 | ...SIMPLE_WEBHOOK_WORKFLOW,
49 | name: createTestWorkflowName('Get Workflow - Complete Data'),
50 | tags: ['mcp-integration-test']
51 | };
52 |
53 | const created = await client.createWorkflow(workflow);
54 | expect(created).toBeDefined();
55 | expect(created.id).toBeTruthy();
56 |
57 | if (!created.id) throw new Error('Workflow ID is missing');
58 | context.trackWorkflow(created.id);
59 |
60 | // Retrieve the workflow using MCP handler
61 | const response = await handleGetWorkflow({ id: created.id }, mcpContext);
62 |
63 | // Verify MCP response structure
64 | expect(response.success).toBe(true);
65 | expect(response.data).toBeDefined();
66 |
67 | const retrieved = response.data as Workflow;
68 |
69 | // Verify all expected fields are present
70 | expect(retrieved).toBeDefined();
71 | expect(retrieved.id).toBe(created.id);
72 | expect(retrieved.name).toBe(workflow.name);
73 | expect(retrieved.nodes).toBeDefined();
74 | expect(retrieved.nodes).toHaveLength(workflow.nodes!.length);
75 | expect(retrieved.connections).toBeDefined();
76 | expect(retrieved.active).toBeDefined();
77 | expect(retrieved.createdAt).toBeDefined();
78 | expect(retrieved.updatedAt).toBeDefined();
79 |
80 | // Verify node data integrity
81 | const retrievedNode = retrieved.nodes[0];
82 | const originalNode = workflow.nodes![0];
83 | expect(retrievedNode.name).toBe(originalNode.name);
84 | expect(retrievedNode.type).toBe(originalNode.type);
85 | expect(retrievedNode.parameters).toBeDefined();
86 | });
87 | });
88 |
89 | // ======================================================================
90 | // Error Handling
91 | // ======================================================================
92 |
93 | describe('Error Handling', () => {
94 | it('should return error for non-existent workflow (invalid ID)', async () => {
95 | const invalidId = '99999999';
96 |
97 | const response = await handleGetWorkflow({ id: invalidId }, mcpContext);
98 |
99 | // MCP handlers return success: false on error
100 | expect(response.success).toBe(false);
101 | expect(response.error).toBeDefined();
102 | });
103 |
104 | it('should return error for malformed workflow ID', async () => {
105 | const malformedId = 'not-a-valid-id-format';
106 |
107 | const response = await handleGetWorkflow({ id: malformedId }, mcpContext);
108 |
109 | // MCP handlers return success: false on error
110 | expect(response.success).toBe(false);
111 | expect(response.error).toBeDefined();
112 | });
113 | });
114 | });
115 |
```
--------------------------------------------------------------------------------
/scripts/test-expression-code-validation.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env npx tsx
2 |
3 | /**
4 | * Test script for Expression vs Code Node validation
5 | * Tests that we properly detect and warn about expression syntax in Code nodes
6 | */
7 |
8 | import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator.js';
9 |
10 | console.log('🧪 Testing Expression vs Code Node Validation\n');
11 |
12 | // Test cases with expression syntax that shouldn't work in Code nodes
13 | const testCases = [
14 | {
15 | name: 'Expression syntax in Code node',
16 | config: {
17 | language: 'javaScript',
18 | jsCode: `// Using expression syntax
19 | const value = {{$json.field}};
20 | return [{json: {value}}];`
21 | },
22 | expectedError: 'Expression syntax {{...}} is not valid in Code nodes'
23 | },
24 | {
25 | name: 'Wrong $node syntax',
26 | config: {
27 | language: 'javaScript',
28 | jsCode: `// Using expression $node syntax
29 | const data = $node['Previous Node'].json;
30 | return [{json: data}];`
31 | },
32 | expectedWarning: 'Use $(\'Node Name\') instead of $node[\'Node Name\'] in Code nodes'
33 | },
34 | {
35 | name: 'Expression-only functions',
36 | config: {
37 | language: 'javaScript',
38 | jsCode: `// Using expression functions
39 | const now = $now();
40 | const unique = items.unique();
41 | return [{json: {now, unique}}];`
42 | },
43 | expectedWarning: '$now() is an expression-only function'
44 | },
45 | {
46 | name: 'Wrong JMESPath parameter order',
47 | config: {
48 | language: 'javaScript',
49 | jsCode: `// Wrong parameter order
50 | const result = $jmespath("users[*].name", data);
51 | return [{json: {result}}];`
52 | },
53 | expectedWarning: 'Code node $jmespath has reversed parameter order'
54 | },
55 | {
56 | name: 'Correct Code node syntax',
57 | config: {
58 | language: 'javaScript',
59 | jsCode: `// Correct syntax
60 | const prevData = $('Previous Node').first();
61 | const now = DateTime.now();
62 | const result = $jmespath(data, "users[*].name");
63 | return [{json: {prevData, now, result}}];`
64 | },
65 | shouldBeValid: true
66 | }
67 | ];
68 |
69 | // Basic node properties for Code node
70 | const codeNodeProperties = [
71 | { name: 'language', type: 'options', options: ['javaScript', 'python'] },
72 | { name: 'jsCode', type: 'string' },
73 | { name: 'pythonCode', type: 'string' },
74 | { name: 'mode', type: 'options', options: ['runOnceForAllItems', 'runOnceForEachItem'] }
75 | ];
76 |
77 | console.log('Running validation tests...\n');
78 |
79 | testCases.forEach((test, index) => {
80 | console.log(`Test ${index + 1}: ${test.name}`);
81 | console.log('─'.repeat(50));
82 |
83 | const result = EnhancedConfigValidator.validateWithMode(
84 | 'nodes-base.code',
85 | test.config,
86 | codeNodeProperties,
87 | 'operation',
88 | 'ai-friendly'
89 | );
90 |
91 | console.log(`Valid: ${result.valid}`);
92 | console.log(`Errors: ${result.errors.length}`);
93 | console.log(`Warnings: ${result.warnings.length}`);
94 |
95 | if (test.expectedError) {
96 | const hasExpectedError = result.errors.some(e =>
97 | e.message.includes(test.expectedError)
98 | );
99 | console.log(`✅ Expected error found: ${hasExpectedError}`);
100 | if (!hasExpectedError) {
101 | console.log('❌ Missing expected error:', test.expectedError);
102 | console.log('Actual errors:', result.errors.map(e => e.message));
103 | }
104 | }
105 |
106 | if (test.expectedWarning) {
107 | const hasExpectedWarning = result.warnings.some(w =>
108 | w.message.includes(test.expectedWarning)
109 | );
110 | console.log(`✅ Expected warning found: ${hasExpectedWarning}`);
111 | if (!hasExpectedWarning) {
112 | console.log('❌ Missing expected warning:', test.expectedWarning);
113 | console.log('Actual warnings:', result.warnings.map(w => w.message));
114 | }
115 | }
116 |
117 | if (test.shouldBeValid) {
118 | console.log(`✅ Should be valid: ${result.valid && result.errors.length === 0}`);
119 | if (!result.valid || result.errors.length > 0) {
120 | console.log('❌ Unexpected errors:', result.errors);
121 | }
122 | }
123 |
124 | // Show actual messages
125 | if (result.errors.length > 0) {
126 | console.log('\nErrors:');
127 | result.errors.forEach(e => console.log(` - ${e.message}`));
128 | }
129 |
130 | if (result.warnings.length > 0) {
131 | console.log('\nWarnings:');
132 | result.warnings.forEach(w => console.log(` - ${w.message}`));
133 | }
134 |
135 | console.log('\n');
136 | });
137 |
138 | console.log('✅ Expression vs Code Node validation tests completed!');
```
--------------------------------------------------------------------------------
/tests/test-sqlite-search.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Test SQLite database search functionality
5 | */
6 |
7 | const { SQLiteStorageService } = require('../dist/services/sqlite-storage-service');
8 | const { NodeSourceExtractor } = require('../dist/utils/node-source-extractor');
9 |
10 | async function testDatabaseSearch() {
11 | console.log('=== SQLite Database Search Test ===\n');
12 |
13 | const storage = new SQLiteStorageService();
14 | const extractor = new NodeSourceExtractor();
15 |
16 | // First, ensure we have some data
17 | console.log('1️⃣ Checking database status...');
18 | let stats = await storage.getStatistics();
19 |
20 | if (stats.totalNodes === 0) {
21 | console.log(' Database is empty. Adding some test nodes...\n');
22 |
23 | const testNodes = [
24 | 'n8n-nodes-base.Function',
25 | 'n8n-nodes-base.Webhook',
26 | 'n8n-nodes-base.HttpRequest',
27 | 'n8n-nodes-base.If',
28 | 'n8n-nodes-base.Slack',
29 | 'n8n-nodes-base.Discord'
30 | ];
31 |
32 | for (const nodeType of testNodes) {
33 | try {
34 | const nodeInfo = await extractor.extractNodeSource(nodeType);
35 | await storage.storeNode(nodeInfo);
36 | console.log(` ✅ Stored ${nodeType}`);
37 | } catch (error) {
38 | console.log(` ❌ Failed to store ${nodeType}: ${error.message}`);
39 | }
40 | }
41 |
42 | stats = await storage.getStatistics();
43 | }
44 |
45 | console.log(`\n Total nodes in database: ${stats.totalNodes}`);
46 | console.log(` Total packages: ${stats.totalPackages}`);
47 | console.log(` Database size: ${(stats.totalCodeSize / 1024).toFixed(2)} KB\n`);
48 |
49 | // Test different search scenarios
50 | console.log('2️⃣ Testing search functionality...\n');
51 |
52 | const searchTests = [
53 | {
54 | name: 'Search by partial name (func)',
55 | query: { query: 'func' }
56 | },
57 | {
58 | name: 'Search by partial name (web)',
59 | query: { query: 'web' }
60 | },
61 | {
62 | name: 'Search for HTTP',
63 | query: { query: 'http' }
64 | },
65 | {
66 | name: 'Search for multiple terms',
67 | query: { query: 'slack discord' }
68 | },
69 | {
70 | name: 'Filter by package',
71 | query: { packageName: 'n8n-nodes-base' }
72 | },
73 | {
74 | name: 'Search with package filter',
75 | query: { query: 'func', packageName: 'n8n-nodes-base' }
76 | },
77 | {
78 | name: 'Search by node type',
79 | query: { nodeType: 'Webhook' }
80 | },
81 | {
82 | name: 'Limit results',
83 | query: { query: 'node', limit: 3 }
84 | }
85 | ];
86 |
87 | for (const test of searchTests) {
88 | console.log(` 📍 ${test.name}:`);
89 | console.log(` Query: ${JSON.stringify(test.query)}`);
90 |
91 | try {
92 | const results = await storage.searchNodes(test.query);
93 | console.log(` Results: ${results.length} nodes found`);
94 |
95 | if (results.length > 0) {
96 | console.log(' Matches:');
97 | results.slice(0, 3).forEach(node => {
98 | console.log(` - ${node.nodeType} (${node.displayName || node.name})`);
99 | });
100 | if (results.length > 3) {
101 | console.log(` ... and ${results.length - 3} more`);
102 | }
103 | }
104 | } catch (error) {
105 | console.log(` ❌ Error: ${error.message}`);
106 | }
107 |
108 | console.log('');
109 | }
110 |
111 | // Test specific node retrieval
112 | console.log('3️⃣ Testing specific node retrieval...\n');
113 |
114 | const specificNode = await storage.getNode('n8n-nodes-base.Function');
115 | if (specificNode) {
116 | console.log(` ✅ Found node: ${specificNode.nodeType}`);
117 | console.log(` Display name: ${specificNode.displayName}`);
118 | console.log(` Code size: ${specificNode.codeLength} bytes`);
119 | console.log(` Has credentials: ${specificNode.hasCredentials}`);
120 | } else {
121 | console.log(' ❌ Node not found');
122 | }
123 |
124 | // Test package listing
125 | console.log('\n4️⃣ Testing package listing...\n');
126 |
127 | const packages = await storage.getPackages();
128 | console.log(` Found ${packages.length} packages:`);
129 | packages.forEach(pkg => {
130 | console.log(` - ${pkg.name}: ${pkg.nodeCount} nodes`);
131 | });
132 |
133 | // Close database
134 | storage.close();
135 |
136 | console.log('\n✅ Search functionality test completed!');
137 | }
138 |
139 | // Run the test
140 | testDatabaseSearch().catch(error => {
141 | console.error('Test failed:', error);
142 | process.exit(1);
143 | });
```
--------------------------------------------------------------------------------
/src/utils/node-classification.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Node Classification Utilities
3 | *
4 | * Provides shared classification logic for workflow nodes.
5 | * Used by validators to consistently identify node types across the codebase.
6 | *
7 | * This module centralizes node type classification to ensure consistent behavior
8 | * between WorkflowValidator and n8n-validation.ts, preventing bugs like sticky
9 | * notes being incorrectly flagged as disconnected nodes.
10 | */
11 |
12 | import { isTriggerNode as isTriggerNodeImpl } from './node-type-utils';
13 |
14 | /**
15 | * Check if a node type is a sticky note (documentation-only node)
16 | *
17 | * Sticky notes are UI-only annotation nodes that:
18 | * - Do not participate in workflow execution
19 | * - Never have connections (by design)
20 | * - Should be excluded from connection validation
21 | * - Serve purely as visual documentation in the workflow canvas
22 | *
23 | * Example sticky note types:
24 | * - 'n8n-nodes-base.stickyNote' (standard format)
25 | * - 'nodes-base.stickyNote' (normalized format)
26 | * - '@n8n/n8n-nodes-base.stickyNote' (scoped format)
27 | *
28 | * @param nodeType - The node type to check (e.g., 'n8n-nodes-base.stickyNote')
29 | * @returns true if the node is a sticky note, false otherwise
30 | */
31 | export function isStickyNote(nodeType: string): boolean {
32 | const stickyNoteTypes = [
33 | 'n8n-nodes-base.stickyNote',
34 | 'nodes-base.stickyNote',
35 | '@n8n/n8n-nodes-base.stickyNote'
36 | ];
37 | return stickyNoteTypes.includes(nodeType);
38 | }
39 |
40 | /**
41 | * Check if a node type is a trigger node
42 | *
43 | * This function delegates to the comprehensive trigger detection implementation
44 | * in node-type-utils.ts which supports 200+ trigger types using flexible
45 | * pattern matching instead of a hardcoded list.
46 | *
47 | * Trigger nodes:
48 | * - Start workflow execution
49 | * - Only need outgoing connections (no incoming connections required)
50 | * - Include webhooks, manual triggers, schedule triggers, email triggers, etc.
51 | * - Are the entry points for workflow execution
52 | *
53 | * Examples:
54 | * - Webhooks: Listen for HTTP requests
55 | * - Manual triggers: Started manually by user
56 | * - Schedule/Cron triggers: Run on a schedule
57 | * - Execute Workflow Trigger: Invoked by other workflows
58 | *
59 | * @param nodeType - The node type to check
60 | * @returns true if the node is a trigger, false otherwise
61 | */
62 | export function isTriggerNode(nodeType: string): boolean {
63 | return isTriggerNodeImpl(nodeType);
64 | }
65 |
66 | /**
67 | * Check if a node type is non-executable (UI-only)
68 | *
69 | * Non-executable nodes:
70 | * - Do not participate in workflow execution
71 | * - Serve documentation/annotation purposes only
72 | * - Should be excluded from all execution-related validation
73 | * - Should be excluded from statistics like "total executable nodes"
74 | * - Should be excluded from connection validation
75 | *
76 | * Currently includes: sticky notes
77 | *
78 | * Future: May include other annotation/comment nodes if n8n adds them
79 | *
80 | * @param nodeType - The node type to check
81 | * @returns true if the node is non-executable, false otherwise
82 | */
83 | export function isNonExecutableNode(nodeType: string): boolean {
84 | return isStickyNote(nodeType);
85 | // Future: Add other non-executable node types here
86 | // Example: || isCommentNode(nodeType) || isAnnotationNode(nodeType)
87 | }
88 |
89 | /**
90 | * Check if a node type requires incoming connections
91 | *
92 | * Most nodes require at least one incoming connection to receive data,
93 | * but there are two categories of exceptions:
94 | *
95 | * 1. Trigger nodes: Only need outgoing connections
96 | * - They start workflow execution
97 | * - They generate their own data
98 | * - Examples: webhook, manualTrigger, scheduleTrigger
99 | *
100 | * 2. Non-executable nodes: Don't need any connections
101 | * - They are UI-only annotations
102 | * - They don't participate in execution
103 | * - Examples: stickyNote
104 | *
105 | * @param nodeType - The node type to check
106 | * @returns true if the node requires incoming connections, false otherwise
107 | */
108 | export function requiresIncomingConnection(nodeType: string): boolean {
109 | // Non-executable nodes don't need any connections
110 | if (isNonExecutableNode(nodeType)) {
111 | return false;
112 | }
113 |
114 | // Trigger nodes only need outgoing connections
115 | if (isTriggerNode(nodeType)) {
116 | return false;
117 | }
118 |
119 | // Regular nodes need incoming connections
120 | return true;
121 | }
122 |
```
--------------------------------------------------------------------------------
/scripts/quick-test.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env ts-node
2 | /**
3 | * Quick test script to validate the essentials implementation
4 | */
5 |
6 | import { spawn } from 'child_process';
7 | import { join } from 'path';
8 |
9 | const colors = {
10 | reset: '\x1b[0m',
11 | bright: '\x1b[1m',
12 | green: '\x1b[32m',
13 | red: '\x1b[31m',
14 | yellow: '\x1b[33m',
15 | blue: '\x1b[34m',
16 | cyan: '\x1b[36m'
17 | };
18 |
19 | function log(message: string, color: string = colors.reset) {
20 | console.log(`${color}${message}${colors.reset}`);
21 | }
22 |
23 | async function runMCPCommand(toolName: string, args: any): Promise<any> {
24 | return new Promise((resolve, reject) => {
25 | const request = {
26 | jsonrpc: '2.0',
27 | method: 'tools/call',
28 | params: {
29 | name: toolName,
30 | arguments: args
31 | },
32 | id: 1
33 | };
34 |
35 | const mcp = spawn('npm', ['start'], {
36 | cwd: join(__dirname, '..'),
37 | stdio: ['pipe', 'pipe', 'pipe']
38 | });
39 |
40 | let output = '';
41 | let error = '';
42 |
43 | mcp.stdout.on('data', (data) => {
44 | output += data.toString();
45 | });
46 |
47 | mcp.stderr.on('data', (data) => {
48 | error += data.toString();
49 | });
50 |
51 | mcp.on('close', (code) => {
52 | if (code !== 0) {
53 | reject(new Error(`Process exited with code ${code}: ${error}`));
54 | return;
55 | }
56 |
57 | try {
58 | // Parse JSON-RPC response
59 | const lines = output.split('\n');
60 | for (const line of lines) {
61 | if (line.trim() && line.includes('"jsonrpc"')) {
62 | const response = JSON.parse(line);
63 | if (response.result) {
64 | resolve(JSON.parse(response.result.content[0].text));
65 | return;
66 | } else if (response.error) {
67 | reject(new Error(response.error.message));
68 | return;
69 | }
70 | }
71 | }
72 | reject(new Error('No valid response found'));
73 | } catch (err) {
74 | reject(err);
75 | }
76 | });
77 |
78 | // Send request
79 | mcp.stdin.write(JSON.stringify(request) + '\n');
80 | mcp.stdin.end();
81 | });
82 | }
83 |
84 | async function quickTest() {
85 | log('\n🚀 Quick Test - n8n MCP Essentials', colors.bright + colors.cyan);
86 |
87 | try {
88 | // Test 1: Get essentials for HTTP Request
89 | log('\n1️⃣ Testing get_node_essentials for HTTP Request...', colors.yellow);
90 | const essentials = await runMCPCommand('get_node_essentials', {
91 | nodeType: 'nodes-base.httpRequest'
92 | });
93 |
94 | log('✅ Success! Got essentials:', colors.green);
95 | log(` Required properties: ${essentials.requiredProperties?.map((p: any) => p.name).join(', ') || 'None'}`);
96 | log(` Common properties: ${essentials.commonProperties?.map((p: any) => p.name).join(', ') || 'None'}`);
97 | log(` Examples: ${Object.keys(essentials.examples || {}).join(', ')}`);
98 | log(` Response size: ${JSON.stringify(essentials).length} bytes`, colors.green);
99 |
100 | // Test 2: Search properties
101 | log('\n2️⃣ Testing search_node_properties...', colors.yellow);
102 | const searchResults = await runMCPCommand('search_node_properties', {
103 | nodeType: 'nodes-base.httpRequest',
104 | query: 'auth'
105 | });
106 |
107 | log('✅ Success! Found properties:', colors.green);
108 | log(` Matches: ${searchResults.totalMatches}`);
109 | searchResults.matches?.slice(0, 3).forEach((match: any) => {
110 | log(` - ${match.name}: ${match.description}`);
111 | });
112 |
113 | // Test 3: Compare sizes
114 | log('\n3️⃣ Comparing response sizes...', colors.yellow);
115 | const fullInfo = await runMCPCommand('get_node_info', {
116 | nodeType: 'nodes-base.httpRequest'
117 | });
118 |
119 | const fullSize = JSON.stringify(fullInfo).length;
120 | const essentialSize = JSON.stringify(essentials).length;
121 | const reduction = ((fullSize - essentialSize) / fullSize * 100).toFixed(1);
122 |
123 | log(`✅ Size comparison:`, colors.green);
124 | log(` Full response: ${(fullSize / 1024).toFixed(1)} KB`);
125 | log(` Essential response: ${(essentialSize / 1024).toFixed(1)} KB`);
126 | log(` Size reduction: ${reduction}% 🎉`, colors.bright + colors.green);
127 |
128 | log('\n✨ All tests passed!', colors.bright + colors.green);
129 |
130 | } catch (error) {
131 | log(`\n❌ Test failed: ${error}`, colors.red);
132 | process.exit(1);
133 | }
134 | }
135 |
136 | // Run if called directly
137 | if (require.main === module) {
138 | quickTest().catch(console.error);
139 | }
```
--------------------------------------------------------------------------------
/scripts/generate-release-notes.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Generate release notes from commit messages between two tags
5 | * Used by GitHub Actions to create automated release notes
6 | */
7 |
8 | const { execSync } = require('child_process');
9 | const fs = require('fs');
10 | const path = require('path');
11 |
12 | function generateReleaseNotes(previousTag, currentTag) {
13 | try {
14 | console.log(`Generating release notes from ${previousTag} to ${currentTag}`);
15 |
16 | // Get commits between tags
17 | const gitLogCommand = `git log --pretty=format:"%H|%s|%an|%ae|%ad" --date=short --no-merges ${previousTag}..${currentTag}`;
18 | const commitsOutput = execSync(gitLogCommand, { encoding: 'utf8' });
19 |
20 | if (!commitsOutput.trim()) {
21 | console.log('No commits found between tags');
22 | return 'No changes in this release.';
23 | }
24 |
25 | const commits = commitsOutput.trim().split('\n').map(line => {
26 | const [hash, subject, author, email, date] = line.split('|');
27 | return { hash, subject, author, email, date };
28 | });
29 |
30 | // Categorize commits
31 | const categories = {
32 | 'feat': { title: '✨ Features', commits: [] },
33 | 'fix': { title: '🐛 Bug Fixes', commits: [] },
34 | 'docs': { title: '📚 Documentation', commits: [] },
35 | 'refactor': { title: '♻️ Refactoring', commits: [] },
36 | 'test': { title: '🧪 Testing', commits: [] },
37 | 'perf': { title: '⚡ Performance', commits: [] },
38 | 'style': { title: '💅 Styling', commits: [] },
39 | 'ci': { title: '🔧 CI/CD', commits: [] },
40 | 'build': { title: '📦 Build', commits: [] },
41 | 'chore': { title: '🔧 Maintenance', commits: [] },
42 | 'other': { title: '📝 Other Changes', commits: [] }
43 | };
44 |
45 | commits.forEach(commit => {
46 | const subject = commit.subject.toLowerCase();
47 | let categorized = false;
48 |
49 | // Check for conventional commit prefixes
50 | for (const [prefix, category] of Object.entries(categories)) {
51 | if (prefix !== 'other' && subject.startsWith(`${prefix}:`)) {
52 | category.commits.push(commit);
53 | categorized = true;
54 | break;
55 | }
56 | }
57 |
58 | // If not categorized, put in other
59 | if (!categorized) {
60 | categories.other.commits.push(commit);
61 | }
62 | });
63 |
64 | // Generate release notes
65 | const releaseNotes = [];
66 |
67 | for (const [key, category] of Object.entries(categories)) {
68 | if (category.commits.length > 0) {
69 | releaseNotes.push(`### ${category.title}`);
70 | releaseNotes.push('');
71 |
72 | category.commits.forEach(commit => {
73 | // Clean up the subject by removing the prefix if it exists
74 | let cleanSubject = commit.subject;
75 | const colonIndex = cleanSubject.indexOf(':');
76 | if (colonIndex !== -1 && cleanSubject.substring(0, colonIndex).match(/^(feat|fix|docs|refactor|test|perf|style|ci|build|chore)$/)) {
77 | cleanSubject = cleanSubject.substring(colonIndex + 1).trim();
78 | // Capitalize first letter
79 | cleanSubject = cleanSubject.charAt(0).toUpperCase() + cleanSubject.slice(1);
80 | }
81 |
82 | releaseNotes.push(`- ${cleanSubject} (${commit.hash.substring(0, 7)})`);
83 | });
84 |
85 | releaseNotes.push('');
86 | }
87 | }
88 |
89 | // Add commit statistics
90 | const totalCommits = commits.length;
91 | const contributors = [...new Set(commits.map(c => c.author))];
92 |
93 | releaseNotes.push('---');
94 | releaseNotes.push('');
95 | releaseNotes.push(`**Release Statistics:**`);
96 | releaseNotes.push(`- ${totalCommits} commit${totalCommits !== 1 ? 's' : ''}`);
97 | releaseNotes.push(`- ${contributors.length} contributor${contributors.length !== 1 ? 's' : ''}`);
98 |
99 | if (contributors.length <= 5) {
100 | releaseNotes.push(`- Contributors: ${contributors.join(', ')}`);
101 | }
102 |
103 | return releaseNotes.join('\n');
104 |
105 | } catch (error) {
106 | console.error(`Error generating release notes: ${error.message}`);
107 | return `Failed to generate release notes: ${error.message}`;
108 | }
109 | }
110 |
111 | // Parse command line arguments
112 | const previousTag = process.argv[2];
113 | const currentTag = process.argv[3];
114 |
115 | if (!previousTag || !currentTag) {
116 | console.error('Usage: generate-release-notes.js <previous-tag> <current-tag>');
117 | process.exit(1);
118 | }
119 |
120 | const releaseNotes = generateReleaseNotes(previousTag, currentTag);
121 | console.log(releaseNotes);
122 |
```
--------------------------------------------------------------------------------
/tests/benchmarks/database-queries.bench.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { bench, describe } from 'vitest';
2 | import { NodeRepository } from '../../src/database/node-repository';
3 | import { SQLiteStorageService } from '../../src/services/sqlite-storage-service';
4 | import { NodeFactory } from '../factories/node-factory';
5 | import { PropertyDefinitionFactory } from '../factories/property-definition-factory';
6 |
7 | /**
8 | * Database Query Performance Benchmarks
9 | *
10 | * NOTE: These benchmarks use MOCK DATA (500 artificial test nodes)
11 | * created with factories, not the real production database.
12 | *
13 | * This is useful for tracking database layer performance in isolation,
14 | * but may not reflect real-world performance characteristics.
15 | *
16 | * For end-to-end MCP tool performance with real data, see mcp-tools.bench.ts
17 | */
18 | describe('Database Query Performance', () => {
19 | let repository: NodeRepository;
20 | let storage: SQLiteStorageService;
21 | const testNodeCount = 500;
22 |
23 | beforeAll(async () => {
24 | storage = new SQLiteStorageService(':memory:');
25 | repository = new NodeRepository(storage);
26 |
27 | // Seed database with test data
28 | for (let i = 0; i < testNodeCount; i++) {
29 | const node = NodeFactory.build({
30 | displayName: `TestNode${i}`,
31 | nodeType: `nodes-base.testNode${i}`,
32 | category: i % 2 === 0 ? 'transform' : 'trigger',
33 | packageName: 'n8n-nodes-base',
34 | documentation: `Test documentation for node ${i}`,
35 | properties: PropertyDefinitionFactory.buildList(5)
36 | });
37 | await repository.upsertNode(node);
38 | }
39 | });
40 |
41 | afterAll(() => {
42 | storage.close();
43 | });
44 |
45 | bench('getNodeByType - existing node', async () => {
46 | await repository.getNodeByType('nodes-base.testNode100');
47 | }, {
48 | iterations: 1000,
49 | warmupIterations: 100,
50 | warmupTime: 500,
51 | time: 3000
52 | });
53 |
54 | bench('getNodeByType - non-existing node', async () => {
55 | await repository.getNodeByType('nodes-base.nonExistentNode');
56 | }, {
57 | iterations: 1000,
58 | warmupIterations: 100,
59 | warmupTime: 500,
60 | time: 3000
61 | });
62 |
63 | bench('getNodesByCategory - transform', async () => {
64 | await repository.getNodesByCategory('transform');
65 | }, {
66 | iterations: 100,
67 | warmupIterations: 10,
68 | warmupTime: 500,
69 | time: 3000
70 | });
71 |
72 | bench('searchNodes - OR mode', async () => {
73 | await repository.searchNodes('test node data', 'OR', 20);
74 | }, {
75 | iterations: 100,
76 | warmupIterations: 10,
77 | warmupTime: 500,
78 | time: 3000
79 | });
80 |
81 | bench('searchNodes - AND mode', async () => {
82 | await repository.searchNodes('test node', 'AND', 20);
83 | }, {
84 | iterations: 100,
85 | warmupIterations: 10,
86 | warmupTime: 500,
87 | time: 3000
88 | });
89 |
90 | bench('searchNodes - FUZZY mode', async () => {
91 | await repository.searchNodes('tst nde', 'FUZZY', 20);
92 | }, {
93 | iterations: 100,
94 | warmupIterations: 10,
95 | warmupTime: 500,
96 | time: 3000
97 | });
98 |
99 | bench('getAllNodes - no limit', async () => {
100 | await repository.getAllNodes();
101 | }, {
102 | iterations: 50,
103 | warmupIterations: 5,
104 | warmupTime: 500,
105 | time: 3000
106 | });
107 |
108 | bench('getAllNodes - with limit', async () => {
109 | await repository.getAllNodes(50);
110 | }, {
111 | iterations: 100,
112 | warmupIterations: 10,
113 | warmupTime: 500,
114 | time: 3000
115 | });
116 |
117 | bench('getNodeCount', async () => {
118 | await repository.getNodeCount();
119 | }, {
120 | iterations: 1000,
121 | warmupIterations: 100,
122 | warmupTime: 100,
123 | time: 2000
124 | });
125 |
126 | bench('getAIToolNodes', async () => {
127 | await repository.getAIToolNodes();
128 | }, {
129 | iterations: 100,
130 | warmupIterations: 10,
131 | warmupTime: 500,
132 | time: 3000
133 | });
134 |
135 | bench('upsertNode - new node', async () => {
136 | const node = NodeFactory.build({
137 | displayName: `BenchNode${Date.now()}`,
138 | nodeType: `nodes-base.benchNode${Date.now()}`
139 | });
140 | await repository.upsertNode(node);
141 | }, {
142 | iterations: 100,
143 | warmupIterations: 10,
144 | warmupTime: 500,
145 | time: 3000
146 | });
147 |
148 | bench('upsertNode - existing node update', async () => {
149 | const existingNode = await repository.getNodeByType('nodes-base.testNode0');
150 | if (existingNode) {
151 | existingNode.description = `Updated description ${Date.now()}`;
152 | await repository.upsertNode(existingNode);
153 | }
154 | }, {
155 | iterations: 100,
156 | warmupIterations: 10,
157 | warmupTime: 500,
158 | time: 3000
159 | });
160 | });
```
--------------------------------------------------------------------------------
/tests/unit/test-infrastructure.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
2 | import { nodeFactory, webhookNodeFactory, slackNodeFactory } from '@tests/fixtures/factories/node.factory';
3 |
4 | // Mock better-sqlite3
5 | vi.mock('better-sqlite3');
6 |
7 | describe('Test Infrastructure', () => {
8 | describe('Database Mock', () => {
9 | it('should create a mock database instance', async () => {
10 | const Database = (await import('better-sqlite3')).default;
11 | const db = new Database(':memory:');
12 |
13 | expect(Database).toHaveBeenCalled();
14 | expect(db).toBeDefined();
15 | expect(db.prepare).toBeDefined();
16 | expect(db.exec).toBeDefined();
17 | expect(db.close).toBeDefined();
18 | });
19 |
20 | it('should handle basic CRUD operations', async () => {
21 | const { MockDatabase } = await import('@tests/unit/database/__mocks__/better-sqlite3');
22 | const db = new MockDatabase();
23 |
24 | // Test data seeding
25 | db._seedData('nodes', [
26 | { id: '1', name: 'test-node', type: 'webhook' }
27 | ]);
28 |
29 | // Test SELECT
30 | const selectStmt = db.prepare('SELECT * FROM nodes');
31 | const allNodes = selectStmt.all();
32 | expect(allNodes).toHaveLength(1);
33 | expect(allNodes[0]).toEqual({ id: '1', name: 'test-node', type: 'webhook' });
34 |
35 | // Test INSERT
36 | const insertStmt = db.prepare('INSERT INTO nodes (id, name, type) VALUES (?, ?, ?)');
37 | const result = insertStmt.run({ id: '2', name: 'new-node', type: 'slack' });
38 | expect(result.changes).toBe(1);
39 |
40 | // Verify insert worked
41 | const allNodesAfter = selectStmt.all();
42 | expect(allNodesAfter).toHaveLength(2);
43 | });
44 | });
45 |
46 | describe('Node Factory', () => {
47 | it('should create a basic node definition', () => {
48 | const node = nodeFactory.build();
49 |
50 | expect(node).toMatchObject({
51 | name: expect.any(String),
52 | displayName: expect.any(String),
53 | description: expect.any(String),
54 | version: expect.any(Number),
55 | defaults: {
56 | name: expect.any(String)
57 | },
58 | inputs: ['main'],
59 | outputs: ['main'],
60 | properties: expect.any(Array),
61 | credentials: []
62 | });
63 | });
64 |
65 | it('should create a webhook node', () => {
66 | const webhook = webhookNodeFactory.build();
67 |
68 | expect(webhook).toMatchObject({
69 | name: 'webhook',
70 | displayName: 'Webhook',
71 | description: 'Starts the workflow when a webhook is called',
72 | group: ['trigger'],
73 | properties: expect.arrayContaining([
74 | expect.objectContaining({
75 | name: 'path',
76 | type: 'string',
77 | required: true
78 | }),
79 | expect.objectContaining({
80 | name: 'method',
81 | type: 'options'
82 | })
83 | ])
84 | });
85 | });
86 |
87 | it('should create a slack node', () => {
88 | const slack = slackNodeFactory.build();
89 |
90 | expect(slack).toMatchObject({
91 | name: 'slack',
92 | displayName: 'Slack',
93 | description: 'Send messages to Slack',
94 | group: ['output'],
95 | credentials: [
96 | {
97 | name: 'slackApi',
98 | required: true
99 | }
100 | ],
101 | properties: expect.arrayContaining([
102 | expect.objectContaining({
103 | name: 'resource',
104 | type: 'options'
105 | }),
106 | expect.objectContaining({
107 | name: 'operation',
108 | type: 'options',
109 | displayOptions: {
110 | show: {
111 | resource: ['message']
112 | }
113 | }
114 | })
115 | ])
116 | });
117 | });
118 |
119 | it('should allow overriding factory defaults', () => {
120 | const customNode = nodeFactory.build({
121 | name: 'custom-node',
122 | displayName: 'Custom Node',
123 | version: 2
124 | });
125 |
126 | expect(customNode.name).toBe('custom-node');
127 | expect(customNode.displayName).toBe('Custom Node');
128 | expect(customNode.version).toBe(2);
129 | });
130 |
131 | it('should create multiple unique nodes', () => {
132 | const nodes = nodeFactory.buildList(5);
133 |
134 | expect(nodes).toHaveLength(5);
135 | const names = nodes.map(n => n.name);
136 | const uniqueNames = new Set(names);
137 | expect(uniqueNames.size).toBe(5);
138 | });
139 | });
140 | });
```
--------------------------------------------------------------------------------
/scripts/test-user-id-persistence.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Test User ID Persistence
3 | * Verifies that user IDs are consistent across sessions and modes
4 | */
5 |
6 | import { TelemetryConfigManager } from '../src/telemetry/config-manager';
7 | import { hostname, platform, arch, homedir } from 'os';
8 | import { createHash } from 'crypto';
9 |
10 | console.log('=== User ID Persistence Test ===\n');
11 |
12 | // Test 1: Verify deterministic ID generation
13 | console.log('Test 1: Deterministic ID Generation');
14 | console.log('-----------------------------------');
15 |
16 | const machineId = `${hostname()}-${platform()}-${arch()}-${homedir()}`;
17 | const expectedUserId = createHash('sha256')
18 | .update(machineId)
19 | .digest('hex')
20 | .substring(0, 16);
21 |
22 | console.log('Machine characteristics:');
23 | console.log(' hostname:', hostname());
24 | console.log(' platform:', platform());
25 | console.log(' arch:', arch());
26 | console.log(' homedir:', homedir());
27 | console.log('\nGenerated machine ID:', machineId);
28 | console.log('Expected user ID:', expectedUserId);
29 |
30 | // Test 2: Load actual config
31 | console.log('\n\nTest 2: Actual Config Manager');
32 | console.log('-----------------------------------');
33 |
34 | const configManager = TelemetryConfigManager.getInstance();
35 | const actualUserId = configManager.getUserId();
36 | const config = configManager.loadConfig();
37 |
38 | console.log('Actual user ID:', actualUserId);
39 | console.log('Config first run:', config.firstRun || 'Unknown');
40 | console.log('Config version:', config.version || 'Unknown');
41 | console.log('Telemetry enabled:', config.enabled);
42 |
43 | // Test 3: Verify consistency
44 | console.log('\n\nTest 3: Consistency Check');
45 | console.log('-----------------------------------');
46 |
47 | const match = actualUserId === expectedUserId;
48 | console.log('User IDs match:', match ? '✓ YES' : '✗ NO');
49 |
50 | if (!match) {
51 | console.log('WARNING: User ID mismatch detected!');
52 | console.log('This could indicate an implementation issue.');
53 | }
54 |
55 | // Test 4: Multiple loads (simulate multiple sessions)
56 | console.log('\n\nTest 4: Multiple Session Simulation');
57 | console.log('-----------------------------------');
58 |
59 | const userId1 = configManager.getUserId();
60 | const userId2 = TelemetryConfigManager.getInstance().getUserId();
61 | const userId3 = configManager.getUserId();
62 |
63 | console.log('Session 1 user ID:', userId1);
64 | console.log('Session 2 user ID:', userId2);
65 | console.log('Session 3 user ID:', userId3);
66 |
67 | const consistent = userId1 === userId2 && userId2 === userId3;
68 | console.log('All sessions consistent:', consistent ? '✓ YES' : '✗ NO');
69 |
70 | // Test 5: Docker environment simulation
71 | console.log('\n\nTest 5: Docker Environment Check');
72 | console.log('-----------------------------------');
73 |
74 | const isDocker = process.env.IS_DOCKER === 'true';
75 | console.log('Running in Docker:', isDocker);
76 |
77 | if (isDocker) {
78 | console.log('\n⚠️ DOCKER MODE DETECTED');
79 | console.log('In Docker, user IDs may change across container recreations because:');
80 | console.log(' 1. Container hostname changes each time');
81 | console.log(' 2. Config file is not persisted (no volume mount)');
82 | console.log(' 3. Each container gets a new ephemeral filesystem');
83 | console.log('\nRecommendation: Mount ~/.n8n-mcp as a volume for persistent user IDs');
84 | }
85 |
86 | // Test 6: Environment variable override check
87 | console.log('\n\nTest 6: Environment Variable Override');
88 | console.log('-----------------------------------');
89 |
90 | const telemetryDisabledVars = [
91 | 'N8N_MCP_TELEMETRY_DISABLED',
92 | 'TELEMETRY_DISABLED',
93 | 'DISABLE_TELEMETRY'
94 | ];
95 |
96 | telemetryDisabledVars.forEach(varName => {
97 | const value = process.env[varName];
98 | if (value !== undefined) {
99 | console.log(`${varName}:`, value);
100 | }
101 | });
102 |
103 | console.log('\nTelemetry status:', configManager.isEnabled() ? 'ENABLED' : 'DISABLED');
104 |
105 | // Summary
106 | console.log('\n\n=== SUMMARY ===');
107 | console.log('User ID:', actualUserId);
108 | console.log('Deterministic:', match ? 'YES ✓' : 'NO ✗');
109 | console.log('Persistent across sessions:', consistent ? 'YES ✓' : 'NO ✗');
110 | console.log('Telemetry enabled:', config.enabled ? 'YES' : 'NO');
111 | console.log('Docker mode:', isDocker ? 'YES' : 'NO');
112 |
113 | if (isDocker && !process.env.N8N_MCP_CONFIG_VOLUME) {
114 | console.log('\n⚠️ WARNING: Running in Docker without persistent volume!');
115 | console.log('User IDs will change on container recreation.');
116 | console.log('Mount /home/nodejs/.n8n-mcp to persist telemetry config.');
117 | }
118 |
119 | console.log('\n');
120 |
```
--------------------------------------------------------------------------------
/src/telemetry/startup-checkpoints.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Startup Checkpoint System
3 | * Defines checkpoints throughout the server initialization process
4 | * to identify where failures occur
5 | */
6 |
7 | /**
8 | * Startup checkpoint constants
9 | * These checkpoints mark key stages in the server initialization process
10 | */
11 | export const STARTUP_CHECKPOINTS = {
12 | /** Process has started, very first checkpoint */
13 | PROCESS_STARTED: 'process_started',
14 |
15 | /** About to connect to database */
16 | DATABASE_CONNECTING: 'database_connecting',
17 |
18 | /** Database connection successful */
19 | DATABASE_CONNECTED: 'database_connected',
20 |
21 | /** About to check n8n API configuration (if applicable) */
22 | N8N_API_CHECKING: 'n8n_api_checking',
23 |
24 | /** n8n API is configured and ready (if applicable) */
25 | N8N_API_READY: 'n8n_api_ready',
26 |
27 | /** About to initialize telemetry system */
28 | TELEMETRY_INITIALIZING: 'telemetry_initializing',
29 |
30 | /** Telemetry system is ready */
31 | TELEMETRY_READY: 'telemetry_ready',
32 |
33 | /** About to start MCP handshake */
34 | MCP_HANDSHAKE_STARTING: 'mcp_handshake_starting',
35 |
36 | /** MCP handshake completed successfully */
37 | MCP_HANDSHAKE_COMPLETE: 'mcp_handshake_complete',
38 |
39 | /** Server is fully ready to handle requests */
40 | SERVER_READY: 'server_ready',
41 | } as const;
42 |
43 | /**
44 | * Type for checkpoint names
45 | */
46 | export type StartupCheckpoint = typeof STARTUP_CHECKPOINTS[keyof typeof STARTUP_CHECKPOINTS];
47 |
48 | /**
49 | * Checkpoint data structure
50 | */
51 | export interface CheckpointData {
52 | name: StartupCheckpoint;
53 | timestamp: number;
54 | success: boolean;
55 | error?: string;
56 | }
57 |
58 | /**
59 | * Get all checkpoint names in order
60 | */
61 | export function getAllCheckpoints(): StartupCheckpoint[] {
62 | return Object.values(STARTUP_CHECKPOINTS);
63 | }
64 |
65 | /**
66 | * Find which checkpoint failed based on the list of passed checkpoints
67 | * Returns the first checkpoint that was not passed
68 | */
69 | export function findFailedCheckpoint(passedCheckpoints: string[]): StartupCheckpoint {
70 | const allCheckpoints = getAllCheckpoints();
71 |
72 | for (const checkpoint of allCheckpoints) {
73 | if (!passedCheckpoints.includes(checkpoint)) {
74 | return checkpoint;
75 | }
76 | }
77 |
78 | // If all checkpoints were passed, the failure must have occurred after SERVER_READY
79 | // This would be an unexpected post-initialization failure
80 | return STARTUP_CHECKPOINTS.SERVER_READY;
81 | }
82 |
83 | /**
84 | * Validate if a string is a valid checkpoint
85 | */
86 | export function isValidCheckpoint(checkpoint: string): checkpoint is StartupCheckpoint {
87 | return getAllCheckpoints().includes(checkpoint as StartupCheckpoint);
88 | }
89 |
90 | /**
91 | * Get human-readable description for a checkpoint
92 | */
93 | export function getCheckpointDescription(checkpoint: StartupCheckpoint): string {
94 | const descriptions: Record<StartupCheckpoint, string> = {
95 | [STARTUP_CHECKPOINTS.PROCESS_STARTED]: 'Process initialization started',
96 | [STARTUP_CHECKPOINTS.DATABASE_CONNECTING]: 'Connecting to database',
97 | [STARTUP_CHECKPOINTS.DATABASE_CONNECTED]: 'Database connection established',
98 | [STARTUP_CHECKPOINTS.N8N_API_CHECKING]: 'Checking n8n API configuration',
99 | [STARTUP_CHECKPOINTS.N8N_API_READY]: 'n8n API ready',
100 | [STARTUP_CHECKPOINTS.TELEMETRY_INITIALIZING]: 'Initializing telemetry system',
101 | [STARTUP_CHECKPOINTS.TELEMETRY_READY]: 'Telemetry system ready',
102 | [STARTUP_CHECKPOINTS.MCP_HANDSHAKE_STARTING]: 'Starting MCP protocol handshake',
103 | [STARTUP_CHECKPOINTS.MCP_HANDSHAKE_COMPLETE]: 'MCP handshake completed',
104 | [STARTUP_CHECKPOINTS.SERVER_READY]: 'Server fully initialized and ready',
105 | };
106 |
107 | return descriptions[checkpoint] || 'Unknown checkpoint';
108 | }
109 |
110 | /**
111 | * Get the next expected checkpoint after the given one
112 | * Returns null if this is the last checkpoint
113 | */
114 | export function getNextCheckpoint(current: StartupCheckpoint): StartupCheckpoint | null {
115 | const allCheckpoints = getAllCheckpoints();
116 | const currentIndex = allCheckpoints.indexOf(current);
117 |
118 | if (currentIndex === -1 || currentIndex === allCheckpoints.length - 1) {
119 | return null;
120 | }
121 |
122 | return allCheckpoints[currentIndex + 1];
123 | }
124 |
125 | /**
126 | * Calculate completion percentage based on checkpoints passed
127 | */
128 | export function getCompletionPercentage(passedCheckpoints: string[]): number {
129 | const totalCheckpoints = getAllCheckpoints().length;
130 | const passedCount = passedCheckpoints.length;
131 |
132 | return Math.round((passedCount / totalCheckpoints) * 100);
133 | }
134 |
```
--------------------------------------------------------------------------------
/tests/unit/services/expression-validator.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, vi, beforeEach } from 'vitest';
2 | import { ExpressionValidator } from '@/services/expression-validator';
3 |
4 | describe('ExpressionValidator', () => {
5 | const defaultContext = {
6 | availableNodes: [],
7 | currentNodeName: 'TestNode',
8 | isInLoop: false,
9 | hasInputData: true
10 | };
11 |
12 | beforeEach(() => {
13 | vi.clearAllMocks();
14 | });
15 |
16 | describe('validateExpression', () => {
17 | it('should be a static method that validates expressions', () => {
18 | expect(typeof ExpressionValidator.validateExpression).toBe('function');
19 | });
20 |
21 | it('should return a validation result', () => {
22 | const result = ExpressionValidator.validateExpression('{{ $json.field }}', defaultContext);
23 |
24 | expect(result).toHaveProperty('valid');
25 | expect(result).toHaveProperty('errors');
26 | expect(result).toHaveProperty('warnings');
27 | expect(result).toHaveProperty('usedVariables');
28 | expect(result).toHaveProperty('usedNodes');
29 | });
30 |
31 | it('should validate expressions with proper syntax', () => {
32 | const validExpr = '{{ $json.field }}';
33 | const result = ExpressionValidator.validateExpression(validExpr, defaultContext);
34 |
35 | expect(result).toBeDefined();
36 | expect(Array.isArray(result.errors)).toBe(true);
37 | });
38 |
39 | it('should detect malformed expressions', () => {
40 | const invalidExpr = '{{ $json.field'; // Missing closing braces
41 | const result = ExpressionValidator.validateExpression(invalidExpr, defaultContext);
42 |
43 | expect(result.errors.length).toBeGreaterThan(0);
44 | });
45 | });
46 |
47 | describe('validateNodeExpressions', () => {
48 | it('should validate all expressions in node parameters', () => {
49 | const parameters = {
50 | field1: '{{ $json.data }}',
51 | nested: {
52 | field2: 'regular text',
53 | field3: '{{ $node["Webhook"].json }}'
54 | }
55 | };
56 |
57 | const result = ExpressionValidator.validateNodeExpressions(parameters, defaultContext);
58 |
59 | expect(result).toHaveProperty('valid');
60 | expect(result).toHaveProperty('errors');
61 | expect(result).toHaveProperty('warnings');
62 | });
63 |
64 | it('should collect errors from invalid expressions', () => {
65 | const parameters = {
66 | badExpr: '{{ $json.field', // Missing closing
67 | goodExpr: '{{ $json.field }}'
68 | };
69 |
70 | const result = ExpressionValidator.validateNodeExpressions(parameters, defaultContext);
71 |
72 | expect(result.errors.length).toBeGreaterThan(0);
73 | });
74 | });
75 |
76 | describe('expression patterns', () => {
77 | it('should recognize n8n variable patterns', () => {
78 | const expressions = [
79 | '{{ $json }}',
80 | '{{ $json.field }}',
81 | '{{ $node["NodeName"].json }}',
82 | '{{ $workflow.id }}',
83 | '{{ $now }}',
84 | '{{ $itemIndex }}'
85 | ];
86 |
87 | expressions.forEach(expr => {
88 | const result = ExpressionValidator.validateExpression(expr, defaultContext);
89 | expect(result).toBeDefined();
90 | });
91 | });
92 | });
93 |
94 | describe('context validation', () => {
95 | it('should use available nodes from context', () => {
96 | const contextWithNodes = {
97 | ...defaultContext,
98 | availableNodes: ['Webhook', 'Function', 'Slack']
99 | };
100 |
101 | const expr = '{{ $node["Webhook"].json }}';
102 | const result = ExpressionValidator.validateExpression(expr, contextWithNodes);
103 |
104 | expect(result.usedNodes.has('Webhook')).toBe(true);
105 | });
106 | });
107 |
108 | describe('edge cases', () => {
109 | it('should handle empty expressions', () => {
110 | const result = ExpressionValidator.validateExpression('{{ }}', defaultContext);
111 | // The implementation might consider empty expressions as valid
112 | expect(result).toBeDefined();
113 | expect(Array.isArray(result.errors)).toBe(true);
114 | });
115 |
116 | it('should handle non-expression text', () => {
117 | const result = ExpressionValidator.validateExpression('regular text without expressions', defaultContext);
118 | expect(result.valid).toBe(true);
119 | expect(result.errors).toHaveLength(0);
120 | });
121 |
122 | it('should handle nested expressions', () => {
123 | const expr = '{{ $json[{{ $json.index }}] }}'; // Nested expressions not allowed
124 | const result = ExpressionValidator.validateExpression(expr, defaultContext);
125 | expect(result).toBeDefined();
126 | });
127 | });
128 | });
```
--------------------------------------------------------------------------------
/src/utils/bridge.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { INodeExecutionData, IDataObject } from 'n8n-workflow';
2 |
3 | export class N8NMCPBridge {
4 | /**
5 | * Convert n8n workflow data to MCP tool arguments
6 | */
7 | static n8nToMCPToolArgs(data: IDataObject): any {
8 | // Handle different data formats from n8n
9 | if (data.json) {
10 | return data.json;
11 | }
12 |
13 | // Remove n8n-specific metadata
14 | const { pairedItem, ...cleanData } = data;
15 | return cleanData;
16 | }
17 |
18 | /**
19 | * Convert MCP tool response to n8n execution data
20 | */
21 | static mcpToN8NExecutionData(mcpResponse: any, itemIndex: number = 0): INodeExecutionData {
22 | // Handle MCP content array format
23 | if (mcpResponse.content && Array.isArray(mcpResponse.content)) {
24 | const textContent = mcpResponse.content
25 | .filter((c: any) => c.type === 'text')
26 | .map((c: any) => c.text)
27 | .join('\n');
28 |
29 | try {
30 | // Try to parse as JSON if possible
31 | const parsed = JSON.parse(textContent);
32 | return {
33 | json: parsed,
34 | pairedItem: itemIndex,
35 | };
36 | } catch {
37 | // Return as text if not JSON
38 | return {
39 | json: { result: textContent },
40 | pairedItem: itemIndex,
41 | };
42 | }
43 | }
44 |
45 | // Handle direct object response
46 | return {
47 | json: mcpResponse,
48 | pairedItem: itemIndex,
49 | };
50 | }
51 |
52 | /**
53 | * Convert n8n workflow definition to MCP-compatible format
54 | */
55 | static n8nWorkflowToMCP(workflow: any): any {
56 | return {
57 | id: workflow.id,
58 | name: workflow.name,
59 | description: workflow.description || '',
60 | nodes: workflow.nodes?.map((node: any) => ({
61 | id: node.id,
62 | type: node.type,
63 | name: node.name,
64 | parameters: node.parameters,
65 | position: node.position,
66 | })),
67 | connections: workflow.connections,
68 | settings: workflow.settings,
69 | metadata: {
70 | createdAt: workflow.createdAt,
71 | updatedAt: workflow.updatedAt,
72 | active: workflow.active,
73 | },
74 | };
75 | }
76 |
77 | /**
78 | * Convert MCP workflow format to n8n-compatible format
79 | */
80 | static mcpToN8NWorkflow(mcpWorkflow: any): any {
81 | return {
82 | name: mcpWorkflow.name,
83 | nodes: mcpWorkflow.nodes || [],
84 | connections: mcpWorkflow.connections || {},
85 | settings: mcpWorkflow.settings || {
86 | executionOrder: 'v1',
87 | },
88 | staticData: null,
89 | pinData: {},
90 | };
91 | }
92 |
93 | /**
94 | * Convert n8n execution data to MCP resource format
95 | */
96 | static n8nExecutionToMCPResource(execution: any): any {
97 | return {
98 | uri: `execution://${execution.id}`,
99 | name: `Execution ${execution.id}`,
100 | description: `Workflow: ${execution.workflowData?.name || 'Unknown'}`,
101 | mimeType: 'application/json',
102 | data: {
103 | id: execution.id,
104 | workflowId: execution.workflowId,
105 | status: execution.finished ? 'completed' : execution.stoppedAt ? 'stopped' : 'running',
106 | mode: execution.mode,
107 | startedAt: execution.startedAt,
108 | stoppedAt: execution.stoppedAt,
109 | error: execution.data?.resultData?.error,
110 | executionData: execution.data,
111 | },
112 | };
113 | }
114 |
115 | /**
116 | * Convert MCP prompt arguments to n8n-compatible format
117 | */
118 | static mcpPromptArgsToN8N(promptArgs: any): IDataObject {
119 | return {
120 | prompt: promptArgs.name || '',
121 | arguments: promptArgs.arguments || {},
122 | messages: promptArgs.messages || [],
123 | };
124 | }
125 |
126 | /**
127 | * Validate and sanitize data before conversion
128 | */
129 | static sanitizeData(data: any): any {
130 | if (data === null || data === undefined) {
131 | return {};
132 | }
133 |
134 | if (typeof data !== 'object') {
135 | return { value: data };
136 | }
137 |
138 | // Remove circular references
139 | const seen = new WeakSet();
140 | return JSON.parse(JSON.stringify(data, (_key, value) => {
141 | if (typeof value === 'object' && value !== null) {
142 | if (seen.has(value)) {
143 | return '[Circular]';
144 | }
145 | seen.add(value);
146 | }
147 | return value;
148 | }));
149 | }
150 |
151 | /**
152 | * Extract error information for both n8n and MCP formats
153 | */
154 | static formatError(error: any): any {
155 | return {
156 | message: error.message || 'Unknown error',
157 | type: error.name || 'Error',
158 | stack: error.stack,
159 | details: {
160 | code: error.code,
161 | statusCode: error.statusCode,
162 | data: error.data,
163 | },
164 | };
165 | }
166 | }
```
--------------------------------------------------------------------------------
/tests/integration/n8n-api/workflows/delete-workflow.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Integration Tests: handleDeleteWorkflow
3 | *
4 | * Tests workflow deletion against a real n8n instance.
5 | * Covers successful deletion, error handling, and cleanup verification.
6 | */
7 |
8 | import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest';
9 | import { createTestContext, TestContext, createTestWorkflowName } from '../utils/test-context';
10 | import { getTestN8nClient } from '../utils/n8n-client';
11 | import { N8nApiClient } from '../../../../src/services/n8n-api-client';
12 | import { SIMPLE_WEBHOOK_WORKFLOW } from '../utils/fixtures';
13 | import { cleanupOrphanedWorkflows } from '../utils/cleanup-helpers';
14 | import { createMcpContext } from '../utils/mcp-context';
15 | import { InstanceContext } from '../../../../src/types/instance-context';
16 | import { handleDeleteWorkflow } from '../../../../src/mcp/handlers-n8n-manager';
17 |
18 | describe('Integration: handleDeleteWorkflow', () => {
19 | let context: TestContext;
20 | let client: N8nApiClient;
21 | let mcpContext: InstanceContext;
22 |
23 | beforeEach(() => {
24 | context = createTestContext();
25 | client = getTestN8nClient();
26 | mcpContext = createMcpContext();
27 | });
28 |
29 | afterEach(async () => {
30 | await context.cleanup();
31 | });
32 |
33 | afterAll(async () => {
34 | if (!process.env.CI) {
35 | await cleanupOrphanedWorkflows();
36 | }
37 | });
38 |
39 | // ======================================================================
40 | // Successful Deletion
41 | // ======================================================================
42 |
43 | describe('Successful Deletion', () => {
44 | it('should delete an existing workflow', async () => {
45 | // Create workflow
46 | const workflow = {
47 | ...SIMPLE_WEBHOOK_WORKFLOW,
48 | name: createTestWorkflowName('Delete - Success'),
49 | tags: ['mcp-integration-test']
50 | };
51 |
52 | const created = await client.createWorkflow(workflow);
53 | expect(created.id).toBeTruthy();
54 | if (!created.id) throw new Error('Workflow ID is missing');
55 |
56 | // Do NOT track workflow since we're testing deletion
57 | // context.trackWorkflow(created.id);
58 |
59 | // Delete using MCP handler
60 | const response = await handleDeleteWorkflow(
61 | { id: created.id },
62 | mcpContext
63 | );
64 |
65 | // Verify MCP response
66 | expect(response.success).toBe(true);
67 | expect(response.data).toBeDefined();
68 |
69 | // Verify workflow is actually deleted
70 | await expect(async () => {
71 | await client.getWorkflow(created.id!);
72 | }).rejects.toThrow();
73 | });
74 | });
75 |
76 | // ======================================================================
77 | // Error Handling
78 | // ======================================================================
79 |
80 | describe('Error Handling', () => {
81 | it('should return error for non-existent workflow ID', async () => {
82 | const response = await handleDeleteWorkflow(
83 | { id: '99999999' },
84 | mcpContext
85 | );
86 |
87 | expect(response.success).toBe(false);
88 | expect(response.error).toBeDefined();
89 | });
90 | });
91 |
92 | // ======================================================================
93 | // Cleanup Verification
94 | // ======================================================================
95 |
96 | describe('Cleanup Verification', () => {
97 | it('should verify workflow is actually deleted from n8n', async () => {
98 | // Create workflow
99 | const workflow = {
100 | ...SIMPLE_WEBHOOK_WORKFLOW,
101 | name: createTestWorkflowName('Delete - Cleanup Check'),
102 | tags: ['mcp-integration-test']
103 | };
104 |
105 | const created = await client.createWorkflow(workflow);
106 | expect(created.id).toBeTruthy();
107 | if (!created.id) throw new Error('Workflow ID is missing');
108 |
109 | // Verify workflow exists
110 | const beforeDelete = await client.getWorkflow(created.id);
111 | expect(beforeDelete.id).toBe(created.id);
112 |
113 | // Delete workflow
114 | const deleteResponse = await handleDeleteWorkflow(
115 | { id: created.id },
116 | mcpContext
117 | );
118 |
119 | expect(deleteResponse.success).toBe(true);
120 |
121 | // Verify workflow no longer exists
122 | try {
123 | await client.getWorkflow(created.id);
124 | // If we reach here, workflow wasn't deleted
125 | throw new Error('Workflow should have been deleted but still exists');
126 | } catch (error: any) {
127 | // Expected: workflow should not be found
128 | expect(error.message).toMatch(/not found|404/i);
129 | }
130 | });
131 | });
132 | });
133 |
```
--------------------------------------------------------------------------------
/tests/fixtures/database/test-nodes.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "nodes": [
3 | {
4 | "style": "programmatic",
5 | "nodeType": "nodes-base.httpRequest",
6 | "displayName": "HTTP Request",
7 | "description": "Makes HTTP requests and returns the response",
8 | "category": "Core Nodes",
9 | "properties": [
10 | {
11 | "name": "url",
12 | "displayName": "URL",
13 | "type": "string",
14 | "required": true,
15 | "default": ""
16 | },
17 | {
18 | "name": "method",
19 | "displayName": "Method",
20 | "type": "options",
21 | "options": [
22 | { "name": "GET", "value": "GET" },
23 | { "name": "POST", "value": "POST" },
24 | { "name": "PUT", "value": "PUT" },
25 | { "name": "DELETE", "value": "DELETE" }
26 | ],
27 | "default": "GET"
28 | }
29 | ],
30 | "credentials": [],
31 | "isAITool": true,
32 | "isTrigger": false,
33 | "isWebhook": false,
34 | "operations": [],
35 | "version": "1",
36 | "isVersioned": false,
37 | "packageName": "n8n-nodes-base",
38 | "documentation": "The HTTP Request node makes HTTP requests and returns the response data."
39 | },
40 | {
41 | "style": "programmatic",
42 | "nodeType": "nodes-base.webhook",
43 | "displayName": "Webhook",
44 | "description": "Receives data from external services via webhooks",
45 | "category": "Core Nodes",
46 | "properties": [
47 | {
48 | "name": "httpMethod",
49 | "displayName": "HTTP Method",
50 | "type": "options",
51 | "options": [
52 | { "name": "GET", "value": "GET" },
53 | { "name": "POST", "value": "POST" }
54 | ],
55 | "default": "POST"
56 | },
57 | {
58 | "name": "path",
59 | "displayName": "Path",
60 | "type": "string",
61 | "default": "webhook"
62 | }
63 | ],
64 | "credentials": [],
65 | "isAITool": false,
66 | "isTrigger": true,
67 | "isWebhook": true,
68 | "operations": [],
69 | "version": "1",
70 | "isVersioned": false,
71 | "packageName": "n8n-nodes-base",
72 | "documentation": "The Webhook node creates an endpoint to receive data from external services."
73 | },
74 | {
75 | "style": "declarative",
76 | "nodeType": "nodes-base.slack",
77 | "displayName": "Slack",
78 | "description": "Send messages and interact with Slack",
79 | "category": "Communication",
80 | "properties": [],
81 | "credentials": [
82 | {
83 | "name": "slackApi",
84 | "required": true
85 | }
86 | ],
87 | "isAITool": true,
88 | "isTrigger": false,
89 | "isWebhook": false,
90 | "operations": [
91 | {
92 | "name": "Message",
93 | "value": "message",
94 | "operations": [
95 | {
96 | "name": "Send",
97 | "value": "send",
98 | "description": "Send a message to a channel or user"
99 | }
100 | ]
101 | }
102 | ],
103 | "version": "2.1",
104 | "isVersioned": true,
105 | "packageName": "n8n-nodes-base",
106 | "documentation": "The Slack node allows you to send messages and interact with Slack workspaces."
107 | }
108 | ],
109 | "templates": [
110 | {
111 | "id": 1001,
112 | "name": "HTTP to Webhook",
113 | "description": "Fetch data from HTTP and send to webhook",
114 | "workflow": {
115 | "nodes": [
116 | {
117 | "id": "1",
118 | "name": "HTTP Request",
119 | "type": "n8n-nodes-base.httpRequest",
120 | "position": [250, 300],
121 | "parameters": {
122 | "url": "https://api.example.com/data",
123 | "method": "GET"
124 | }
125 | },
126 | {
127 | "id": "2",
128 | "name": "Webhook",
129 | "type": "n8n-nodes-base.webhook",
130 | "position": [450, 300],
131 | "parameters": {
132 | "path": "data-webhook",
133 | "httpMethod": "POST"
134 | }
135 | }
136 | ],
137 | "connections": {
138 | "HTTP Request": {
139 | "main": [[{ "node": "Webhook", "type": "main", "index": 0 }]]
140 | }
141 | }
142 | },
143 | "nodes": [
144 | { "id": 1, "name": "HTTP Request", "icon": "http" },
145 | { "id": 2, "name": "Webhook", "icon": "webhook" }
146 | ],
147 | "categories": ["Data Processing"],
148 | "user": {
149 | "id": 1,
150 | "name": "Test User",
151 | "username": "testuser",
152 | "verified": false
153 | },
154 | "views": 150,
155 | "createdAt": "2024-01-15T10:00:00Z",
156 | "updatedAt": "2024-01-20T15:30:00Z",
157 | "totalViews": 150
158 | }
159 | ]
160 | }
```
--------------------------------------------------------------------------------
/src/mcp/tool-docs/validation/validate-workflow.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ToolDocumentation } from '../types';
2 |
3 | export const validateWorkflowDoc: ToolDocumentation = {
4 | name: 'validate_workflow',
5 | category: 'validation',
6 | essentials: {
7 | description: 'Full workflow validation: structure, connections, expressions, AI tools. Returns errors/warnings/fixes. Essential before deploy.',
8 | keyParameters: ['workflow', 'options'],
9 | example: 'validate_workflow({workflow: {nodes: [...], connections: {...}}})',
10 | performance: 'Moderate (100-500ms)',
11 | tips: [
12 | 'Always validate before n8n_create_workflow to catch errors early',
13 | 'Use options.profile="minimal" for quick checks during development',
14 | 'AI tool connections are automatically validated for proper node references',
15 | 'Detects operator structure issues (binary vs unary, singleValue requirements)'
16 | ]
17 | },
18 | full: {
19 | description: 'Performs comprehensive validation of n8n workflows including structure, node configurations, connections, and expressions. This is a three-layer validation system that catches errors before deployment, validates complex multi-node workflows, checks all n8n expressions for syntax errors, and ensures proper node connections and data flow.',
20 | parameters: {
21 | workflow: {
22 | type: 'object',
23 | required: true,
24 | description: 'The complete workflow JSON to validate. Must include nodes array and connections object.'
25 | },
26 | options: {
27 | type: 'object',
28 | required: false,
29 | description: 'Validation options object'
30 | },
31 | 'options.validateNodes': {
32 | type: 'boolean',
33 | required: false,
34 | description: 'Validate individual node configurations. Default: true'
35 | },
36 | 'options.validateConnections': {
37 | type: 'boolean',
38 | required: false,
39 | description: 'Validate node connections and flow. Default: true'
40 | },
41 | 'options.validateExpressions': {
42 | type: 'boolean',
43 | required: false,
44 | description: 'Validate n8n expressions syntax and references. Default: true'
45 | },
46 | 'options.profile': {
47 | type: 'string',
48 | required: false,
49 | description: 'Validation profile for node validation: minimal, runtime (default), ai-friendly, strict'
50 | }
51 | },
52 | returns: 'Object with valid (boolean), errors (array), warnings (array), statistics (object), and suggestions (array)',
53 | examples: [
54 | 'validate_workflow({workflow: myWorkflow}) - Full validation with default settings',
55 | 'validate_workflow({workflow: myWorkflow, options: {profile: "minimal"}}) - Quick validation for editing',
56 | 'validate_workflow({workflow: myWorkflow, options: {validateExpressions: false}}) - Skip expression validation'
57 | ],
58 | useCases: [
59 | 'Pre-deployment validation to catch all workflow issues',
60 | 'Quick validation during workflow development',
61 | 'Validate workflows with AI Agent nodes and tool connections',
62 | 'Check expression syntax before workflow execution',
63 | 'Ensure workflow structure integrity after modifications'
64 | ],
65 | performance: 'Moderate (100-500ms). Depends on workflow size and validation options. Expression validation adds ~50-100ms.',
66 | bestPractices: [
67 | 'Always validate workflows before creating or updating in n8n',
68 | 'Use minimal profile during development, strict profile before production',
69 | 'Pay attention to warnings - they often indicate potential runtime issues',
70 | 'Validate after any workflow modifications, especially connection changes',
71 | 'Check statistics to understand workflow complexity',
72 | '**Auto-sanitization runs during create/update**: Operator structures and missing metadata are automatically fixed when workflows are created or updated, but validation helps catch issues before they reach n8n',
73 | 'If validation detects operator issues, they will be auto-fixed during n8n_create_workflow or n8n_update_partial_workflow'
74 | ],
75 | pitfalls: [
76 | 'Large workflows (100+ nodes) may take longer to validate',
77 | 'Expression validation requires proper node references to exist',
78 | 'Some warnings may be acceptable depending on use case',
79 | 'Validation cannot catch all runtime errors (e.g., API failures)',
80 | 'Profile setting only affects node validation, not connection/expression checks'
81 | ],
82 | relatedTools: ['validate_node', 'n8n_create_workflow', 'n8n_update_partial_workflow', 'n8n_autofix_workflow']
83 | }
84 | };
```
--------------------------------------------------------------------------------
/tests/integration/n8n-api/system/health-check.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Integration Tests: handleHealthCheck
3 | *
4 | * Tests API health check against a real n8n instance.
5 | * Covers connectivity verification and feature availability.
6 | */
7 |
8 | import { describe, it, expect, beforeEach } from 'vitest';
9 | import { createMcpContext } from '../utils/mcp-context';
10 | import { InstanceContext } from '../../../../src/types/instance-context';
11 | import { handleHealthCheck } from '../../../../src/mcp/handlers-n8n-manager';
12 | import { HealthCheckResponse } from '../utils/response-types';
13 |
14 | describe('Integration: handleHealthCheck', () => {
15 | let mcpContext: InstanceContext;
16 |
17 | beforeEach(() => {
18 | mcpContext = createMcpContext();
19 | });
20 |
21 | // ======================================================================
22 | // Successful Health Check
23 | // ======================================================================
24 |
25 | describe('API Available', () => {
26 | it('should successfully check n8n API health', async () => {
27 | const response = await handleHealthCheck(mcpContext);
28 |
29 | expect(response.success).toBe(true);
30 | expect(response.data).toBeDefined();
31 |
32 | const data = response.data as HealthCheckResponse;
33 |
34 | // Verify required fields
35 | expect(data).toHaveProperty('status');
36 | expect(data).toHaveProperty('apiUrl');
37 | expect(data).toHaveProperty('mcpVersion');
38 | expect(data).toHaveProperty('versionCheck');
39 | expect(data).toHaveProperty('performance');
40 | expect(data).toHaveProperty('nextSteps');
41 |
42 | // Status should be a string (e.g., "ok", "healthy")
43 | if (data.status) {
44 | expect(typeof data.status).toBe('string');
45 | }
46 |
47 | // API URL should match configuration
48 | expect(data.apiUrl).toBeDefined();
49 | expect(typeof data.apiUrl).toBe('string');
50 |
51 | // MCP version should be defined
52 | expect(data.mcpVersion).toBeDefined();
53 | expect(typeof data.mcpVersion).toBe('string');
54 |
55 | // Version check should be present
56 | expect(data.versionCheck).toBeDefined();
57 | expect(data.versionCheck).toHaveProperty('current');
58 | expect(data.versionCheck).toHaveProperty('upToDate');
59 | expect(typeof data.versionCheck.upToDate).toBe('boolean');
60 |
61 | // Performance metrics should be present
62 | expect(data.performance).toBeDefined();
63 | expect(data.performance).toHaveProperty('responseTimeMs');
64 | expect(typeof data.performance.responseTimeMs).toBe('number');
65 | expect(data.performance.responseTimeMs).toBeGreaterThan(0);
66 |
67 | // Next steps should be present
68 | expect(data.nextSteps).toBeDefined();
69 | expect(Array.isArray(data.nextSteps)).toBe(true);
70 | });
71 |
72 | it('should include feature availability information', async () => {
73 | const response = await handleHealthCheck(mcpContext);
74 |
75 | expect(response.success).toBe(true);
76 | const data = response.data as HealthCheckResponse;
77 |
78 | // Check for feature information
79 | // Note: Features may vary by n8n instance configuration
80 | if (data.features) {
81 | expect(typeof data.features).toBe('object');
82 | }
83 |
84 | // Check for version information
85 | if (data.n8nVersion) {
86 | expect(typeof data.n8nVersion).toBe('string');
87 | }
88 |
89 | if (data.supportedN8nVersion) {
90 | expect(typeof data.supportedN8nVersion).toBe('string');
91 | }
92 |
93 | // Should include version note for AI agents
94 | if (data.versionNote) {
95 | expect(typeof data.versionNote).toBe('string');
96 | expect(data.versionNote).toContain('version');
97 | }
98 | });
99 | });
100 |
101 | // ======================================================================
102 | // Response Format Verification
103 | // ======================================================================
104 |
105 | describe('Response Format', () => {
106 | it('should return complete health check response structure', async () => {
107 | const response = await handleHealthCheck(mcpContext);
108 |
109 | expect(response.success).toBe(true);
110 | expect(response.data).toBeDefined();
111 |
112 | const data = response.data as HealthCheckResponse;
113 |
114 | // Verify all expected fields are present
115 | const expectedFields = ['status', 'apiUrl', 'mcpVersion'];
116 | expectedFields.forEach(field => {
117 | expect(data).toHaveProperty(field);
118 | });
119 |
120 | // Optional fields that may be present
121 | const optionalFields = ['instanceId', 'n8nVersion', 'features', 'supportedN8nVersion', 'versionNote'];
122 | optionalFields.forEach(field => {
123 | if (data[field] !== undefined) {
124 | expect(data[field]).not.toBeNull();
125 | }
126 | });
127 | });
128 | });
129 | });
130 |
```
--------------------------------------------------------------------------------
/tests/integration/n8n-api/executions/delete-execution.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Integration Tests: handleDeleteExecution
3 | *
4 | * Tests execution deletion against a real n8n instance.
5 | * Covers successful deletion, error handling, and cleanup verification.
6 | */
7 |
8 | import { describe, it, expect, beforeEach, beforeAll } from 'vitest';
9 | import { createMcpContext } from '../utils/mcp-context';
10 | import { InstanceContext } from '../../../../src/types/instance-context';
11 | import { handleDeleteExecution, handleTriggerWebhookWorkflow, handleGetExecution } from '../../../../src/mcp/handlers-n8n-manager';
12 | import { getN8nCredentials } from '../utils/credentials';
13 |
14 | describe('Integration: handleDeleteExecution', () => {
15 | let mcpContext: InstanceContext;
16 | let webhookUrl: string;
17 |
18 | beforeEach(() => {
19 | mcpContext = createMcpContext();
20 | });
21 |
22 | beforeAll(() => {
23 | const creds = getN8nCredentials();
24 | webhookUrl = creds.webhookUrls.get;
25 | });
26 |
27 | // ======================================================================
28 | // Successful Deletion
29 | // ======================================================================
30 |
31 | describe('Successful Deletion', () => {
32 | it('should delete an execution successfully', async () => {
33 | // First, create an execution to delete
34 | const triggerResponse = await handleTriggerWebhookWorkflow(
35 | {
36 | webhookUrl,
37 | httpMethod: 'GET',
38 | waitForResponse: true
39 | },
40 | mcpContext
41 | );
42 |
43 | // Try to extract execution ID
44 | let executionId: string | undefined;
45 | if (triggerResponse.success && triggerResponse.data) {
46 | const responseData = triggerResponse.data as any;
47 | executionId = responseData.executionId ||
48 | responseData.id ||
49 | responseData.execution?.id ||
50 | responseData.workflowData?.executionId;
51 | }
52 |
53 | if (!executionId) {
54 | console.warn('Could not extract execution ID for deletion test');
55 | return;
56 | }
57 |
58 | // Delete the execution
59 | const response = await handleDeleteExecution(
60 | { id: executionId },
61 | mcpContext
62 | );
63 |
64 | expect(response.success).toBe(true);
65 | expect(response.data).toBeDefined();
66 | }, 30000);
67 |
68 | it('should verify execution is actually deleted', async () => {
69 | // Create an execution
70 | const triggerResponse = await handleTriggerWebhookWorkflow(
71 | {
72 | webhookUrl,
73 | httpMethod: 'GET',
74 | waitForResponse: true
75 | },
76 | mcpContext
77 | );
78 |
79 | let executionId: string | undefined;
80 | if (triggerResponse.success && triggerResponse.data) {
81 | const responseData = triggerResponse.data as any;
82 | executionId = responseData.executionId ||
83 | responseData.id ||
84 | responseData.execution?.id ||
85 | responseData.workflowData?.executionId;
86 | }
87 |
88 | if (!executionId) {
89 | console.warn('Could not extract execution ID for deletion verification test');
90 | return;
91 | }
92 |
93 | // Delete it
94 | const deleteResponse = await handleDeleteExecution(
95 | { id: executionId },
96 | mcpContext
97 | );
98 |
99 | expect(deleteResponse.success).toBe(true);
100 |
101 | // Try to fetch the deleted execution
102 | const getResponse = await handleGetExecution(
103 | { id: executionId },
104 | mcpContext
105 | );
106 |
107 | // Should fail to find the deleted execution
108 | expect(getResponse.success).toBe(false);
109 | expect(getResponse.error).toBeDefined();
110 | }, 30000);
111 | });
112 |
113 | // ======================================================================
114 | // Error Handling
115 | // ======================================================================
116 |
117 | describe('Error Handling', () => {
118 | it('should handle non-existent execution ID', async () => {
119 | const response = await handleDeleteExecution(
120 | { id: '99999999' },
121 | mcpContext
122 | );
123 |
124 | expect(response.success).toBe(false);
125 | expect(response.error).toBeDefined();
126 | });
127 |
128 | it('should handle invalid execution ID format', async () => {
129 | const response = await handleDeleteExecution(
130 | { id: 'invalid-id-format' },
131 | mcpContext
132 | );
133 |
134 | expect(response.success).toBe(false);
135 | expect(response.error).toBeDefined();
136 | });
137 |
138 | it('should handle missing execution ID', async () => {
139 | const response = await handleDeleteExecution(
140 | {} as any,
141 | mcpContext
142 | );
143 |
144 | expect(response.success).toBe(false);
145 | expect(response.error).toBeDefined();
146 | });
147 | });
148 | });
149 |
```
--------------------------------------------------------------------------------
/src/mcp/tool-docs/validation/validate-node.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ToolDocumentation } from '../types';
2 |
3 | export const validateNodeDoc: ToolDocumentation = {
4 | name: 'validate_node',
5 | category: 'validation',
6 | essentials: {
7 | description: 'Validate n8n node configuration. Use mode="full" for comprehensive validation with errors/warnings/suggestions, mode="minimal" for quick required fields check.',
8 | keyParameters: ['nodeType', 'config', 'mode', 'profile'],
9 | example: 'validate_node({nodeType: "nodes-base.slack", config: {resource: "channel", operation: "create"}})',
10 | performance: 'Fast (<100ms)',
11 | tips: [
12 | 'Always call get_node({detail:"standard"}) first to see required fields',
13 | 'Use mode="minimal" for quick checks during development',
14 | 'Use mode="full" with profile="strict" before production deployment',
15 | 'Includes automatic structure validation for filter, resourceMapper, etc.'
16 | ]
17 | },
18 | full: {
19 | description: `**Validation Modes:**
20 | - full (default): Comprehensive validation with errors, warnings, suggestions, and automatic structure validation
21 | - minimal: Quick check for required fields only - fast but less thorough
22 |
23 | **Validation Profiles (for mode="full"):**
24 | - minimal: Very lenient, basic checks only
25 | - runtime: Standard validation (default)
26 | - ai-friendly: Balanced for AI agent workflows
27 | - strict: Most thorough, recommended for production
28 |
29 | **Automatic Structure Validation:**
30 | Validates complex n8n types automatically:
31 | - filter (FilterValue): 40+ operations (equals, contains, regex, etc.)
32 | - resourceMapper (ResourceMapperValue): Data mapping configuration
33 | - assignmentCollection (AssignmentCollectionValue): Variable assignments
34 | - resourceLocator (INodeParameterResourceLocator): Resource selection modes`,
35 | parameters: {
36 | nodeType: { type: 'string', required: true, description: 'Node type with prefix: "nodes-base.slack"' },
37 | config: { type: 'object', required: true, description: 'Configuration object to validate. Use {} for empty config' },
38 | mode: { type: 'string', required: false, description: 'Validation mode: "full" (default) or "minimal"' },
39 | profile: { type: 'string', required: false, description: 'Validation profile for mode=full: "minimal", "runtime" (default), "ai-friendly", "strict"' }
40 | },
41 | returns: `Object containing:
42 | - nodeType: The validated node type
43 | - workflowNodeType: Type to use in workflow JSON
44 | - displayName: Human-readable node name
45 | - valid: Boolean indicating if configuration is valid
46 | - errors: Array of error objects with type, property, message, fix
47 | - warnings: Array of warning objects with suggestions
48 | - suggestions: Array of improvement suggestions
49 | - missingRequiredFields: (mode=minimal only) Array of missing required field names
50 | - summary: Object with hasErrors, errorCount, warningCount, suggestionCount`,
51 | examples: [
52 | '// Full validation with default profile\nvalidate_node({nodeType: "nodes-base.slack", config: {resource: "channel", operation: "create"}})',
53 | '// Quick required fields check\nvalidate_node({nodeType: "nodes-base.webhook", config: {}, mode: "minimal"})',
54 | '// Strict validation for production\nvalidate_node({nodeType: "nodes-base.httpRequest", config: {...}, mode: "full", profile: "strict"})',
55 | '// Validate IF node with filter\nvalidate_node({nodeType: "nodes-base.if", config: {conditions: {combinator: "and", conditions: [...]}}})'
56 | ],
57 | useCases: [
58 | 'Validate node configuration before adding to workflow',
59 | 'Quick check for required fields during development',
60 | 'Pre-production validation with strict profile',
61 | 'Validate complex structures (filters, resource mappers)',
62 | 'Get suggestions for improving node configuration'
63 | ],
64 | performance: 'Fast validation: <50ms for minimal mode, <100ms for full mode. Structure validation adds minimal overhead.',
65 | bestPractices: [
66 | 'Always call get_node() first to understand required fields',
67 | 'Use mode="minimal" for rapid iteration during development',
68 | 'Use profile="strict" before deploying to production',
69 | 'Pay attention to warnings - they often prevent runtime issues',
70 | 'Validate after any configuration changes'
71 | ],
72 | pitfalls: [
73 | 'Empty config {} is valid for some nodes (e.g., manual trigger)',
74 | 'mode="minimal" only checks required fields, not value validity',
75 | 'Some warnings may be acceptable for specific use cases',
76 | 'Credential validation requires runtime context'
77 | ],
78 | relatedTools: ['get_node', 'validate_workflow', 'n8n_autofix_workflow']
79 | }
80 | };
81 |
```
--------------------------------------------------------------------------------
/src/scripts/validate.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 | /**
3 | * Copyright (c) 2024 AiAdvisors Romuald Czlonkowski
4 | * Licensed under the Sustainable Use License v1.0
5 | */
6 | import { createDatabaseAdapter } from '../database/database-adapter';
7 |
8 | interface NodeRow {
9 | node_type: string;
10 | package_name: string;
11 | display_name: string;
12 | description?: string;
13 | category?: string;
14 | development_style?: string;
15 | is_ai_tool: number;
16 | is_trigger: number;
17 | is_webhook: number;
18 | is_versioned: number;
19 | version?: string;
20 | documentation?: string;
21 | properties_schema?: string;
22 | operations?: string;
23 | credentials_required?: string;
24 | updated_at: string;
25 | }
26 |
27 | async function validate() {
28 | const db = await createDatabaseAdapter('./data/nodes.db');
29 |
30 | console.log('🔍 Validating critical nodes...\n');
31 |
32 | const criticalChecks = [
33 | {
34 | type: 'nodes-base.httpRequest',
35 | checks: {
36 | hasDocumentation: true,
37 | documentationContains: 'HTTP Request',
38 | style: 'programmatic'
39 | }
40 | },
41 | {
42 | type: 'nodes-base.code',
43 | checks: {
44 | hasDocumentation: true,
45 | documentationContains: 'Code'
46 | }
47 | },
48 | {
49 | type: 'nodes-base.slack',
50 | checks: {
51 | hasOperations: true,
52 | style: 'programmatic'
53 | }
54 | },
55 | {
56 | type: 'nodes-langchain.agent',
57 | checks: {
58 | isAITool: false, // According to the database, it's not marked as AI tool
59 | packageName: '@n8n/n8n-nodes-langchain'
60 | }
61 | }
62 | ];
63 |
64 | let passed = 0;
65 | let failed = 0;
66 |
67 | for (const check of criticalChecks) {
68 | const node = db.prepare('SELECT * FROM nodes WHERE node_type = ?').get(check.type) as NodeRow | undefined;
69 |
70 | if (!node) {
71 | console.log(`❌ ${check.type}: NOT FOUND`);
72 | failed++;
73 | continue;
74 | }
75 |
76 | let nodeOk = true;
77 | const issues: string[] = [];
78 |
79 | // Run checks
80 | if (check.checks.hasDocumentation && !node.documentation) {
81 | nodeOk = false;
82 | issues.push('missing documentation');
83 | }
84 |
85 | if (check.checks.documentationContains &&
86 | !node.documentation?.includes(check.checks.documentationContains)) {
87 | nodeOk = false;
88 | issues.push(`documentation doesn't contain "${check.checks.documentationContains}"`);
89 | }
90 |
91 | if (check.checks.style && node.development_style !== check.checks.style) {
92 | nodeOk = false;
93 | issues.push(`wrong style: ${node.development_style}`);
94 | }
95 |
96 | if (check.checks.hasOperations) {
97 | const operations = JSON.parse(node.operations || '[]');
98 | if (!operations.length) {
99 | nodeOk = false;
100 | issues.push('no operations found');
101 | }
102 | }
103 |
104 | if (check.checks.isAITool !== undefined && !!node.is_ai_tool !== check.checks.isAITool) {
105 | nodeOk = false;
106 | issues.push(`AI tool flag mismatch: expected ${check.checks.isAITool}, got ${!!node.is_ai_tool}`);
107 | }
108 |
109 | if ('isVersioned' in check.checks && check.checks.isVersioned && !node.is_versioned) {
110 | nodeOk = false;
111 | issues.push('not marked as versioned');
112 | }
113 |
114 | if (check.checks.packageName && node.package_name !== check.checks.packageName) {
115 | nodeOk = false;
116 | issues.push(`wrong package: ${node.package_name}`);
117 | }
118 |
119 | if (nodeOk) {
120 | console.log(`✅ ${check.type}`);
121 | passed++;
122 | } else {
123 | console.log(`❌ ${check.type}: ${issues.join(', ')}`);
124 | failed++;
125 | }
126 | }
127 |
128 | console.log(`\n📊 Results: ${passed} passed, ${failed} failed`);
129 |
130 | // Additional statistics
131 | const stats = db.prepare(`
132 | SELECT
133 | COUNT(*) as total,
134 | SUM(is_ai_tool) as ai_tools,
135 | SUM(is_trigger) as triggers,
136 | SUM(is_versioned) as versioned,
137 | COUNT(DISTINCT package_name) as packages
138 | FROM nodes
139 | `).get() as any;
140 |
141 | console.log('\n📈 Database Statistics:');
142 | console.log(` Total nodes: ${stats.total}`);
143 | console.log(` AI tools: ${stats.ai_tools}`);
144 | console.log(` Triggers: ${stats.triggers}`);
145 | console.log(` Versioned: ${stats.versioned}`);
146 | console.log(` Packages: ${stats.packages}`);
147 |
148 | // Check documentation coverage
149 | const docStats = db.prepare(`
150 | SELECT
151 | COUNT(*) as total,
152 | SUM(CASE WHEN documentation IS NOT NULL THEN 1 ELSE 0 END) as with_docs
153 | FROM nodes
154 | `).get() as any;
155 |
156 | console.log(`\n📚 Documentation Coverage:`);
157 | console.log(` Nodes with docs: ${docStats.with_docs}/${docStats.total} (${Math.round(docStats.with_docs / docStats.total * 100)}%)`);
158 |
159 | db.close();
160 | process.exit(failed > 0 ? 1 : 0);
161 | }
162 |
163 | if (require.main === module) {
164 | validate().catch(console.error);
165 | }
```
--------------------------------------------------------------------------------
/src/scripts/fetch-templates-robust.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 | import { createDatabaseAdapter } from '../database/database-adapter';
3 | import { TemplateRepository } from '../templates/template-repository';
4 | import { TemplateFetcher } from '../templates/template-fetcher';
5 | import * as fs from 'fs';
6 | import * as path from 'path';
7 |
8 | async function fetchTemplatesRobust() {
9 | console.log('🌐 Fetching n8n workflow templates (last year)...\n');
10 |
11 | // Ensure data directory exists
12 | const dataDir = './data';
13 | if (!fs.existsSync(dataDir)) {
14 | fs.mkdirSync(dataDir, { recursive: true });
15 | }
16 |
17 | // Initialize database
18 | const db = await createDatabaseAdapter('./data/nodes.db');
19 |
20 | // Drop existing templates table to ensure clean schema
21 | try {
22 | db.exec('DROP TABLE IF EXISTS templates');
23 | db.exec('DROP TABLE IF EXISTS templates_fts');
24 | console.log('🗑️ Dropped existing templates tables\n');
25 | } catch (error) {
26 | // Ignore errors if tables don't exist
27 | }
28 |
29 | // Apply schema with updated constraint
30 | const schema = fs.readFileSync(path.join(__dirname, '../../src/database/schema.sql'), 'utf8');
31 | db.exec(schema);
32 |
33 | // Create repository and fetcher
34 | const repository = new TemplateRepository(db);
35 | const fetcher = new TemplateFetcher();
36 |
37 | // Progress tracking
38 | let lastMessage = '';
39 | const startTime = Date.now();
40 |
41 | try {
42 | // Fetch template list
43 | console.log('📋 Phase 1: Fetching template list from n8n.io API\n');
44 | const templates = await fetcher.fetchTemplates((current, total) => {
45 | // Clear previous line
46 | if (lastMessage) {
47 | process.stdout.write('\r' + ' '.repeat(lastMessage.length) + '\r');
48 | }
49 |
50 | const progress = Math.round((current / total) * 100);
51 | lastMessage = `📊 Fetching template list: ${current}/${total} (${progress}%)`;
52 | process.stdout.write(lastMessage);
53 | });
54 |
55 | console.log('\n');
56 | console.log(`✅ Found ${templates.length} templates from last year\n`);
57 |
58 | // Fetch details and save incrementally
59 | console.log('📥 Phase 2: Fetching details and saving to database\n');
60 | let saved = 0;
61 | let errors = 0;
62 |
63 | for (let i = 0; i < templates.length; i++) {
64 | const template = templates[i];
65 |
66 | try {
67 | // Clear previous line
68 | if (lastMessage) {
69 | process.stdout.write('\r' + ' '.repeat(lastMessage.length) + '\r');
70 | }
71 |
72 | const progress = Math.round(((i + 1) / templates.length) * 100);
73 | lastMessage = `📊 Processing: ${i + 1}/${templates.length} (${progress}%) - Saved: ${saved}, Errors: ${errors}`;
74 | process.stdout.write(lastMessage);
75 |
76 | // Fetch detail
77 | const detail = await fetcher.fetchTemplateDetail(template.id);
78 |
79 | if (detail !== null) {
80 | // Save immediately
81 | repository.saveTemplate(template, detail);
82 | saved++;
83 | } else {
84 | errors++;
85 | console.error(`\n❌ Failed to fetch template ${template.id} (${template.name}) after retries`);
86 | }
87 |
88 | // Rate limiting
89 | await new Promise(resolve => setTimeout(resolve, 200));
90 | } catch (error: any) {
91 | errors++;
92 | console.error(`\n❌ Error processing template ${template.id} (${template.name}): ${error.message}`);
93 | // Continue with next template
94 | }
95 | }
96 |
97 | console.log('\n');
98 |
99 | // Get stats
100 | const elapsed = Math.round((Date.now() - startTime) / 1000);
101 | const stats = await repository.getTemplateStats();
102 |
103 | console.log('✅ Template fetch complete!\n');
104 | console.log('📈 Statistics:');
105 | console.log(` - Templates found: ${templates.length}`);
106 | console.log(` - Templates saved: ${saved}`);
107 | console.log(` - Errors: ${errors}`);
108 | console.log(` - Success rate: ${Math.round((saved / templates.length) * 100)}%`);
109 | console.log(` - Time elapsed: ${elapsed} seconds`);
110 | console.log(` - Average time per template: ${(elapsed / saved).toFixed(2)} seconds`);
111 |
112 | if (stats.topUsedNodes && stats.topUsedNodes.length > 0) {
113 | console.log('\n🔝 Top used nodes:');
114 | stats.topUsedNodes.slice(0, 10).forEach((node: any, index: number) => {
115 | console.log(` ${index + 1}. ${node.node} (${node.count} templates)`);
116 | });
117 | }
118 |
119 | } catch (error) {
120 | console.error('\n❌ Fatal error:', error);
121 | process.exit(1);
122 | }
123 |
124 | // Close database
125 | if ('close' in db && typeof db.close === 'function') {
126 | db.close();
127 | }
128 | }
129 |
130 | // Run if called directly
131 | if (require.main === module) {
132 | fetchTemplatesRobust().catch(console.error);
133 | }
134 |
135 | export { fetchTemplatesRobust };
```
--------------------------------------------------------------------------------
/src/scripts/test-telemetry-mutations-verbose.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Test telemetry mutations with enhanced logging
3 | * Verifies that mutations are properly tracked and persisted
4 | */
5 |
6 | import { telemetry } from '../telemetry/telemetry-manager.js';
7 | import { TelemetryConfigManager } from '../telemetry/config-manager.js';
8 | import { logger } from '../utils/logger.js';
9 |
10 | async function testMutations() {
11 | console.log('Starting verbose telemetry mutation test...\n');
12 |
13 | const configManager = TelemetryConfigManager.getInstance();
14 | console.log('Telemetry config is enabled:', configManager.isEnabled());
15 | console.log('Telemetry config file:', configManager['configPath']);
16 |
17 | // Test data with valid workflow structure
18 | const testMutation = {
19 | sessionId: 'test_session_' + Date.now(),
20 | toolName: 'n8n_update_partial_workflow',
21 | userIntent: 'Add a Merge node for data consolidation',
22 | operations: [
23 | {
24 | type: 'addNode',
25 | nodeId: 'Merge1',
26 | node: {
27 | id: 'Merge1',
28 | type: 'n8n-nodes-base.merge',
29 | name: 'Merge',
30 | position: [600, 200],
31 | parameters: {}
32 | }
33 | },
34 | {
35 | type: 'addConnection',
36 | source: 'previous_node',
37 | target: 'Merge1'
38 | }
39 | ],
40 | workflowBefore: {
41 | id: 'test-workflow',
42 | name: 'Test Workflow',
43 | active: true,
44 | nodes: [
45 | {
46 | id: 'previous_node',
47 | type: 'n8n-nodes-base.manualTrigger',
48 | name: 'When called',
49 | position: [300, 200],
50 | parameters: {}
51 | }
52 | ],
53 | connections: {},
54 | nodeIds: []
55 | },
56 | workflowAfter: {
57 | id: 'test-workflow',
58 | name: 'Test Workflow',
59 | active: true,
60 | nodes: [
61 | {
62 | id: 'previous_node',
63 | type: 'n8n-nodes-base.manualTrigger',
64 | name: 'When called',
65 | position: [300, 200],
66 | parameters: {}
67 | },
68 | {
69 | id: 'Merge1',
70 | type: 'n8n-nodes-base.merge',
71 | name: 'Merge',
72 | position: [600, 200],
73 | parameters: {}
74 | }
75 | ],
76 | connections: {
77 | 'previous_node': [
78 | {
79 | node: 'Merge1',
80 | type: 'main',
81 | index: 0,
82 | source: 0,
83 | destination: 0
84 | }
85 | ]
86 | },
87 | nodeIds: []
88 | },
89 | mutationSuccess: true,
90 | durationMs: 125
91 | };
92 |
93 | console.log('\nTest Mutation Data:');
94 | console.log('==================');
95 | console.log(JSON.stringify({
96 | intent: testMutation.userIntent,
97 | tool: testMutation.toolName,
98 | operationCount: testMutation.operations.length,
99 | sessionId: testMutation.sessionId
100 | }, null, 2));
101 | console.log('\n');
102 |
103 | // Call trackWorkflowMutation
104 | console.log('Calling telemetry.trackWorkflowMutation...');
105 | try {
106 | await telemetry.trackWorkflowMutation(testMutation);
107 | console.log('✓ trackWorkflowMutation completed successfully\n');
108 | } catch (error) {
109 | console.error('✗ trackWorkflowMutation failed:', error);
110 | console.error('\n');
111 | }
112 |
113 | // Check queue size before flush
114 | const metricsBeforeFlush = telemetry.getMetrics();
115 | console.log('Metrics before flush:');
116 | console.log('- mutationQueueSize:', metricsBeforeFlush.tracking.mutationQueueSize);
117 | console.log('- eventsTracked:', metricsBeforeFlush.processing.eventsTracked);
118 | console.log('- eventsFailed:', metricsBeforeFlush.processing.eventsFailed);
119 | console.log('\n');
120 |
121 | // Flush telemetry with 10-second wait for Supabase
122 | console.log('Flushing telemetry (waiting 10 seconds for Supabase)...');
123 | try {
124 | await telemetry.flush();
125 | console.log('✓ Telemetry flush completed\n');
126 | } catch (error) {
127 | console.error('✗ Flush failed:', error);
128 | console.error('\n');
129 | }
130 |
131 | // Wait a bit for async operations
132 | await new Promise(resolve => setTimeout(resolve, 2000));
133 |
134 | // Get final metrics
135 | const metricsAfterFlush = telemetry.getMetrics();
136 | console.log('Metrics after flush:');
137 | console.log('- mutationQueueSize:', metricsAfterFlush.tracking.mutationQueueSize);
138 | console.log('- eventsTracked:', metricsAfterFlush.processing.eventsTracked);
139 | console.log('- eventsFailed:', metricsAfterFlush.processing.eventsFailed);
140 | console.log('- batchesSent:', metricsAfterFlush.processing.batchesSent);
141 | console.log('- batchesFailed:', metricsAfterFlush.processing.batchesFailed);
142 | console.log('- circuitBreakerState:', metricsAfterFlush.processing.circuitBreakerState);
143 | console.log('\n');
144 |
145 | console.log('Test completed. Check workflow_mutations table in Supabase.');
146 | }
147 |
148 | testMutations().catch(error => {
149 | console.error('Test failed:', error);
150 | process.exit(1);
151 | });
152 |
```
--------------------------------------------------------------------------------
/src/mappers/docs-mapper.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { promises as fs } from 'fs';
2 | import path from 'path';
3 |
4 | export class DocsMapper {
5 | private docsPath = path.join(process.cwd(), 'n8n-docs');
6 |
7 | // Known documentation mapping fixes
8 | private readonly KNOWN_FIXES: Record<string, string> = {
9 | 'httpRequest': 'httprequest',
10 | 'code': 'code',
11 | 'webhook': 'webhook',
12 | 'respondToWebhook': 'respondtowebhook',
13 | // With package prefix
14 | 'n8n-nodes-base.httpRequest': 'httprequest',
15 | 'n8n-nodes-base.code': 'code',
16 | 'n8n-nodes-base.webhook': 'webhook',
17 | 'n8n-nodes-base.respondToWebhook': 'respondtowebhook'
18 | };
19 |
20 | async fetchDocumentation(nodeType: string): Promise<string | null> {
21 | // Apply known fixes first
22 | const fixedType = this.KNOWN_FIXES[nodeType] || nodeType;
23 |
24 | // Extract node name
25 | const nodeName = fixedType.split('.').pop()?.toLowerCase();
26 | if (!nodeName) {
27 | console.log(`⚠️ Could not extract node name from: ${nodeType}`);
28 | return null;
29 | }
30 |
31 | console.log(`📄 Looking for docs for: ${nodeType} -> ${nodeName}`);
32 |
33 | // Try different documentation paths - both files and directories
34 | const possiblePaths = [
35 | // Direct file paths
36 | `docs/integrations/builtin/core-nodes/n8n-nodes-base.${nodeName}.md`,
37 | `docs/integrations/builtin/app-nodes/n8n-nodes-base.${nodeName}.md`,
38 | `docs/integrations/builtin/trigger-nodes/n8n-nodes-base.${nodeName}.md`,
39 | `docs/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.${nodeName}.md`,
40 | `docs/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.${nodeName}.md`,
41 | // Directory with index.md
42 | `docs/integrations/builtin/core-nodes/n8n-nodes-base.${nodeName}/index.md`,
43 | `docs/integrations/builtin/app-nodes/n8n-nodes-base.${nodeName}/index.md`,
44 | `docs/integrations/builtin/trigger-nodes/n8n-nodes-base.${nodeName}/index.md`,
45 | `docs/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.${nodeName}/index.md`,
46 | `docs/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.${nodeName}/index.md`
47 | ];
48 |
49 | // Try each path
50 | for (const relativePath of possiblePaths) {
51 | try {
52 | const fullPath = path.join(this.docsPath, relativePath);
53 | let content = await fs.readFile(fullPath, 'utf-8');
54 | console.log(` ✓ Found docs at: ${relativePath}`);
55 |
56 | // Inject special guidance for loop nodes
57 | content = this.enhanceLoopNodeDocumentation(nodeType, content);
58 |
59 | return content;
60 | } catch (error) {
61 | // File doesn't exist, try next
62 | continue;
63 | }
64 | }
65 |
66 | console.log(` ✗ No docs found for ${nodeName}`);
67 | return null;
68 | }
69 |
70 | private enhanceLoopNodeDocumentation(nodeType: string, content: string): string {
71 | // Add critical output index information for SplitInBatches
72 | if (nodeType.includes('splitInBatches')) {
73 | const outputGuidance = `
74 |
75 | ## CRITICAL OUTPUT CONNECTION INFORMATION
76 |
77 | **⚠️ OUTPUT INDICES ARE COUNTERINTUITIVE ⚠️**
78 |
79 | The SplitInBatches node has TWO outputs with specific indices:
80 | - **Output 0 (index 0) = "done"**: Receives final processed data when loop completes
81 | - **Output 1 (index 1) = "loop"**: Receives current batch data during iteration
82 |
83 | ### Correct Connection Pattern:
84 | 1. Connect nodes that PROCESS items inside the loop to **Output 1 ("loop")**
85 | 2. Connect nodes that run AFTER the loop completes to **Output 0 ("done")**
86 | 3. The last processing node in the loop must connect back to the SplitInBatches node
87 |
88 | ### Common Mistake:
89 | AI assistants often connect these backwards because the logical flow (loop first, then done) doesn't match the technical indices (done=0, loop=1).
90 |
91 | `;
92 | // Insert after the main description
93 | const insertPoint = content.indexOf('## When to use');
94 | if (insertPoint > -1) {
95 | content = content.slice(0, insertPoint) + outputGuidance + content.slice(insertPoint);
96 | } else {
97 | // Append if no good insertion point found
98 | content = outputGuidance + '\n' + content;
99 | }
100 | }
101 |
102 | // Add guidance for IF node
103 | if (nodeType.includes('.if')) {
104 | const outputGuidance = `
105 |
106 | ## Output Connection Information
107 |
108 | The IF node has TWO outputs:
109 | - **Output 0 (index 0) = "true"**: Items that match the condition
110 | - **Output 1 (index 1) = "false"**: Items that do not match the condition
111 |
112 | `;
113 | const insertPoint = content.indexOf('## Node parameters');
114 | if (insertPoint > -1) {
115 | content = content.slice(0, insertPoint) + outputGuidance + content.slice(insertPoint);
116 | }
117 | }
118 |
119 | return content;
120 | }
121 | }
```
--------------------------------------------------------------------------------
/src/mcp/tool-docs/system/n8n-health-check.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ToolDocumentation } from '../types';
2 |
3 | export const n8nHealthCheckDoc: ToolDocumentation = {
4 | name: 'n8n_health_check',
5 | category: 'system',
6 | essentials: {
7 | description: 'Check n8n instance health, API connectivity, version status, and performance metrics',
8 | keyParameters: ['mode', 'verbose'],
9 | example: 'n8n_health_check({mode: "status"})',
10 | performance: 'Fast - single API call (~150-200ms median)',
11 | tips: [
12 | 'Use before starting workflow operations to ensure n8n is responsive',
13 | 'Automatically checks if n8n-mcp version is outdated',
14 | 'Returns version info, performance metrics, and next-step recommendations',
15 | 'New: Shows cache hit rate and response time for performance monitoring'
16 | ]
17 | },
18 | full: {
19 | description: `Performs a comprehensive health check of the configured n8n instance through its API.
20 |
21 | This tool verifies:
22 | - API endpoint accessibility and response time
23 | - n8n instance version and build information
24 | - Authentication status and permissions
25 | - Available features and enterprise capabilities
26 | - Database connectivity (as reported by n8n)
27 | - Queue system status (if configured)
28 |
29 | Health checks are crucial for:
30 | - Monitoring n8n instance availability
31 | - Detecting performance degradation
32 | - Verifying API compatibility before operations
33 | - Ensuring authentication is working correctly`,
34 | parameters: {
35 | mode: {
36 | type: 'string',
37 | required: false,
38 | description: 'Operation mode: "status" (default) for quick health check, "diagnostic" for detailed debug info including env vars and tool status',
39 | default: 'status',
40 | enum: ['status', 'diagnostic']
41 | },
42 | verbose: {
43 | type: 'boolean',
44 | required: false,
45 | description: 'Include extra details in diagnostic mode',
46 | default: false
47 | }
48 | },
49 | returns: `Health status object containing:
50 | - status: Overall health status ('healthy', 'degraded', 'error')
51 | - n8nVersion: n8n instance version information
52 | - instanceId: Unique identifier for the n8n instance
53 | - features: Object listing available features and their status
54 | - mcpVersion: Current n8n-mcp version
55 | - supportedN8nVersion: Recommended n8n version for compatibility
56 | - versionCheck: Version status information
57 | - current: Current n8n-mcp version
58 | - latest: Latest available version from npm
59 | - upToDate: Boolean indicating if version is current
60 | - message: Formatted version status message
61 | - updateCommand: Command to update (if outdated)
62 | - performance: Performance metrics
63 | - responseTimeMs: API response time in milliseconds
64 | - cacheHitRate: Cache efficiency percentage
65 | - cachedInstances: Number of cached API instances
66 | - nextSteps: Recommended actions after health check
67 | - updateWarning: Warning if version is outdated (if applicable)`,
68 | examples: [
69 | 'n8n_health_check({}) - Complete health check with version and performance data',
70 | '// Use in monitoring scripts\nconst health = await n8n_health_check({});\nif (health.status !== "ok") alert("n8n is down!");\nif (!health.versionCheck.upToDate) console.log("Update available:", health.versionCheck.updateCommand);',
71 | '// Check before critical operations\nconst health = await n8n_health_check({});\nif (health.performance.responseTimeMs > 1000) console.warn("n8n is slow");\nif (health.versionCheck.isOutdated) console.log(health.updateWarning);'
72 | ],
73 | useCases: [
74 | 'Pre-flight checks before workflow deployments',
75 | 'Continuous monitoring of n8n instance health',
76 | 'Troubleshooting connectivity or performance issues',
77 | 'Verifying n8n version compatibility with workflows',
78 | 'Detecting feature availability (enterprise features, queue mode, etc.)'
79 | ],
80 | performance: `Fast response expected:
81 | - Single HTTP request to /health endpoint
82 | - Typically responds in <100ms for healthy instances
83 | - Timeout after 10 seconds indicates severe issues
84 | - Minimal server load - safe for frequent polling`,
85 | bestPractices: [
86 | 'Run health checks before batch operations or deployments',
87 | 'Set up automated monitoring with regular health checks',
88 | 'Log response times to detect performance trends',
89 | 'Check version compatibility when deploying workflows',
90 | 'Use health status to implement circuit breaker patterns'
91 | ],
92 | pitfalls: [
93 | 'Requires N8N_API_URL and N8N_API_KEY to be configured',
94 | 'Network issues may cause false negatives',
95 | 'Does not check individual workflow health',
96 | 'Health endpoint might be cached - not real-time for all metrics'
97 | ],
98 | relatedTools: ['n8n_list_workflows', 'n8n_validate_workflow', 'n8n_workflow_versions']
99 | }
100 | };
```
--------------------------------------------------------------------------------
/src/scripts/test-webhook-autofix.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Test script for webhook path autofixer functionality
5 | */
6 |
7 | import { NodeRepository } from '../database/node-repository';
8 | import { createDatabaseAdapter } from '../database/database-adapter';
9 | import { WorkflowAutoFixer } from '../services/workflow-auto-fixer';
10 | import { WorkflowValidator } from '../services/workflow-validator';
11 | import { EnhancedConfigValidator } from '../services/enhanced-config-validator';
12 | import { Workflow } from '../types/n8n-api';
13 | import { Logger } from '../utils/logger';
14 | import { join } from 'path';
15 |
16 | const logger = new Logger({ prefix: '[TestWebhookAutofix]' });
17 |
18 | // Test workflow with webhook missing path
19 | const testWorkflow: Workflow = {
20 | id: 'test_webhook_fix',
21 | name: 'Test Webhook Autofix',
22 | active: false,
23 | nodes: [
24 | {
25 | id: '1',
26 | name: 'Webhook',
27 | type: 'n8n-nodes-base.webhook',
28 | typeVersion: 2.1,
29 | position: [250, 300],
30 | parameters: {}, // Empty parameters - missing path
31 | },
32 | {
33 | id: '2',
34 | name: 'HTTP Request',
35 | type: 'n8n-nodes-base.httpRequest',
36 | typeVersion: 4.2,
37 | position: [450, 300],
38 | parameters: {
39 | url: 'https://api.example.com/data',
40 | method: 'GET'
41 | }
42 | }
43 | ],
44 | connections: {
45 | 'Webhook': {
46 | main: [[{
47 | node: 'HTTP Request',
48 | type: 'main',
49 | index: 0
50 | }]]
51 | }
52 | },
53 | settings: {
54 | executionOrder: 'v1'
55 | },
56 | staticData: undefined
57 | };
58 |
59 | async function testWebhookAutofix() {
60 | logger.info('Testing webhook path autofixer...');
61 |
62 | // Initialize database and repository
63 | const dbPath = join(process.cwd(), 'data', 'nodes.db');
64 | const adapter = await createDatabaseAdapter(dbPath);
65 | const repository = new NodeRepository(adapter);
66 |
67 | // Create validators
68 | const validator = new WorkflowValidator(repository, EnhancedConfigValidator);
69 | const autoFixer = new WorkflowAutoFixer(repository);
70 |
71 | // Step 1: Validate workflow to identify issues
72 | logger.info('Step 1: Validating workflow to identify issues...');
73 | const validationResult = await validator.validateWorkflow(testWorkflow);
74 |
75 | console.log('\n📋 Validation Summary:');
76 | console.log(`- Valid: ${validationResult.valid}`);
77 | console.log(`- Errors: ${validationResult.errors.length}`);
78 | console.log(`- Warnings: ${validationResult.warnings.length}`);
79 |
80 | if (validationResult.errors.length > 0) {
81 | console.log('\n❌ Errors found:');
82 | validationResult.errors.forEach(error => {
83 | console.log(` - [${error.nodeName || error.nodeId}] ${error.message}`);
84 | });
85 | }
86 |
87 | // Step 2: Generate fixes (preview mode)
88 | logger.info('\nStep 2: Generating fixes in preview mode...');
89 |
90 | const fixResult = await autoFixer.generateFixes(
91 | testWorkflow,
92 | validationResult,
93 | [], // No expression format issues to pass
94 | {
95 | applyFixes: false, // Preview mode
96 | fixTypes: ['webhook-missing-path'] // Only test webhook fixes
97 | }
98 | );
99 |
100 | console.log('\n🔧 Fix Results:');
101 | console.log(`- Summary: ${fixResult.summary}`);
102 | console.log(`- Total fixes: ${fixResult.stats.total}`);
103 | console.log(`- Webhook path fixes: ${fixResult.stats.byType['webhook-missing-path']}`);
104 |
105 | if (fixResult.fixes.length > 0) {
106 | console.log('\n📝 Detailed Fixes:');
107 | fixResult.fixes.forEach(fix => {
108 | console.log(` - Node: ${fix.node}`);
109 | console.log(` Field: ${fix.field}`);
110 | console.log(` Type: ${fix.type}`);
111 | console.log(` Before: ${fix.before || 'undefined'}`);
112 | console.log(` After: ${fix.after}`);
113 | console.log(` Confidence: ${fix.confidence}`);
114 | console.log(` Description: ${fix.description}`);
115 | });
116 | }
117 |
118 | if (fixResult.operations.length > 0) {
119 | console.log('\n🔄 Operations to Apply:');
120 | fixResult.operations.forEach(op => {
121 | if (op.type === 'updateNode') {
122 | console.log(` - Update Node: ${op.nodeId}`);
123 | console.log(` Updates: ${JSON.stringify(op.updates, null, 2)}`);
124 | }
125 | });
126 | }
127 |
128 | // Step 3: Verify UUID format
129 | if (fixResult.fixes.length > 0) {
130 | const webhookFix = fixResult.fixes.find(f => f.type === 'webhook-missing-path');
131 | if (webhookFix) {
132 | const uuid = webhookFix.after as string;
133 | const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
134 | const isValidUUID = uuidRegex.test(uuid);
135 |
136 | console.log('\n✅ UUID Validation:');
137 | console.log(` - Generated UUID: ${uuid}`);
138 | console.log(` - Valid format: ${isValidUUID ? 'Yes' : 'No'}`);
139 | }
140 | }
141 |
142 | logger.info('\n✨ Webhook autofix test completed successfully!');
143 | }
144 |
145 | // Run test
146 | testWebhookAutofix().catch(error => {
147 | logger.error('Test failed:', error);
148 | process.exit(1);
149 | });
```
--------------------------------------------------------------------------------
/src/mcp/tool-docs/workflow_management/n8n-executions.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ToolDocumentation } from '../types';
2 |
3 | export const n8nExecutionsDoc: ToolDocumentation = {
4 | name: 'n8n_executions',
5 | category: 'workflow_management',
6 | essentials: {
7 | description: 'Manage workflow executions: get details, list, or delete. Unified tool for all execution operations.',
8 | keyParameters: ['action', 'id', 'workflowId', 'status'],
9 | example: 'n8n_executions({action: "list", workflowId: "abc123", status: "error"})',
10 | performance: 'Fast (50-200ms)',
11 | tips: [
12 | 'action="get": Get execution details by ID',
13 | 'action="list": List executions with filters',
14 | 'action="delete": Delete execution record',
15 | 'Use mode parameter for action=get to control detail level'
16 | ]
17 | },
18 | full: {
19 | description: `**Actions:**
20 | - get: Retrieve execution details by ID with configurable detail level
21 | - list: List executions with filtering and pagination
22 | - delete: Remove an execution record from history
23 |
24 | **Detail Modes for action="get":**
25 | - preview: Structure only, no data
26 | - summary: 2 items per node (default)
27 | - filtered: Custom items limit, optionally filter by node names
28 | - full: All execution data (can be very large)`,
29 | parameters: {
30 | action: { type: 'string', required: true, description: 'Operation: "get", "list", or "delete"' },
31 | id: { type: 'string', required: false, description: 'Execution ID (required for action=get or action=delete)' },
32 | mode: { type: 'string', required: false, description: 'For action=get: "preview", "summary" (default), "filtered", "full"' },
33 | nodeNames: { type: 'array', required: false, description: 'For action=get with mode=filtered: Filter to specific nodes by name' },
34 | itemsLimit: { type: 'number', required: false, description: 'For action=get with mode=filtered: Items per node (0=structure, 2=default, -1=unlimited)' },
35 | includeInputData: { type: 'boolean', required: false, description: 'For action=get: Include input data in addition to output (default: false)' },
36 | workflowId: { type: 'string', required: false, description: 'For action=list: Filter by workflow ID' },
37 | status: { type: 'string', required: false, description: 'For action=list: Filter by status ("success", "error", "waiting")' },
38 | limit: { type: 'number', required: false, description: 'For action=list: Number of results (1-100, default: 100)' },
39 | cursor: { type: 'string', required: false, description: 'For action=list: Pagination cursor from previous response' },
40 | projectId: { type: 'string', required: false, description: 'For action=list: Filter by project ID (enterprise)' },
41 | includeData: { type: 'boolean', required: false, description: 'For action=list: Include execution data (default: false)' }
42 | },
43 | returns: `Depends on action:
44 | - get: Execution object with data based on mode
45 | - list: { data: [...executions], nextCursor?: string }
46 | - delete: { success: boolean, message: string }`,
47 | examples: [
48 | '// List recent executions for a workflow\nn8n_executions({action: "list", workflowId: "abc123", limit: 10})',
49 | '// List failed executions\nn8n_executions({action: "list", status: "error"})',
50 | '// Get execution summary\nn8n_executions({action: "get", id: "exec_456"})',
51 | '// Get full execution data\nn8n_executions({action: "get", id: "exec_456", mode: "full"})',
52 | '// Get specific nodes from execution\nn8n_executions({action: "get", id: "exec_456", mode: "filtered", nodeNames: ["HTTP Request", "Slack"]})',
53 | '// Delete an execution\nn8n_executions({action: "delete", id: "exec_456"})'
54 | ],
55 | useCases: [
56 | 'Debug workflow failures (get with mode=full)',
57 | 'Monitor workflow health (list with status filter)',
58 | 'Audit execution history',
59 | 'Clean up old execution records',
60 | 'Analyze specific node outputs'
61 | ],
62 | performance: `Response times:
63 | - list: 50-150ms depending on filters
64 | - get (preview/summary): 30-100ms
65 | - get (full): 100-500ms+ depending on data size
66 | - delete: 30-80ms`,
67 | bestPractices: [
68 | 'Use mode="summary" (default) for debugging - shows enough data',
69 | 'Use mode="filtered" with nodeNames for large workflows',
70 | 'Filter by workflowId when listing to reduce results',
71 | 'Use cursor for pagination through large result sets',
72 | 'Delete old executions to save storage'
73 | ],
74 | pitfalls: [
75 | 'Requires N8N_API_URL and N8N_API_KEY configured',
76 | 'mode="full" can return very large responses for complex workflows',
77 | 'Execution must exist or returns 404',
78 | 'Delete is permanent - cannot undo'
79 | ],
80 | relatedTools: ['n8n_get_workflow', 'n8n_trigger_webhook_workflow', 'n8n_validate_workflow']
81 | }
82 | };
83 |
```
--------------------------------------------------------------------------------
/src/scripts/test-autofix-documentation.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env npx tsx
2 |
3 | /**
4 | * Test script to verify n8n_autofix_workflow documentation is properly integrated
5 | */
6 |
7 | import { toolsDocumentation } from '../mcp/tool-docs';
8 | import { getToolDocumentation } from '../mcp/tools-documentation';
9 | import { Logger } from '../utils/logger';
10 |
11 | const logger = new Logger({ prefix: '[AutofixDoc Test]' });
12 |
13 | async function testAutofixDocumentation() {
14 | logger.info('Testing n8n_autofix_workflow documentation...\n');
15 |
16 | // Test 1: Check if documentation exists in the registry
17 | logger.info('Test 1: Checking documentation registry');
18 | const hasDoc = 'n8n_autofix_workflow' in toolsDocumentation;
19 | if (hasDoc) {
20 | logger.info('✅ Documentation found in registry');
21 | } else {
22 | logger.error('❌ Documentation NOT found in registry');
23 | logger.info('Available tools:', Object.keys(toolsDocumentation).filter(k => k.includes('autofix')));
24 | }
25 |
26 | // Test 2: Check documentation structure
27 | if (hasDoc) {
28 | logger.info('\nTest 2: Checking documentation structure');
29 | const doc = toolsDocumentation['n8n_autofix_workflow'];
30 |
31 | const hasEssentials = doc.essentials &&
32 | doc.essentials.description &&
33 | doc.essentials.keyParameters &&
34 | doc.essentials.example;
35 |
36 | const hasFull = doc.full &&
37 | doc.full.description &&
38 | doc.full.parameters &&
39 | doc.full.examples;
40 |
41 | if (hasEssentials) {
42 | logger.info('✅ Essentials documentation complete');
43 | logger.info(` Description: ${doc.essentials.description.substring(0, 80)}...`);
44 | logger.info(` Key params: ${doc.essentials.keyParameters.join(', ')}`);
45 | } else {
46 | logger.error('❌ Essentials documentation incomplete');
47 | }
48 |
49 | if (hasFull) {
50 | logger.info('✅ Full documentation complete');
51 | logger.info(` Parameters: ${Object.keys(doc.full.parameters).join(', ')}`);
52 | logger.info(` Examples: ${doc.full.examples.length} provided`);
53 | } else {
54 | logger.error('❌ Full documentation incomplete');
55 | }
56 | }
57 |
58 | // Test 3: Test getToolDocumentation function
59 | logger.info('\nTest 3: Testing getToolDocumentation function');
60 |
61 | try {
62 | const essentialsDoc = getToolDocumentation('n8n_autofix_workflow', 'essentials');
63 | if (essentialsDoc.includes("Tool 'n8n_autofix_workflow' not found")) {
64 | logger.error('❌ Essentials documentation retrieval failed');
65 | } else {
66 | logger.info('✅ Essentials documentation retrieved');
67 | const lines = essentialsDoc.split('\n').slice(0, 3);
68 | lines.forEach(line => logger.info(` ${line}`));
69 | }
70 | } catch (error) {
71 | logger.error('❌ Error retrieving essentials documentation:', error);
72 | }
73 |
74 | try {
75 | const fullDoc = getToolDocumentation('n8n_autofix_workflow', 'full');
76 | if (fullDoc.includes("Tool 'n8n_autofix_workflow' not found")) {
77 | logger.error('❌ Full documentation retrieval failed');
78 | } else {
79 | logger.info('✅ Full documentation retrieved');
80 | const lines = fullDoc.split('\n').slice(0, 3);
81 | lines.forEach(line => logger.info(` ${line}`));
82 | }
83 | } catch (error) {
84 | logger.error('❌ Error retrieving full documentation:', error);
85 | }
86 |
87 | // Test 4: Check if tool is listed in workflow management tools
88 | logger.info('\nTest 4: Checking workflow management tools listing');
89 | const workflowTools = Object.keys(toolsDocumentation).filter(k => k.startsWith('n8n_'));
90 | const hasAutofix = workflowTools.includes('n8n_autofix_workflow');
91 |
92 | if (hasAutofix) {
93 | logger.info('✅ n8n_autofix_workflow is listed in workflow management tools');
94 | logger.info(` Total workflow tools: ${workflowTools.length}`);
95 |
96 | // Show related tools
97 | const relatedTools = workflowTools.filter(t =>
98 | t.includes('validate') || t.includes('update') || t.includes('fix')
99 | );
100 | logger.info(` Related tools: ${relatedTools.join(', ')}`);
101 | } else {
102 | logger.error('❌ n8n_autofix_workflow NOT listed in workflow management tools');
103 | }
104 |
105 | // Summary
106 | logger.info('\n' + '='.repeat(60));
107 | logger.info('Summary:');
108 |
109 | if (hasDoc && hasAutofix) {
110 | logger.info('✨ Documentation integration successful!');
111 | logger.info('The n8n_autofix_workflow tool documentation is properly integrated.');
112 | logger.info('\nTo use in MCP:');
113 | logger.info(' - Essentials: tools_documentation({topic: "n8n_autofix_workflow"})');
114 | logger.info(' - Full: tools_documentation({topic: "n8n_autofix_workflow", depth: "full"})');
115 | } else {
116 | logger.error('⚠️ Documentation integration incomplete');
117 | logger.info('Please check the implementation and rebuild the project.');
118 | }
119 | }
120 |
121 | testAutofixDocumentation().catch(console.error);
```