This is page 2 of 63. Use http://codebase.md/czlonkowski/n8n-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── _config.yml ├── .claude │ └── agents │ ├── code-reviewer.md │ ├── context-manager.md │ ├── debugger.md │ ├── deployment-engineer.md │ ├── mcp-backend-engineer.md │ ├── n8n-mcp-tester.md │ ├── technical-researcher.md │ └── test-automator.md ├── .dockerignore ├── .env.docker ├── .env.example ├── .env.n8n.example ├── .env.test ├── .env.test.example ├── .github │ ├── ABOUT.md │ ├── BENCHMARK_THRESHOLDS.md │ ├── FUNDING.yml │ ├── gh-pages.yml │ ├── secret_scanning.yml │ └── workflows │ ├── benchmark-pr.yml │ ├── benchmark.yml │ ├── docker-build-fast.yml │ ├── docker-build-n8n.yml │ ├── docker-build.yml │ ├── release.yml │ ├── test.yml │ └── update-n8n-deps.yml ├── .gitignore ├── .npmignore ├── ATTRIBUTION.md ├── CHANGELOG.md ├── CLAUDE.md ├── codecov.yml ├── coverage.json ├── data │ ├── .gitkeep │ ├── nodes.db │ ├── nodes.db-shm │ ├── nodes.db-wal │ └── templates.db ├── deploy │ └── quick-deploy-n8n.sh ├── docker │ ├── docker-entrypoint.sh │ ├── n8n-mcp │ ├── parse-config.js │ └── README.md ├── docker-compose.buildkit.yml ├── docker-compose.extract.yml ├── docker-compose.n8n.yml ├── docker-compose.override.yml.example ├── docker-compose.test-n8n.yml ├── docker-compose.yml ├── Dockerfile ├── Dockerfile.railway ├── Dockerfile.test ├── docs │ ├── AUTOMATED_RELEASES.md │ ├── BENCHMARKS.md │ ├── bugfix-onSessionCreated-event.md │ ├── CHANGELOG.md │ ├── CLAUDE_CODE_SETUP.md │ ├── CLAUDE_INTERVIEW.md │ ├── CODECOV_SETUP.md │ ├── CODEX_SETUP.md │ ├── CURSOR_SETUP.md │ ├── DEPENDENCY_UPDATES.md │ ├── DOCKER_README.md │ ├── DOCKER_TROUBLESHOOTING.md │ ├── FINAL_AI_VALIDATION_SPEC.md │ ├── FLEXIBLE_INSTANCE_CONFIGURATION.md │ ├── HTTP_DEPLOYMENT.md │ ├── img │ │ ├── cc_command.png │ │ ├── cc_connected.png │ │ ├── codex_connected.png │ │ ├── cursor_tut.png │ │ ├── Railway_api.png │ │ ├── Railway_server_address.png │ │ ├── vsc_ghcp_chat_agent_mode.png │ │ ├── vsc_ghcp_chat_instruction_files.png │ │ ├── vsc_ghcp_chat_thinking_tool.png │ │ └── windsurf_tut.png │ ├── INSTALLATION.md │ ├── LIBRARY_USAGE.md │ ├── local │ │ ├── DEEP_DIVE_ANALYSIS_2025-10-02.md │ │ ├── DEEP_DIVE_ANALYSIS_README.md │ │ ├── Deep_dive_p1_p2.md │ │ ├── integration-testing-plan.md │ │ ├── integration-tests-phase1-summary.md │ │ ├── N8N_AI_WORKFLOW_BUILDER_ANALYSIS.md │ │ ├── P0_IMPLEMENTATION_PLAN.md │ │ └── TEMPLATE_MINING_ANALYSIS.md │ ├── MCP_ESSENTIALS_README.md │ ├── MCP_QUICK_START_GUIDE.md │ ├── N8N_DEPLOYMENT.md │ ├── RAILWAY_DEPLOYMENT.md │ ├── README_CLAUDE_SETUP.md │ ├── README.md │ ├── tools-documentation-usage.md │ ├── VS_CODE_PROJECT_SETUP.md │ ├── WINDSURF_SETUP.md │ └── workflow-diff-examples.md ├── examples │ └── enhanced-documentation-demo.js ├── fetch_log.txt ├── IMPLEMENTATION_GUIDE.md ├── LICENSE ├── MEMORY_N8N_UPDATE.md ├── MEMORY_TEMPLATE_UPDATE.md ├── monitor_fetch.sh ├── MVP_DEPLOYMENT_PLAN.md ├── N8N_HTTP_STREAMABLE_SETUP.md ├── n8n-nodes.db ├── P0-R3-TEST-PLAN.md ├── package-lock.json ├── package.json ├── package.runtime.json ├── PRIVACY.md ├── railway.json ├── README.md ├── renovate.json ├── scripts │ ├── analyze-optimization.sh │ ├── audit-schema-coverage.ts │ ├── build-optimized.sh │ ├── compare-benchmarks.js │ ├── demo-optimization.sh │ ├── deploy-http.sh │ ├── deploy-to-vm.sh │ ├── export-webhook-workflows.ts │ ├── extract-changelog.js │ ├── extract-from-docker.js │ ├── extract-nodes-docker.sh │ ├── extract-nodes-simple.sh │ ├── format-benchmark-results.js │ ├── generate-benchmark-stub.js │ ├── generate-detailed-reports.js │ ├── generate-test-summary.js │ ├── http-bridge.js │ ├── mcp-http-client.js │ ├── migrate-nodes-fts.ts │ ├── migrate-tool-docs.ts │ ├── n8n-docs-mcp.service │ ├── nginx-n8n-mcp.conf │ ├── prebuild-fts5.ts │ ├── prepare-release.js │ ├── publish-npm-quick.sh │ ├── publish-npm.sh │ ├── quick-test.ts │ ├── run-benchmarks-ci.js │ ├── sync-runtime-version.js │ ├── test-ai-validation-debug.ts │ ├── test-code-node-enhancements.ts │ ├── test-code-node-fixes.ts │ ├── test-docker-config.sh │ ├── test-docker-fingerprint.ts │ ├── test-docker-optimization.sh │ ├── test-docker.sh │ ├── test-empty-connection-validation.ts │ ├── test-error-message-tracking.ts │ ├── test-error-output-validation.ts │ ├── test-error-validation.js │ ├── test-essentials.ts │ ├── test-expression-code-validation.ts │ ├── test-expression-format-validation.js │ ├── test-fts5-search.ts │ ├── test-fuzzy-fix.ts │ ├── test-fuzzy-simple.ts │ ├── test-helpers-validation.ts │ ├── test-http-search.ts │ ├── test-http.sh │ ├── test-jmespath-validation.ts │ ├── test-multi-tenant-simple.ts │ ├── test-multi-tenant.ts │ ├── test-n8n-integration.sh │ ├── test-node-info.js │ ├── test-node-type-validation.ts │ ├── test-nodes-base-prefix.ts │ ├── test-operation-validation.ts │ ├── test-optimized-docker.sh │ ├── test-release-automation.js │ ├── test-search-improvements.ts │ ├── test-security.ts │ ├── test-single-session.sh │ ├── test-sqljs-triggers.ts │ ├── test-telemetry-debug.ts │ ├── test-telemetry-direct.ts │ ├── test-telemetry-env.ts │ ├── test-telemetry-integration.ts │ ├── test-telemetry-no-select.ts │ ├── test-telemetry-security.ts │ ├── test-telemetry-simple.ts │ ├── test-typeversion-validation.ts │ ├── test-url-configuration.ts │ ├── test-user-id-persistence.ts │ ├── test-webhook-validation.ts │ ├── test-workflow-insert.ts │ ├── test-workflow-sanitizer.ts │ ├── test-workflow-tracking-debug.ts │ ├── update-and-publish-prep.sh │ ├── update-n8n-deps.js │ ├── update-readme-version.js │ ├── vitest-benchmark-json-reporter.js │ └── vitest-benchmark-reporter.ts ├── SECURITY.md ├── src │ ├── config │ │ └── n8n-api.ts │ ├── data │ │ └── canonical-ai-tool-examples.json │ ├── database │ │ ├── database-adapter.ts │ │ ├── migrations │ │ │ └── add-template-node-configs.sql │ │ ├── node-repository.ts │ │ ├── nodes.db │ │ ├── schema-optimized.sql │ │ └── schema.sql │ ├── errors │ │ └── validation-service-error.ts │ ├── http-server-single-session.ts │ ├── http-server.ts │ ├── index.ts │ ├── loaders │ │ └── node-loader.ts │ ├── mappers │ │ └── docs-mapper.ts │ ├── mcp │ │ ├── handlers-n8n-manager.ts │ │ ├── handlers-workflow-diff.ts │ │ ├── index.ts │ │ ├── server.ts │ │ ├── stdio-wrapper.ts │ │ ├── tool-docs │ │ │ ├── configuration │ │ │ │ ├── get-node-as-tool-info.ts │ │ │ │ ├── get-node-documentation.ts │ │ │ │ ├── get-node-essentials.ts │ │ │ │ ├── get-node-info.ts │ │ │ │ ├── get-property-dependencies.ts │ │ │ │ ├── index.ts │ │ │ │ └── search-node-properties.ts │ │ │ ├── discovery │ │ │ │ ├── get-database-statistics.ts │ │ │ │ ├── index.ts │ │ │ │ ├── list-ai-tools.ts │ │ │ │ ├── list-nodes.ts │ │ │ │ └── search-nodes.ts │ │ │ ├── guides │ │ │ │ ├── ai-agents-guide.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── system │ │ │ │ ├── index.ts │ │ │ │ ├── n8n-diagnostic.ts │ │ │ │ ├── n8n-health-check.ts │ │ │ │ ├── n8n-list-available-tools.ts │ │ │ │ └── tools-documentation.ts │ │ │ ├── templates │ │ │ │ ├── get-template.ts │ │ │ │ ├── get-templates-for-task.ts │ │ │ │ ├── index.ts │ │ │ │ ├── list-node-templates.ts │ │ │ │ ├── list-tasks.ts │ │ │ │ ├── search-templates-by-metadata.ts │ │ │ │ └── search-templates.ts │ │ │ ├── types.ts │ │ │ ├── validation │ │ │ │ ├── index.ts │ │ │ │ ├── validate-node-minimal.ts │ │ │ │ ├── validate-node-operation.ts │ │ │ │ ├── validate-workflow-connections.ts │ │ │ │ ├── validate-workflow-expressions.ts │ │ │ │ └── validate-workflow.ts │ │ │ └── workflow_management │ │ │ ├── index.ts │ │ │ ├── n8n-autofix-workflow.ts │ │ │ ├── n8n-create-workflow.ts │ │ │ ├── n8n-delete-execution.ts │ │ │ ├── n8n-delete-workflow.ts │ │ │ ├── n8n-get-execution.ts │ │ │ ├── n8n-get-workflow-details.ts │ │ │ ├── n8n-get-workflow-minimal.ts │ │ │ ├── n8n-get-workflow-structure.ts │ │ │ ├── n8n-get-workflow.ts │ │ │ ├── n8n-list-executions.ts │ │ │ ├── n8n-list-workflows.ts │ │ │ ├── n8n-trigger-webhook-workflow.ts │ │ │ ├── n8n-update-full-workflow.ts │ │ │ ├── n8n-update-partial-workflow.ts │ │ │ └── n8n-validate-workflow.ts │ │ ├── tools-documentation.ts │ │ ├── tools-n8n-friendly.ts │ │ ├── tools-n8n-manager.ts │ │ ├── tools.ts │ │ └── workflow-examples.ts │ ├── mcp-engine.ts │ ├── mcp-tools-engine.ts │ ├── n8n │ │ ├── MCPApi.credentials.ts │ │ └── MCPNode.node.ts │ ├── parsers │ │ ├── node-parser.ts │ │ ├── property-extractor.ts │ │ └── simple-parser.ts │ ├── scripts │ │ ├── debug-http-search.ts │ │ ├── extract-from-docker.ts │ │ ├── fetch-templates-robust.ts │ │ ├── fetch-templates.ts │ │ ├── rebuild-database.ts │ │ ├── rebuild-optimized.ts │ │ ├── rebuild.ts │ │ ├── sanitize-templates.ts │ │ ├── seed-canonical-ai-examples.ts │ │ ├── test-autofix-documentation.ts │ │ ├── test-autofix-workflow.ts │ │ ├── test-execution-filtering.ts │ │ ├── test-node-suggestions.ts │ │ ├── test-protocol-negotiation.ts │ │ ├── test-summary.ts │ │ ├── test-webhook-autofix.ts │ │ ├── validate.ts │ │ └── validation-summary.ts │ ├── services │ │ ├── ai-node-validator.ts │ │ ├── ai-tool-validators.ts │ │ ├── confidence-scorer.ts │ │ ├── config-validator.ts │ │ ├── enhanced-config-validator.ts │ │ ├── example-generator.ts │ │ ├── execution-processor.ts │ │ ├── expression-format-validator.ts │ │ ├── expression-validator.ts │ │ ├── n8n-api-client.ts │ │ ├── n8n-validation.ts │ │ ├── node-documentation-service.ts │ │ ├── node-similarity-service.ts │ │ ├── node-specific-validators.ts │ │ ├── operation-similarity-service.ts │ │ ├── property-dependencies.ts │ │ ├── property-filter.ts │ │ ├── resource-similarity-service.ts │ │ ├── sqlite-storage-service.ts │ │ ├── task-templates.ts │ │ ├── universal-expression-validator.ts │ │ ├── workflow-auto-fixer.ts │ │ ├── workflow-diff-engine.ts │ │ └── workflow-validator.ts │ ├── telemetry │ │ ├── batch-processor.ts │ │ ├── config-manager.ts │ │ ├── early-error-logger.ts │ │ ├── error-sanitization-utils.ts │ │ ├── error-sanitizer.ts │ │ ├── event-tracker.ts │ │ ├── event-validator.ts │ │ ├── index.ts │ │ ├── performance-monitor.ts │ │ ├── rate-limiter.ts │ │ ├── startup-checkpoints.ts │ │ ├── telemetry-error.ts │ │ ├── telemetry-manager.ts │ │ ├── telemetry-types.ts │ │ └── workflow-sanitizer.ts │ ├── templates │ │ ├── batch-processor.ts │ │ ├── metadata-generator.ts │ │ ├── README.md │ │ ├── template-fetcher.ts │ │ ├── template-repository.ts │ │ └── template-service.ts │ ├── types │ │ ├── index.ts │ │ ├── instance-context.ts │ │ ├── n8n-api.ts │ │ ├── node-types.ts │ │ ├── session-restoration.ts │ │ └── workflow-diff.ts │ └── utils │ ├── auth.ts │ ├── bridge.ts │ ├── cache-utils.ts │ ├── console-manager.ts │ ├── documentation-fetcher.ts │ ├── enhanced-documentation-fetcher.ts │ ├── error-handler.ts │ ├── example-generator.ts │ ├── fixed-collection-validator.ts │ ├── logger.ts │ ├── mcp-client.ts │ ├── n8n-errors.ts │ ├── node-source-extractor.ts │ ├── node-type-normalizer.ts │ ├── node-type-utils.ts │ ├── node-utils.ts │ ├── npm-version-checker.ts │ ├── protocol-version.ts │ ├── simple-cache.ts │ ├── ssrf-protection.ts │ ├── template-node-resolver.ts │ ├── template-sanitizer.ts │ ├── url-detector.ts │ ├── validation-schemas.ts │ └── version.ts ├── supabase-telemetry-aggregation.sql ├── TELEMETRY_PRUNING_GUIDE.md ├── telemetry-pruning-analysis.md ├── test-output.txt ├── test-reinit-fix.sh ├── tests │ ├── __snapshots__ │ │ └── .gitkeep │ ├── auth.test.ts │ ├── benchmarks │ │ ├── database-queries.bench.ts │ │ ├── index.ts │ │ ├── mcp-tools.bench.ts │ │ ├── mcp-tools.bench.ts.disabled │ │ ├── mcp-tools.bench.ts.skip │ │ ├── node-loading.bench.ts.disabled │ │ ├── README.md │ │ ├── search-operations.bench.ts.disabled │ │ └── validation-performance.bench.ts.disabled │ ├── bridge.test.ts │ ├── comprehensive-extraction-test.js │ ├── data │ │ └── .gitkeep │ ├── debug-slack-doc.js │ ├── demo-enhanced-documentation.js │ ├── docker-tests-README.md │ ├── error-handler.test.ts │ ├── examples │ │ └── using-database-utils.test.ts │ ├── extracted-nodes-db │ │ ├── database-import.json │ │ ├── extraction-report.json │ │ ├── insert-nodes.sql │ │ ├── n8n-nodes-base__Airtable.json │ │ ├── n8n-nodes-base__Discord.json │ │ ├── n8n-nodes-base__Function.json │ │ ├── n8n-nodes-base__HttpRequest.json │ │ ├── n8n-nodes-base__If.json │ │ ├── n8n-nodes-base__Slack.json │ │ ├── n8n-nodes-base__SplitInBatches.json │ │ └── n8n-nodes-base__Webhook.json │ ├── factories │ │ ├── node-factory.ts │ │ └── property-definition-factory.ts │ ├── fixtures │ │ ├── .gitkeep │ │ ├── database │ │ │ └── test-nodes.json │ │ ├── factories │ │ │ ├── node.factory.ts │ │ │ └── parser-node.factory.ts │ │ └── template-configs.ts │ ├── helpers │ │ └── env-helpers.ts │ ├── http-server-auth.test.ts │ ├── integration │ │ ├── ai-validation │ │ │ ├── ai-agent-validation.test.ts │ │ │ ├── ai-tool-validation.test.ts │ │ │ ├── chat-trigger-validation.test.ts │ │ │ ├── e2e-validation.test.ts │ │ │ ├── helpers.ts │ │ │ ├── llm-chain-validation.test.ts │ │ │ ├── README.md │ │ │ └── TEST_REPORT.md │ │ ├── ci │ │ │ └── database-population.test.ts │ │ ├── database │ │ │ ├── connection-management.test.ts │ │ │ ├── empty-database.test.ts │ │ │ ├── fts5-search.test.ts │ │ │ ├── node-fts5-search.test.ts │ │ │ ├── node-repository.test.ts │ │ │ ├── performance.test.ts │ │ │ ├── template-node-configs.test.ts │ │ │ ├── template-repository.test.ts │ │ │ ├── test-utils.ts │ │ │ └── transactions.test.ts │ │ ├── database-integration.test.ts │ │ ├── docker │ │ │ ├── docker-config.test.ts │ │ │ ├── docker-entrypoint.test.ts │ │ │ └── test-helpers.ts │ │ ├── flexible-instance-config.test.ts │ │ ├── mcp │ │ │ └── template-examples-e2e.test.ts │ │ ├── mcp-protocol │ │ │ ├── basic-connection.test.ts │ │ │ ├── error-handling.test.ts │ │ │ ├── performance.test.ts │ │ │ ├── protocol-compliance.test.ts │ │ │ ├── README.md │ │ │ ├── session-management.test.ts │ │ │ ├── test-helpers.ts │ │ │ ├── tool-invocation.test.ts │ │ │ └── workflow-error-validation.test.ts │ │ ├── msw-setup.test.ts │ │ ├── n8n-api │ │ │ ├── executions │ │ │ │ ├── delete-execution.test.ts │ │ │ │ ├── get-execution.test.ts │ │ │ │ ├── list-executions.test.ts │ │ │ │ └── trigger-webhook.test.ts │ │ │ ├── scripts │ │ │ │ └── cleanup-orphans.ts │ │ │ ├── system │ │ │ │ ├── diagnostic.test.ts │ │ │ │ ├── health-check.test.ts │ │ │ │ └── list-tools.test.ts │ │ │ ├── test-connection.ts │ │ │ ├── types │ │ │ │ └── mcp-responses.ts │ │ │ ├── utils │ │ │ │ ├── cleanup-helpers.ts │ │ │ │ ├── credentials.ts │ │ │ │ ├── factories.ts │ │ │ │ ├── fixtures.ts │ │ │ │ ├── mcp-context.ts │ │ │ │ ├── n8n-client.ts │ │ │ │ ├── node-repository.ts │ │ │ │ ├── response-types.ts │ │ │ │ ├── test-context.ts │ │ │ │ └── webhook-workflows.ts │ │ │ └── workflows │ │ │ ├── autofix-workflow.test.ts │ │ │ ├── create-workflow.test.ts │ │ │ ├── delete-workflow.test.ts │ │ │ ├── get-workflow-details.test.ts │ │ │ ├── get-workflow-minimal.test.ts │ │ │ ├── get-workflow-structure.test.ts │ │ │ ├── get-workflow.test.ts │ │ │ ├── list-workflows.test.ts │ │ │ ├── smart-parameters.test.ts │ │ │ ├── update-partial-workflow.test.ts │ │ │ ├── update-workflow.test.ts │ │ │ └── validate-workflow.test.ts │ │ ├── security │ │ │ ├── command-injection-prevention.test.ts │ │ │ └── rate-limiting.test.ts │ │ ├── session │ │ │ └── test-onSessionCreated-event.ts │ │ ├── session-lifecycle-retry.test.ts │ │ ├── session-persistence.test.ts │ │ ├── setup │ │ │ ├── integration-setup.ts │ │ │ └── msw-test-server.ts │ │ ├── telemetry │ │ │ ├── docker-user-id-stability.test.ts │ │ │ └── mcp-telemetry.test.ts │ │ ├── templates │ │ │ └── metadata-operations.test.ts │ │ └── workflow-creation-node-type-format.test.ts │ ├── logger.test.ts │ ├── MOCKING_STRATEGY.md │ ├── mocks │ │ ├── n8n-api │ │ │ ├── data │ │ │ │ ├── credentials.ts │ │ │ │ ├── executions.ts │ │ │ │ └── workflows.ts │ │ │ ├── handlers.ts │ │ │ └── index.ts │ │ └── README.md │ ├── node-storage-export.json │ ├── setup │ │ ├── global-setup.ts │ │ ├── msw-setup.ts │ │ ├── TEST_ENV_DOCUMENTATION.md │ │ └── test-env.ts │ ├── test-database-extraction.js │ ├── test-direct-extraction.js │ ├── test-enhanced-documentation.js │ ├── test-enhanced-integration.js │ ├── test-mcp-extraction.js │ ├── test-mcp-server-extraction.js │ ├── test-mcp-tools-integration.js │ ├── test-node-documentation-service.js │ ├── test-node-list.js │ ├── test-package-info.js │ ├── test-parsing-operations.js │ ├── test-slack-node-complete.js │ ├── test-small-rebuild.js │ ├── test-sqlite-search.js │ ├── test-storage-system.js │ ├── unit │ │ ├── __mocks__ │ │ │ ├── n8n-nodes-base.test.ts │ │ │ ├── n8n-nodes-base.ts │ │ │ └── README.md │ │ ├── database │ │ │ ├── __mocks__ │ │ │ │ └── better-sqlite3.ts │ │ │ ├── database-adapter-unit.test.ts │ │ │ ├── node-repository-core.test.ts │ │ │ ├── node-repository-operations.test.ts │ │ │ ├── node-repository-outputs.test.ts │ │ │ ├── README.md │ │ │ └── template-repository-core.test.ts │ │ ├── docker │ │ │ ├── config-security.test.ts │ │ │ ├── edge-cases.test.ts │ │ │ ├── parse-config.test.ts │ │ │ └── serve-command.test.ts │ │ ├── errors │ │ │ └── validation-service-error.test.ts │ │ ├── examples │ │ │ └── using-n8n-nodes-base-mock.test.ts │ │ ├── flexible-instance-security-advanced.test.ts │ │ ├── flexible-instance-security.test.ts │ │ ├── http-server │ │ │ └── multi-tenant-support.test.ts │ │ ├── http-server-n8n-mode.test.ts │ │ ├── http-server-n8n-reinit.test.ts │ │ ├── http-server-session-management.test.ts │ │ ├── loaders │ │ │ └── node-loader.test.ts │ │ ├── mappers │ │ │ └── docs-mapper.test.ts │ │ ├── mcp │ │ │ ├── get-node-essentials-examples.test.ts │ │ │ ├── handlers-n8n-manager-simple.test.ts │ │ │ ├── handlers-n8n-manager.test.ts │ │ │ ├── handlers-workflow-diff.test.ts │ │ │ ├── lru-cache-behavior.test.ts │ │ │ ├── multi-tenant-tool-listing.test.ts.disabled │ │ │ ├── parameter-validation.test.ts │ │ │ ├── search-nodes-examples.test.ts │ │ │ ├── tools-documentation.test.ts │ │ │ └── tools.test.ts │ │ ├── monitoring │ │ │ └── cache-metrics.test.ts │ │ ├── MULTI_TENANT_TEST_COVERAGE.md │ │ ├── multi-tenant-integration.test.ts │ │ ├── parsers │ │ │ ├── node-parser-outputs.test.ts │ │ │ ├── node-parser.test.ts │ │ │ ├── property-extractor.test.ts │ │ │ └── simple-parser.test.ts │ │ ├── scripts │ │ │ └── fetch-templates-extraction.test.ts │ │ ├── services │ │ │ ├── ai-node-validator.test.ts │ │ │ ├── ai-tool-validators.test.ts │ │ │ ├── confidence-scorer.test.ts │ │ │ ├── config-validator-basic.test.ts │ │ │ ├── config-validator-edge-cases.test.ts │ │ │ ├── config-validator-node-specific.test.ts │ │ │ ├── config-validator-security.test.ts │ │ │ ├── debug-validator.test.ts │ │ │ ├── enhanced-config-validator-integration.test.ts │ │ │ ├── enhanced-config-validator-operations.test.ts │ │ │ ├── enhanced-config-validator.test.ts │ │ │ ├── example-generator.test.ts │ │ │ ├── execution-processor.test.ts │ │ │ ├── expression-format-validator.test.ts │ │ │ ├── expression-validator-edge-cases.test.ts │ │ │ ├── expression-validator.test.ts │ │ │ ├── fixed-collection-validation.test.ts │ │ │ ├── loop-output-edge-cases.test.ts │ │ │ ├── n8n-api-client.test.ts │ │ │ ├── n8n-validation.test.ts │ │ │ ├── node-similarity-service.test.ts │ │ │ ├── node-specific-validators.test.ts │ │ │ ├── operation-similarity-service-comprehensive.test.ts │ │ │ ├── operation-similarity-service.test.ts │ │ │ ├── property-dependencies.test.ts │ │ │ ├── property-filter-edge-cases.test.ts │ │ │ ├── property-filter.test.ts │ │ │ ├── resource-similarity-service-comprehensive.test.ts │ │ │ ├── resource-similarity-service.test.ts │ │ │ ├── task-templates.test.ts │ │ │ ├── template-service.test.ts │ │ │ ├── universal-expression-validator.test.ts │ │ │ ├── validation-fixes.test.ts │ │ │ ├── workflow-auto-fixer.test.ts │ │ │ ├── workflow-diff-engine.test.ts │ │ │ ├── workflow-fixed-collection-validation.test.ts │ │ │ ├── workflow-validator-comprehensive.test.ts │ │ │ ├── workflow-validator-edge-cases.test.ts │ │ │ ├── workflow-validator-error-outputs.test.ts │ │ │ ├── workflow-validator-expression-format.test.ts │ │ │ ├── workflow-validator-loops-simple.test.ts │ │ │ ├── workflow-validator-loops.test.ts │ │ │ ├── workflow-validator-mocks.test.ts │ │ │ ├── workflow-validator-performance.test.ts │ │ │ ├── workflow-validator-with-mocks.test.ts │ │ │ └── workflow-validator.test.ts │ │ ├── session-lifecycle-events.test.ts │ │ ├── session-management-api.test.ts │ │ ├── session-restoration-retry.test.ts │ │ ├── session-restoration.test.ts │ │ ├── telemetry │ │ │ ├── batch-processor.test.ts │ │ │ ├── config-manager.test.ts │ │ │ ├── event-tracker.test.ts │ │ │ ├── event-validator.test.ts │ │ │ ├── rate-limiter.test.ts │ │ │ ├── telemetry-error.test.ts │ │ │ ├── telemetry-manager.test.ts │ │ │ ├── v2.18.3-fixes-verification.test.ts │ │ │ └── workflow-sanitizer.test.ts │ │ ├── templates │ │ │ ├── batch-processor.test.ts │ │ │ ├── metadata-generator.test.ts │ │ │ ├── template-repository-metadata.test.ts │ │ │ └── template-repository-security.test.ts │ │ ├── test-env-example.test.ts │ │ ├── test-infrastructure.test.ts │ │ ├── types │ │ │ ├── instance-context-coverage.test.ts │ │ │ └── instance-context-multi-tenant.test.ts │ │ ├── utils │ │ │ ├── auth-timing-safe.test.ts │ │ │ ├── cache-utils.test.ts │ │ │ ├── console-manager.test.ts │ │ │ ├── database-utils.test.ts │ │ │ ├── fixed-collection-validator.test.ts │ │ │ ├── n8n-errors.test.ts │ │ │ ├── node-type-normalizer.test.ts │ │ │ ├── node-type-utils.test.ts │ │ │ ├── node-utils.test.ts │ │ │ ├── simple-cache-memory-leak-fix.test.ts │ │ │ ├── ssrf-protection.test.ts │ │ │ └── template-node-resolver.test.ts │ │ └── validation-fixes.test.ts │ └── utils │ ├── assertions.ts │ ├── builders │ │ └── workflow.builder.ts │ ├── data-generators.ts │ ├── database-utils.ts │ ├── README.md │ └── test-helpers.ts ├── thumbnail.png ├── tsconfig.build.json ├── tsconfig.json ├── types │ ├── mcp.d.ts │ └── test-env.d.ts ├── verify-telemetry-fix.js ├── versioned-nodes.md ├── vitest.config.benchmark.ts ├── vitest.config.integration.ts └── vitest.config.ts ``` # Files -------------------------------------------------------------------------------- /tests/test-package-info.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | const { NodeSourceExtractor } = require('../dist/utils/node-source-extractor'); 4 | 5 | async function testPackageInfo() { 6 | console.log('🧪 Testing Package Info Extraction\n'); 7 | 8 | const extractor = new NodeSourceExtractor(); 9 | 10 | const testNodes = [ 11 | 'n8n-nodes-base.Slack', 12 | 'n8n-nodes-base.HttpRequest', 13 | 'n8n-nodes-base.Function' 14 | ]; 15 | 16 | for (const nodeType of testNodes) { 17 | console.log(`\n📦 Testing ${nodeType}:`); 18 | try { 19 | const result = await extractor.extractNodeSource(nodeType); 20 | console.log(` - Source Code: ${result.sourceCode ? '✅' : '❌'} (${result.sourceCode?.length || 0} bytes)`); 21 | console.log(` - Credential Code: ${result.credentialCode ? '✅' : '❌'} (${result.credentialCode?.length || 0} bytes)`); 22 | console.log(` - Package Name: ${result.packageInfo?.name || '❌ undefined'}`); 23 | console.log(` - Package Version: ${result.packageInfo?.version || '❌ undefined'}`); 24 | } catch (error) { 25 | console.log(` ❌ Error: ${error.message}`); 26 | } 27 | } 28 | } 29 | 30 | testPackageInfo().catch(console.error); ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["ES2020"], 6 | "types": ["node", "vitest/globals", "./types/test-env"], 7 | "outDir": "./dist", 8 | "rootDir": "./", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "resolveJsonModule": true, 14 | "declaration": true, 15 | "declarationMap": true, 16 | "sourceMap": true, 17 | "removeComments": true, 18 | "noImplicitAny": true, 19 | "strictNullChecks": true, 20 | "strictFunctionTypes": true, 21 | "strictBindCallApply": true, 22 | "strictPropertyInitialization": true, 23 | "noImplicitThis": true, 24 | "alwaysStrict": true, 25 | "noUnusedLocals": false, 26 | "noUnusedParameters": false, 27 | "noImplicitReturns": true, 28 | "noFallthroughCasesInSwitch": true, 29 | "moduleResolution": "node", 30 | "allowJs": true, 31 | "baseUrl": ".", 32 | "paths": { 33 | "@/*": ["src/*"], 34 | "@tests/*": ["tests/*"] 35 | } 36 | }, 37 | "include": ["src/**/*", "tests/**/*", "vitest.config.ts", "types/**/*"], 38 | "exclude": ["node_modules", "dist"] 39 | } ``` -------------------------------------------------------------------------------- /scripts/generate-benchmark-stub.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Generates a stub benchmark-results.json file when benchmarks fail to produce output. 5 | * This ensures the CI pipeline doesn't fail due to missing files. 6 | */ 7 | 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | 11 | const stubResults = { 12 | timestamp: new Date().toISOString(), 13 | files: [ 14 | { 15 | filepath: 'tests/benchmarks/stub.bench.ts', 16 | groups: [ 17 | { 18 | name: 'Stub Benchmarks', 19 | benchmarks: [ 20 | { 21 | name: 'stub-benchmark', 22 | result: { 23 | mean: 0.001, 24 | min: 0.001, 25 | max: 0.001, 26 | hz: 1000, 27 | p75: 0.001, 28 | p99: 0.001, 29 | p995: 0.001, 30 | p999: 0.001, 31 | rme: 0, 32 | samples: 1 33 | } 34 | } 35 | ] 36 | } 37 | ] 38 | } 39 | ] 40 | }; 41 | 42 | const outputPath = path.join(process.cwd(), 'benchmark-results.json'); 43 | fs.writeFileSync(outputPath, JSON.stringify(stubResults, null, 2)); 44 | console.log(`Generated stub benchmark results at ${outputPath}`); ``` -------------------------------------------------------------------------------- /tests/mocks/n8n-api/data/credentials.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Mock credential data for MSW handlers 3 | */ 4 | 5 | export interface MockCredential { 6 | id: string; 7 | name: string; 8 | type: string; 9 | data?: Record<string, any>; // Usually encrypted in real n8n 10 | createdAt: string; 11 | updatedAt: string; 12 | } 13 | 14 | export const mockCredentials: MockCredential[] = [ 15 | { 16 | id: 'cred_1', 17 | name: 'Slack Account', 18 | type: 'slackApi', 19 | createdAt: '2024-01-01T00:00:00.000Z', 20 | updatedAt: '2024-01-01T00:00:00.000Z' 21 | }, 22 | { 23 | id: 'cred_2', 24 | name: 'HTTP Header Auth', 25 | type: 'httpHeaderAuth', 26 | createdAt: '2024-01-01T00:00:00.000Z', 27 | updatedAt: '2024-01-01T00:00:00.000Z' 28 | }, 29 | { 30 | id: 'cred_3', 31 | name: 'OpenAI API', 32 | type: 'openAiApi', 33 | createdAt: '2024-01-01T00:00:00.000Z', 34 | updatedAt: '2024-01-01T00:00:00.000Z' 35 | } 36 | ]; 37 | 38 | /** 39 | * Factory for creating mock credentials 40 | */ 41 | export const credentialFactory = { 42 | create: (type: string, name?: string): MockCredential => ({ 43 | id: `cred_${Date.now()}`, 44 | name: name || `${type} Credential`, 45 | type, 46 | createdAt: new Date().toISOString(), 47 | updatedAt: new Date().toISOString() 48 | }) 49 | }; ``` -------------------------------------------------------------------------------- /docker-compose.buildkit.yml: -------------------------------------------------------------------------------- ```yaml 1 | # Docker Compose file with BuildKit optimizations for faster builds 2 | # This now builds without n8n dependencies for ultra-fast builds 3 | version: '3.8' 4 | 5 | services: 6 | n8n-mcp: 7 | build: 8 | context: . 9 | dockerfile: Dockerfile 10 | # Enable BuildKit 11 | x-bake: 12 | cache-from: 13 | - type=gha 14 | - type=local,src=/tmp/.buildx-cache 15 | cache-to: 16 | - type=gha,mode=max 17 | - type=local,dest=/tmp/.buildx-cache-new,mode=max 18 | args: 19 | BUILDKIT_INLINE_CACHE: 1 20 | image: n8n-mcp:latest 21 | container_name: n8n-mcp 22 | ports: 23 | - "3000:3000" 24 | environment: 25 | - MCP_MODE=${MCP_MODE:-http} 26 | - AUTH_TOKEN=${AUTH_TOKEN} 27 | - NODE_ENV=${NODE_ENV:-production} 28 | - LOG_LEVEL=${LOG_LEVEL:-info} 29 | - PORT=3000 30 | volumes: 31 | # Mount data directory for persistence 32 | - ./data:/app/data 33 | restart: unless-stopped 34 | healthcheck: 35 | test: ["CMD", "curl", "-f", "http://localhost:3000/health"] 36 | interval: 30s 37 | timeout: 10s 38 | retries: 3 39 | start_period: 5s 40 | 41 | # Use external network if needed 42 | networks: 43 | default: 44 | name: n8n-mcp-network ``` -------------------------------------------------------------------------------- /scripts/test-fuzzy-simple.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | import { N8NDocumentationMCPServer } from '../src/mcp/server'; 4 | 5 | async function testSimple() { 6 | const server = new N8NDocumentationMCPServer(); 7 | await new Promise(resolve => setTimeout(resolve, 1000)); 8 | 9 | // Just test one query 10 | const result = await server.executeTool('search_nodes', { 11 | query: 'slak', 12 | mode: 'FUZZY', 13 | limit: 5 14 | }); 15 | 16 | console.log('Query: "slak" (FUZZY mode)'); 17 | console.log(`Results: ${result.results.length}`); 18 | 19 | if (result.results.length === 0) { 20 | // Let's check with a lower threshold 21 | const serverAny = server as any; 22 | const slackNode = { 23 | node_type: 'nodes-base.slack', 24 | display_name: 'Slack', 25 | description: 'Consume Slack API' 26 | }; 27 | const score = serverAny.calculateFuzzyScore(slackNode, 'slak'); 28 | console.log(`\nSlack node score for "slak": ${score}`); 29 | console.log('Current threshold: 400'); 30 | console.log('Should it match?', score >= 400 ? 'YES' : 'NO'); 31 | } else { 32 | result.results.forEach((r: any, i: number) => { 33 | console.log(`${i + 1}. ${r.displayName}`); 34 | }); 35 | } 36 | } 37 | 38 | testSimple().catch(console.error); ``` -------------------------------------------------------------------------------- /src/n8n/MCPApi.credentials.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | ICredentialType, 3 | INodeProperties, 4 | } from 'n8n-workflow'; 5 | 6 | export class MCPApi implements ICredentialType { 7 | name = 'mcpApi'; 8 | displayName = 'MCP API'; 9 | documentationUrl = 'mcp'; 10 | properties: INodeProperties[] = [ 11 | { 12 | displayName: 'Server URL', 13 | name: 'serverUrl', 14 | type: 'string', 15 | default: 'http://localhost:3000', 16 | placeholder: 'http://localhost:3000', 17 | description: 'The URL of the MCP server', 18 | }, 19 | { 20 | displayName: 'Authentication Token', 21 | name: 'authToken', 22 | type: 'string', 23 | typeOptions: { 24 | password: true, 25 | }, 26 | default: '', 27 | description: 'Authentication token for the MCP server (if required)', 28 | }, 29 | { 30 | displayName: 'Connection Type', 31 | name: 'connectionType', 32 | type: 'options', 33 | options: [ 34 | { 35 | name: 'HTTP', 36 | value: 'http', 37 | }, 38 | { 39 | name: 'WebSocket', 40 | value: 'websocket', 41 | }, 42 | { 43 | name: 'STDIO', 44 | value: 'stdio', 45 | }, 46 | ], 47 | default: 'http', 48 | description: 'How to connect to the MCP server', 49 | }, 50 | ]; 51 | } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * n8n-MCP - Model Context Protocol Server for n8n 3 | * Copyright (c) 2024 AiAdvisors Romuald Czlonkowski 4 | * Licensed under the Sustainable Use License v1.0 5 | */ 6 | 7 | // Engine exports for service integration 8 | export { N8NMCPEngine, EngineHealth, EngineOptions } from './mcp-engine'; 9 | export { SingleSessionHTTPServer } from './http-server-single-session'; 10 | export { ConsoleManager } from './utils/console-manager'; 11 | export { N8NDocumentationMCPServer } from './mcp/server'; 12 | 13 | // Type exports for multi-tenant and library usage 14 | export type { 15 | InstanceContext 16 | } from './types/instance-context'; 17 | export { 18 | validateInstanceContext, 19 | isInstanceContext 20 | } from './types/instance-context'; 21 | 22 | // Session restoration types (v2.19.0) 23 | export type { 24 | SessionRestoreHook, 25 | SessionRestorationOptions, 26 | SessionState 27 | } from './types/session-restoration'; 28 | 29 | // Re-export MCP SDK types for convenience 30 | export type { 31 | Tool, 32 | CallToolResult, 33 | ListToolsResult 34 | } from '@modelcontextprotocol/sdk/types.js'; 35 | 36 | // Default export for convenience 37 | import N8NMCPEngine from './mcp-engine'; 38 | export default N8NMCPEngine; 39 | 40 | // Legacy CLI functionality - moved to ./mcp/index.ts 41 | // This file now serves as the main entry point for library usage ``` -------------------------------------------------------------------------------- /scripts/test-docker-config.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | # Script to run Docker config tests 4 | # Usage: ./scripts/test-docker-config.sh [unit|integration|all] 5 | 6 | set -e 7 | 8 | MODE=${1:-all} 9 | 10 | echo "Running Docker config tests in mode: $MODE" 11 | 12 | case $MODE in 13 | unit) 14 | echo "Running unit tests..." 15 | npm test -- tests/unit/docker/ 16 | ;; 17 | integration) 18 | echo "Running integration tests (requires Docker)..." 19 | RUN_DOCKER_TESTS=true npm run test:integration -- tests/integration/docker/ 20 | ;; 21 | all) 22 | echo "Running all Docker config tests..." 23 | npm test -- tests/unit/docker/ 24 | if command -v docker &> /dev/null; then 25 | echo "Docker found, running integration tests..." 26 | RUN_DOCKER_TESTS=true npm run test:integration -- tests/integration/docker/ 27 | else 28 | echo "Docker not found, skipping integration tests" 29 | fi 30 | ;; 31 | coverage) 32 | echo "Running Docker config tests with coverage..." 33 | npm run test:coverage -- tests/unit/docker/ 34 | ;; 35 | security) 36 | echo "Running security-focused tests..." 37 | npm test -- tests/unit/docker/config-security.test.ts tests/unit/docker/parse-config.test.ts 38 | ;; 39 | *) 40 | echo "Usage: $0 [unit|integration|all|coverage|security]" 41 | exit 1 42 | ;; 43 | esac 44 | 45 | echo "Docker config tests completed!" ``` -------------------------------------------------------------------------------- /src/utils/simple-cache.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Simple in-memory cache with TTL support 3 | * No external dependencies needed 4 | */ 5 | export class SimpleCache { 6 | private cache = new Map<string, { data: any; expires: number }>(); 7 | private cleanupTimer: NodeJS.Timeout | null = null; 8 | 9 | constructor() { 10 | // Clean up expired entries every minute 11 | this.cleanupTimer = setInterval(() => { 12 | const now = Date.now(); 13 | for (const [key, item] of this.cache.entries()) { 14 | if (item.expires < now) this.cache.delete(key); 15 | } 16 | }, 60000); 17 | } 18 | 19 | get(key: string): any { 20 | const item = this.cache.get(key); 21 | if (!item || item.expires < Date.now()) { 22 | this.cache.delete(key); 23 | return null; 24 | } 25 | return item.data; 26 | } 27 | 28 | set(key: string, data: any, ttlSeconds: number = 300): void { 29 | this.cache.set(key, { 30 | data, 31 | expires: Date.now() + (ttlSeconds * 1000) 32 | }); 33 | } 34 | 35 | clear(): void { 36 | this.cache.clear(); 37 | } 38 | 39 | /** 40 | * Clean up the cache and stop the cleanup timer 41 | * Essential for preventing memory leaks in long-running servers 42 | */ 43 | destroy(): void { 44 | if (this.cleanupTimer) { 45 | clearInterval(this.cleanupTimer); 46 | this.cleanupTimer = null; 47 | } 48 | this.cache.clear(); 49 | } 50 | } ``` -------------------------------------------------------------------------------- /docs/CODEX_SETUP.md: -------------------------------------------------------------------------------- ```markdown 1 | # Codex Setup 2 | 3 | Connect n8n-MCP to Codex for enhanced n8n workflow development. 4 | 5 | ## Update your Codex configuration 6 | 7 | Go to your Codex settings at `~/.codex/config.toml` and add the following configuration: 8 | 9 | ### Basic configuration (documentation tools only): 10 | ```toml 11 | [mcp_servers.n8n] 12 | command = "npx" 13 | args = ["n8n-mcp"] 14 | env = { "MCP_MODE" = "stdio", "LOG_LEVEL" = "error", "DISABLE_CONSOLE_OUTPUT" = "true" } 15 | ``` 16 | 17 | ### Full configuration (with n8n management tools): 18 | ```toml 19 | [mcp_servers.n8n] 20 | command = "npx" 21 | args = ["n8n-mcp"] 22 | env = { "MCP_MODE" = "stdio", "LOG_LEVEL" = "error", "DISABLE_CONSOLE_OUTPUT" = "true", "N8N_API_URL" = "https://your-n8n-instance.com", "N8N_API_KEY" = "your-api-key" } 23 | ``` 24 | 25 | Make sure to replace `https://your-n8n-instance.com` with your actual n8n URL and `your-api-key` with your n8n API key. 26 | 27 | ## Managing Your MCP Server 28 | Enter the Codex CLI and use the `/mcp` command to see server status and available tools. 29 | 30 |  31 | 32 | ## Project Instructions 33 | 34 | For optimal results, create a `AGENTS.md` file in your project root with the instructions same with [main README's Claude Project Setup section](../README.md#-claude-project-setup). 35 | ``` -------------------------------------------------------------------------------- /tests/integration/n8n-api/scripts/cleanup-orphans.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env tsx 2 | /** 3 | * Cleanup Orphaned Test Resources 4 | * 5 | * Standalone script to clean up orphaned workflows and executions 6 | * from failed test runs. Run this periodically in CI or manually 7 | * to maintain a clean test environment. 8 | * 9 | * Usage: 10 | * npm run test:cleanup:orphans 11 | * tsx tests/integration/n8n-api/scripts/cleanup-orphans.ts 12 | */ 13 | 14 | import { cleanupAllTestResources } from '../utils/cleanup-helpers'; 15 | import { getN8nCredentials, validateCredentials } from '../utils/credentials'; 16 | 17 | async function main() { 18 | console.log('Starting cleanup of orphaned test resources...\n'); 19 | 20 | try { 21 | // Validate credentials 22 | const creds = getN8nCredentials(); 23 | validateCredentials(creds); 24 | 25 | console.log(`n8n Instance: ${creds.url}`); 26 | console.log(`Cleanup Tag: ${creds.cleanup.tag}`); 27 | console.log(`Cleanup Prefix: ${creds.cleanup.namePrefix}\n`); 28 | 29 | // Run cleanup 30 | const result = await cleanupAllTestResources(); 31 | 32 | console.log('\n✅ Cleanup complete!'); 33 | console.log(` Workflows deleted: ${result.workflows}`); 34 | console.log(` Executions deleted: ${result.executions}`); 35 | 36 | process.exit(0); 37 | } catch (error) { 38 | console.error('\n❌ Cleanup failed:', error); 39 | process.exit(1); 40 | } 41 | } 42 | 43 | main(); 44 | ``` -------------------------------------------------------------------------------- /scripts/test-telemetry-direct.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env npx tsx 2 | /** 3 | * Direct telemetry test with hardcoded credentials 4 | */ 5 | 6 | import { createClient } from '@supabase/supabase-js'; 7 | 8 | const TELEMETRY_BACKEND = { 9 | URL: 'https://ydyufsohxdfpopqbubwk.supabase.co', 10 | ANON_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlkeXVmc29oeGRmcG9wcWJ1YndrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Mzc2MzAxMDgsImV4cCI6MjA1MzIwNjEwOH0.LsUTx9OsNtnqg-jxXaJPc84aBHVDehHiMaFoF2Ir8s0' 11 | }; 12 | 13 | async function testDirect() { 14 | console.log('🧪 Direct Telemetry Test\n'); 15 | 16 | const supabase = createClient(TELEMETRY_BACKEND.URL, TELEMETRY_BACKEND.ANON_KEY, { 17 | auth: { 18 | persistSession: false, 19 | autoRefreshToken: false, 20 | } 21 | }); 22 | 23 | const testEvent = { 24 | user_id: 'direct-test-' + Date.now(), 25 | event: 'direct_test', 26 | properties: { 27 | source: 'test-telemetry-direct.ts', 28 | timestamp: new Date().toISOString() 29 | } 30 | }; 31 | 32 | console.log('Sending event:', testEvent); 33 | 34 | const { data, error } = await supabase 35 | .from('telemetry_events') 36 | .insert([testEvent]); 37 | 38 | if (error) { 39 | console.error('❌ Failed:', error); 40 | } else { 41 | console.log('✅ Success! Event sent directly to Supabase'); 42 | console.log('Response:', data); 43 | } 44 | } 45 | 46 | testDirect().catch(console.error); 47 | ``` -------------------------------------------------------------------------------- /scripts/test-docker.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | # scripts/test-docker.sh 3 | 4 | echo "🧪 Testing n8n-MCP Docker Deployment" 5 | 6 | # Test 1: Build simple image 7 | echo "1. Building simple Docker image..." 8 | docker build -t n8n-mcp:test . 9 | 10 | # Test 2: Test stdio mode 11 | echo "2. Testing stdio mode..." 12 | echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | \ 13 | docker run --rm -i -e MCP_MODE=stdio n8n-mcp:test 14 | 15 | # Test 3: Test HTTP mode 16 | echo "3. Testing HTTP mode..." 17 | docker run -d --name test-http \ 18 | -e MCP_MODE=http \ 19 | -e AUTH_TOKEN=test-token \ 20 | -p 3001:3000 \ 21 | n8n-mcp:test 22 | 23 | sleep 5 24 | 25 | # Check health 26 | curl -f http://localhost:3001/health || echo "Health check failed" 27 | 28 | # Test auth 29 | curl -H "Authorization: Bearer test-token" \ 30 | -H "Content-Type: application/json" \ 31 | -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' \ 32 | http://localhost:3001/mcp 33 | 34 | docker stop test-http && docker rm test-http 35 | 36 | # Test 4: Volume persistence 37 | echo "4. Testing volume persistence..." 38 | docker volume create test-data 39 | docker run -d --name test-persist \ 40 | -v test-data:/app/data \ 41 | -e MCP_MODE=http \ 42 | -e AUTH_TOKEN=test \ 43 | -p 3002:3000 \ 44 | n8n-mcp:test 45 | 46 | sleep 10 47 | docker exec test-persist ls -la /app/data/nodes.db 48 | docker stop test-persist && docker rm test-persist 49 | docker volume rm test-data 50 | 51 | echo "✅ Docker tests completed!" ``` -------------------------------------------------------------------------------- /src/errors/validation-service-error.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Custom error class for validation service failures 3 | */ 4 | export class ValidationServiceError extends Error { 5 | constructor( 6 | message: string, 7 | public readonly nodeType?: string, 8 | public readonly property?: string, 9 | public readonly cause?: Error 10 | ) { 11 | super(message); 12 | this.name = 'ValidationServiceError'; 13 | 14 | // Maintains proper stack trace for where our error was thrown (only available on V8) 15 | if (Error.captureStackTrace) { 16 | Error.captureStackTrace(this, ValidationServiceError); 17 | } 18 | } 19 | 20 | /** 21 | * Create error for JSON parsing failure 22 | */ 23 | static jsonParseError(nodeType: string, cause: Error): ValidationServiceError { 24 | return new ValidationServiceError( 25 | `Failed to parse JSON data for node ${nodeType}`, 26 | nodeType, 27 | undefined, 28 | cause 29 | ); 30 | } 31 | 32 | /** 33 | * Create error for node not found 34 | */ 35 | static nodeNotFound(nodeType: string): ValidationServiceError { 36 | return new ValidationServiceError( 37 | `Node type ${nodeType} not found in repository`, 38 | nodeType 39 | ); 40 | } 41 | 42 | /** 43 | * Create error for critical data extraction failure 44 | */ 45 | static dataExtractionError(nodeType: string, dataType: string, cause?: Error): ValidationServiceError { 46 | return new ValidationServiceError( 47 | `Failed to extract ${dataType} for node ${nodeType}`, 48 | nodeType, 49 | dataType, 50 | cause 51 | ); 52 | } 53 | } ``` -------------------------------------------------------------------------------- /scripts/build-optimized.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | # Optimized Docker build script - no n8n dependencies! 3 | 4 | set -e 5 | 6 | # Enable BuildKit 7 | export DOCKER_BUILDKIT=1 8 | export COMPOSE_DOCKER_CLI_BUILD=1 9 | 10 | echo "🚀 Building n8n-mcp (runtime-only, no n8n deps)..." 11 | echo "💡 This build assumes database is pre-built" 12 | 13 | # Check if nodes.db exists 14 | if [ ! -f "data/nodes.db" ]; then 15 | echo "⚠️ Warning: data/nodes.db not found!" 16 | echo " Run 'npm run rebuild' first to create the database" 17 | exit 1 18 | fi 19 | 20 | # Build with BuildKit 21 | echo "📦 Building Docker image..." 22 | 23 | docker build \ 24 | --progress=plain \ 25 | --cache-from type=gha \ 26 | --cache-from type=registry,ref=ghcr.io/czlonkowski/n8n-mcp:buildcache \ 27 | --build-arg BUILDKIT_INLINE_CACHE=1 \ 28 | -t "n8n-mcp:latest" \ 29 | . 30 | 31 | # Show image size 32 | echo "" 33 | echo "📊 Image size:" 34 | docker images n8n-mcp:latest --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" 35 | 36 | # Test the build 37 | echo "" 38 | echo "🧪 Testing build..." 39 | docker run --rm n8n-mcp:latest node -e "console.log('Build OK - Runtime dependencies only!')" 40 | 41 | # Estimate size savings 42 | echo "" 43 | echo "💰 Size comparison:" 44 | echo " Old approach (with n8n deps): ~1.5GB" 45 | echo " New approach (runtime only): ~280MB" 46 | echo " Savings: ~82% smaller!" 47 | 48 | echo "" 49 | echo "✅ Build complete!" 50 | echo "" 51 | echo "🎯 Next steps:" 52 | echo " - Use 'docker run -p 3000:3000 -e AUTH_TOKEN=your-token n8n-mcp:latest' to run" 53 | echo " - Use 'docker-compose up' for production deployment" 54 | echo " - Remember to rebuild database locally before pushing!" ``` -------------------------------------------------------------------------------- /scripts/test-http-search.ts: -------------------------------------------------------------------------------- ```typescript 1 | #\!/usr/bin/env node 2 | 3 | import { N8NDocumentationMCPServer } from '../src/mcp/server'; 4 | 5 | async function testHttpSearch() { 6 | const server = new N8NDocumentationMCPServer(); 7 | await new Promise(resolve => setTimeout(resolve, 1000)); 8 | 9 | console.log('Testing search for "http"...\n'); 10 | 11 | const result = await server.executeTool('search_nodes', { 12 | query: 'http', 13 | limit: 50 // Get more results to see where HTTP Request is 14 | }); 15 | 16 | console.log(`Total results: ${result.results.length}\n`); 17 | 18 | // Find HTTP Request node in results 19 | const httpRequestIndex = result.results.findIndex((r: any) => 20 | r.nodeType === 'nodes-base.httpRequest' 21 | ); 22 | 23 | if (httpRequestIndex === -1) { 24 | console.log('❌ HTTP Request node NOT FOUND in results\!'); 25 | } else { 26 | console.log(`✅ HTTP Request found at position ${httpRequestIndex + 1}`); 27 | } 28 | 29 | console.log('\nTop 10 results:'); 30 | result.results.slice(0, 10).forEach((r: any, i: number) => { 31 | console.log(`${i + 1}. ${r.nodeType} - ${r.displayName}`); 32 | }); 33 | 34 | // Also check LIKE search directly 35 | console.log('\n\nTesting LIKE search fallback:'); 36 | const serverAny = server as any; 37 | const likeResult = await serverAny.searchNodesLIKE('http', 20); 38 | 39 | console.log(`LIKE search found ${likeResult.results.length} results`); 40 | console.log('Top 5 LIKE results:'); 41 | likeResult.results.slice(0, 5).forEach((r: any, i: number) => { 42 | console.log(`${i + 1}. ${r.nodeType} - ${r.displayName}`); 43 | }); 44 | } 45 | 46 | testHttpSearch().catch(console.error); 47 | ``` -------------------------------------------------------------------------------- /docker-compose.extract.yml: -------------------------------------------------------------------------------- ```yaml 1 | version: '3.8' 2 | 3 | services: 4 | # Latest n8n container for node extraction 5 | n8n-latest: 6 | image: n8nio/n8n:latest 7 | container_name: n8n-latest-extractor 8 | environment: 9 | - N8N_BASIC_AUTH_ACTIVE=false 10 | - N8N_PORT=5678 11 | - N8N_ENCRYPTION_KEY=dummy-key-for-extraction 12 | volumes: 13 | # Mount n8n's node_modules for extraction 14 | - n8n_modules:/usr/local/lib/node_modules/n8n/node_modules 15 | # Provide writable directory for n8n config 16 | - n8n_config:/home/node/.n8n 17 | # We don't need n8n to stay running, just to install modules 18 | entrypoint: ["sh", "-c", "sleep 300"] 19 | healthcheck: 20 | test: ["CMD-SHELL", "ls /usr/local/lib/node_modules/n8n/node_modules/n8n-nodes-base > /dev/null 2>&1"] 21 | interval: 5s 22 | timeout: 30s 23 | retries: 20 24 | 25 | # Extractor service that will read from the mounted volumes 26 | node-extractor: 27 | image: node:22-alpine 28 | container_name: n8n-node-extractor 29 | working_dir: /app 30 | depends_on: 31 | n8n-latest: 32 | condition: service_healthy 33 | volumes: 34 | # Mount the n8n modules from the n8n container 35 | - n8n_modules:/n8n-modules:ro 36 | - n8n_custom:/n8n-custom:ro 37 | # Mount our project files 38 | - ./:/app 39 | environment: 40 | - NODE_ENV=development 41 | - NODE_DB_PATH=/app/data/nodes-fresh.db 42 | - N8N_MODULES_PATH=/n8n-modules 43 | - N8N_CUSTOM_PATH=/n8n-custom 44 | command: /bin/sh -c "apk add --no-cache sqlite && node /app/scripts/extract-from-docker.js" 45 | 46 | volumes: 47 | n8n_modules: 48 | n8n_custom: 49 | n8n_config: ``` -------------------------------------------------------------------------------- /scripts/test-workflow-insert.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env npx tsx 2 | /** 3 | * Test direct workflow insert to Supabase 4 | */ 5 | 6 | import { createClient } from '@supabase/supabase-js'; 7 | 8 | const TELEMETRY_BACKEND = { 9 | URL: 'https://ydyufsohxdfpopqbubwk.supabase.co', 10 | ANON_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlkeXVmc29oeGRmcG9wcWJ1YndrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTg3OTYyMDAsImV4cCI6MjA3NDM3MjIwMH0.xESphg6h5ozaDsm4Vla3QnDJGc6Nc_cpfoqTHRynkCk' 11 | }; 12 | 13 | async function testWorkflowInsert() { 14 | const supabase = createClient(TELEMETRY_BACKEND.URL, TELEMETRY_BACKEND.ANON_KEY, { 15 | auth: { 16 | persistSession: false, 17 | autoRefreshToken: false, 18 | } 19 | }); 20 | 21 | const testWorkflow = { 22 | user_id: 'direct-test-' + Date.now(), 23 | workflow_hash: 'hash-direct-' + Date.now(), 24 | node_count: 2, 25 | node_types: ['webhook', 'http'], 26 | has_trigger: true, 27 | has_webhook: true, 28 | complexity: 'simple' as const, 29 | sanitized_workflow: { 30 | nodes: [ 31 | { id: '1', type: 'webhook', parameters: {} }, 32 | { id: '2', type: 'http', parameters: {} } 33 | ], 34 | connections: {} 35 | } 36 | }; 37 | 38 | console.log('Attempting direct insert to telemetry_workflows...'); 39 | console.log('Data:', JSON.stringify(testWorkflow, null, 2)); 40 | 41 | const { data, error } = await supabase 42 | .from('telemetry_workflows') 43 | .insert([testWorkflow]); 44 | 45 | if (error) { 46 | console.error('\n❌ Error:', error); 47 | } else { 48 | console.log('\n✅ Success! Workflow inserted'); 49 | if (data) { 50 | console.log('Response:', data); 51 | } 52 | } 53 | } 54 | 55 | testWorkflowInsert().catch(console.error); ``` -------------------------------------------------------------------------------- /.github/ABOUT.md: -------------------------------------------------------------------------------- ```markdown 1 | # About n8n-MCP 2 | 3 | **n8n-MCP** is a Model Context Protocol (MCP) server that gives AI assistants like Claude deep understanding of n8n's 525+ workflow automation nodes. 4 | 5 | ## 🎯 What it does 6 | 7 | - Provides AI assistants with instant access to n8n node documentation, properties, and examples 8 | - Reduces workflow creation time from 45 minutes to 3 minutes (as tested by Claude) 9 | - Eliminates guesswork with accurate node configurations and validation 10 | - Enables AI to build production-ready n8n workflows on the first try 11 | 12 | ## 🚀 Key Features 13 | 14 | - **Smart Search** - Find the right nodes instantly 15 | - **Essential Properties** - Get only what matters (10-20 properties instead of 200+) 16 | - **Task Templates** - Pre-configured settings for common automations 17 | - **Real-time Validation** - Catch errors before deployment 18 | - **Universal Compatibility** - Works with any Node.js version 19 | 20 | ## 📊 Impact 21 | 22 | > "Before MCP, I was translating. Now I'm composing." - Claude 23 | 24 | - **6 errors → 0 errors** in workflow creation 25 | - **45 minutes → 3 minutes** development time 26 | - **100% node coverage** with 90% documentation 27 | - **263 AI-capable nodes** fully documented 28 | 29 | ## 🔧 Use Cases 30 | 31 | Perfect for: 32 | - AI assistants building n8n workflows 33 | - Developers learning n8n 34 | - Teams using AI for automation 35 | - Anyone tired of trial-and-error workflow building 36 | 37 | ## 🏃 Get Started 38 | 39 | ```bash 40 | # Quick start with Docker 41 | docker run -it ghcr.io/czlonkowski/n8n-mcp:latest 42 | ``` 43 | 44 | See the [README](../README.md) for full setup instructions. 45 | 46 | --- 47 | 48 | **Built with ❤️ for the n8n community** | Making AI + n8n workflow creation delightful ``` -------------------------------------------------------------------------------- /tests/integration/docker/test-helpers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { promisify } from 'util'; 2 | import { exec as execCallback } from 'child_process'; 3 | 4 | export const exec = promisify(execCallback); 5 | 6 | /** 7 | * Wait for a container to be healthy by checking the health endpoint 8 | */ 9 | export async function waitForHealthy(containerName: string, timeout = 10000): Promise<boolean> { 10 | const startTime = Date.now(); 11 | 12 | while (Date.now() - startTime < timeout) { 13 | try { 14 | const { stdout } = await exec( 15 | `docker exec ${containerName} curl -s http://localhost:3000/health` 16 | ); 17 | 18 | if (stdout.includes('ok')) { 19 | return true; 20 | } 21 | } catch (error) { 22 | // Container might not be ready yet 23 | } 24 | 25 | await new Promise(resolve => setTimeout(resolve, 500)); 26 | } 27 | 28 | return false; 29 | } 30 | 31 | /** 32 | * Check if a container is running in HTTP mode by verifying the server is listening 33 | */ 34 | export async function isRunningInHttpMode(containerName: string): Promise<boolean> { 35 | try { 36 | const { stdout } = await exec( 37 | `docker exec ${containerName} sh -c "netstat -tln 2>/dev/null | grep :3000 || echo 'Not listening'"` 38 | ); 39 | 40 | return stdout.includes(':3000'); 41 | } catch { 42 | return false; 43 | } 44 | } 45 | 46 | /** 47 | * Get process environment variables from inside a running container 48 | */ 49 | export async function getProcessEnv(containerName: string, varName: string): Promise<string | null> { 50 | try { 51 | const { stdout } = await exec( 52 | `docker exec ${containerName} sh -c "cat /proc/1/environ | tr '\\0' '\\n' | grep '^${varName}=' | cut -d= -f2-"` 53 | ); 54 | 55 | return stdout.trim() || null; 56 | } catch { 57 | return null; 58 | } 59 | } ``` -------------------------------------------------------------------------------- /tests/debug-slack-doc.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | const { execSync } = require('child_process'); 4 | const path = require('path'); 5 | 6 | const tempDir = path.join(process.cwd(), 'temp', 'n8n-docs'); 7 | 8 | console.log('🔍 Debugging Slack documentation search...\n'); 9 | 10 | // Search for all Slack related files 11 | console.log('All Slack-related markdown files:'); 12 | try { 13 | const allSlackFiles = execSync( 14 | `find ${tempDir}/docs/integrations/builtin -name "*slack*.md" -type f`, 15 | { encoding: 'utf-8' } 16 | ).trim().split('\n'); 17 | 18 | allSlackFiles.forEach(file => { 19 | console.log(` - ${file}`); 20 | }); 21 | } catch (error) { 22 | console.log(' No files found'); 23 | } 24 | 25 | console.log('\n📄 Checking file paths:'); 26 | const possiblePaths = [ 27 | 'docs/integrations/builtin/app-nodes/n8n-nodes-base.Slack.md', 28 | 'docs/integrations/builtin/app-nodes/n8n-nodes-base.slack.md', 29 | 'docs/integrations/builtin/core-nodes/n8n-nodes-base.Slack.md', 30 | 'docs/integrations/builtin/core-nodes/n8n-nodes-base.slack.md', 31 | 'docs/integrations/builtin/trigger-nodes/n8n-nodes-base.Slack.md', 32 | 'docs/integrations/builtin/trigger-nodes/n8n-nodes-base.slack.md', 33 | 'docs/integrations/builtin/credentials/slack.md', 34 | ]; 35 | 36 | const fs = require('fs'); 37 | possiblePaths.forEach(p => { 38 | const fullPath = path.join(tempDir, p); 39 | const exists = fs.existsSync(fullPath); 40 | console.log(` ${exists ? '✓' : '✗'} ${p}`); 41 | 42 | if (exists) { 43 | // Read first few lines 44 | const content = fs.readFileSync(fullPath, 'utf-8'); 45 | const lines = content.split('\n').slice(0, 10); 46 | const title = lines.find(l => l.includes('title:')); 47 | if (title) { 48 | console.log(` Title: ${title.trim()}`); 49 | } 50 | } 51 | }); ``` -------------------------------------------------------------------------------- /scripts/analyze-optimization.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | # Analyze potential optimization savings 3 | 4 | echo "🔍 Analyzing Docker Optimization Potential" 5 | echo "==========================================" 6 | 7 | # Check current database size 8 | if [ -f data/nodes.db ]; then 9 | DB_SIZE=$(du -h data/nodes.db | cut -f1) 10 | echo "Current database size: $DB_SIZE" 11 | fi 12 | 13 | # Check node_modules size 14 | if [ -d node_modules ]; then 15 | echo -e "\n📦 Package sizes:" 16 | echo "Total node_modules: $(du -sh node_modules | cut -f1)" 17 | echo "n8n packages:" 18 | for pkg in n8n n8n-core n8n-workflow @n8n/n8n-nodes-langchain; do 19 | if [ -d "node_modules/$pkg" ]; then 20 | SIZE=$(du -sh "node_modules/$pkg" 2>/dev/null | cut -f1 || echo "N/A") 21 | echo " - $pkg: $SIZE" 22 | fi 23 | done 24 | fi 25 | 26 | # Check runtime dependencies 27 | echo -e "\n🎯 Runtime-only dependencies:" 28 | RUNTIME_DEPS="@modelcontextprotocol/sdk better-sqlite3 sql.js express dotenv" 29 | RUNTIME_SIZE=0 30 | for dep in $RUNTIME_DEPS; do 31 | if [ -d "node_modules/$dep" ]; then 32 | SIZE=$(du -sh "node_modules/$dep" 2>/dev/null | cut -f1 || echo "0") 33 | echo " - $dep: $SIZE" 34 | fi 35 | done 36 | 37 | # Estimate savings 38 | echo -e "\n💡 Optimization potential:" 39 | echo "- Current image: 2.61GB" 40 | echo "- Estimated optimized: ~200MB" 41 | echo "- Savings: ~92%" 42 | 43 | # Show what would be removed 44 | echo -e "\n🗑️ Would remove in optimization:" 45 | echo "- n8n packages (>2GB)" 46 | echo "- Build dependencies" 47 | echo "- Documentation files" 48 | echo "- Test files" 49 | echo "- Source maps" 50 | 51 | # Check if optimized database exists 52 | if [ -f data/nodes-optimized.db ]; then 53 | OPT_SIZE=$(du -h data/nodes-optimized.db | cut -f1) 54 | echo -e "\n✅ Optimized database exists: $OPT_SIZE" 55 | fi ``` -------------------------------------------------------------------------------- /scripts/export-webhook-workflows.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env tsx 2 | 3 | /** 4 | * Export Webhook Workflow JSONs 5 | * 6 | * Generates the 4 webhook workflow JSON files needed for integration testing. 7 | * These workflows must be imported into n8n and activated manually. 8 | */ 9 | 10 | import { writeFileSync, mkdirSync } from 'fs'; 11 | import { join } from 'path'; 12 | import { exportAllWebhookWorkflows } from '../tests/integration/n8n-api/utils/webhook-workflows'; 13 | 14 | const OUTPUT_DIR = join(process.cwd(), 'workflows-for-import'); 15 | 16 | // Create output directory 17 | mkdirSync(OUTPUT_DIR, { recursive: true }); 18 | 19 | // Generate all workflow JSONs 20 | const workflows = exportAllWebhookWorkflows(); 21 | 22 | // Write each workflow to a separate file 23 | Object.entries(workflows).forEach(([method, workflow]) => { 24 | const filename = `webhook-${method.toLowerCase()}.json`; 25 | const filepath = join(OUTPUT_DIR, filename); 26 | 27 | writeFileSync(filepath, JSON.stringify(workflow, null, 2), 'utf-8'); 28 | 29 | console.log(`✓ Generated: ${filename}`); 30 | }); 31 | 32 | console.log(`\n✓ All workflow JSONs written to: ${OUTPUT_DIR}`); 33 | console.log('\nNext steps:'); 34 | console.log('1. Import each JSON file into your n8n instance'); 35 | console.log('2. Activate each workflow in the n8n UI'); 36 | console.log('3. Copy the webhook URLs from each workflow (open workflow → Webhook node → copy URL)'); 37 | console.log('4. Add them to your .env file:'); 38 | console.log(' N8N_TEST_WEBHOOK_GET_URL=https://your-n8n.com/webhook/mcp-test-get'); 39 | console.log(' N8N_TEST_WEBHOOK_POST_URL=https://your-n8n.com/webhook/mcp-test-post'); 40 | console.log(' N8N_TEST_WEBHOOK_PUT_URL=https://your-n8n.com/webhook/mcp-test-put'); 41 | console.log(' N8N_TEST_WEBHOOK_DELETE_URL=https://your-n8n.com/webhook/mcp-test-delete'); 42 | ``` -------------------------------------------------------------------------------- /.github/workflows/docker-build-fast.yml: -------------------------------------------------------------------------------- ```yaml 1 | # .github/workflows/docker-build-fast.yml 2 | name: Build Docker (AMD64 only - Fast) 3 | 4 | on: 5 | workflow_dispatch: 6 | pull_request: 7 | branches: 8 | - main 9 | paths: 10 | - 'Dockerfile' 11 | - 'package.runtime.json' 12 | - '.github/workflows/docker-build-fast.yml' 13 | 14 | env: 15 | REGISTRY: ghcr.io 16 | IMAGE_NAME: ${{ github.repository }} 17 | 18 | jobs: 19 | build-amd64: 20 | name: Build Docker Image (AMD64 only) 21 | runs-on: ubuntu-latest 22 | permissions: 23 | contents: read 24 | packages: write 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | with: 30 | lfs: true 31 | 32 | - name: Set up Docker Buildx 33 | uses: docker/setup-buildx-action@v3 34 | 35 | - name: Log in to GitHub Container Registry 36 | if: github.event_name != 'pull_request' 37 | uses: docker/login-action@v3 38 | with: 39 | registry: ${{ env.REGISTRY }} 40 | username: ${{ github.actor }} 41 | password: ${{ secrets.GITHUB_TOKEN }} 42 | 43 | - name: Extract metadata 44 | id: meta 45 | uses: docker/metadata-action@v5 46 | with: 47 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 48 | tags: | 49 | type=ref,event=pr,suffix=-amd64 50 | type=raw,value=test-amd64 51 | 52 | - name: Build and push Docker image (AMD64 only) 53 | uses: docker/build-push-action@v5 54 | with: 55 | context: . 56 | platforms: linux/amd64 57 | push: ${{ github.event_name != 'pull_request' }} 58 | tags: ${{ steps.meta.outputs.tags }} 59 | labels: ${{ steps.meta.outputs.labels }} 60 | cache-from: type=gha 61 | cache-to: type=gha,mode=max 62 | provenance: false ``` -------------------------------------------------------------------------------- /scripts/test-telemetry-no-select.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env npx tsx 2 | /** 3 | * Test telemetry without requesting data back 4 | */ 5 | 6 | import { createClient } from '@supabase/supabase-js'; 7 | import dotenv from 'dotenv'; 8 | 9 | dotenv.config(); 10 | 11 | async function testNoSelect() { 12 | const supabaseUrl = process.env.SUPABASE_URL!; 13 | const supabaseAnonKey = process.env.SUPABASE_ANON_KEY!; 14 | 15 | console.log('🧪 Telemetry Test (No Select)\n'); 16 | 17 | const supabase = createClient(supabaseUrl, supabaseAnonKey, { 18 | auth: { 19 | persistSession: false, 20 | autoRefreshToken: false, 21 | } 22 | }); 23 | 24 | // Insert WITHOUT .select() - just fire and forget 25 | const testData = { 26 | user_id: 'test-' + Date.now(), 27 | event: 'test_event', 28 | properties: { test: true } 29 | }; 30 | 31 | console.log('Inserting:', testData); 32 | 33 | const { error } = await supabase 34 | .from('telemetry_events') 35 | .insert([testData]); // No .select() here! 36 | 37 | if (error) { 38 | console.error('❌ Failed:', error); 39 | } else { 40 | console.log('✅ Success! Data inserted (no response data)'); 41 | } 42 | 43 | // Test workflow insert too 44 | const testWorkflow = { 45 | user_id: 'test-' + Date.now(), 46 | workflow_hash: 'hash-' + Date.now(), 47 | node_count: 3, 48 | node_types: ['webhook', 'http', 'slack'], 49 | has_trigger: true, 50 | has_webhook: true, 51 | complexity: 'simple', 52 | sanitized_workflow: { nodes: [], connections: {} } 53 | }; 54 | 55 | console.log('\nInserting workflow:', testWorkflow); 56 | 57 | const { error: workflowError } = await supabase 58 | .from('telemetry_workflows') 59 | .insert([testWorkflow]); // No .select() here! 60 | 61 | if (workflowError) { 62 | console.error('❌ Workflow failed:', workflowError); 63 | } else { 64 | console.log('✅ Workflow inserted successfully!'); 65 | } 66 | } 67 | 68 | testNoSelect().catch(console.error); ``` -------------------------------------------------------------------------------- /scripts/test-single-session.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | # Test script for single-session HTTP server 3 | 4 | set -e 5 | 6 | echo "🧪 Testing Single-Session HTTP Server..." 7 | echo 8 | 9 | # Generate test auth token if not set 10 | if [ -z "$AUTH_TOKEN" ]; then 11 | export AUTH_TOKEN="test-token-$(date +%s)" 12 | echo "Generated test AUTH_TOKEN: $AUTH_TOKEN" 13 | fi 14 | 15 | # Start server in background 16 | echo "Starting server..." 17 | MCP_MODE=http npm start > server.log 2>&1 & 18 | SERVER_PID=$! 19 | 20 | # Wait for server to start 21 | echo "Waiting for server to start..." 22 | sleep 3 23 | 24 | # Check health endpoint 25 | echo 26 | echo "Testing health endpoint..." 27 | curl -s http://localhost:3000/health | jq . 28 | 29 | # Test authentication failure 30 | echo 31 | echo "Testing authentication failure..." 32 | curl -s -X POST http://localhost:3000/mcp \ 33 | -H "Content-Type: application/json" \ 34 | -H "Authorization: Bearer wrong-token" \ 35 | -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' | jq . 36 | 37 | # Test successful request 38 | echo 39 | echo "Testing successful request..." 40 | curl -s -X POST http://localhost:3000/mcp \ 41 | -H "Content-Type: application/json" \ 42 | -H "Authorization: Bearer $AUTH_TOKEN" \ 43 | -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' | jq . 44 | 45 | # Test session reuse 46 | echo 47 | echo "Testing session reuse (second request)..." 48 | curl -s -X POST http://localhost:3000/mcp \ 49 | -H "Content-Type: application/json" \ 50 | -H "Authorization: Bearer $AUTH_TOKEN" \ 51 | -d '{"jsonrpc":"2.0","method":"get_database_statistics","id":2}' | jq . 52 | 53 | # Check health again to see session info 54 | echo 55 | echo "Checking health to see session info..." 56 | curl -s http://localhost:3000/health | jq . 57 | 58 | # Clean up 59 | echo 60 | echo "Stopping server..." 61 | kill $SERVER_PID 2>/dev/null || true 62 | wait $SERVER_PID 2>/dev/null || true 63 | 64 | echo 65 | echo "✅ Test complete! Check server.log for details." ``` -------------------------------------------------------------------------------- /tests/integration/n8n-api/utils/n8n-client.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Pre-configured n8n API Client for Integration Tests 3 | * 4 | * Provides a singleton API client instance configured with test credentials. 5 | * Automatically loads credentials from .env (local) or GitHub secrets (CI). 6 | */ 7 | 8 | import { N8nApiClient } from '../../../../src/services/n8n-api-client'; 9 | import { getN8nCredentials, validateCredentials } from './credentials'; 10 | 11 | let client: N8nApiClient | null = null; 12 | 13 | /** 14 | * Get or create the test n8n API client 15 | * 16 | * Creates a singleton instance configured with credentials from 17 | * the environment. Validates that required credentials are present. 18 | * 19 | * @returns Configured N8nApiClient instance 20 | * @throws Error if credentials are missing or invalid 21 | * 22 | * @example 23 | * const client = getTestN8nClient(); 24 | * const workflow = await client.createWorkflow({ ... }); 25 | */ 26 | export function getTestN8nClient(): N8nApiClient { 27 | if (!client) { 28 | const creds = getN8nCredentials(); 29 | validateCredentials(creds); 30 | client = new N8nApiClient({ 31 | baseUrl: creds.url, 32 | apiKey: creds.apiKey, 33 | timeout: 30000, 34 | maxRetries: 3 35 | }); 36 | } 37 | return client; 38 | } 39 | 40 | /** 41 | * Reset the test client instance 42 | * 43 | * Forces recreation of the client on next call to getTestN8nClient(). 44 | * Useful for testing or when credentials change. 45 | */ 46 | export function resetTestN8nClient(): void { 47 | client = null; 48 | } 49 | 50 | /** 51 | * Check if the n8n API is accessible 52 | * 53 | * Performs a health check to verify API connectivity. 54 | * 55 | * @returns true if API is accessible, false otherwise 56 | */ 57 | export async function isN8nApiAccessible(): Promise<boolean> { 58 | try { 59 | const client = getTestN8nClient(); 60 | await client.healthCheck(); 61 | return true; 62 | } catch { 63 | return false; 64 | } 65 | } 66 | ``` -------------------------------------------------------------------------------- /tests/factories/node-factory.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Factory } from 'fishery'; 2 | import { faker } from '@faker-js/faker'; 3 | import { ParsedNode } from '../../src/parsers/node-parser'; 4 | 5 | /** 6 | * Factory for generating ParsedNode test data using Fishery. 7 | * Creates realistic node configurations with random but valid data. 8 | * 9 | * @example 10 | * ```typescript 11 | * // Create a single node with defaults 12 | * const node = NodeFactory.build(); 13 | * 14 | * // Create a node with specific properties 15 | * const slackNode = NodeFactory.build({ 16 | * nodeType: 'nodes-base.slack', 17 | * displayName: 'Slack', 18 | * isAITool: true 19 | * }); 20 | * 21 | * // Create multiple nodes 22 | * const nodes = NodeFactory.buildList(5); 23 | * 24 | * // Create with custom sequence 25 | * const sequencedNodes = NodeFactory.buildList(3, { 26 | * displayName: (i) => `Node ${i}` 27 | * }); 28 | * ``` 29 | */ 30 | export const NodeFactory = Factory.define<ParsedNode>(() => ({ 31 | nodeType: faker.helpers.arrayElement(['nodes-base.', 'nodes-langchain.']) + faker.word.noun(), 32 | displayName: faker.helpers.arrayElement(['HTTP', 'Slack', 'Google', 'AWS']) + ' ' + faker.word.noun(), 33 | description: faker.lorem.sentence(), 34 | packageName: faker.helpers.arrayElement(['n8n-nodes-base', '@n8n/n8n-nodes-langchain']), 35 | category: faker.helpers.arrayElement(['transform', 'trigger', 'output', 'input']), 36 | style: faker.helpers.arrayElement(['declarative', 'programmatic']), 37 | isAITool: faker.datatype.boolean(), 38 | isTrigger: faker.datatype.boolean(), 39 | isWebhook: faker.datatype.boolean(), 40 | isVersioned: faker.datatype.boolean(), 41 | version: faker.helpers.arrayElement(['1.0', '2.0', '3.0', '4.2']), 42 | documentation: faker.datatype.boolean() ? faker.lorem.paragraphs(3) : undefined, 43 | properties: [], 44 | operations: [], 45 | credentials: [] 46 | })); ``` -------------------------------------------------------------------------------- /docs/WINDSURF_SETUP.md: -------------------------------------------------------------------------------- ```markdown 1 | # Windsurf Setup 2 | 3 | Connect n8n-MCP to Windsurf IDE for enhanced n8n workflow development with AI assistance. 4 | 5 | [](https://www.youtube.com/watch?v=klxxT1__izg) 6 | 7 | ## Video Tutorial 8 | 9 | Watch the complete setup process: [n8n-MCP Windsurf Setup Tutorial](https://www.youtube.com/watch?v=klxxT1__izg) 10 | 11 | ## Setup Process 12 | 13 | ### 1. Access MCP Configuration 14 | 15 | 1. Go to Settings in Windsurf 16 | 2. Navigate to Windsurf Settings 17 | 3. Go to MCP Servers > Manage Plugins 18 | 4. Click "View Raw Config" 19 | 20 | ### 2. Add n8n-MCP Configuration 21 | 22 | Copy the configuration from this repository and add it to your MCP config: 23 | 24 | **Basic configuration (documentation tools only):** 25 | ```json 26 | { 27 | "mcpServers": { 28 | "n8n-mcp": { 29 | "command": "npx", 30 | "args": ["n8n-mcp"], 31 | "env": { 32 | "MCP_MODE": "stdio", 33 | "LOG_LEVEL": "error", 34 | "DISABLE_CONSOLE_OUTPUT": "true" 35 | } 36 | } 37 | } 38 | } 39 | ``` 40 | 41 | **Full configuration (with n8n management tools):** 42 | ```json 43 | { 44 | "mcpServers": { 45 | "n8n-mcp": { 46 | "command": "npx", 47 | "args": ["n8n-mcp"], 48 | "env": { 49 | "MCP_MODE": "stdio", 50 | "LOG_LEVEL": "error", 51 | "DISABLE_CONSOLE_OUTPUT": "true", 52 | "N8N_API_URL": "https://your-n8n-instance.com", 53 | "N8N_API_KEY": "your-api-key" 54 | } 55 | } 56 | } 57 | } 58 | ``` 59 | 60 | ### 3. Configure n8n Connection 61 | 62 | 1. Replace `https://your-n8n-instance.com` with your actual n8n URL 63 | 2. Replace `your-api-key` with your n8n API key 64 | 3. Click refresh to apply the changes 65 | 66 | ### 4. Set Up Project Instructions 67 | 68 | 1. Create a `.windsurfrules` file in your project root 69 | 2. Copy the Claude Project instructions from the [main README's Claude Project Setup section](../README.md#-claude-project-setup) 70 | ``` -------------------------------------------------------------------------------- /scripts/test-workflow-tracking-debug.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env npx tsx 2 | /** 3 | * Debug workflow tracking in telemetry manager 4 | */ 5 | 6 | import { TelemetryManager } from '../src/telemetry/telemetry-manager'; 7 | 8 | // Get the singleton instance 9 | const telemetry = TelemetryManager.getInstance(); 10 | 11 | const testWorkflow = { 12 | nodes: [ 13 | { 14 | id: 'webhook1', 15 | type: 'n8n-nodes-base.webhook', 16 | name: 'Webhook', 17 | position: [0, 0], 18 | parameters: { 19 | path: '/test-' + Date.now(), 20 | httpMethod: 'POST' 21 | } 22 | }, 23 | { 24 | id: 'http1', 25 | type: 'n8n-nodes-base.httpRequest', 26 | name: 'HTTP Request', 27 | position: [250, 0], 28 | parameters: { 29 | url: 'https://api.example.com/data', 30 | method: 'GET' 31 | } 32 | }, 33 | { 34 | id: 'slack1', 35 | type: 'n8n-nodes-base.slack', 36 | name: 'Slack', 37 | position: [500, 0], 38 | parameters: { 39 | channel: '#general', 40 | text: 'Workflow complete!' 41 | } 42 | } 43 | ], 44 | connections: { 45 | 'webhook1': { 46 | main: [[{ node: 'http1', type: 'main', index: 0 }]] 47 | }, 48 | 'http1': { 49 | main: [[{ node: 'slack1', type: 'main', index: 0 }]] 50 | } 51 | } 52 | }; 53 | 54 | console.log('🧪 Testing Workflow Tracking\n'); 55 | console.log('Workflow has', testWorkflow.nodes.length, 'nodes'); 56 | 57 | // Track the workflow 58 | console.log('Calling trackWorkflowCreation...'); 59 | telemetry.trackWorkflowCreation(testWorkflow, true); 60 | 61 | console.log('Waiting for async processing...'); 62 | 63 | // Wait for setImmediate to process 64 | setTimeout(async () => { 65 | console.log('\nForcing flush...'); 66 | await telemetry.flush(); 67 | console.log('✅ Flush complete!'); 68 | 69 | console.log('\nWorkflow should now be in the telemetry_workflows table.'); 70 | console.log('Check with: SELECT * FROM telemetry_workflows ORDER BY created_at DESC LIMIT 1;'); 71 | }, 2000); 72 | ``` -------------------------------------------------------------------------------- /scripts/test-workflow-sanitizer.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env npx tsx 2 | /** 3 | * Test workflow sanitizer 4 | */ 5 | 6 | import { WorkflowSanitizer } from '../src/telemetry/workflow-sanitizer'; 7 | 8 | const testWorkflow = { 9 | nodes: [ 10 | { 11 | id: 'webhook1', 12 | type: 'n8n-nodes-base.webhook', 13 | name: 'Webhook', 14 | position: [0, 0], 15 | parameters: { 16 | path: '/test-webhook', 17 | httpMethod: 'POST' 18 | } 19 | }, 20 | { 21 | id: 'http1', 22 | type: 'n8n-nodes-base.httpRequest', 23 | name: 'HTTP Request', 24 | position: [250, 0], 25 | parameters: { 26 | url: 'https://api.example.com/endpoint', 27 | method: 'GET', 28 | authentication: 'genericCredentialType', 29 | sendHeaders: true, 30 | headerParameters: { 31 | parameters: [ 32 | { 33 | name: 'Authorization', 34 | value: 'Bearer sk-1234567890abcdef' 35 | } 36 | ] 37 | } 38 | } 39 | } 40 | ], 41 | connections: { 42 | 'webhook1': { 43 | main: [[{ node: 'http1', type: 'main', index: 0 }]] 44 | } 45 | } 46 | }; 47 | 48 | console.log('🧪 Testing Workflow Sanitizer\n'); 49 | console.log('Original workflow has', testWorkflow.nodes.length, 'nodes'); 50 | 51 | try { 52 | const sanitized = WorkflowSanitizer.sanitizeWorkflow(testWorkflow); 53 | 54 | console.log('\n✅ Sanitization successful!'); 55 | console.log('\nSanitized output:'); 56 | console.log(JSON.stringify(sanitized, null, 2)); 57 | 58 | console.log('\n📊 Metrics:'); 59 | console.log('- Workflow Hash:', sanitized.workflowHash); 60 | console.log('- Node Count:', sanitized.nodeCount); 61 | console.log('- Node Types:', sanitized.nodeTypes); 62 | console.log('- Has Trigger:', sanitized.hasTrigger); 63 | console.log('- Has Webhook:', sanitized.hasWebhook); 64 | console.log('- Complexity:', sanitized.complexity); 65 | } catch (error) { 66 | console.error('❌ Sanitization failed:', error); 67 | } 68 | ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/templates/list-tasks.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolDocumentation } from '../types'; 2 | 3 | export const listTasksDoc: ToolDocumentation = { 4 | name: 'list_tasks', 5 | category: 'templates', 6 | essentials: { 7 | description: 'List task templates by category: HTTP/API, Webhooks, Database, AI, Data Processing, Communication.', 8 | keyParameters: ['category'], 9 | example: 'list_tasks({category: "HTTP/API"})', 10 | performance: 'Instant', 11 | tips: [ 12 | 'Categories: HTTP/API, Webhooks, Database, AI', 13 | 'Shows pre-configured node settings', 14 | 'Use get_node_for_task for details' 15 | ] 16 | }, 17 | full: { 18 | description: 'Lists available task templates organized by category. Each task represents a common automation pattern with pre-configured node settings. Categories include HTTP/API, Webhooks, Database, AI, Data Processing, and Communication.', 19 | parameters: { 20 | category: { type: 'string', description: 'Filter by category (optional)' } 21 | }, 22 | returns: 'Array of tasks with name, category, description, nodeType', 23 | examples: [ 24 | 'list_tasks() - Get all task templates', 25 | 'list_tasks({category: "Database"}) - Database-related tasks', 26 | 'list_tasks({category: "AI"}) - AI automation tasks' 27 | ], 28 | useCases: [ 29 | 'Discover common automation patterns', 30 | 'Find pre-configured solutions', 31 | 'Learn node usage patterns', 32 | 'Quick workflow setup' 33 | ], 34 | performance: 'Instant - Static task list', 35 | bestPractices: [ 36 | 'Browse all categories first', 37 | 'Use get_node_for_task for config', 38 | 'Combine multiple tasks in workflows' 39 | ], 40 | pitfalls: [ 41 | 'Tasks are templates, customize as needed', 42 | 'Not all nodes have task templates' 43 | ], 44 | relatedTools: ['get_node_for_task', 'search_templates', 'get_templates_for_task'] 45 | } 46 | }; ``` -------------------------------------------------------------------------------- /scripts/test-node-info.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | /** 3 | * Test get_node_info to diagnose timeout issues 4 | */ 5 | 6 | const { N8NDocumentationMCPServer } = require('../dist/mcp/server'); 7 | 8 | async function testNodeInfo() { 9 | console.log('🔍 Testing get_node_info...\n'); 10 | 11 | try { 12 | const server = new N8NDocumentationMCPServer(); 13 | await new Promise(resolve => setTimeout(resolve, 500)); 14 | 15 | const nodes = [ 16 | 'nodes-base.httpRequest', 17 | 'nodes-base.webhook', 18 | 'nodes-langchain.agent' 19 | ]; 20 | 21 | for (const nodeType of nodes) { 22 | console.log(`Testing ${nodeType}...`); 23 | const start = Date.now(); 24 | 25 | try { 26 | const result = await server.executeTool('get_node_info', { nodeType }); 27 | const elapsed = Date.now() - start; 28 | const size = JSON.stringify(result).length; 29 | 30 | console.log(`✅ Success in ${elapsed}ms`); 31 | console.log(` Size: ${(size / 1024).toFixed(1)}KB`); 32 | console.log(` Properties: ${result.properties?.length || 0}`); 33 | console.log(` Operations: ${result.operations?.length || 0}`); 34 | 35 | // Check for issues 36 | if (size > 50000) { 37 | console.log(` ⚠️ WARNING: Response over 50KB!`); 38 | } 39 | 40 | // Check property quality 41 | const propsWithoutDesc = result.properties?.filter(p => !p.description && !p.displayName).length || 0; 42 | if (propsWithoutDesc > 0) { 43 | console.log(` ⚠️ ${propsWithoutDesc} properties without descriptions`); 44 | } 45 | 46 | } catch (error) { 47 | const elapsed = Date.now() - start; 48 | console.log(`❌ Failed after ${elapsed}ms: ${error.message}`); 49 | } 50 | 51 | console.log(''); 52 | } 53 | 54 | } catch (error) { 55 | console.error('Fatal error:', error); 56 | } 57 | } 58 | 59 | testNodeInfo().catch(console.error); ``` -------------------------------------------------------------------------------- /docs/CURSOR_SETUP.md: -------------------------------------------------------------------------------- ```markdown 1 | # Cursor Setup 2 | 3 | Connect n8n-MCP to Cursor IDE for enhanced n8n workflow development with AI assistance. 4 | 5 | [](https://www.youtube.com/watch?v=hRmVxzLGJWI) 6 | 7 | ## Video Tutorial 8 | 9 | Watch the complete setup process: [n8n-MCP Cursor Setup Tutorial](https://www.youtube.com/watch?v=hRmVxzLGJWI) 10 | 11 | ## Setup Process 12 | 13 | ### 1. Create MCP Configuration 14 | 15 | 1. Create a `.cursor` folder in your project root 16 | 2. Create `mcp.json` file inside the `.cursor` folder 17 | 3. Copy the configuration from this repository 18 | 19 | **Basic configuration (documentation tools only):** 20 | ```json 21 | { 22 | "mcpServers": { 23 | "n8n-mcp": { 24 | "command": "npx", 25 | "args": ["n8n-mcp"], 26 | "env": { 27 | "MCP_MODE": "stdio", 28 | "LOG_LEVEL": "error", 29 | "DISABLE_CONSOLE_OUTPUT": "true" 30 | } 31 | } 32 | } 33 | } 34 | ``` 35 | 36 | **Full configuration (with n8n management tools):** 37 | ```json 38 | { 39 | "mcpServers": { 40 | "n8n-mcp": { 41 | "command": "npx", 42 | "args": ["n8n-mcp"], 43 | "env": { 44 | "MCP_MODE": "stdio", 45 | "LOG_LEVEL": "error", 46 | "DISABLE_CONSOLE_OUTPUT": "true", 47 | "N8N_API_URL": "https://your-n8n-instance.com", 48 | "N8N_API_KEY": "your-api-key" 49 | } 50 | } 51 | } 52 | } 53 | ``` 54 | 55 | ### 2. Configure n8n Connection 56 | 57 | 1. Replace `https://your-n8n-instance.com` with your actual n8n URL 58 | 2. Replace `your-api-key` with your n8n API key 59 | 60 | ### 3. Enable MCP Server 61 | 62 | 1. Click "Enable MCP Server" button in Cursor 63 | 2. Go to Cursor Settings 64 | 3. Search for "mcp" 65 | 4. Confirm MCP is working 66 | 67 | ### 4. Set Up Project Instructions 68 | 69 | 1. In your Cursor chat, invoke "create rule" and hit Tab 70 | 2. Name the rule (e.g., "n8n-mcp") 71 | 3. Set rule type to "always" 72 | 4. Copy the Claude Project instructions from the [main README's Claude Project Setup section](../README.md#-claude-project-setup) 73 | 74 | ``` -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "schedule": ["after 9am on monday"], 7 | "timezone": "UTC", 8 | "packageRules": [ 9 | { 10 | "description": "Group all n8n-related updates", 11 | "groupName": "n8n dependencies", 12 | "matchPackagePatterns": ["^n8n", "^@n8n/"], 13 | "matchUpdateTypes": ["minor", "patch"], 14 | "schedule": ["after 9am on monday"] 15 | }, 16 | { 17 | "description": "Require approval for major n8n updates", 18 | "matchPackagePatterns": ["^n8n", "^@n8n/"], 19 | "matchUpdateTypes": ["major"], 20 | "dependencyDashboardApproval": true 21 | }, 22 | { 23 | "description": "Disable updates for other dependencies", 24 | "excludePackagePatterns": ["^n8n", "^@n8n/"], 25 | "enabled": false 26 | } 27 | ], 28 | "postUpdateOptions": [ 29 | "npmDedupe" 30 | ], 31 | "prConcurrentLimit": 1, 32 | "prCreation": "immediate", 33 | "labels": ["dependencies", "n8n-update"], 34 | "assignees": ["@czlonkowski"], 35 | "reviewers": ["@czlonkowski"], 36 | "commitMessagePrefix": "chore: ", 37 | "commitMessageTopic": "{{depName}}", 38 | "commitMessageExtra": "from {{currentVersion}} to {{newVersion}}", 39 | "prBodyDefinitions": { 40 | "Package": "{{depName}}", 41 | "Type": "{{depType}}", 42 | "Update": "{{updateType}}", 43 | "Current": "{{currentVersion}}", 44 | "New": "{{newVersion}}", 45 | "Change": "[Compare]({{compareUrl}})" 46 | }, 47 | "prBodyColumns": ["Package", "Type", "Update", "Current", "New", "Change"], 48 | "prBodyNotes": [ 49 | "**Important**: Please review the [n8n release notes](https://docs.n8n.io/release-notes/) for breaking changes.", 50 | "", 51 | "After merging, please:", 52 | "1. Run `npm run rebuild` to update the node database", 53 | "2. Run `npm run validate` to ensure all nodes are properly loaded", 54 | "3. Test critical functionality" 55 | ] 56 | } ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/configuration/get-node-documentation.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolDocumentation } from '../types'; 2 | 3 | export const getNodeDocumentationDoc: ToolDocumentation = { 4 | name: 'get_node_documentation', 5 | category: 'configuration', 6 | essentials: { 7 | description: 'Get readable docs with examples/auth/patterns. Better than raw schema! 87% coverage. Format: "nodes-base.slack"', 8 | keyParameters: ['nodeType'], 9 | example: 'get_node_documentation({nodeType: "nodes-base.slack"})', 10 | performance: 'Fast - pre-parsed', 11 | tips: [ 12 | '87% coverage', 13 | 'Includes auth examples', 14 | 'Human-readable format' 15 | ] 16 | }, 17 | full: { 18 | description: 'Returns human-readable documentation parsed from n8n-docs including examples, authentication setup, and common patterns. More useful than raw schema for understanding node usage.', 19 | parameters: { 20 | nodeType: { type: 'string', required: true, description: 'Full node type with prefix (e.g., "nodes-base.slack")' } 21 | }, 22 | returns: 'Parsed markdown documentation with examples, authentication guides, common patterns', 23 | examples: [ 24 | 'get_node_documentation({nodeType: "nodes-base.slack"}) - Slack usage guide', 25 | 'get_node_documentation({nodeType: "nodes-base.googleSheets"}) - Sheets examples' 26 | ], 27 | useCases: [ 28 | 'Understanding authentication setup', 29 | 'Finding usage examples', 30 | 'Learning common patterns' 31 | ], 32 | performance: 'Fast - Pre-parsed documentation stored in database', 33 | bestPractices: [ 34 | 'Use for learning node usage', 35 | 'Check coverage with get_database_statistics', 36 | 'Combine with get_node_essentials' 37 | ], 38 | pitfalls: [ 39 | 'Not all nodes have docs (87% coverage)', 40 | 'May be outdated for new features', 41 | 'Requires full node type prefix' 42 | ], 43 | relatedTools: ['get_node_info', 'get_node_essentials', 'search_nodes'] 44 | } 45 | }; ``` -------------------------------------------------------------------------------- /scripts/test-error-message-tracking.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Test script to verify error message tracking is working 3 | */ 4 | 5 | import { telemetry } from '../src/telemetry'; 6 | 7 | async function testErrorTracking() { 8 | console.log('=== Testing Error Message Tracking ===\n'); 9 | 10 | // Track session first 11 | console.log('1. Starting session...'); 12 | telemetry.trackSessionStart(); 13 | 14 | // Track an error WITH a message 15 | console.log('\n2. Tracking error WITH message:'); 16 | const testErrorMessage = 'This is a test error message with sensitive data: password=secret123 and [email protected]'; 17 | telemetry.trackError( 18 | 'TypeError', 19 | 'tool_execution', 20 | 'test_tool', 21 | testErrorMessage 22 | ); 23 | console.log(` Original message: "${testErrorMessage}"`); 24 | 25 | // Track an error WITHOUT a message 26 | console.log('\n3. Tracking error WITHOUT message:'); 27 | telemetry.trackError( 28 | 'Error', 29 | 'tool_execution', 30 | 'test_tool2' 31 | ); 32 | 33 | // Check the event queue 34 | const metrics = telemetry.getMetrics(); 35 | console.log('\n4. Telemetry metrics:'); 36 | console.log(' Status:', metrics.status); 37 | console.log(' Events queued:', metrics.tracking.eventsQueued); 38 | 39 | // Get raw event queue to inspect 40 | const eventTracker = (telemetry as any).eventTracker; 41 | const queue = eventTracker.getEventQueue(); 42 | 43 | console.log('\n5. Event queue contents:'); 44 | queue.forEach((event, i) => { 45 | console.log(`\n Event ${i + 1}:`); 46 | console.log(` - Type: ${event.event}`); 47 | console.log(` - Properties:`, JSON.stringify(event.properties, null, 6)); 48 | }); 49 | 50 | // Flush to database 51 | console.log('\n6. Flushing to database...'); 52 | await telemetry.flush(); 53 | 54 | console.log('\n7. Done! Check Supabase for error events with "error" field.'); 55 | console.log(' Query: SELECT * FROM telemetry_events WHERE event = \'error_occurred\' ORDER BY created_at DESC LIMIT 5;'); 56 | } 57 | 58 | testErrorTracking().catch(console.error); 59 | ``` -------------------------------------------------------------------------------- /scripts/sync-runtime-version.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Sync version from package.json to package.runtime.json and README.md 5 | * This ensures all files always have the same version 6 | */ 7 | 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | 11 | const packageJsonPath = path.join(__dirname, '..', 'package.json'); 12 | const packageRuntimePath = path.join(__dirname, '..', 'package.runtime.json'); 13 | const readmePath = path.join(__dirname, '..', 'README.md'); 14 | 15 | try { 16 | // Read package.json 17 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); 18 | const version = packageJson.version; 19 | 20 | // Read package.runtime.json 21 | const packageRuntime = JSON.parse(fs.readFileSync(packageRuntimePath, 'utf-8')); 22 | 23 | // Update version if different 24 | if (packageRuntime.version !== version) { 25 | packageRuntime.version = version; 26 | 27 | // Write back with proper formatting 28 | fs.writeFileSync( 29 | packageRuntimePath, 30 | JSON.stringify(packageRuntime, null, 2) + '\n', 31 | 'utf-8' 32 | ); 33 | 34 | console.log(`✅ Updated package.runtime.json version to ${version}`); 35 | } else { 36 | console.log(`✓ package.runtime.json already at version ${version}`); 37 | } 38 | 39 | // Update README.md version badge 40 | let readmeContent = fs.readFileSync(readmePath, 'utf-8'); 41 | const versionBadgeRegex = /(\[!\[Version\]\(https:\/\/img\.shields\.io\/badge\/version-)[^-]+(-.+?\)\])/; 42 | const newVersionBadge = `$1${version}$2`; 43 | const updatedReadmeContent = readmeContent.replace(versionBadgeRegex, newVersionBadge); 44 | 45 | if (updatedReadmeContent !== readmeContent) { 46 | fs.writeFileSync(readmePath, updatedReadmeContent); 47 | console.log(`✅ Updated README.md version badge to ${version}`); 48 | } else { 49 | console.log(`✓ README.md already has version badge ${version}`); 50 | } 51 | } catch (error) { 52 | console.error('❌ Error syncing version:', error.message); 53 | process.exit(1); 54 | } ``` -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- ```yaml 1 | # docker-compose.yml 2 | # For optimized builds with BuildKit, use: docker-compose -f docker-compose.buildkit.yml up 3 | version: '3.8' 4 | 5 | services: 6 | n8n-mcp: 7 | image: ghcr.io/czlonkowski/n8n-mcp:latest 8 | container_name: n8n-mcp 9 | restart: unless-stopped 10 | 11 | # Environment configuration 12 | environment: 13 | # Mode configuration 14 | MCP_MODE: ${MCP_MODE:-http} 15 | USE_FIXED_HTTP: ${USE_FIXED_HTTP:-true} # Use fixed implementation for stability 16 | AUTH_TOKEN: ${AUTH_TOKEN:?AUTH_TOKEN is required for HTTP mode} 17 | 18 | # Application settings 19 | NODE_ENV: ${NODE_ENV:-production} 20 | LOG_LEVEL: ${LOG_LEVEL:-info} 21 | PORT: ${PORT:-3000} 22 | 23 | # Database 24 | NODE_DB_PATH: ${NODE_DB_PATH:-/app/data/nodes.db} 25 | REBUILD_ON_START: ${REBUILD_ON_START:-false} 26 | 27 | # Telemetry: Anonymous usage statistics are ENABLED by default 28 | # To opt-out, uncomment and set to 'true': 29 | # N8N_MCP_TELEMETRY_DISABLED: ${N8N_MCP_TELEMETRY_DISABLED:-true} 30 | 31 | # Optional: n8n API configuration (enables 16 additional management tools) 32 | # Uncomment and configure to enable n8n workflow management 33 | # N8N_API_URL: ${N8N_API_URL} 34 | # N8N_API_KEY: ${N8N_API_KEY} 35 | # N8N_API_TIMEOUT: ${N8N_API_TIMEOUT:-30000} 36 | # N8N_API_MAX_RETRIES: ${N8N_API_MAX_RETRIES:-3} 37 | 38 | # Volumes for persistence 39 | volumes: 40 | - n8n-mcp-data:/app/data 41 | 42 | # Port mapping 43 | ports: 44 | - "${PORT:-3000}:3000" 45 | 46 | # Resource limits 47 | deploy: 48 | resources: 49 | limits: 50 | memory: 512M 51 | reservations: 52 | memory: 256M 53 | 54 | # Health check 55 | healthcheck: 56 | test: ["CMD", "curl", "-f", "http://127.0.0.1:3000/health"] 57 | interval: 30s 58 | timeout: 10s 59 | retries: 3 60 | start_period: 40s 61 | 62 | # Named volume for data persistence 63 | volumes: 64 | n8n-mcp-data: 65 | driver: local ``` -------------------------------------------------------------------------------- /src/config/n8n-api.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import dotenv from 'dotenv'; 3 | import { logger } from '../utils/logger'; 4 | 5 | // n8n API configuration schema 6 | const n8nApiConfigSchema = z.object({ 7 | N8N_API_URL: z.string().url().optional(), 8 | N8N_API_KEY: z.string().min(1).optional(), 9 | N8N_API_TIMEOUT: z.coerce.number().positive().default(30000), 10 | N8N_API_MAX_RETRIES: z.coerce.number().positive().default(3), 11 | }); 12 | 13 | // Track if we've loaded env vars 14 | let envLoaded = false; 15 | 16 | // Parse and validate n8n API configuration 17 | export function getN8nApiConfig() { 18 | // Load environment variables on first access 19 | if (!envLoaded) { 20 | dotenv.config(); 21 | envLoaded = true; 22 | } 23 | 24 | const result = n8nApiConfigSchema.safeParse(process.env); 25 | 26 | if (!result.success) { 27 | return null; 28 | } 29 | 30 | const config = result.data; 31 | 32 | // Check if both URL and API key are provided 33 | if (!config.N8N_API_URL || !config.N8N_API_KEY) { 34 | return null; 35 | } 36 | 37 | return { 38 | baseUrl: config.N8N_API_URL, 39 | apiKey: config.N8N_API_KEY, 40 | timeout: config.N8N_API_TIMEOUT, 41 | maxRetries: config.N8N_API_MAX_RETRIES, 42 | }; 43 | } 44 | 45 | // Helper to check if n8n API is configured (lazy check) 46 | export function isN8nApiConfigured(): boolean { 47 | const config = getN8nApiConfig(); 48 | return config !== null; 49 | } 50 | 51 | /** 52 | * Create n8n API configuration from instance context 53 | * Used for flexible instance configuration support 54 | */ 55 | export function getN8nApiConfigFromContext(context: { 56 | n8nApiUrl?: string; 57 | n8nApiKey?: string; 58 | n8nApiTimeout?: number; 59 | n8nApiMaxRetries?: number; 60 | }): N8nApiConfig | null { 61 | if (!context.n8nApiUrl || !context.n8nApiKey) { 62 | return null; 63 | } 64 | 65 | return { 66 | baseUrl: context.n8nApiUrl, 67 | apiKey: context.n8nApiKey, 68 | timeout: context.n8nApiTimeout ?? 30000, 69 | maxRetries: context.n8nApiMaxRetries ?? 3, 70 | }; 71 | } 72 | 73 | // Type export 74 | export type N8nApiConfig = NonNullable<ReturnType<typeof getN8nApiConfig>>; ``` -------------------------------------------------------------------------------- /tests/integration/n8n-api/utils/node-repository.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Node Repository Utility for Integration Tests 3 | * 4 | * Provides a singleton NodeRepository instance for integration tests 5 | * that require validation or autofix functionality. 6 | */ 7 | 8 | import path from 'path'; 9 | import { createDatabaseAdapter, DatabaseAdapter } from '../../../../src/database/database-adapter'; 10 | import { NodeRepository } from '../../../../src/database/node-repository'; 11 | 12 | let repositoryInstance: NodeRepository | null = null; 13 | let dbInstance: DatabaseAdapter | null = null; 14 | 15 | /** 16 | * Get or create NodeRepository instance 17 | * 18 | * Uses the production nodes.db database (data/nodes.db). 19 | * 20 | * @returns Singleton NodeRepository instance 21 | * @throws {Error} If database file cannot be found or opened 22 | * 23 | * @example 24 | * const repository = await getNodeRepository(); 25 | * const nodeInfo = await repository.getNodeByType('n8n-nodes-base.webhook'); 26 | */ 27 | export async function getNodeRepository(): Promise<NodeRepository> { 28 | if (repositoryInstance) { 29 | return repositoryInstance; 30 | } 31 | 32 | const dbPath = path.join(process.cwd(), 'data/nodes.db'); 33 | dbInstance = await createDatabaseAdapter(dbPath); 34 | repositoryInstance = new NodeRepository(dbInstance); 35 | 36 | return repositoryInstance; 37 | } 38 | 39 | /** 40 | * Close database and reset repository instance 41 | * 42 | * Should be called in test cleanup (afterAll) to prevent resource leaks. 43 | * Properly closes the database connection and resets the singleton. 44 | * 45 | * @example 46 | * afterAll(async () => { 47 | * await closeNodeRepository(); 48 | * }); 49 | */ 50 | export async function closeNodeRepository(): Promise<void> { 51 | if (dbInstance && typeof dbInstance.close === 'function') { 52 | await dbInstance.close(); 53 | } 54 | dbInstance = null; 55 | repositoryInstance = null; 56 | } 57 | 58 | /** 59 | * Reset repository instance (useful for test cleanup) 60 | * 61 | * @deprecated Use closeNodeRepository() instead to properly close database connections 62 | */ 63 | export function resetNodeRepository(): void { 64 | repositoryInstance = null; 65 | } 66 | ``` -------------------------------------------------------------------------------- /tests/factories/property-definition-factory.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Factory } from 'fishery'; 2 | import { faker } from '@faker-js/faker'; 3 | 4 | /** 5 | * Interface for n8n node property definitions. 6 | * Represents the structure of properties that configure node behavior. 7 | */ 8 | interface PropertyDefinition { 9 | name: string; 10 | displayName: string; 11 | type: string; 12 | default?: any; 13 | required?: boolean; 14 | description?: string; 15 | options?: any[]; 16 | } 17 | 18 | /** 19 | * Factory for generating PropertyDefinition test data. 20 | * Creates realistic property configurations for testing node validation and processing. 21 | * 22 | * @example 23 | * ```typescript 24 | * // Create a single property 25 | * const prop = PropertyDefinitionFactory.build(); 26 | * 27 | * // Create a required string property 28 | * const urlProp = PropertyDefinitionFactory.build({ 29 | * name: 'url', 30 | * displayName: 'URL', 31 | * type: 'string', 32 | * required: true 33 | * }); 34 | * 35 | * // Create an options property with choices 36 | * const methodProp = PropertyDefinitionFactory.build({ 37 | * name: 'method', 38 | * type: 'options', 39 | * options: [ 40 | * { name: 'GET', value: 'GET' }, 41 | * { name: 'POST', value: 'POST' } 42 | * ] 43 | * }); 44 | * 45 | * // Create multiple properties for a node 46 | * const nodeProperties = PropertyDefinitionFactory.buildList(5); 47 | * ``` 48 | */ 49 | export const PropertyDefinitionFactory = Factory.define<PropertyDefinition>(() => ({ 50 | name: faker.word.noun() + faker.word.adjective().charAt(0).toUpperCase() + faker.word.adjective().slice(1), 51 | displayName: faker.helpers.arrayElement(['URL', 'Method', 'Headers', 'Body', 'Authentication']), 52 | type: faker.helpers.arrayElement(['string', 'number', 'boolean', 'options', 'json']), 53 | default: faker.datatype.boolean() ? faker.word.sample() : undefined, 54 | required: faker.datatype.boolean(), 55 | description: faker.lorem.sentence(), 56 | options: faker.datatype.boolean() ? [ 57 | { 58 | name: faker.word.noun(), 59 | value: faker.word.noun(), 60 | description: faker.lorem.sentence() 61 | } 62 | ] : undefined 63 | })); ``` -------------------------------------------------------------------------------- /docker-compose.n8n.yml: -------------------------------------------------------------------------------- ```yaml 1 | version: '3.8' 2 | 3 | services: 4 | # n8n workflow automation 5 | n8n: 6 | image: n8nio/n8n:latest 7 | container_name: n8n 8 | restart: unless-stopped 9 | ports: 10 | - "${N8N_PORT:-5678}:5678" 11 | environment: 12 | - N8N_BASIC_AUTH_ACTIVE=${N8N_BASIC_AUTH_ACTIVE:-true} 13 | - N8N_BASIC_AUTH_USER=${N8N_BASIC_AUTH_USER:-admin} 14 | - N8N_BASIC_AUTH_PASSWORD=${N8N_BASIC_AUTH_PASSWORD:-password} 15 | - N8N_HOST=${N8N_HOST:-localhost} 16 | - N8N_PORT=5678 17 | - N8N_PROTOCOL=${N8N_PROTOCOL:-http} 18 | - WEBHOOK_URL=${N8N_WEBHOOK_URL:-http://localhost:5678/} 19 | - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY} 20 | volumes: 21 | - n8n_data:/home/node/.n8n 22 | networks: 23 | - n8n-network 24 | healthcheck: 25 | test: ["CMD", "sh", "-c", "wget --quiet --spider --tries=1 --timeout=10 http://localhost:5678/healthz || exit 1"] 26 | interval: 30s 27 | timeout: 10s 28 | retries: 3 29 | start_period: 30s 30 | 31 | # n8n-mcp server for AI assistance 32 | n8n-mcp: 33 | build: 34 | context: . 35 | dockerfile: Dockerfile # Uses standard Dockerfile with N8N_MODE=true env var 36 | image: ghcr.io/${GITHUB_REPOSITORY:-czlonkowski/n8n-mcp}/n8n-mcp:${VERSION:-latest} 37 | container_name: n8n-mcp 38 | restart: unless-stopped 39 | ports: 40 | - "${MCP_PORT:-3000}:3000" 41 | environment: 42 | - NODE_ENV=production 43 | - N8N_MODE=true 44 | - MCP_MODE=http 45 | - N8N_API_URL=http://n8n:5678 46 | - N8N_API_KEY=${N8N_API_KEY} 47 | - MCP_AUTH_TOKEN=${MCP_AUTH_TOKEN} 48 | - AUTH_TOKEN=${MCP_AUTH_TOKEN} 49 | - LOG_LEVEL=${LOG_LEVEL:-info} 50 | volumes: 51 | - ./data:/app/data:ro 52 | - mcp_logs:/app/logs 53 | networks: 54 | - n8n-network 55 | depends_on: 56 | n8n: 57 | condition: service_healthy 58 | healthcheck: 59 | test: ["CMD", "curl", "-f", "http://localhost:3000/health"] 60 | interval: 30s 61 | timeout: 10s 62 | retries: 3 63 | start_period: 40s 64 | 65 | volumes: 66 | n8n_data: 67 | driver: local 68 | mcp_logs: 69 | driver: local 70 | 71 | networks: 72 | n8n-network: 73 | driver: bridge ``` -------------------------------------------------------------------------------- /tests/extracted-nodes-db/extraction-report.json: -------------------------------------------------------------------------------- ```json 1 | [ 2 | { 3 | "nodeType": "n8n-nodes-base.Slack", 4 | "success": true, 5 | "hasPackageInfo": true, 6 | "hasCredentials": true, 7 | "sourceSize": 1007, 8 | "credentialSize": 7553, 9 | "packageName": "n8n-nodes-base", 10 | "packageVersion": "1.14.1" 11 | }, 12 | { 13 | "nodeType": "n8n-nodes-base.Discord", 14 | "success": true, 15 | "hasPackageInfo": true, 16 | "hasCredentials": false, 17 | "sourceSize": 10049, 18 | "credentialSize": 0, 19 | "packageName": "n8n-nodes-base", 20 | "packageVersion": "1.14.1" 21 | }, 22 | { 23 | "nodeType": "n8n-nodes-base.HttpRequest", 24 | "success": true, 25 | "hasPackageInfo": true, 26 | "hasCredentials": false, 27 | "sourceSize": 1343, 28 | "credentialSize": 0, 29 | "packageName": "n8n-nodes-base", 30 | "packageVersion": "1.14.1" 31 | }, 32 | { 33 | "nodeType": "n8n-nodes-base.Webhook", 34 | "success": true, 35 | "hasPackageInfo": true, 36 | "hasCredentials": false, 37 | "sourceSize": 10667, 38 | "credentialSize": 0, 39 | "packageName": "n8n-nodes-base", 40 | "packageVersion": "1.14.1" 41 | }, 42 | { 43 | "nodeType": "n8n-nodes-base.If", 44 | "success": true, 45 | "hasPackageInfo": true, 46 | "hasCredentials": false, 47 | "sourceSize": 20533, 48 | "credentialSize": 0, 49 | "packageName": "n8n-nodes-base", 50 | "packageVersion": "1.14.1" 51 | }, 52 | { 53 | "nodeType": "n8n-nodes-base.SplitInBatches", 54 | "success": true, 55 | "hasPackageInfo": true, 56 | "hasCredentials": false, 57 | "sourceSize": 1135, 58 | "credentialSize": 0, 59 | "packageName": "n8n-nodes-base", 60 | "packageVersion": "1.14.1" 61 | }, 62 | { 63 | "nodeType": "n8n-nodes-base.Airtable", 64 | "success": true, 65 | "hasPackageInfo": true, 66 | "hasCredentials": true, 67 | "sourceSize": 936, 68 | "credentialSize": 5985, 69 | "packageName": "n8n-nodes-base", 70 | "packageVersion": "1.14.1" 71 | }, 72 | { 73 | "nodeType": "n8n-nodes-base.Function", 74 | "success": true, 75 | "hasPackageInfo": true, 76 | "hasCredentials": false, 77 | "sourceSize": 7449, 78 | "credentialSize": 0, 79 | "packageName": "n8n-nodes-base", 80 | "packageVersion": "1.14.1" 81 | } 82 | ] ``` -------------------------------------------------------------------------------- /tests/setup/global-setup.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { beforeEach, afterEach, vi } from 'vitest'; 2 | import { loadTestEnvironment, getTestConfig, getTestTimeout } from './test-env'; 3 | 4 | // CI Debug: Log environment loading in CI only 5 | if (process.env.CI === 'true') { 6 | console.log('[CI-DEBUG] Global setup starting, NODE_ENV:', process.env.NODE_ENV); 7 | } 8 | 9 | // Load test environment configuration 10 | loadTestEnvironment(); 11 | 12 | if (process.env.CI === 'true') { 13 | console.log('[CI-DEBUG] Global setup complete, N8N_API_URL:', process.env.N8N_API_URL); 14 | } 15 | 16 | // Get test configuration 17 | const testConfig = getTestConfig(); 18 | 19 | // Reset mocks between tests 20 | beforeEach(() => { 21 | vi.clearAllMocks(); 22 | }); 23 | 24 | // Clean up after each test 25 | afterEach(() => { 26 | vi.restoreAllMocks(); 27 | 28 | // Perform cleanup if enabled 29 | if (testConfig.cleanup.enabled) { 30 | // Add cleanup logic here if needed 31 | } 32 | }); 33 | 34 | // Global test timeout from configuration 35 | vi.setConfig({ testTimeout: getTestTimeout('global') }); 36 | 37 | // Configure console output based on test configuration 38 | if (!testConfig.logging.debug) { 39 | global.console = { 40 | ...console, 41 | log: vi.fn(), 42 | debug: vi.fn(), 43 | info: vi.fn(), 44 | warn: testConfig.logging.level === 'error' ? vi.fn() : console.warn, 45 | error: console.error, // Always show errors 46 | }; 47 | } 48 | 49 | // Set up performance monitoring if enabled 50 | if (testConfig.performance) { 51 | // Use a high-resolution timer that maintains timing precision 52 | let startTime = process.hrtime.bigint(); 53 | 54 | global.performance = global.performance || { 55 | now: () => { 56 | // Convert nanoseconds to milliseconds with high precision 57 | const currentTime = process.hrtime.bigint(); 58 | return Number(currentTime - startTime) / 1000000; // Convert nanoseconds to milliseconds 59 | }, 60 | mark: vi.fn(), 61 | measure: vi.fn(), 62 | getEntriesByName: vi.fn(() => []), 63 | getEntriesByType: vi.fn(() => []), 64 | clearMarks: vi.fn(), 65 | clearMeasures: vi.fn(), 66 | } as any; 67 | } 68 | 69 | // Export test configuration for use in tests 70 | export { testConfig, getTestTimeout, getTestConfig }; ``` -------------------------------------------------------------------------------- /src/telemetry/error-sanitizer.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Error Sanitizer for Startup Errors (v2.18.3) 3 | * Extracts and sanitizes error messages with security-focused patterns 4 | * Now uses shared sanitization utilities to avoid code duplication 5 | */ 6 | 7 | import { logger } from '../utils/logger'; 8 | import { sanitizeErrorMessageCore } from './error-sanitization-utils'; 9 | 10 | /** 11 | * Extract error message from unknown error type 12 | * Safely handles Error objects, strings, and other types 13 | */ 14 | export function extractErrorMessage(error: unknown): string { 15 | try { 16 | if (error instanceof Error) { 17 | // Include stack trace if available (will be truncated later) 18 | return error.stack || error.message || 'Unknown error'; 19 | } 20 | 21 | if (typeof error === 'string') { 22 | return error; 23 | } 24 | 25 | if (error && typeof error === 'object') { 26 | // Try to extract message from object 27 | const errorObj = error as any; 28 | if (errorObj.message) { 29 | return String(errorObj.message); 30 | } 31 | if (errorObj.error) { 32 | return String(errorObj.error); 33 | } 34 | // Fall back to JSON stringify with truncation 35 | try { 36 | return JSON.stringify(error).substring(0, 500); 37 | } catch { 38 | return 'Error object (unstringifiable)'; 39 | } 40 | } 41 | 42 | return String(error); 43 | } catch (extractError) { 44 | logger.debug('Error during message extraction:', extractError); 45 | return 'Error message extraction failed'; 46 | } 47 | } 48 | 49 | /** 50 | * Sanitize startup error message to remove sensitive data 51 | * Now uses shared sanitization core from error-sanitization-utils.ts (v2.18.3) 52 | * This eliminates code duplication and the ReDoS vulnerability 53 | */ 54 | export function sanitizeStartupError(errorMessage: string): string { 55 | return sanitizeErrorMessageCore(errorMessage); 56 | } 57 | 58 | /** 59 | * Combined operation: Extract and sanitize error message 60 | * This is the main entry point for startup error processing 61 | */ 62 | export function processStartupError(error: unknown): string { 63 | const message = extractErrorMessage(error); 64 | return sanitizeStartupError(message); 65 | } 66 | ``` -------------------------------------------------------------------------------- /src/database/migrations/add-template-node-configs.sql: -------------------------------------------------------------------------------- ```sql 1 | -- Migration: Add template_node_configs table 2 | -- Run during `npm run rebuild` or `npm run fetch:templates` 3 | -- This migration is idempotent - safe to run multiple times 4 | 5 | -- Create table if it doesn't exist 6 | CREATE TABLE IF NOT EXISTS template_node_configs ( 7 | id INTEGER PRIMARY KEY, 8 | node_type TEXT NOT NULL, 9 | template_id INTEGER NOT NULL, 10 | template_name TEXT NOT NULL, 11 | template_views INTEGER DEFAULT 0, 12 | 13 | -- Node configuration (extracted from workflow) 14 | node_name TEXT, -- Node name in workflow (e.g., "HTTP Request") 15 | parameters_json TEXT NOT NULL, -- JSON: node.parameters 16 | credentials_json TEXT, -- JSON: node.credentials (if present) 17 | 18 | -- Pre-calculated metadata for filtering 19 | has_credentials INTEGER DEFAULT 0, 20 | has_expressions INTEGER DEFAULT 0, -- Contains {{...}} or $json/$node 21 | complexity TEXT CHECK(complexity IN ('simple', 'medium', 'complex')), 22 | use_cases TEXT, -- JSON array from template.metadata.use_cases 23 | 24 | -- Pre-calculated ranking (1 = best, 2 = second best, etc.) 25 | rank INTEGER DEFAULT 0, 26 | 27 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 28 | FOREIGN KEY (template_id) REFERENCES templates(id) ON DELETE CASCADE 29 | ); 30 | 31 | -- Create indexes if they don't exist 32 | CREATE INDEX IF NOT EXISTS idx_config_node_type_rank 33 | ON template_node_configs(node_type, rank); 34 | 35 | CREATE INDEX IF NOT EXISTS idx_config_complexity 36 | ON template_node_configs(node_type, complexity, rank); 37 | 38 | CREATE INDEX IF NOT EXISTS idx_config_auth 39 | ON template_node_configs(node_type, has_credentials, rank); 40 | 41 | -- Create view if it doesn't exist 42 | CREATE VIEW IF NOT EXISTS ranked_node_configs AS 43 | SELECT 44 | node_type, 45 | template_name, 46 | template_views, 47 | parameters_json, 48 | credentials_json, 49 | has_credentials, 50 | has_expressions, 51 | complexity, 52 | use_cases, 53 | rank 54 | FROM template_node_configs 55 | WHERE rank <= 5 -- Top 5 per node type 56 | ORDER BY node_type, rank; 57 | 58 | -- Note: Actual data population is handled by the fetch-templates script 59 | -- This migration only creates the schema 60 | ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/validation/validate-node-minimal.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolDocumentation } from '../types'; 2 | 3 | export const validateNodeMinimalDoc: ToolDocumentation = { 4 | name: 'validate_node_minimal', 5 | category: 'validation', 6 | essentials: { 7 | description: 'Fast check for missing required fields only. No warnings/suggestions. Returns: list of missing fields.', 8 | keyParameters: ['nodeType', 'config'], 9 | example: 'validate_node_minimal("nodes-base.slack", {resource: "message"})', 10 | performance: 'Instant', 11 | tips: [ 12 | 'Returns only missing required fields', 13 | 'No warnings or suggestions', 14 | 'Perfect for real-time validation' 15 | ] 16 | }, 17 | full: { 18 | description: 'Minimal validation that only checks for missing required fields. Returns array of missing field names without any warnings or suggestions. Ideal for quick validation during node configuration.', 19 | parameters: { 20 | nodeType: { type: 'string', required: true, description: 'Node type with prefix (e.g., "nodes-base.slack")' }, 21 | config: { type: 'object', required: true, description: 'Node configuration to validate' } 22 | }, 23 | returns: 'Array of missing required field names (empty if valid)', 24 | examples: [ 25 | 'validate_node_minimal("nodes-base.slack", {resource: "message", operation: "post"}) - Check Slack config', 26 | 'validate_node_minimal("nodes-base.httpRequest", {method: "GET"}) - Check HTTP config' 27 | ], 28 | useCases: [ 29 | 'Real-time form validation', 30 | 'Quick configuration checks', 31 | 'Pre-deployment validation', 32 | 'Interactive configuration builders' 33 | ], 34 | performance: 'Instant - Simple field checking without complex validation', 35 | bestPractices: [ 36 | 'Use for quick feedback loops', 37 | 'Follow with validate_node_operation for thorough check', 38 | 'Check return array length for validity' 39 | ], 40 | pitfalls: [ 41 | 'Only checks required fields', 42 | 'No type validation', 43 | 'No operation-specific validation' 44 | ], 45 | relatedTools: ['validate_node_operation', 'get_node_essentials', 'get_property_dependencies'] 46 | } 47 | }; ``` -------------------------------------------------------------------------------- /scripts/demo-optimization.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | # Demonstrate the optimization concept 3 | 4 | echo "🎯 Demonstrating Docker Optimization" 5 | echo "====================================" 6 | 7 | # Create a demo directory structure 8 | DEMO_DIR="optimization-demo" 9 | rm -rf $DEMO_DIR 10 | mkdir -p $DEMO_DIR 11 | 12 | # Copy only runtime files 13 | echo -e "\n📦 Creating minimal runtime package..." 14 | cat > $DEMO_DIR/package.json << 'EOF' 15 | { 16 | "name": "n8n-mcp-optimized", 17 | "version": "1.0.0", 18 | "private": true, 19 | "main": "dist/mcp/index.js", 20 | "dependencies": { 21 | "@modelcontextprotocol/sdk": "^1.12.1", 22 | "better-sqlite3": "^11.10.0", 23 | "sql.js": "^1.13.0", 24 | "express": "^5.1.0", 25 | "dotenv": "^16.5.0" 26 | } 27 | } 28 | EOF 29 | 30 | # Copy built files 31 | echo "📁 Copying built application..." 32 | cp -r dist $DEMO_DIR/ 33 | cp -r data $DEMO_DIR/ 34 | mkdir -p $DEMO_DIR/src/database 35 | cp src/database/schema*.sql $DEMO_DIR/src/database/ 36 | 37 | # Calculate sizes 38 | echo -e "\n📊 Size comparison:" 39 | echo "Original project: $(du -sh . | cut -f1)" 40 | echo "Optimized runtime: $(du -sh $DEMO_DIR | cut -f1)" 41 | 42 | # Show what's included 43 | echo -e "\n✅ Optimized package includes:" 44 | echo "- Pre-built SQLite database with all node info" 45 | echo "- Compiled JavaScript (dist/)" 46 | echo "- Minimal runtime dependencies" 47 | echo "- No n8n packages needed!" 48 | 49 | # Create a simple test 50 | echo -e "\n🧪 Testing database content..." 51 | if command -v sqlite3 &> /dev/null; then 52 | NODE_COUNT=$(sqlite3 data/nodes.db "SELECT COUNT(*) FROM nodes;" 2>/dev/null || echo "0") 53 | AI_COUNT=$(sqlite3 data/nodes.db "SELECT COUNT(*) FROM nodes WHERE is_ai_tool = 1;" 2>/dev/null || echo "0") 54 | echo "- Total nodes in database: $NODE_COUNT" 55 | echo "- AI-capable nodes: $AI_COUNT" 56 | else 57 | echo "- SQLite CLI not installed, skipping count" 58 | fi 59 | 60 | echo -e "\n💡 This demonstrates that we can run n8n-MCP with:" 61 | echo "- ~50MB of runtime dependencies (vs 1.6GB)" 62 | echo "- Pre-built database (11MB)" 63 | echo "- No n8n packages at runtime" 64 | echo "- Total optimized size: ~200MB (vs 2.6GB)" 65 | 66 | # Cleanup 67 | echo -e "\n🧹 Cleaning up demo..." 68 | rm -rf $DEMO_DIR 69 | 70 | echo -e "\n✨ Optimization concept demonstrated!" ``` -------------------------------------------------------------------------------- /scripts/publish-npm-quick.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | # Quick publish script that skips tests 3 | set -e 4 | 5 | # Color codes 6 | GREEN='\033[0;32m' 7 | YELLOW='\033[1;33m' 8 | NC='\033[0m' 9 | 10 | echo "🚀 Preparing n8n-mcp for npm publish (quick mode)..." 11 | 12 | # Sync version 13 | echo "🔄 Syncing version to package.runtime.json..." 14 | npm run sync:runtime-version 15 | 16 | VERSION=$(node -e "console.log(require('./package.json').version)") 17 | echo -e "${GREEN}📌 Version: $VERSION${NC}" 18 | 19 | # Prepare publish directory 20 | PUBLISH_DIR="npm-publish-temp" 21 | rm -rf $PUBLISH_DIR 22 | mkdir -p $PUBLISH_DIR 23 | 24 | echo "📦 Copying files..." 25 | cp -r dist $PUBLISH_DIR/ 26 | cp -r data $PUBLISH_DIR/ 27 | cp README.md LICENSE .env.example $PUBLISH_DIR/ 28 | cp .npmignore $PUBLISH_DIR/ 2>/dev/null || true 29 | cp package.runtime.json $PUBLISH_DIR/package.json 30 | 31 | cd $PUBLISH_DIR 32 | 33 | # Configure package.json 34 | node -e " 35 | const pkg = require('./package.json'); 36 | pkg.name = 'n8n-mcp'; 37 | pkg.description = 'Integration between n8n workflow automation and Model Context Protocol (MCP)'; 38 | pkg.bin = { 'n8n-mcp': './dist/mcp/index.js' }; 39 | pkg.repository = { type: 'git', url: 'git+https://github.com/czlonkowski/n8n-mcp.git' }; 40 | pkg.keywords = ['n8n', 'mcp', 'model-context-protocol', 'ai', 'workflow', 'automation']; 41 | pkg.author = 'Romuald Czlonkowski @ www.aiadvisors.pl/en'; 42 | pkg.license = 'MIT'; 43 | pkg.bugs = { url: 'https://github.com/czlonkowski/n8n-mcp/issues' }; 44 | pkg.homepage = 'https://github.com/czlonkowski/n8n-mcp#readme'; 45 | pkg.files = ['dist/**/*', 'data/nodes.db', '.env.example', 'README.md', 'LICENSE']; 46 | delete pkg.private; 47 | require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)); 48 | " 49 | 50 | echo "" 51 | echo "📋 Package details:" 52 | echo -e "${GREEN}Name:${NC} $(node -e "console.log(require('./package.json').name)")" 53 | echo -e "${GREEN}Version:${NC} $(node -e "console.log(require('./package.json').version)")" 54 | echo -e "${GREEN}Size:${NC} ~50MB" 55 | echo "" 56 | echo "✅ Ready to publish!" 57 | echo "" 58 | echo -e "${YELLOW}⚠️ Note: Tests were skipped in quick mode${NC}" 59 | echo "" 60 | echo "To publish, run:" 61 | echo -e " ${GREEN}cd $PUBLISH_DIR${NC}" 62 | echo -e " ${GREEN}npm publish --otp=YOUR_OTP_CODE${NC}" ``` -------------------------------------------------------------------------------- /src/utils/console-manager.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Console Manager for MCP HTTP Server 3 | * 4 | * Prevents console output from interfering with StreamableHTTPServerTransport 5 | * by silencing console methods during MCP request handling. 6 | */ 7 | export class ConsoleManager { 8 | private originalConsole = { 9 | log: console.log, 10 | error: console.error, 11 | warn: console.warn, 12 | info: console.info, 13 | debug: console.debug, 14 | trace: console.trace 15 | }; 16 | 17 | private isSilenced = false; 18 | 19 | /** 20 | * Silence all console output 21 | */ 22 | public silence(): void { 23 | if (this.isSilenced || process.env.MCP_MODE !== 'http') { 24 | return; 25 | } 26 | 27 | this.isSilenced = true; 28 | process.env.MCP_REQUEST_ACTIVE = 'true'; 29 | console.log = () => {}; 30 | console.error = () => {}; 31 | console.warn = () => {}; 32 | console.info = () => {}; 33 | console.debug = () => {}; 34 | console.trace = () => {}; 35 | } 36 | 37 | /** 38 | * Restore original console methods 39 | */ 40 | public restore(): void { 41 | if (!this.isSilenced) { 42 | return; 43 | } 44 | 45 | this.isSilenced = false; 46 | process.env.MCP_REQUEST_ACTIVE = 'false'; 47 | console.log = this.originalConsole.log; 48 | console.error = this.originalConsole.error; 49 | console.warn = this.originalConsole.warn; 50 | console.info = this.originalConsole.info; 51 | console.debug = this.originalConsole.debug; 52 | console.trace = this.originalConsole.trace; 53 | } 54 | 55 | /** 56 | * Wrap an operation with console silencing 57 | * Automatically restores console on completion or error 58 | */ 59 | public async wrapOperation<T>(operation: () => T | Promise<T>): Promise<T> { 60 | this.silence(); 61 | try { 62 | const result = operation(); 63 | if (result instanceof Promise) { 64 | return await result.finally(() => this.restore()); 65 | } 66 | this.restore(); 67 | return result; 68 | } catch (error) { 69 | this.restore(); 70 | throw error; 71 | } 72 | } 73 | 74 | /** 75 | * Check if console is currently silenced 76 | */ 77 | public get isActive(): boolean { 78 | return this.isSilenced; 79 | } 80 | } 81 | 82 | // Export singleton instance for easy use 83 | export const consoleManager = new ConsoleManager(); ``` -------------------------------------------------------------------------------- /tests/integration/n8n-api/types/mcp-responses.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * TypeScript interfaces for MCP handler responses 3 | * 4 | * These interfaces provide type safety for integration tests, 5 | * replacing unsafe `as any` casts with proper type definitions. 6 | */ 7 | 8 | /** 9 | * Workflow validation response from handleValidateWorkflow 10 | */ 11 | export interface ValidationResponse { 12 | valid: boolean; 13 | workflowId: string; 14 | workflowName: string; 15 | summary: { 16 | totalNodes: number; 17 | enabledNodes: number; 18 | triggerNodes: number; 19 | validConnections?: number; 20 | invalidConnections?: number; 21 | expressionsValidated?: number; 22 | errorCount: number; 23 | warningCount: number; 24 | }; 25 | errors?: Array<{ 26 | node: string; 27 | nodeName?: string; 28 | message: string; 29 | details?: { 30 | code?: string; 31 | [key: string]: unknown; 32 | }; 33 | code?: string; 34 | }>; 35 | warnings?: Array<{ 36 | node: string; 37 | nodeName?: string; 38 | message: string; 39 | details?: { 40 | code?: string; 41 | [key: string]: unknown; 42 | }; 43 | code?: string; 44 | }>; 45 | info?: Array<{ 46 | node: string; 47 | nodeName?: string; 48 | message: string; 49 | severity?: string; 50 | details?: unknown; 51 | }>; 52 | suggestions?: string[]; 53 | } 54 | 55 | /** 56 | * Workflow autofix response from handleAutofixWorkflow 57 | */ 58 | export interface AutofixResponse { 59 | workflowId: string; 60 | workflowName: string; 61 | preview?: boolean; 62 | fixesAvailable?: number; 63 | fixesApplied?: number; 64 | fixes?: Array<{ 65 | type: 'expression-format' | 'typeversion-correction' | 'error-output-config' | 'node-type-correction' | 'webhook-missing-path'; 66 | confidence: 'high' | 'medium' | 'low'; 67 | description: string; 68 | nodeName?: string; 69 | nodeId?: string; 70 | before?: unknown; 71 | after?: unknown; 72 | }>; 73 | summary?: { 74 | totalFixes: number; 75 | byType: Record<string, number>; 76 | byConfidence: Record<string, number>; 77 | }; 78 | stats?: { 79 | expressionFormat?: number; 80 | typeVersionCorrection?: number; 81 | errorOutputConfig?: number; 82 | nodeTypeCorrection?: number; 83 | webhookMissingPath?: number; 84 | }; 85 | message?: string; 86 | validationSummary?: { 87 | errors: number; 88 | warnings: number; 89 | }; 90 | } 91 | ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/workflow_management/n8n-delete-workflow.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolDocumentation } from '../types'; 2 | 3 | export const n8nDeleteWorkflowDoc: ToolDocumentation = { 4 | name: 'n8n_delete_workflow', 5 | category: 'workflow_management', 6 | essentials: { 7 | description: 'Permanently delete a workflow. This action cannot be undone.', 8 | keyParameters: ['id'], 9 | example: 'n8n_delete_workflow({id: "workflow_123"})', 10 | performance: 'Fast (50-150ms)', 11 | tips: [ 12 | 'Action is irreversible', 13 | 'Deletes all execution history', 14 | 'Check workflow first with get_minimal' 15 | ] 16 | }, 17 | full: { 18 | description: 'Permanently deletes a workflow from n8n including all associated data, execution history, and settings. This is an irreversible operation that should be used with caution. The workflow must exist and the user must have appropriate permissions.', 19 | parameters: { 20 | id: { type: 'string', required: true, description: 'Workflow ID to delete permanently' } 21 | }, 22 | returns: 'Success confirmation or error if workflow not found/cannot be deleted', 23 | examples: [ 24 | 'n8n_delete_workflow({id: "abc123"}) - Delete specific workflow', 25 | 'if (confirm) { n8n_delete_workflow({id: wf.id}); } // With confirmation' 26 | ], 27 | useCases: [ 28 | 'Remove obsolete workflows', 29 | 'Clean up test workflows', 30 | 'Delete failed experiments', 31 | 'Manage workflow limits', 32 | 'Remove duplicates' 33 | ], 34 | performance: 'Fast operation - typically 50-150ms. May take longer if workflow has extensive execution history.', 35 | bestPractices: [ 36 | 'Always confirm before deletion', 37 | 'Check workflow with get_minimal first', 38 | 'Consider deactivating instead of deleting', 39 | 'Export workflow before deletion for backup' 40 | ], 41 | pitfalls: [ 42 | 'Requires N8N_API_URL and N8N_API_KEY configured', 43 | 'Cannot be undone - permanent deletion', 44 | 'Deletes all execution history', 45 | 'Active workflows can be deleted', 46 | 'No built-in confirmation' 47 | ], 48 | relatedTools: ['n8n_get_workflow_minimal', 'n8n_list_workflows', 'n8n_update_partial_workflow', 'n8n_delete_execution'] 49 | } 50 | }; ``` -------------------------------------------------------------------------------- /src/database/schema-optimized.sql: -------------------------------------------------------------------------------- ```sql 1 | -- Optimized schema with source code storage for Docker optimization 2 | CREATE TABLE IF NOT EXISTS nodes ( 3 | node_type TEXT PRIMARY KEY, 4 | package_name TEXT NOT NULL, 5 | display_name TEXT NOT NULL, 6 | description TEXT, 7 | category TEXT, 8 | development_style TEXT CHECK(development_style IN ('declarative', 'programmatic')), 9 | is_ai_tool INTEGER DEFAULT 0, 10 | is_trigger INTEGER DEFAULT 0, 11 | is_webhook INTEGER DEFAULT 0, 12 | is_versioned INTEGER DEFAULT 0, 13 | version TEXT, 14 | documentation TEXT, 15 | properties_schema TEXT, 16 | operations TEXT, 17 | credentials_required TEXT, 18 | -- New columns for source code storage 19 | node_source_code TEXT, 20 | credential_source_code TEXT, 21 | source_location TEXT, 22 | source_extracted_at DATETIME, 23 | -- Metadata 24 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP 25 | ); 26 | 27 | -- Indexes for performance 28 | CREATE INDEX IF NOT EXISTS idx_package ON nodes(package_name); 29 | CREATE INDEX IF NOT EXISTS idx_ai_tool ON nodes(is_ai_tool); 30 | CREATE INDEX IF NOT EXISTS idx_category ON nodes(category); 31 | 32 | -- FTS5 table for full-text search including source code 33 | CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5( 34 | node_type, 35 | display_name, 36 | description, 37 | documentation, 38 | operations, 39 | node_source_code, 40 | content=nodes, 41 | content_rowid=rowid 42 | ); 43 | 44 | -- Trigger to keep FTS in sync 45 | CREATE TRIGGER IF NOT EXISTS nodes_fts_insert AFTER INSERT ON nodes 46 | BEGIN 47 | INSERT INTO nodes_fts(rowid, node_type, display_name, description, documentation, operations, node_source_code) 48 | VALUES (new.rowid, new.node_type, new.display_name, new.description, new.documentation, new.operations, new.node_source_code); 49 | END; 50 | 51 | CREATE TRIGGER IF NOT EXISTS nodes_fts_update AFTER UPDATE ON nodes 52 | BEGIN 53 | UPDATE nodes_fts 54 | SET node_type = new.node_type, 55 | display_name = new.display_name, 56 | description = new.description, 57 | documentation = new.documentation, 58 | operations = new.operations, 59 | node_source_code = new.node_source_code 60 | WHERE rowid = new.rowid; 61 | END; 62 | 63 | CREATE TRIGGER IF NOT EXISTS nodes_fts_delete AFTER DELETE ON nodes 64 | BEGIN 65 | DELETE FROM nodes_fts WHERE rowid = old.rowid; 66 | END; ``` -------------------------------------------------------------------------------- /tests/test-small-rebuild.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | const { NodeDocumentationService } = require('../dist/services/node-documentation-service'); 4 | 5 | async function testSmallRebuild() { 6 | console.log('Testing small rebuild...\n'); 7 | 8 | const service = new NodeDocumentationService('./data/nodes-v2-test.db'); 9 | 10 | try { 11 | // First, let's just try the IF node specifically 12 | const extractor = service.extractor; 13 | console.log('1️⃣ Testing extraction of IF node...'); 14 | 15 | try { 16 | const ifNodeData = await extractor.extractNodeSource('n8n-nodes-base.If'); 17 | console.log(' ✅ Successfully extracted IF node'); 18 | console.log(' Source code length:', ifNodeData.sourceCode.length); 19 | console.log(' Has credentials:', !!ifNodeData.credentialCode); 20 | } catch (error) { 21 | console.log(' ❌ Failed to extract IF node:', error.message); 22 | } 23 | 24 | // Try the Webhook node 25 | console.log('\n2️⃣ Testing extraction of Webhook node...'); 26 | try { 27 | const webhookNodeData = await extractor.extractNodeSource('n8n-nodes-base.Webhook'); 28 | console.log(' ✅ Successfully extracted Webhook node'); 29 | console.log(' Source code length:', webhookNodeData.sourceCode.length); 30 | } catch (error) { 31 | console.log(' ❌ Failed to extract Webhook node:', error.message); 32 | } 33 | 34 | // Now try storing just these nodes 35 | console.log('\n3️⃣ Testing storage of a single node...'); 36 | const nodeInfo = { 37 | nodeType: 'n8n-nodes-base.If', 38 | name: 'If', 39 | displayName: 'If', 40 | description: 'Route items based on comparison operations', 41 | sourceCode: 'test source code', 42 | packageName: 'n8n-nodes-base', 43 | hasCredentials: false, 44 | isTrigger: false, 45 | isWebhook: false 46 | }; 47 | 48 | await service.storeNode(nodeInfo); 49 | console.log(' ✅ Successfully stored test node'); 50 | 51 | // Check if it was stored 52 | const retrievedNode = await service.getNodeInfo('n8n-nodes-base.If'); 53 | console.log(' Retrieved node:', retrievedNode ? 'Found' : 'Not found'); 54 | 55 | } catch (error) { 56 | console.error('❌ Test failed:', error); 57 | } finally { 58 | service.close(); 59 | } 60 | } 61 | 62 | testSmallRebuild(); ``` -------------------------------------------------------------------------------- /src/mcp/workflow-examples.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Example workflows for n8n AI agents to understand the structure 3 | */ 4 | 5 | export const MINIMAL_WORKFLOW_EXAMPLE = { 6 | nodes: [ 7 | { 8 | name: "Webhook", 9 | type: "n8n-nodes-base.webhook", 10 | typeVersion: 2, 11 | position: [250, 300], 12 | parameters: { 13 | httpMethod: "POST", 14 | path: "webhook" 15 | } 16 | } 17 | ], 18 | connections: {} 19 | }; 20 | 21 | export const SIMPLE_WORKFLOW_EXAMPLE = { 22 | nodes: [ 23 | { 24 | name: "Webhook", 25 | type: "n8n-nodes-base.webhook", 26 | typeVersion: 2, 27 | position: [250, 300], 28 | parameters: { 29 | httpMethod: "POST", 30 | path: "webhook" 31 | } 32 | }, 33 | { 34 | name: "Set", 35 | type: "n8n-nodes-base.set", 36 | typeVersion: 2, 37 | position: [450, 300], 38 | parameters: { 39 | mode: "manual", 40 | assignments: { 41 | assignments: [ 42 | { 43 | name: "message", 44 | type: "string", 45 | value: "Hello" 46 | } 47 | ] 48 | } 49 | } 50 | }, 51 | { 52 | name: "Respond to Webhook", 53 | type: "n8n-nodes-base.respondToWebhook", 54 | typeVersion: 1, 55 | position: [650, 300], 56 | parameters: { 57 | respondWith: "firstIncomingItem" 58 | } 59 | } 60 | ], 61 | connections: { 62 | "Webhook": { 63 | "main": [ 64 | [ 65 | { 66 | "node": "Set", 67 | "type": "main", 68 | "index": 0 69 | } 70 | ] 71 | ] 72 | }, 73 | "Set": { 74 | "main": [ 75 | [ 76 | { 77 | "node": "Respond to Webhook", 78 | "type": "main", 79 | "index": 0 80 | } 81 | ] 82 | ] 83 | } 84 | } 85 | }; 86 | 87 | export function getWorkflowExampleString(): string { 88 | return `Example workflow structure: 89 | ${JSON.stringify(MINIMAL_WORKFLOW_EXAMPLE, null, 2)} 90 | 91 | Each node MUST have: 92 | - name: unique string identifier 93 | - type: full node type with prefix (e.g., "n8n-nodes-base.webhook") 94 | - typeVersion: number (usually 1 or 2) 95 | - position: [x, y] coordinates array 96 | - parameters: object with node-specific settings 97 | 98 | Connections format: 99 | { 100 | "SourceNodeName": { 101 | "main": [ 102 | [ 103 | { 104 | "node": "TargetNodeName", 105 | "type": "main", 106 | "index": 0 107 | } 108 | ] 109 | ] 110 | } 111 | }`; 112 | } ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/workflow_management/n8n-get-workflow-minimal.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolDocumentation } from '../types'; 2 | 3 | export const n8nGetWorkflowMinimalDoc: ToolDocumentation = { 4 | name: 'n8n_get_workflow_minimal', 5 | category: 'workflow_management', 6 | essentials: { 7 | description: 'Get minimal info: ID, name, active status, tags. Fast for listings.', 8 | keyParameters: ['id'], 9 | example: 'n8n_get_workflow_minimal({id: "workflow_123"})', 10 | performance: 'Very fast (<50ms)', 11 | tips: [ 12 | 'Fastest way to check workflow exists', 13 | 'Perfect for status checks', 14 | 'Use in list displays' 15 | ] 16 | }, 17 | full: { 18 | description: 'Retrieves only essential workflow information without nodes or connections. Returns minimal data needed for listings, status checks, and quick lookups. Optimized for performance when full workflow data is not needed.', 19 | parameters: { 20 | id: { type: 'string', required: true, description: 'Workflow ID to retrieve minimal info for' } 21 | }, 22 | returns: 'Minimal workflow object with: id, name, active status, tags array, createdAt, updatedAt. No nodes, connections, or settings included.', 23 | examples: [ 24 | 'n8n_get_workflow_minimal({id: "abc123"}) - Quick existence check', 25 | 'const info = n8n_get_workflow_minimal({id: "xyz789"}); // Check if active' 26 | ], 27 | useCases: [ 28 | 'Quick workflow existence checks', 29 | 'Display workflow lists', 30 | 'Check active/inactive status', 31 | 'Get workflow tags', 32 | 'Performance-critical operations' 33 | ], 34 | performance: 'Extremely fast - typically under 50ms. Returns only database metadata without loading workflow definition.', 35 | bestPractices: [ 36 | 'Use for list displays and dashboards', 37 | 'Ideal for existence checks before operations', 38 | 'Cache results for UI responsiveness', 39 | 'Combine with list_workflows for bulk checks' 40 | ], 41 | pitfalls: [ 42 | 'Requires N8N_API_URL and N8N_API_KEY configured', 43 | 'No workflow content - cannot edit or validate', 44 | 'Tags may be empty array', 45 | 'Must use get_workflow for actual workflow data' 46 | ], 47 | relatedTools: ['n8n_list_workflows', 'n8n_get_workflow', 'n8n_get_workflow_structure', 'n8n_update_partial_workflow'] 48 | } 49 | }; ``` -------------------------------------------------------------------------------- /scripts/test-docker-optimization.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | # Test script to verify Docker optimization (no n8n deps) 3 | 4 | set -e 5 | 6 | echo "🧪 Testing Docker optimization..." 7 | echo "" 8 | 9 | # Check if nodes.db exists 10 | if [ ! -f "data/nodes.db" ]; then 11 | echo "❌ ERROR: data/nodes.db not found!" 12 | echo " Run 'npm run rebuild' first to create the database" 13 | exit 1 14 | fi 15 | 16 | # Build the image 17 | echo "📦 Building Docker image..." 18 | DOCKER_BUILDKIT=1 docker build -t n8n-mcp:test . > /dev/null 2>&1 19 | 20 | # Check image size 21 | echo "📊 Checking image size..." 22 | SIZE=$(docker images n8n-mcp:test --format "{{.Size}}") 23 | echo " Image size: $SIZE" 24 | 25 | # Test that n8n is NOT in the image 26 | echo "" 27 | echo "🔍 Verifying no n8n dependencies..." 28 | if docker run --rm n8n-mcp:test sh -c "ls node_modules | grep -E '^n8n$|^n8n-|^@n8n'" 2>/dev/null; then 29 | echo "❌ ERROR: Found n8n dependencies in runtime image!" 30 | exit 1 31 | else 32 | echo "✅ No n8n dependencies found (as expected)" 33 | fi 34 | 35 | # Test that runtime dependencies ARE present 36 | echo "" 37 | echo "🔍 Verifying runtime dependencies..." 38 | EXPECTED_DEPS=("@modelcontextprotocol" "better-sqlite3" "express" "dotenv") 39 | for dep in "${EXPECTED_DEPS[@]}"; do 40 | if docker run --rm n8n-mcp:test sh -c "ls node_modules | grep -q '$dep'" 2>/dev/null; then 41 | echo "✅ Found: $dep" 42 | else 43 | echo "❌ Missing: $dep" 44 | exit 1 45 | fi 46 | done 47 | 48 | # Test that the server starts 49 | echo "" 50 | echo "🚀 Testing server startup..." 51 | docker run --rm -d \ 52 | --name n8n-mcp-test \ 53 | -e MCP_MODE=http \ 54 | -e AUTH_TOKEN=test-token \ 55 | -e LOG_LEVEL=error \ 56 | n8n-mcp:test > /dev/null 2>&1 57 | 58 | # Wait for startup 59 | sleep 3 60 | 61 | # Check if running 62 | if docker ps | grep -q n8n-mcp-test; then 63 | echo "✅ Server started successfully" 64 | docker stop n8n-mcp-test > /dev/null 2>&1 65 | else 66 | echo "❌ Server failed to start" 67 | docker logs n8n-mcp-test 2>&1 68 | exit 1 69 | fi 70 | 71 | # Clean up 72 | docker rmi n8n-mcp:test > /dev/null 2>&1 73 | 74 | echo "" 75 | echo "🎉 All tests passed! Docker optimization is working correctly." 76 | echo "" 77 | echo "📈 Benefits:" 78 | echo " - No n8n dependencies in runtime image" 79 | echo " - Image size: ~200MB (vs ~1.5GB with n8n)" 80 | echo " - Build time: ~1-2 minutes (vs ~12 minutes)" 81 | echo " - No version conflicts at runtime" ``` -------------------------------------------------------------------------------- /.github/BENCHMARK_THRESHOLDS.md: -------------------------------------------------------------------------------- ```markdown 1 | # Performance Benchmark Thresholds 2 | 3 | This file defines the expected performance thresholds for n8n-mcp operations. 4 | 5 | ## Critical Operations 6 | 7 | | Operation | Expected Time | Warning Threshold | Error Threshold | 8 | |-----------|---------------|-------------------|-----------------| 9 | | Node Loading (per package) | <100ms | 150ms | 200ms | 10 | | Database Query (simple) | <5ms | 10ms | 20ms | 11 | | Search (simple word) | <10ms | 20ms | 50ms | 12 | | Search (complex query) | <50ms | 100ms | 200ms | 13 | | Validation (simple config) | <1ms | 2ms | 5ms | 14 | | Validation (complex config) | <10ms | 20ms | 50ms | 15 | | MCP Tool Execution | <50ms | 100ms | 200ms | 16 | 17 | ## Benchmark Categories 18 | 19 | ### Node Loading Performance 20 | - **loadPackage**: Should handle large packages efficiently 21 | - **loadNodesFromPath**: Individual file loading should be fast 22 | - **parsePackageJson**: JSON parsing overhead should be minimal 23 | 24 | ### Database Query Performance 25 | - **getNodeByType**: Direct lookups should be instant 26 | - **searchNodes**: Full-text search should scale well 27 | - **getAllNodes**: Pagination should prevent performance issues 28 | 29 | ### Search Operations 30 | - **OR mode**: Should handle multiple terms efficiently 31 | - **AND mode**: More restrictive but still performant 32 | - **FUZZY mode**: Slower but acceptable for typo tolerance 33 | 34 | ### Validation Performance 35 | - **minimal profile**: Fastest, only required fields 36 | - **ai-friendly profile**: Balanced performance 37 | - **strict profile**: Comprehensive but slower 38 | 39 | ### MCP Tool Execution 40 | - Tools should respond quickly for interactive use 41 | - Complex operations may take longer but should remain responsive 42 | 43 | ## Regression Detection 44 | 45 | Performance regressions are detected when: 46 | 1. Any operation exceeds its warning threshold by 10% 47 | 2. Multiple operations show degradation in the same category 48 | 3. Average performance across all benchmarks degrades by 5% 49 | 50 | ## Optimization Targets 51 | 52 | Future optimization efforts should focus on: 53 | 1. **Search performance**: Implement FTS5 for better full-text search 54 | 2. **Caching**: Add intelligent caching for frequently accessed nodes 55 | 3. **Lazy loading**: Defer loading of large property schemas 56 | 4. **Batch operations**: Optimize bulk inserts and updates ``` -------------------------------------------------------------------------------- /scripts/test-fuzzy-fix.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | import { N8NDocumentationMCPServer } from '../src/mcp/server'; 4 | 5 | async function testFuzzyFix() { 6 | console.log('Testing FUZZY mode fix...\n'); 7 | 8 | const server = new N8NDocumentationMCPServer(); 9 | 10 | // Wait for initialization 11 | await new Promise(resolve => setTimeout(resolve, 1000)); 12 | 13 | // Test 1: FUZZY mode with typo 14 | console.log('Test 1: FUZZY mode with "slak" (typo for "slack")'); 15 | const fuzzyResult = await server.executeTool('search_nodes', { 16 | query: 'slak', 17 | mode: 'FUZZY', 18 | limit: 5 19 | }); 20 | 21 | console.log(`Results: ${fuzzyResult.results.length} found`); 22 | if (fuzzyResult.results.length > 0) { 23 | console.log('✅ FUZZY mode now finds results!'); 24 | fuzzyResult.results.forEach((node: any, i: number) => { 25 | console.log(` ${i + 1}. ${node.nodeType} - ${node.displayName}`); 26 | }); 27 | } else { 28 | console.log('❌ FUZZY mode still not working'); 29 | } 30 | 31 | // Test 2: AND mode with explanation 32 | console.log('\n\nTest 2: AND mode with "send message"'); 33 | const andResult = await server.executeTool('search_nodes', { 34 | query: 'send message', 35 | mode: 'AND', 36 | limit: 5 37 | }); 38 | 39 | console.log(`Results: ${andResult.results.length} found`); 40 | if (andResult.searchInfo) { 41 | console.log('✅ AND mode now includes search info:'); 42 | console.log(` ${andResult.searchInfo.message}`); 43 | console.log(` Tip: ${andResult.searchInfo.tip}`); 44 | } 45 | 46 | console.log('\nFirst 5 results:'); 47 | andResult.results.slice(0, 5).forEach((node: any, i: number) => { 48 | console.log(` ${i + 1}. ${node.nodeType} - ${node.displayName}`); 49 | }); 50 | 51 | // Test 3: More typos 52 | console.log('\n\nTest 3: More FUZZY tests'); 53 | const typos = ['htpp', 'webook', 'slck', 'emial']; 54 | 55 | for (const typo of typos) { 56 | const result = await server.executeTool('search_nodes', { 57 | query: typo, 58 | mode: 'FUZZY', 59 | limit: 1 60 | }); 61 | 62 | if (result.results.length > 0) { 63 | console.log(`✅ "${typo}" → ${result.results[0].displayName}`); 64 | } else { 65 | console.log(`❌ "${typo}" → No results`); 66 | } 67 | } 68 | 69 | process.exit(0); 70 | } 71 | 72 | // Run tests 73 | testFuzzyFix().catch(error => { 74 | console.error('Test failed:', error); 75 | process.exit(1); 76 | }); ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/workflow_management/n8n-get-workflow-structure.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolDocumentation } from '../types'; 2 | 3 | export const n8nGetWorkflowStructureDoc: ToolDocumentation = { 4 | name: 'n8n_get_workflow_structure', 5 | category: 'workflow_management', 6 | essentials: { 7 | description: 'Get workflow structure: nodes and connections only. No parameter details.', 8 | keyParameters: ['id'], 9 | example: 'n8n_get_workflow_structure({id: "workflow_123"})', 10 | performance: 'Fast (75-150ms)', 11 | tips: [ 12 | 'Shows workflow topology', 13 | 'Node types without parameters', 14 | 'Perfect for visualization' 15 | ] 16 | }, 17 | full: { 18 | description: 'Retrieves workflow structural information including node types, positions, and connections, but without detailed node parameters. Ideal for understanding workflow topology, creating visualizations, or analyzing workflow complexity without the overhead of full parameter data.', 19 | parameters: { 20 | id: { type: 'string', required: true, description: 'Workflow ID to retrieve structure for' } 21 | }, 22 | returns: 'Workflow structure with: id, name, nodes array (id, name, type, position only), connections object. No node parameters, credentials, or settings included.', 23 | examples: [ 24 | 'n8n_get_workflow_structure({id: "abc123"}) - Visualize workflow', 25 | 'const structure = n8n_get_workflow_structure({id: "xyz789"}); // Analyze complexity' 26 | ], 27 | useCases: [ 28 | 'Generate workflow visualizations', 29 | 'Analyze workflow complexity', 30 | 'Understand node relationships', 31 | 'Create workflow diagrams', 32 | 'Quick topology validation' 33 | ], 34 | performance: 'Fast retrieval - typically 75-150ms. Faster than get_workflow as parameters are stripped.', 35 | bestPractices: [ 36 | 'Use for visualization tools', 37 | 'Ideal for workflow analysis', 38 | 'Good for connection validation', 39 | 'Cache for UI diagram rendering' 40 | ], 41 | pitfalls: [ 42 | 'Requires N8N_API_URL and N8N_API_KEY configured', 43 | 'No parameter data for configuration', 44 | 'Cannot validate node settings', 45 | 'Must use get_workflow for editing' 46 | ], 47 | relatedTools: ['n8n_get_workflow', 'n8n_validate_workflow_connections', 'n8n_get_workflow_minimal', 'validate_workflow_connections'] 48 | } 49 | }; ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/workflow_management/n8n-get-workflow.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolDocumentation } from '../types'; 2 | 3 | export const n8nGetWorkflowDoc: ToolDocumentation = { 4 | name: 'n8n_get_workflow', 5 | category: 'workflow_management', 6 | essentials: { 7 | description: 'Get a workflow by ID. Returns the complete workflow including nodes, connections, and settings.', 8 | keyParameters: ['id'], 9 | example: 'n8n_get_workflow({id: "workflow_123"})', 10 | performance: 'Fast (50-200ms)', 11 | tips: [ 12 | 'Returns complete workflow JSON', 13 | 'Includes all node parameters', 14 | 'Use get_workflow_minimal for faster listings' 15 | ] 16 | }, 17 | full: { 18 | description: 'Retrieves a complete workflow from n8n by its ID. Returns full workflow definition including all nodes with their parameters, connections between nodes, and workflow settings. This is the primary tool for fetching workflows for viewing, editing, or cloning.', 19 | parameters: { 20 | id: { type: 'string', required: true, description: 'Workflow ID to retrieve' } 21 | }, 22 | returns: 'Complete workflow object containing: id, name, active status, nodes array (with full parameters), connections object, settings, createdAt, updatedAt', 23 | examples: [ 24 | 'n8n_get_workflow({id: "abc123"}) - Get workflow for editing', 25 | 'const wf = n8n_get_workflow({id: "xyz789"}); // Clone workflow structure' 26 | ], 27 | useCases: [ 28 | 'View workflow configuration', 29 | 'Export workflow for backup', 30 | 'Clone workflow structure', 31 | 'Debug workflow issues', 32 | 'Prepare for updates' 33 | ], 34 | performance: 'Fast retrieval - typically 50-200ms depending on workflow size. Cached by n8n for performance.', 35 | bestPractices: [ 36 | 'Check workflow exists before updating', 37 | 'Use for complete workflow data needs', 38 | 'Cache results when making multiple operations', 39 | 'Validate after retrieving if modifying' 40 | ], 41 | pitfalls: [ 42 | 'Requires N8N_API_URL and N8N_API_KEY configured', 43 | 'Returns all data - use minimal/structure for performance', 44 | 'Workflow must exist or returns 404', 45 | 'Credentials are referenced but not included' 46 | ], 47 | relatedTools: ['n8n_get_workflow_minimal', 'n8n_get_workflow_structure', 'n8n_update_full_workflow', 'n8n_validate_workflow'] 48 | } 49 | }; ``` -------------------------------------------------------------------------------- /tests/unit/database/__mocks__/better-sqlite3.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { vi } from 'vitest'; 2 | 3 | export class MockDatabase { 4 | private data = new Map<string, any[]>(); 5 | private prepared = new Map<string, any>(); 6 | public inTransaction = false; 7 | 8 | constructor() { 9 | this.data.set('nodes', []); 10 | this.data.set('templates', []); 11 | this.data.set('tools_documentation', []); 12 | } 13 | 14 | prepare(sql: string) { 15 | const key = this.extractTableName(sql); 16 | const self = this; 17 | 18 | return { 19 | all: vi.fn(() => self.data.get(key) || []), 20 | get: vi.fn((id: string) => { 21 | const items = self.data.get(key) || []; 22 | return items.find(item => item.id === id); 23 | }), 24 | run: vi.fn((params: any) => { 25 | const items = self.data.get(key) || []; 26 | items.push(params); 27 | self.data.set(key, items); 28 | return { changes: 1, lastInsertRowid: items.length }; 29 | }), 30 | iterate: vi.fn(function* () { 31 | const items = self.data.get(key) || []; 32 | for (const item of items) { 33 | yield item; 34 | } 35 | }), 36 | pluck: vi.fn(function(this: any) { return this; }), 37 | expand: vi.fn(function(this: any) { return this; }), 38 | raw: vi.fn(function(this: any) { return this; }), 39 | columns: vi.fn(() => []), 40 | bind: vi.fn(function(this: any) { return this; }) 41 | }; 42 | } 43 | 44 | exec(sql: string) { 45 | // Mock schema creation 46 | return true; 47 | } 48 | 49 | close() { 50 | // Mock close 51 | return true; 52 | } 53 | 54 | pragma(key: string, value?: any) { 55 | // Mock pragma 56 | if (key === 'journal_mode' && value === 'WAL') { 57 | return 'wal'; 58 | } 59 | return null; 60 | } 61 | 62 | transaction<T>(fn: () => T): T { 63 | this.inTransaction = true; 64 | try { 65 | const result = fn(); 66 | this.inTransaction = false; 67 | return result; 68 | } catch (error) { 69 | this.inTransaction = false; 70 | throw error; 71 | } 72 | } 73 | 74 | // Helper to extract table name from SQL 75 | private extractTableName(sql: string): string { 76 | const match = sql.match(/FROM\s+(\w+)|INTO\s+(\w+)|UPDATE\s+(\w+)/i); 77 | return match ? (match[1] || match[2] || match[3]) : 'nodes'; 78 | } 79 | 80 | // Test helper to seed data 81 | _seedData(table: string, data: any[]) { 82 | this.data.set(table, data); 83 | } 84 | } 85 | 86 | export default vi.fn(() => new MockDatabase()); ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/workflow_management/n8n-get-workflow-details.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ToolDocumentation } from '../types'; 2 | 3 | export const n8nGetWorkflowDetailsDoc: ToolDocumentation = { 4 | name: 'n8n_get_workflow_details', 5 | category: 'workflow_management', 6 | essentials: { 7 | description: 'Get workflow details with metadata, version, execution stats. More info than get_workflow.', 8 | keyParameters: ['id'], 9 | example: 'n8n_get_workflow_details({id: "workflow_123"})', 10 | performance: 'Fast (100-300ms)', 11 | tips: [ 12 | 'Includes execution statistics', 13 | 'Shows version history info', 14 | 'Contains metadata like tags' 15 | ] 16 | }, 17 | full: { 18 | description: 'Retrieves comprehensive workflow details including metadata, execution statistics, version information, and usage analytics. Provides more information than get_workflow, including data not typically needed for editing but useful for monitoring and analysis.', 19 | parameters: { 20 | id: { type: 'string', required: true, description: 'Workflow ID to retrieve details for' } 21 | }, 22 | returns: 'Extended workflow object with: id, name, nodes, connections, settings, plus metadata (tags, owner, shared users), execution stats (success/error counts, average runtime), version info, created/updated timestamps', 23 | examples: [ 24 | 'n8n_get_workflow_details({id: "abc123"}) - Get workflow with stats', 25 | 'const details = n8n_get_workflow_details({id: "xyz789"}); // Analyze performance' 26 | ], 27 | useCases: [ 28 | 'Monitor workflow performance', 29 | 'Analyze execution patterns', 30 | 'View workflow metadata', 31 | 'Check version information', 32 | 'Audit workflow usage' 33 | ], 34 | performance: 'Slightly slower than get_workflow due to additional metadata - typically 100-300ms. Stats may be cached.', 35 | bestPractices: [ 36 | 'Use for monitoring and analysis', 37 | 'Check execution stats before optimization', 38 | 'Review error counts for debugging', 39 | 'Monitor average execution times' 40 | ], 41 | pitfalls: [ 42 | 'Requires N8N_API_URL and N8N_API_KEY configured', 43 | 'More data than needed for simple edits', 44 | 'Stats may have slight delay', 45 | 'Not all n8n versions support all fields' 46 | ], 47 | relatedTools: ['n8n_get_workflow', 'n8n_list_executions', 'n8n_get_execution', 'n8n_list_workflows'] 48 | } 49 | }; ``` -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { defineConfig } from 'vitest/config'; 2 | import path from 'path'; 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true, 7 | environment: 'node', 8 | // Only include global-setup.ts, remove msw-setup.ts from global setup 9 | setupFiles: ['./tests/setup/global-setup.ts'], 10 | // Load environment variables from .env.test 11 | env: { 12 | NODE_ENV: 'test' 13 | }, 14 | // Test execution settings 15 | pool: 'threads', 16 | poolOptions: { 17 | threads: { 18 | singleThread: process.env.TEST_PARALLEL !== 'true', 19 | maxThreads: parseInt(process.env.TEST_MAX_WORKERS || '4', 10), 20 | minThreads: 1 21 | } 22 | }, 23 | // Retry configuration 24 | retry: parseInt(process.env.TEST_RETRY_ATTEMPTS || '2', 10), 25 | // Test reporter - reduce reporters in CI to prevent hanging 26 | reporters: process.env.CI ? ['default', 'junit'] : ['default'], 27 | outputFile: { 28 | junit: './test-results/junit.xml' 29 | }, 30 | coverage: { 31 | provider: 'v8', 32 | enabled: process.env.FEATURE_TEST_COVERAGE !== 'false', 33 | reporter: process.env.CI ? ['lcov', 'text-summary'] : (process.env.COVERAGE_REPORTER || 'lcov,html,text-summary').split(','), 34 | reportsDirectory: process.env.COVERAGE_DIR || './coverage', 35 | exclude: [ 36 | 'node_modules/', 37 | 'tests/', 38 | '**/*.d.ts', 39 | '**/*.test.ts', 40 | '**/*.spec.ts', 41 | 'scripts/', 42 | 'dist/', 43 | '**/test-*.ts', 44 | '**/mock-*.ts', 45 | '**/__mocks__/**' 46 | ], 47 | thresholds: { 48 | lines: 80, 49 | functions: 80, 50 | branches: 75, 51 | statements: 80 52 | }, 53 | // Add coverage-specific settings to prevent hanging 54 | all: false, // Don't collect coverage for untested files 55 | skipFull: true // Skip files with 100% coverage 56 | }, 57 | // Test isolation 58 | isolate: true, 59 | // Force exit after tests complete in CI to prevent hanging 60 | forceRerunTriggers: ['**/tests/**/*.ts'], 61 | teardownTimeout: 1000 62 | }, 63 | resolve: { 64 | alias: { 65 | '@': path.resolve(__dirname, './src'), 66 | '@tests': path.resolve(__dirname, './tests') 67 | } 68 | }, 69 | // TypeScript configuration 70 | esbuild: { 71 | target: 'node18' 72 | }, 73 | // Define global constants 74 | define: { 75 | 'process.env.TEST_ENVIRONMENT': JSON.stringify('true') 76 | } 77 | }); ``` -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- ```markdown 1 | # Privacy Policy for n8n-mcp Telemetry 2 | 3 | ## Overview 4 | n8n-mcp collects anonymous usage statistics to help improve the tool. This data collection is designed to respect user privacy while providing valuable insights into how the tool is used. 5 | 6 | ## What We Collect 7 | - **Anonymous User ID**: A hashed identifier derived from your machine characteristics (no personal information) 8 | - **Tool Usage**: Which MCP tools are used and their performance metrics 9 | - **Workflow Patterns**: Sanitized workflow structures (all sensitive data removed) 10 | - **Error Types**: Categories of errors encountered (no error messages with user data) 11 | - **System Information**: Platform, architecture, Node.js version, and n8n-mcp version 12 | 13 | ## What We DON'T Collect 14 | - Personal information or usernames 15 | - API keys, tokens, or credentials 16 | - URLs, endpoints, or hostnames 17 | - Email addresses or contact information 18 | - File paths or directory structures 19 | - Actual workflow data or parameters 20 | - Database connection strings 21 | - Any authentication information 22 | 23 | ## Data Sanitization 24 | All collected data undergoes automatic sanitization: 25 | - URLs are replaced with `[URL]` or `[REDACTED]` 26 | - Long alphanumeric strings (potential keys) are replaced with `[KEY]` 27 | - Email addresses are replaced with `[EMAIL]` 28 | - Authentication-related fields are completely removed 29 | 30 | ## Data Storage 31 | - Data is stored securely using Supabase 32 | - Anonymous users have write-only access (cannot read data back) 33 | - Row Level Security (RLS) policies prevent data access by anonymous users 34 | 35 | ## Opt-Out 36 | You can disable telemetry at any time: 37 | ```bash 38 | npx n8n-mcp telemetry disable 39 | ``` 40 | 41 | To re-enable: 42 | ```bash 43 | npx n8n-mcp telemetry enable 44 | ``` 45 | 46 | To check status: 47 | ```bash 48 | npx n8n-mcp telemetry status 49 | ``` 50 | 51 | ## Data Usage 52 | Collected data is used solely to: 53 | - Understand which features are most used 54 | - Identify common error patterns 55 | - Improve tool performance and reliability 56 | - Guide development priorities 57 | 58 | ## Data Retention 59 | - Data is retained for analysis purposes 60 | - No personal identification is possible from the collected data 61 | 62 | ## Changes to This Policy 63 | We may update this privacy policy from time to time. Updates will be reflected in this document. 64 | 65 | ## Contact 66 | For questions about telemetry or privacy, please open an issue on GitHub: 67 | https://github.com/czlonkowski/n8n-mcp/issues 68 | 69 | Last updated: 2025-09-25 ``` -------------------------------------------------------------------------------- /scripts/extract-changelog.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Extract changelog content for a specific version 5 | * Used by GitHub Actions to extract release notes 6 | */ 7 | 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | 11 | function extractChangelog(version, changelogPath) { 12 | try { 13 | if (!fs.existsSync(changelogPath)) { 14 | console.error(`Changelog file not found at ${changelogPath}`); 15 | process.exit(1); 16 | } 17 | 18 | const content = fs.readFileSync(changelogPath, 'utf8'); 19 | const lines = content.split('\n'); 20 | 21 | // Find the start of this version's section 22 | const versionHeaderRegex = new RegExp(`^## \\[${version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]`); 23 | let startIndex = -1; 24 | let endIndex = -1; 25 | 26 | for (let i = 0; i < lines.length; i++) { 27 | if (versionHeaderRegex.test(lines[i])) { 28 | startIndex = i; 29 | break; 30 | } 31 | } 32 | 33 | if (startIndex === -1) { 34 | console.error(`No changelog entries found for version ${version}`); 35 | process.exit(1); 36 | } 37 | 38 | // Find the end of this version's section (next version or end of file) 39 | for (let i = startIndex + 1; i < lines.length; i++) { 40 | if (lines[i].startsWith('## [') && !lines[i].includes('Unreleased')) { 41 | endIndex = i; 42 | break; 43 | } 44 | } 45 | 46 | if (endIndex === -1) { 47 | endIndex = lines.length; 48 | } 49 | 50 | // Extract the section content 51 | const sectionLines = lines.slice(startIndex, endIndex); 52 | 53 | // Remove the version header and any trailing empty lines 54 | let contentLines = sectionLines.slice(1); 55 | while (contentLines.length > 0 && contentLines[contentLines.length - 1].trim() === '') { 56 | contentLines.pop(); 57 | } 58 | 59 | if (contentLines.length === 0) { 60 | console.error(`No content found for version ${version}`); 61 | process.exit(1); 62 | } 63 | 64 | const releaseNotes = contentLines.join('\n').trim(); 65 | 66 | // Write to stdout for GitHub Actions 67 | console.log(releaseNotes); 68 | 69 | } catch (error) { 70 | console.error(`Error extracting changelog: ${error.message}`); 71 | process.exit(1); 72 | } 73 | } 74 | 75 | // Parse command line arguments 76 | const version = process.argv[2]; 77 | const changelogPath = process.argv[3]; 78 | 79 | if (!version || !changelogPath) { 80 | console.error('Usage: extract-changelog.js <version> <changelog-path>'); 81 | process.exit(1); 82 | } 83 | 84 | extractChangelog(version, changelogPath); ```