This is page 2 of 46. Use http://codebase.md/czlonkowski/n8n-mcp?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 -------------------------------------------------------------------------------- /docs/WINDSURF_SETUP.md: -------------------------------------------------------------------------------- ```markdown # Windsurf Setup Connect n8n-MCP to Windsurf IDE for enhanced n8n workflow development with AI assistance. [](https://www.youtube.com/watch?v=klxxT1__izg) ## Video Tutorial Watch the complete setup process: [n8n-MCP Windsurf Setup Tutorial](https://www.youtube.com/watch?v=klxxT1__izg) ## Setup Process ### 1. Access MCP Configuration 1. Go to Settings in Windsurf 2. Navigate to Windsurf Settings 3. Go to MCP Servers > Manage Plugins 4. Click "View Raw Config" ### 2. Add n8n-MCP Configuration Copy the configuration from this repository and add it to your MCP config: **Basic configuration (documentation tools only):** ```json { "mcpServers": { "n8n-mcp": { "command": "npx", "args": ["n8n-mcp"], "env": { "MCP_MODE": "stdio", "LOG_LEVEL": "error", "DISABLE_CONSOLE_OUTPUT": "true" } } } } ``` **Full configuration (with n8n management tools):** ```json { "mcpServers": { "n8n-mcp": { "command": "npx", "args": ["n8n-mcp"], "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" } } } } ``` ### 3. Configure n8n Connection 1. Replace `https://your-n8n-instance.com` with your actual n8n URL 2. Replace `your-api-key` with your n8n API key 3. Click refresh to apply the changes ### 4. Set Up Project Instructions 1. Create a `.windsurfrules` file in your project root 2. Copy the Claude Project instructions from the [main README's Claude Project Setup section](../README.md#-claude-project-setup) ``` -------------------------------------------------------------------------------- /scripts/test-workflow-tracking-debug.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env npx tsx /** * Debug workflow tracking in telemetry manager */ import { TelemetryManager } from '../src/telemetry/telemetry-manager'; // Get the singleton instance const telemetry = TelemetryManager.getInstance(); const testWorkflow = { nodes: [ { id: 'webhook1', type: 'n8n-nodes-base.webhook', name: 'Webhook', position: [0, 0], parameters: { path: '/test-' + Date.now(), httpMethod: 'POST' } }, { id: 'http1', type: 'n8n-nodes-base.httpRequest', name: 'HTTP Request', position: [250, 0], parameters: { url: 'https://api.example.com/data', method: 'GET' } }, { id: 'slack1', type: 'n8n-nodes-base.slack', name: 'Slack', position: [500, 0], parameters: { channel: '#general', text: 'Workflow complete!' } } ], connections: { 'webhook1': { main: [[{ node: 'http1', type: 'main', index: 0 }]] }, 'http1': { main: [[{ node: 'slack1', type: 'main', index: 0 }]] } } }; console.log('🧪 Testing Workflow Tracking\n'); console.log('Workflow has', testWorkflow.nodes.length, 'nodes'); // Track the workflow console.log('Calling trackWorkflowCreation...'); telemetry.trackWorkflowCreation(testWorkflow, true); console.log('Waiting for async processing...'); // Wait for setImmediate to process setTimeout(async () => { console.log('\nForcing flush...'); await telemetry.flush(); console.log('✅ Flush complete!'); console.log('\nWorkflow should now be in the telemetry_workflows table.'); console.log('Check with: SELECT * FROM telemetry_workflows ORDER BY created_at DESC LIMIT 1;'); }, 2000); ``` -------------------------------------------------------------------------------- /scripts/test-workflow-sanitizer.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env npx tsx /** * Test workflow sanitizer */ import { WorkflowSanitizer } from '../src/telemetry/workflow-sanitizer'; const testWorkflow = { nodes: [ { id: 'webhook1', type: 'n8n-nodes-base.webhook', name: 'Webhook', position: [0, 0], parameters: { path: '/test-webhook', httpMethod: 'POST' } }, { id: 'http1', type: 'n8n-nodes-base.httpRequest', name: 'HTTP Request', position: [250, 0], parameters: { url: 'https://api.example.com/endpoint', method: 'GET', authentication: 'genericCredentialType', sendHeaders: true, headerParameters: { parameters: [ { name: 'Authorization', value: 'Bearer sk-1234567890abcdef' } ] } } } ], connections: { 'webhook1': { main: [[{ node: 'http1', type: 'main', index: 0 }]] } } }; console.log('🧪 Testing Workflow Sanitizer\n'); console.log('Original workflow has', testWorkflow.nodes.length, 'nodes'); try { const sanitized = WorkflowSanitizer.sanitizeWorkflow(testWorkflow); console.log('\n✅ Sanitization successful!'); console.log('\nSanitized output:'); console.log(JSON.stringify(sanitized, null, 2)); console.log('\n📊 Metrics:'); console.log('- Workflow Hash:', sanitized.workflowHash); console.log('- Node Count:', sanitized.nodeCount); console.log('- Node Types:', sanitized.nodeTypes); console.log('- Has Trigger:', sanitized.hasTrigger); console.log('- Has Webhook:', sanitized.hasWebhook); console.log('- Complexity:', sanitized.complexity); } catch (error) { console.error('❌ Sanitization failed:', error); } ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/templates/list-tasks.ts: -------------------------------------------------------------------------------- ```typescript import { ToolDocumentation } from '../types'; export const listTasksDoc: ToolDocumentation = { name: 'list_tasks', category: 'templates', essentials: { description: 'List task templates by category: HTTP/API, Webhooks, Database, AI, Data Processing, Communication.', keyParameters: ['category'], example: 'list_tasks({category: "HTTP/API"})', performance: 'Instant', tips: [ 'Categories: HTTP/API, Webhooks, Database, AI', 'Shows pre-configured node settings', 'Use get_node_for_task for details' ] }, full: { 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.', parameters: { category: { type: 'string', description: 'Filter by category (optional)' } }, returns: 'Array of tasks with name, category, description, nodeType', examples: [ 'list_tasks() - Get all task templates', 'list_tasks({category: "Database"}) - Database-related tasks', 'list_tasks({category: "AI"}) - AI automation tasks' ], useCases: [ 'Discover common automation patterns', 'Find pre-configured solutions', 'Learn node usage patterns', 'Quick workflow setup' ], performance: 'Instant - Static task list', bestPractices: [ 'Browse all categories first', 'Use get_node_for_task for config', 'Combine multiple tasks in workflows' ], pitfalls: [ 'Tasks are templates, customize as needed', 'Not all nodes have task templates' ], relatedTools: ['get_node_for_task', 'search_templates', 'get_templates_for_task'] } }; ``` -------------------------------------------------------------------------------- /scripts/test-node-info.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node /** * Test get_node_info to diagnose timeout issues */ const { N8NDocumentationMCPServer } = require('../dist/mcp/server'); async function testNodeInfo() { console.log('🔍 Testing get_node_info...\n'); try { const server = new N8NDocumentationMCPServer(); await new Promise(resolve => setTimeout(resolve, 500)); const nodes = [ 'nodes-base.httpRequest', 'nodes-base.webhook', 'nodes-langchain.agent' ]; for (const nodeType of nodes) { console.log(`Testing ${nodeType}...`); const start = Date.now(); try { const result = await server.executeTool('get_node_info', { nodeType }); const elapsed = Date.now() - start; const size = JSON.stringify(result).length; console.log(`✅ Success in ${elapsed}ms`); console.log(` Size: ${(size / 1024).toFixed(1)}KB`); console.log(` Properties: ${result.properties?.length || 0}`); console.log(` Operations: ${result.operations?.length || 0}`); // Check for issues if (size > 50000) { console.log(` ⚠️ WARNING: Response over 50KB!`); } // Check property quality const propsWithoutDesc = result.properties?.filter(p => !p.description && !p.displayName).length || 0; if (propsWithoutDesc > 0) { console.log(` ⚠️ ${propsWithoutDesc} properties without descriptions`); } } catch (error) { const elapsed = Date.now() - start; console.log(`❌ Failed after ${elapsed}ms: ${error.message}`); } console.log(''); } } catch (error) { console.error('Fatal error:', error); } } testNodeInfo().catch(console.error); ``` -------------------------------------------------------------------------------- /docs/CURSOR_SETUP.md: -------------------------------------------------------------------------------- ```markdown # Cursor Setup Connect n8n-MCP to Cursor IDE for enhanced n8n workflow development with AI assistance. [](https://www.youtube.com/watch?v=hRmVxzLGJWI) ## Video Tutorial Watch the complete setup process: [n8n-MCP Cursor Setup Tutorial](https://www.youtube.com/watch?v=hRmVxzLGJWI) ## Setup Process ### 1. Create MCP Configuration 1. Create a `.cursor` folder in your project root 2. Create `mcp.json` file inside the `.cursor` folder 3. Copy the configuration from this repository **Basic configuration (documentation tools only):** ```json { "mcpServers": { "n8n-mcp": { "command": "npx", "args": ["n8n-mcp"], "env": { "MCP_MODE": "stdio", "LOG_LEVEL": "error", "DISABLE_CONSOLE_OUTPUT": "true" } } } } ``` **Full configuration (with n8n management tools):** ```json { "mcpServers": { "n8n-mcp": { "command": "npx", "args": ["n8n-mcp"], "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" } } } } ``` ### 2. Configure n8n Connection 1. Replace `https://your-n8n-instance.com` with your actual n8n URL 2. Replace `your-api-key` with your n8n API key ### 3. Enable MCP Server 1. Click "Enable MCP Server" button in Cursor 2. Go to Cursor Settings 3. Search for "mcp" 4. Confirm MCP is working ### 4. Set Up Project Instructions 1. In your Cursor chat, invoke "create rule" and hit Tab 2. Name the rule (e.g., "n8n-mcp") 3. Set rule type to "always" 4. Copy the Claude Project instructions from the [main README's Claude Project Setup section](../README.md#-claude-project-setup) ``` -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- ```json { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:base" ], "schedule": ["after 9am on monday"], "timezone": "UTC", "packageRules": [ { "description": "Group all n8n-related updates", "groupName": "n8n dependencies", "matchPackagePatterns": ["^n8n", "^@n8n/"], "matchUpdateTypes": ["minor", "patch"], "schedule": ["after 9am on monday"] }, { "description": "Require approval for major n8n updates", "matchPackagePatterns": ["^n8n", "^@n8n/"], "matchUpdateTypes": ["major"], "dependencyDashboardApproval": true }, { "description": "Disable updates for other dependencies", "excludePackagePatterns": ["^n8n", "^@n8n/"], "enabled": false } ], "postUpdateOptions": [ "npmDedupe" ], "prConcurrentLimit": 1, "prCreation": "immediate", "labels": ["dependencies", "n8n-update"], "assignees": ["@czlonkowski"], "reviewers": ["@czlonkowski"], "commitMessagePrefix": "chore: ", "commitMessageTopic": "{{depName}}", "commitMessageExtra": "from {{currentVersion}} to {{newVersion}}", "prBodyDefinitions": { "Package": "{{depName}}", "Type": "{{depType}}", "Update": "{{updateType}}", "Current": "{{currentVersion}}", "New": "{{newVersion}}", "Change": "[Compare]({{compareUrl}})" }, "prBodyColumns": ["Package", "Type", "Update", "Current", "New", "Change"], "prBodyNotes": [ "**Important**: Please review the [n8n release notes](https://docs.n8n.io/release-notes/) for breaking changes.", "", "After merging, please:", "1. Run `npm run rebuild` to update the node database", "2. Run `npm run validate` to ensure all nodes are properly loaded", "3. Test critical functionality" ] } ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/configuration/get-node-documentation.ts: -------------------------------------------------------------------------------- ```typescript import { ToolDocumentation } from '../types'; export const getNodeDocumentationDoc: ToolDocumentation = { name: 'get_node_documentation', category: 'configuration', essentials: { description: 'Get readable docs with examples/auth/patterns. Better than raw schema! 87% coverage. Format: "nodes-base.slack"', keyParameters: ['nodeType'], example: 'get_node_documentation({nodeType: "nodes-base.slack"})', performance: 'Fast - pre-parsed', tips: [ '87% coverage', 'Includes auth examples', 'Human-readable format' ] }, full: { 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.', parameters: { nodeType: { type: 'string', required: true, description: 'Full node type with prefix (e.g., "nodes-base.slack")' } }, returns: 'Parsed markdown documentation with examples, authentication guides, common patterns', examples: [ 'get_node_documentation({nodeType: "nodes-base.slack"}) - Slack usage guide', 'get_node_documentation({nodeType: "nodes-base.googleSheets"}) - Sheets examples' ], useCases: [ 'Understanding authentication setup', 'Finding usage examples', 'Learning common patterns' ], performance: 'Fast - Pre-parsed documentation stored in database', bestPractices: [ 'Use for learning node usage', 'Check coverage with get_database_statistics', 'Combine with get_node_essentials' ], pitfalls: [ 'Not all nodes have docs (87% coverage)', 'May be outdated for new features', 'Requires full node type prefix' ], relatedTools: ['get_node_info', 'get_node_essentials', 'search_nodes'] } }; ``` -------------------------------------------------------------------------------- /scripts/test-error-message-tracking.ts: -------------------------------------------------------------------------------- ```typescript /** * Test script to verify error message tracking is working */ import { telemetry } from '../src/telemetry'; async function testErrorTracking() { console.log('=== Testing Error Message Tracking ===\n'); // Track session first console.log('1. Starting session...'); telemetry.trackSessionStart(); // Track an error WITH a message console.log('\n2. Tracking error WITH message:'); const testErrorMessage = 'This is a test error message with sensitive data: password=secret123 and [email protected]'; telemetry.trackError( 'TypeError', 'tool_execution', 'test_tool', testErrorMessage ); console.log(` Original message: "${testErrorMessage}"`); // Track an error WITHOUT a message console.log('\n3. Tracking error WITHOUT message:'); telemetry.trackError( 'Error', 'tool_execution', 'test_tool2' ); // Check the event queue const metrics = telemetry.getMetrics(); console.log('\n4. Telemetry metrics:'); console.log(' Status:', metrics.status); console.log(' Events queued:', metrics.tracking.eventsQueued); // Get raw event queue to inspect const eventTracker = (telemetry as any).eventTracker; const queue = eventTracker.getEventQueue(); console.log('\n5. Event queue contents:'); queue.forEach((event, i) => { console.log(`\n Event ${i + 1}:`); console.log(` - Type: ${event.event}`); console.log(` - Properties:`, JSON.stringify(event.properties, null, 6)); }); // Flush to database console.log('\n6. Flushing to database...'); await telemetry.flush(); console.log('\n7. Done! Check Supabase for error events with "error" field.'); console.log(' Query: SELECT * FROM telemetry_events WHERE event = \'error_occurred\' ORDER BY created_at DESC LIMIT 5;'); } testErrorTracking().catch(console.error); ``` -------------------------------------------------------------------------------- /scripts/sync-runtime-version.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node /** * Sync version from package.json to package.runtime.json and README.md * This ensures all files always have the same version */ const fs = require('fs'); const path = require('path'); const packageJsonPath = path.join(__dirname, '..', 'package.json'); const packageRuntimePath = path.join(__dirname, '..', 'package.runtime.json'); const readmePath = path.join(__dirname, '..', 'README.md'); try { // Read package.json const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); const version = packageJson.version; // Read package.runtime.json const packageRuntime = JSON.parse(fs.readFileSync(packageRuntimePath, 'utf-8')); // Update version if different if (packageRuntime.version !== version) { packageRuntime.version = version; // Write back with proper formatting fs.writeFileSync( packageRuntimePath, JSON.stringify(packageRuntime, null, 2) + '\n', 'utf-8' ); console.log(`✅ Updated package.runtime.json version to ${version}`); } else { console.log(`✓ package.runtime.json already at version ${version}`); } // Update README.md version badge let readmeContent = fs.readFileSync(readmePath, 'utf-8'); const versionBadgeRegex = /(\[!\[Version\]\(https:\/\/img\.shields\.io\/badge\/version-)[^-]+(-.+?\)\])/; const newVersionBadge = `$1${version}$2`; const updatedReadmeContent = readmeContent.replace(versionBadgeRegex, newVersionBadge); if (updatedReadmeContent !== readmeContent) { fs.writeFileSync(readmePath, updatedReadmeContent); console.log(`✅ Updated README.md version badge to ${version}`); } else { console.log(`✓ README.md already has version badge ${version}`); } } catch (error) { console.error('❌ Error syncing version:', error.message); process.exit(1); } ``` -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- ```yaml # docker-compose.yml # For optimized builds with BuildKit, use: docker-compose -f docker-compose.buildkit.yml up version: '3.8' services: n8n-mcp: image: ghcr.io/czlonkowski/n8n-mcp:latest container_name: n8n-mcp restart: unless-stopped # Environment configuration environment: # Mode configuration MCP_MODE: ${MCP_MODE:-http} USE_FIXED_HTTP: ${USE_FIXED_HTTP:-true} # Use fixed implementation for stability AUTH_TOKEN: ${AUTH_TOKEN:?AUTH_TOKEN is required for HTTP mode} # Application settings NODE_ENV: ${NODE_ENV:-production} LOG_LEVEL: ${LOG_LEVEL:-info} PORT: ${PORT:-3000} # Database NODE_DB_PATH: ${NODE_DB_PATH:-/app/data/nodes.db} REBUILD_ON_START: ${REBUILD_ON_START:-false} # Telemetry: Anonymous usage statistics are ENABLED by default # To opt-out, uncomment and set to 'true': # N8N_MCP_TELEMETRY_DISABLED: ${N8N_MCP_TELEMETRY_DISABLED:-true} # Optional: n8n API configuration (enables 16 additional management tools) # Uncomment and configure to enable n8n workflow management # N8N_API_URL: ${N8N_API_URL} # N8N_API_KEY: ${N8N_API_KEY} # N8N_API_TIMEOUT: ${N8N_API_TIMEOUT:-30000} # N8N_API_MAX_RETRIES: ${N8N_API_MAX_RETRIES:-3} # Volumes for persistence volumes: - n8n-mcp-data:/app/data # Port mapping ports: - "${PORT:-3000}:3000" # Resource limits deploy: resources: limits: memory: 512M reservations: memory: 256M # Health check healthcheck: test: ["CMD", "curl", "-f", "http://127.0.0.1:3000/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s # Named volume for data persistence volumes: n8n-mcp-data: driver: local ``` -------------------------------------------------------------------------------- /src/config/n8n-api.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import dotenv from 'dotenv'; import { logger } from '../utils/logger'; // n8n API configuration schema const n8nApiConfigSchema = z.object({ N8N_API_URL: z.string().url().optional(), N8N_API_KEY: z.string().min(1).optional(), N8N_API_TIMEOUT: z.coerce.number().positive().default(30000), N8N_API_MAX_RETRIES: z.coerce.number().positive().default(3), }); // Track if we've loaded env vars let envLoaded = false; // Parse and validate n8n API configuration export function getN8nApiConfig() { // Load environment variables on first access if (!envLoaded) { dotenv.config(); envLoaded = true; } const result = n8nApiConfigSchema.safeParse(process.env); if (!result.success) { return null; } const config = result.data; // Check if both URL and API key are provided if (!config.N8N_API_URL || !config.N8N_API_KEY) { return null; } return { baseUrl: config.N8N_API_URL, apiKey: config.N8N_API_KEY, timeout: config.N8N_API_TIMEOUT, maxRetries: config.N8N_API_MAX_RETRIES, }; } // Helper to check if n8n API is configured (lazy check) export function isN8nApiConfigured(): boolean { const config = getN8nApiConfig(); return config !== null; } /** * Create n8n API configuration from instance context * Used for flexible instance configuration support */ export function getN8nApiConfigFromContext(context: { n8nApiUrl?: string; n8nApiKey?: string; n8nApiTimeout?: number; n8nApiMaxRetries?: number; }): N8nApiConfig | null { if (!context.n8nApiUrl || !context.n8nApiKey) { return null; } return { baseUrl: context.n8nApiUrl, apiKey: context.n8nApiKey, timeout: context.n8nApiTimeout ?? 30000, maxRetries: context.n8nApiMaxRetries ?? 3, }; } // Type export export type N8nApiConfig = NonNullable<ReturnType<typeof getN8nApiConfig>>; ``` -------------------------------------------------------------------------------- /tests/integration/n8n-api/utils/node-repository.ts: -------------------------------------------------------------------------------- ```typescript /** * Node Repository Utility for Integration Tests * * Provides a singleton NodeRepository instance for integration tests * that require validation or autofix functionality. */ import path from 'path'; import { createDatabaseAdapter, DatabaseAdapter } from '../../../../src/database/database-adapter'; import { NodeRepository } from '../../../../src/database/node-repository'; let repositoryInstance: NodeRepository | null = null; let dbInstance: DatabaseAdapter | null = null; /** * Get or create NodeRepository instance * * Uses the production nodes.db database (data/nodes.db). * * @returns Singleton NodeRepository instance * @throws {Error} If database file cannot be found or opened * * @example * const repository = await getNodeRepository(); * const nodeInfo = await repository.getNodeByType('n8n-nodes-base.webhook'); */ export async function getNodeRepository(): Promise<NodeRepository> { if (repositoryInstance) { return repositoryInstance; } const dbPath = path.join(process.cwd(), 'data/nodes.db'); dbInstance = await createDatabaseAdapter(dbPath); repositoryInstance = new NodeRepository(dbInstance); return repositoryInstance; } /** * Close database and reset repository instance * * Should be called in test cleanup (afterAll) to prevent resource leaks. * Properly closes the database connection and resets the singleton. * * @example * afterAll(async () => { * await closeNodeRepository(); * }); */ export async function closeNodeRepository(): Promise<void> { if (dbInstance && typeof dbInstance.close === 'function') { await dbInstance.close(); } dbInstance = null; repositoryInstance = null; } /** * Reset repository instance (useful for test cleanup) * * @deprecated Use closeNodeRepository() instead to properly close database connections */ export function resetNodeRepository(): void { repositoryInstance = null; } ``` -------------------------------------------------------------------------------- /tests/factories/property-definition-factory.ts: -------------------------------------------------------------------------------- ```typescript import { Factory } from 'fishery'; import { faker } from '@faker-js/faker'; /** * Interface for n8n node property definitions. * Represents the structure of properties that configure node behavior. */ interface PropertyDefinition { name: string; displayName: string; type: string; default?: any; required?: boolean; description?: string; options?: any[]; } /** * Factory for generating PropertyDefinition test data. * Creates realistic property configurations for testing node validation and processing. * * @example * ```typescript * // Create a single property * const prop = PropertyDefinitionFactory.build(); * * // Create a required string property * const urlProp = PropertyDefinitionFactory.build({ * name: 'url', * displayName: 'URL', * type: 'string', * required: true * }); * * // Create an options property with choices * const methodProp = PropertyDefinitionFactory.build({ * name: 'method', * type: 'options', * options: [ * { name: 'GET', value: 'GET' }, * { name: 'POST', value: 'POST' } * ] * }); * * // Create multiple properties for a node * const nodeProperties = PropertyDefinitionFactory.buildList(5); * ``` */ export const PropertyDefinitionFactory = Factory.define<PropertyDefinition>(() => ({ name: faker.word.noun() + faker.word.adjective().charAt(0).toUpperCase() + faker.word.adjective().slice(1), displayName: faker.helpers.arrayElement(['URL', 'Method', 'Headers', 'Body', 'Authentication']), type: faker.helpers.arrayElement(['string', 'number', 'boolean', 'options', 'json']), default: faker.datatype.boolean() ? faker.word.sample() : undefined, required: faker.datatype.boolean(), description: faker.lorem.sentence(), options: faker.datatype.boolean() ? [ { name: faker.word.noun(), value: faker.word.noun(), description: faker.lorem.sentence() } ] : undefined })); ``` -------------------------------------------------------------------------------- /docker-compose.n8n.yml: -------------------------------------------------------------------------------- ```yaml version: '3.8' services: # n8n workflow automation n8n: image: n8nio/n8n:latest container_name: n8n restart: unless-stopped ports: - "${N8N_PORT:-5678}:5678" environment: - N8N_BASIC_AUTH_ACTIVE=${N8N_BASIC_AUTH_ACTIVE:-true} - N8N_BASIC_AUTH_USER=${N8N_BASIC_AUTH_USER:-admin} - N8N_BASIC_AUTH_PASSWORD=${N8N_BASIC_AUTH_PASSWORD:-password} - N8N_HOST=${N8N_HOST:-localhost} - N8N_PORT=5678 - N8N_PROTOCOL=${N8N_PROTOCOL:-http} - WEBHOOK_URL=${N8N_WEBHOOK_URL:-http://localhost:5678/} - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY} volumes: - n8n_data:/home/node/.n8n networks: - n8n-network healthcheck: test: ["CMD", "sh", "-c", "wget --quiet --spider --tries=1 --timeout=10 http://localhost:5678/healthz || exit 1"] interval: 30s timeout: 10s retries: 3 start_period: 30s # n8n-mcp server for AI assistance n8n-mcp: build: context: . dockerfile: Dockerfile # Uses standard Dockerfile with N8N_MODE=true env var image: ghcr.io/${GITHUB_REPOSITORY:-czlonkowski/n8n-mcp}/n8n-mcp:${VERSION:-latest} container_name: n8n-mcp restart: unless-stopped ports: - "${MCP_PORT:-3000}:3000" environment: - NODE_ENV=production - N8N_MODE=true - MCP_MODE=http - N8N_API_URL=http://n8n:5678 - N8N_API_KEY=${N8N_API_KEY} - MCP_AUTH_TOKEN=${MCP_AUTH_TOKEN} - AUTH_TOKEN=${MCP_AUTH_TOKEN} - LOG_LEVEL=${LOG_LEVEL:-info} volumes: - ./data:/app/data:ro - mcp_logs:/app/logs networks: - n8n-network depends_on: n8n: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s volumes: n8n_data: driver: local mcp_logs: driver: local networks: n8n-network: driver: bridge ``` -------------------------------------------------------------------------------- /tests/extracted-nodes-db/extraction-report.json: -------------------------------------------------------------------------------- ```json [ { "nodeType": "n8n-nodes-base.Slack", "success": true, "hasPackageInfo": true, "hasCredentials": true, "sourceSize": 1007, "credentialSize": 7553, "packageName": "n8n-nodes-base", "packageVersion": "1.14.1" }, { "nodeType": "n8n-nodes-base.Discord", "success": true, "hasPackageInfo": true, "hasCredentials": false, "sourceSize": 10049, "credentialSize": 0, "packageName": "n8n-nodes-base", "packageVersion": "1.14.1" }, { "nodeType": "n8n-nodes-base.HttpRequest", "success": true, "hasPackageInfo": true, "hasCredentials": false, "sourceSize": 1343, "credentialSize": 0, "packageName": "n8n-nodes-base", "packageVersion": "1.14.1" }, { "nodeType": "n8n-nodes-base.Webhook", "success": true, "hasPackageInfo": true, "hasCredentials": false, "sourceSize": 10667, "credentialSize": 0, "packageName": "n8n-nodes-base", "packageVersion": "1.14.1" }, { "nodeType": "n8n-nodes-base.If", "success": true, "hasPackageInfo": true, "hasCredentials": false, "sourceSize": 20533, "credentialSize": 0, "packageName": "n8n-nodes-base", "packageVersion": "1.14.1" }, { "nodeType": "n8n-nodes-base.SplitInBatches", "success": true, "hasPackageInfo": true, "hasCredentials": false, "sourceSize": 1135, "credentialSize": 0, "packageName": "n8n-nodes-base", "packageVersion": "1.14.1" }, { "nodeType": "n8n-nodes-base.Airtable", "success": true, "hasPackageInfo": true, "hasCredentials": true, "sourceSize": 936, "credentialSize": 5985, "packageName": "n8n-nodes-base", "packageVersion": "1.14.1" }, { "nodeType": "n8n-nodes-base.Function", "success": true, "hasPackageInfo": true, "hasCredentials": false, "sourceSize": 7449, "credentialSize": 0, "packageName": "n8n-nodes-base", "packageVersion": "1.14.1" } ] ``` -------------------------------------------------------------------------------- /tests/setup/global-setup.ts: -------------------------------------------------------------------------------- ```typescript import { beforeEach, afterEach, vi } from 'vitest'; import { loadTestEnvironment, getTestConfig, getTestTimeout } from './test-env'; // CI Debug: Log environment loading in CI only if (process.env.CI === 'true') { console.log('[CI-DEBUG] Global setup starting, NODE_ENV:', process.env.NODE_ENV); } // Load test environment configuration loadTestEnvironment(); if (process.env.CI === 'true') { console.log('[CI-DEBUG] Global setup complete, N8N_API_URL:', process.env.N8N_API_URL); } // Get test configuration const testConfig = getTestConfig(); // Reset mocks between tests beforeEach(() => { vi.clearAllMocks(); }); // Clean up after each test afterEach(() => { vi.restoreAllMocks(); // Perform cleanup if enabled if (testConfig.cleanup.enabled) { // Add cleanup logic here if needed } }); // Global test timeout from configuration vi.setConfig({ testTimeout: getTestTimeout('global') }); // Configure console output based on test configuration if (!testConfig.logging.debug) { global.console = { ...console, log: vi.fn(), debug: vi.fn(), info: vi.fn(), warn: testConfig.logging.level === 'error' ? vi.fn() : console.warn, error: console.error, // Always show errors }; } // Set up performance monitoring if enabled if (testConfig.performance) { // Use a high-resolution timer that maintains timing precision let startTime = process.hrtime.bigint(); global.performance = global.performance || { now: () => { // Convert nanoseconds to milliseconds with high precision const currentTime = process.hrtime.bigint(); return Number(currentTime - startTime) / 1000000; // Convert nanoseconds to milliseconds }, mark: vi.fn(), measure: vi.fn(), getEntriesByName: vi.fn(() => []), getEntriesByType: vi.fn(() => []), clearMarks: vi.fn(), clearMeasures: vi.fn(), } as any; } // Export test configuration for use in tests export { testConfig, getTestTimeout, getTestConfig }; ``` -------------------------------------------------------------------------------- /src/telemetry/error-sanitizer.ts: -------------------------------------------------------------------------------- ```typescript /** * Error Sanitizer for Startup Errors (v2.18.3) * Extracts and sanitizes error messages with security-focused patterns * Now uses shared sanitization utilities to avoid code duplication */ import { logger } from '../utils/logger'; import { sanitizeErrorMessageCore } from './error-sanitization-utils'; /** * Extract error message from unknown error type * Safely handles Error objects, strings, and other types */ export function extractErrorMessage(error: unknown): string { try { if (error instanceof Error) { // Include stack trace if available (will be truncated later) return error.stack || error.message || 'Unknown error'; } if (typeof error === 'string') { return error; } if (error && typeof error === 'object') { // Try to extract message from object const errorObj = error as any; if (errorObj.message) { return String(errorObj.message); } if (errorObj.error) { return String(errorObj.error); } // Fall back to JSON stringify with truncation try { return JSON.stringify(error).substring(0, 500); } catch { return 'Error object (unstringifiable)'; } } return String(error); } catch (extractError) { logger.debug('Error during message extraction:', extractError); return 'Error message extraction failed'; } } /** * Sanitize startup error message to remove sensitive data * Now uses shared sanitization core from error-sanitization-utils.ts (v2.18.3) * This eliminates code duplication and the ReDoS vulnerability */ export function sanitizeStartupError(errorMessage: string): string { return sanitizeErrorMessageCore(errorMessage); } /** * Combined operation: Extract and sanitize error message * This is the main entry point for startup error processing */ export function processStartupError(error: unknown): string { const message = extractErrorMessage(error); return sanitizeStartupError(message); } ``` -------------------------------------------------------------------------------- /src/database/migrations/add-template-node-configs.sql: -------------------------------------------------------------------------------- ```sql -- Migration: Add template_node_configs table -- Run during `npm run rebuild` or `npm run fetch:templates` -- This migration is idempotent - safe to run multiple times -- Create table if it doesn't exist CREATE TABLE IF NOT EXISTS template_node_configs ( id INTEGER PRIMARY KEY, node_type TEXT NOT NULL, template_id INTEGER NOT NULL, template_name TEXT NOT NULL, template_views INTEGER DEFAULT 0, -- Node configuration (extracted from workflow) node_name TEXT, -- Node name in workflow (e.g., "HTTP Request") parameters_json TEXT NOT NULL, -- JSON: node.parameters credentials_json TEXT, -- JSON: node.credentials (if present) -- Pre-calculated metadata for filtering has_credentials INTEGER DEFAULT 0, has_expressions INTEGER DEFAULT 0, -- Contains {{...}} or $json/$node complexity TEXT CHECK(complexity IN ('simple', 'medium', 'complex')), use_cases TEXT, -- JSON array from template.metadata.use_cases -- Pre-calculated ranking (1 = best, 2 = second best, etc.) rank INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (template_id) REFERENCES templates(id) ON DELETE CASCADE ); -- Create indexes if they don't exist CREATE INDEX IF NOT EXISTS idx_config_node_type_rank ON template_node_configs(node_type, rank); CREATE INDEX IF NOT EXISTS idx_config_complexity ON template_node_configs(node_type, complexity, rank); CREATE INDEX IF NOT EXISTS idx_config_auth ON template_node_configs(node_type, has_credentials, rank); -- Create view if it doesn't exist CREATE VIEW IF NOT EXISTS ranked_node_configs AS SELECT node_type, template_name, template_views, parameters_json, credentials_json, has_credentials, has_expressions, complexity, use_cases, rank FROM template_node_configs WHERE rank <= 5 -- Top 5 per node type ORDER BY node_type, rank; -- Note: Actual data population is handled by the fetch-templates script -- This migration only creates the schema ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/validation/validate-node-minimal.ts: -------------------------------------------------------------------------------- ```typescript import { ToolDocumentation } from '../types'; export const validateNodeMinimalDoc: ToolDocumentation = { name: 'validate_node_minimal', category: 'validation', essentials: { description: 'Fast check for missing required fields only. No warnings/suggestions. Returns: list of missing fields.', keyParameters: ['nodeType', 'config'], example: 'validate_node_minimal("nodes-base.slack", {resource: "message"})', performance: 'Instant', tips: [ 'Returns only missing required fields', 'No warnings or suggestions', 'Perfect for real-time validation' ] }, full: { 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.', parameters: { nodeType: { type: 'string', required: true, description: 'Node type with prefix (e.g., "nodes-base.slack")' }, config: { type: 'object', required: true, description: 'Node configuration to validate' } }, returns: 'Array of missing required field names (empty if valid)', examples: [ 'validate_node_minimal("nodes-base.slack", {resource: "message", operation: "post"}) - Check Slack config', 'validate_node_minimal("nodes-base.httpRequest", {method: "GET"}) - Check HTTP config' ], useCases: [ 'Real-time form validation', 'Quick configuration checks', 'Pre-deployment validation', 'Interactive configuration builders' ], performance: 'Instant - Simple field checking without complex validation', bestPractices: [ 'Use for quick feedback loops', 'Follow with validate_node_operation for thorough check', 'Check return array length for validity' ], pitfalls: [ 'Only checks required fields', 'No type validation', 'No operation-specific validation' ], relatedTools: ['validate_node_operation', 'get_node_essentials', 'get_property_dependencies'] } }; ``` -------------------------------------------------------------------------------- /scripts/demo-optimization.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # Demonstrate the optimization concept echo "🎯 Demonstrating Docker Optimization" echo "====================================" # Create a demo directory structure DEMO_DIR="optimization-demo" rm -rf $DEMO_DIR mkdir -p $DEMO_DIR # Copy only runtime files echo -e "\n📦 Creating minimal runtime package..." cat > $DEMO_DIR/package.json << 'EOF' { "name": "n8n-mcp-optimized", "version": "1.0.0", "private": true, "main": "dist/mcp/index.js", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", "better-sqlite3": "^11.10.0", "sql.js": "^1.13.0", "express": "^5.1.0", "dotenv": "^16.5.0" } } EOF # Copy built files echo "📁 Copying built application..." cp -r dist $DEMO_DIR/ cp -r data $DEMO_DIR/ mkdir -p $DEMO_DIR/src/database cp src/database/schema*.sql $DEMO_DIR/src/database/ # Calculate sizes echo -e "\n📊 Size comparison:" echo "Original project: $(du -sh . | cut -f1)" echo "Optimized runtime: $(du -sh $DEMO_DIR | cut -f1)" # Show what's included echo -e "\n✅ Optimized package includes:" echo "- Pre-built SQLite database with all node info" echo "- Compiled JavaScript (dist/)" echo "- Minimal runtime dependencies" echo "- No n8n packages needed!" # Create a simple test echo -e "\n🧪 Testing database content..." if command -v sqlite3 &> /dev/null; then NODE_COUNT=$(sqlite3 data/nodes.db "SELECT COUNT(*) FROM nodes;" 2>/dev/null || echo "0") AI_COUNT=$(sqlite3 data/nodes.db "SELECT COUNT(*) FROM nodes WHERE is_ai_tool = 1;" 2>/dev/null || echo "0") echo "- Total nodes in database: $NODE_COUNT" echo "- AI-capable nodes: $AI_COUNT" else echo "- SQLite CLI not installed, skipping count" fi echo -e "\n💡 This demonstrates that we can run n8n-MCP with:" echo "- ~50MB of runtime dependencies (vs 1.6GB)" echo "- Pre-built database (11MB)" echo "- No n8n packages at runtime" echo "- Total optimized size: ~200MB (vs 2.6GB)" # Cleanup echo -e "\n🧹 Cleaning up demo..." rm -rf $DEMO_DIR echo -e "\n✨ Optimization concept demonstrated!" ``` -------------------------------------------------------------------------------- /scripts/publish-npm-quick.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # Quick publish script that skips tests set -e # Color codes GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' echo "🚀 Preparing n8n-mcp for npm publish (quick mode)..." # Sync version echo "🔄 Syncing version to package.runtime.json..." npm run sync:runtime-version VERSION=$(node -e "console.log(require('./package.json').version)") echo -e "${GREEN}📌 Version: $VERSION${NC}" # Prepare publish directory PUBLISH_DIR="npm-publish-temp" rm -rf $PUBLISH_DIR mkdir -p $PUBLISH_DIR echo "📦 Copying files..." cp -r dist $PUBLISH_DIR/ cp -r data $PUBLISH_DIR/ cp README.md LICENSE .env.example $PUBLISH_DIR/ cp .npmignore $PUBLISH_DIR/ 2>/dev/null || true cp package.runtime.json $PUBLISH_DIR/package.json cd $PUBLISH_DIR # Configure package.json node -e " const pkg = require('./package.json'); pkg.name = 'n8n-mcp'; pkg.description = 'Integration between n8n workflow automation and Model Context Protocol (MCP)'; pkg.bin = { 'n8n-mcp': './dist/mcp/index.js' }; pkg.repository = { type: 'git', url: 'git+https://github.com/czlonkowski/n8n-mcp.git' }; pkg.keywords = ['n8n', 'mcp', 'model-context-protocol', 'ai', 'workflow', 'automation']; pkg.author = 'Romuald Czlonkowski @ www.aiadvisors.pl/en'; pkg.license = 'MIT'; pkg.bugs = { url: 'https://github.com/czlonkowski/n8n-mcp/issues' }; pkg.homepage = 'https://github.com/czlonkowski/n8n-mcp#readme'; pkg.files = ['dist/**/*', 'data/nodes.db', '.env.example', 'README.md', 'LICENSE']; delete pkg.private; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)); " echo "" echo "📋 Package details:" echo -e "${GREEN}Name:${NC} $(node -e "console.log(require('./package.json').name)")" echo -e "${GREEN}Version:${NC} $(node -e "console.log(require('./package.json').version)")" echo -e "${GREEN}Size:${NC} ~50MB" echo "" echo "✅ Ready to publish!" echo "" echo -e "${YELLOW}⚠️ Note: Tests were skipped in quick mode${NC}" echo "" echo "To publish, run:" echo -e " ${GREEN}cd $PUBLISH_DIR${NC}" echo -e " ${GREEN}npm publish --otp=YOUR_OTP_CODE${NC}" ``` -------------------------------------------------------------------------------- /src/utils/console-manager.ts: -------------------------------------------------------------------------------- ```typescript /** * Console Manager for MCP HTTP Server * * Prevents console output from interfering with StreamableHTTPServerTransport * by silencing console methods during MCP request handling. */ export class ConsoleManager { private originalConsole = { log: console.log, error: console.error, warn: console.warn, info: console.info, debug: console.debug, trace: console.trace }; private isSilenced = false; /** * Silence all console output */ public silence(): void { if (this.isSilenced || process.env.MCP_MODE !== 'http') { return; } this.isSilenced = true; process.env.MCP_REQUEST_ACTIVE = 'true'; console.log = () => {}; console.error = () => {}; console.warn = () => {}; console.info = () => {}; console.debug = () => {}; console.trace = () => {}; } /** * Restore original console methods */ public restore(): void { if (!this.isSilenced) { return; } this.isSilenced = false; process.env.MCP_REQUEST_ACTIVE = 'false'; console.log = this.originalConsole.log; console.error = this.originalConsole.error; console.warn = this.originalConsole.warn; console.info = this.originalConsole.info; console.debug = this.originalConsole.debug; console.trace = this.originalConsole.trace; } /** * Wrap an operation with console silencing * Automatically restores console on completion or error */ public async wrapOperation<T>(operation: () => T | Promise<T>): Promise<T> { this.silence(); try { const result = operation(); if (result instanceof Promise) { return await result.finally(() => this.restore()); } this.restore(); return result; } catch (error) { this.restore(); throw error; } } /** * Check if console is currently silenced */ public get isActive(): boolean { return this.isSilenced; } } // Export singleton instance for easy use export const consoleManager = new ConsoleManager(); ``` -------------------------------------------------------------------------------- /tests/integration/n8n-api/types/mcp-responses.ts: -------------------------------------------------------------------------------- ```typescript /** * TypeScript interfaces for MCP handler responses * * These interfaces provide type safety for integration tests, * replacing unsafe `as any` casts with proper type definitions. */ /** * Workflow validation response from handleValidateWorkflow */ export interface ValidationResponse { valid: boolean; workflowId: string; workflowName: string; summary: { totalNodes: number; enabledNodes: number; triggerNodes: number; validConnections?: number; invalidConnections?: number; expressionsValidated?: number; errorCount: number; warningCount: number; }; errors?: Array<{ node: string; nodeName?: string; message: string; details?: { code?: string; [key: string]: unknown; }; code?: string; }>; warnings?: Array<{ node: string; nodeName?: string; message: string; details?: { code?: string; [key: string]: unknown; }; code?: string; }>; info?: Array<{ node: string; nodeName?: string; message: string; severity?: string; details?: unknown; }>; suggestions?: string[]; } /** * Workflow autofix response from handleAutofixWorkflow */ export interface AutofixResponse { workflowId: string; workflowName: string; preview?: boolean; fixesAvailable?: number; fixesApplied?: number; fixes?: Array<{ type: 'expression-format' | 'typeversion-correction' | 'error-output-config' | 'node-type-correction' | 'webhook-missing-path'; confidence: 'high' | 'medium' | 'low'; description: string; nodeName?: string; nodeId?: string; before?: unknown; after?: unknown; }>; summary?: { totalFixes: number; byType: Record<string, number>; byConfidence: Record<string, number>; }; stats?: { expressionFormat?: number; typeVersionCorrection?: number; errorOutputConfig?: number; nodeTypeCorrection?: number; webhookMissingPath?: number; }; message?: string; validationSummary?: { errors: number; warnings: number; }; } ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/workflow_management/n8n-delete-workflow.ts: -------------------------------------------------------------------------------- ```typescript import { ToolDocumentation } from '../types'; export const n8nDeleteWorkflowDoc: ToolDocumentation = { name: 'n8n_delete_workflow', category: 'workflow_management', essentials: { description: 'Permanently delete a workflow. This action cannot be undone.', keyParameters: ['id'], example: 'n8n_delete_workflow({id: "workflow_123"})', performance: 'Fast (50-150ms)', tips: [ 'Action is irreversible', 'Deletes all execution history', 'Check workflow first with get_minimal' ] }, full: { 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.', parameters: { id: { type: 'string', required: true, description: 'Workflow ID to delete permanently' } }, returns: 'Success confirmation or error if workflow not found/cannot be deleted', examples: [ 'n8n_delete_workflow({id: "abc123"}) - Delete specific workflow', 'if (confirm) { n8n_delete_workflow({id: wf.id}); } // With confirmation' ], useCases: [ 'Remove obsolete workflows', 'Clean up test workflows', 'Delete failed experiments', 'Manage workflow limits', 'Remove duplicates' ], performance: 'Fast operation - typically 50-150ms. May take longer if workflow has extensive execution history.', bestPractices: [ 'Always confirm before deletion', 'Check workflow with get_minimal first', 'Consider deactivating instead of deleting', 'Export workflow before deletion for backup' ], pitfalls: [ 'Requires N8N_API_URL and N8N_API_KEY configured', 'Cannot be undone - permanent deletion', 'Deletes all execution history', 'Active workflows can be deleted', 'No built-in confirmation' ], relatedTools: ['n8n_get_workflow_minimal', 'n8n_list_workflows', 'n8n_update_partial_workflow', 'n8n_delete_execution'] } }; ``` -------------------------------------------------------------------------------- /src/database/schema-optimized.sql: -------------------------------------------------------------------------------- ```sql -- Optimized schema with source code storage for Docker optimization CREATE TABLE IF NOT EXISTS nodes ( node_type TEXT PRIMARY KEY, package_name TEXT NOT NULL, display_name TEXT NOT NULL, description TEXT, category TEXT, development_style TEXT CHECK(development_style IN ('declarative', 'programmatic')), is_ai_tool INTEGER DEFAULT 0, is_trigger INTEGER DEFAULT 0, is_webhook INTEGER DEFAULT 0, is_versioned INTEGER DEFAULT 0, version TEXT, documentation TEXT, properties_schema TEXT, operations TEXT, credentials_required TEXT, -- New columns for source code storage node_source_code TEXT, credential_source_code TEXT, source_location TEXT, source_extracted_at DATETIME, -- Metadata updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- Indexes for performance CREATE INDEX IF NOT EXISTS idx_package ON nodes(package_name); CREATE INDEX IF NOT EXISTS idx_ai_tool ON nodes(is_ai_tool); CREATE INDEX IF NOT EXISTS idx_category ON nodes(category); -- FTS5 table for full-text search including source code CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5( node_type, display_name, description, documentation, operations, node_source_code, content=nodes, content_rowid=rowid ); -- Trigger to keep FTS in sync CREATE TRIGGER IF NOT EXISTS nodes_fts_insert AFTER INSERT ON nodes BEGIN INSERT INTO nodes_fts(rowid, node_type, display_name, description, documentation, operations, node_source_code) VALUES (new.rowid, new.node_type, new.display_name, new.description, new.documentation, new.operations, new.node_source_code); END; CREATE TRIGGER IF NOT EXISTS nodes_fts_update AFTER UPDATE ON nodes BEGIN UPDATE nodes_fts SET node_type = new.node_type, display_name = new.display_name, description = new.description, documentation = new.documentation, operations = new.operations, node_source_code = new.node_source_code WHERE rowid = new.rowid; END; CREATE TRIGGER IF NOT EXISTS nodes_fts_delete AFTER DELETE ON nodes BEGIN DELETE FROM nodes_fts WHERE rowid = old.rowid; END; ``` -------------------------------------------------------------------------------- /tests/test-small-rebuild.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node const { NodeDocumentationService } = require('../dist/services/node-documentation-service'); async function testSmallRebuild() { console.log('Testing small rebuild...\n'); const service = new NodeDocumentationService('./data/nodes-v2-test.db'); try { // First, let's just try the IF node specifically const extractor = service.extractor; console.log('1️⃣ Testing extraction of IF node...'); try { const ifNodeData = await extractor.extractNodeSource('n8n-nodes-base.If'); console.log(' ✅ Successfully extracted IF node'); console.log(' Source code length:', ifNodeData.sourceCode.length); console.log(' Has credentials:', !!ifNodeData.credentialCode); } catch (error) { console.log(' ❌ Failed to extract IF node:', error.message); } // Try the Webhook node console.log('\n2️⃣ Testing extraction of Webhook node...'); try { const webhookNodeData = await extractor.extractNodeSource('n8n-nodes-base.Webhook'); console.log(' ✅ Successfully extracted Webhook node'); console.log(' Source code length:', webhookNodeData.sourceCode.length); } catch (error) { console.log(' ❌ Failed to extract Webhook node:', error.message); } // Now try storing just these nodes console.log('\n3️⃣ Testing storage of a single node...'); const nodeInfo = { nodeType: 'n8n-nodes-base.If', name: 'If', displayName: 'If', description: 'Route items based on comparison operations', sourceCode: 'test source code', packageName: 'n8n-nodes-base', hasCredentials: false, isTrigger: false, isWebhook: false }; await service.storeNode(nodeInfo); console.log(' ✅ Successfully stored test node'); // Check if it was stored const retrievedNode = await service.getNodeInfo('n8n-nodes-base.If'); console.log(' Retrieved node:', retrievedNode ? 'Found' : 'Not found'); } catch (error) { console.error('❌ Test failed:', error); } finally { service.close(); } } testSmallRebuild(); ``` -------------------------------------------------------------------------------- /src/mcp/workflow-examples.ts: -------------------------------------------------------------------------------- ```typescript /** * Example workflows for n8n AI agents to understand the structure */ export const MINIMAL_WORKFLOW_EXAMPLE = { nodes: [ { name: "Webhook", type: "n8n-nodes-base.webhook", typeVersion: 2, position: [250, 300], parameters: { httpMethod: "POST", path: "webhook" } } ], connections: {} }; export const SIMPLE_WORKFLOW_EXAMPLE = { nodes: [ { name: "Webhook", type: "n8n-nodes-base.webhook", typeVersion: 2, position: [250, 300], parameters: { httpMethod: "POST", path: "webhook" } }, { name: "Set", type: "n8n-nodes-base.set", typeVersion: 2, position: [450, 300], parameters: { mode: "manual", assignments: { assignments: [ { name: "message", type: "string", value: "Hello" } ] } } }, { name: "Respond to Webhook", type: "n8n-nodes-base.respondToWebhook", typeVersion: 1, position: [650, 300], parameters: { respondWith: "firstIncomingItem" } } ], connections: { "Webhook": { "main": [ [ { "node": "Set", "type": "main", "index": 0 } ] ] }, "Set": { "main": [ [ { "node": "Respond to Webhook", "type": "main", "index": 0 } ] ] } } }; export function getWorkflowExampleString(): string { return `Example workflow structure: ${JSON.stringify(MINIMAL_WORKFLOW_EXAMPLE, null, 2)} Each node MUST have: - name: unique string identifier - type: full node type with prefix (e.g., "n8n-nodes-base.webhook") - typeVersion: number (usually 1 or 2) - position: [x, y] coordinates array - parameters: object with node-specific settings Connections format: { "SourceNodeName": { "main": [ [ { "node": "TargetNodeName", "type": "main", "index": 0 } ] ] } }`; } ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/workflow_management/n8n-get-workflow-minimal.ts: -------------------------------------------------------------------------------- ```typescript import { ToolDocumentation } from '../types'; export const n8nGetWorkflowMinimalDoc: ToolDocumentation = { name: 'n8n_get_workflow_minimal', category: 'workflow_management', essentials: { description: 'Get minimal info: ID, name, active status, tags. Fast for listings.', keyParameters: ['id'], example: 'n8n_get_workflow_minimal({id: "workflow_123"})', performance: 'Very fast (<50ms)', tips: [ 'Fastest way to check workflow exists', 'Perfect for status checks', 'Use in list displays' ] }, full: { 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.', parameters: { id: { type: 'string', required: true, description: 'Workflow ID to retrieve minimal info for' } }, returns: 'Minimal workflow object with: id, name, active status, tags array, createdAt, updatedAt. No nodes, connections, or settings included.', examples: [ 'n8n_get_workflow_minimal({id: "abc123"}) - Quick existence check', 'const info = n8n_get_workflow_minimal({id: "xyz789"}); // Check if active' ], useCases: [ 'Quick workflow existence checks', 'Display workflow lists', 'Check active/inactive status', 'Get workflow tags', 'Performance-critical operations' ], performance: 'Extremely fast - typically under 50ms. Returns only database metadata without loading workflow definition.', bestPractices: [ 'Use for list displays and dashboards', 'Ideal for existence checks before operations', 'Cache results for UI responsiveness', 'Combine with list_workflows for bulk checks' ], pitfalls: [ 'Requires N8N_API_URL and N8N_API_KEY configured', 'No workflow content - cannot edit or validate', 'Tags may be empty array', 'Must use get_workflow for actual workflow data' ], relatedTools: ['n8n_list_workflows', 'n8n_get_workflow', 'n8n_get_workflow_structure', 'n8n_update_partial_workflow'] } }; ``` -------------------------------------------------------------------------------- /scripts/test-docker-optimization.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # Test script to verify Docker optimization (no n8n deps) set -e echo "🧪 Testing Docker optimization..." echo "" # Check if nodes.db exists if [ ! -f "data/nodes.db" ]; then echo "❌ ERROR: data/nodes.db not found!" echo " Run 'npm run rebuild' first to create the database" exit 1 fi # Build the image echo "📦 Building Docker image..." DOCKER_BUILDKIT=1 docker build -t n8n-mcp:test . > /dev/null 2>&1 # Check image size echo "📊 Checking image size..." SIZE=$(docker images n8n-mcp:test --format "{{.Size}}") echo " Image size: $SIZE" # Test that n8n is NOT in the image echo "" echo "🔍 Verifying no n8n dependencies..." if docker run --rm n8n-mcp:test sh -c "ls node_modules | grep -E '^n8n$|^n8n-|^@n8n'" 2>/dev/null; then echo "❌ ERROR: Found n8n dependencies in runtime image!" exit 1 else echo "✅ No n8n dependencies found (as expected)" fi # Test that runtime dependencies ARE present echo "" echo "🔍 Verifying runtime dependencies..." EXPECTED_DEPS=("@modelcontextprotocol" "better-sqlite3" "express" "dotenv") for dep in "${EXPECTED_DEPS[@]}"; do if docker run --rm n8n-mcp:test sh -c "ls node_modules | grep -q '$dep'" 2>/dev/null; then echo "✅ Found: $dep" else echo "❌ Missing: $dep" exit 1 fi done # Test that the server starts echo "" echo "🚀 Testing server startup..." docker run --rm -d \ --name n8n-mcp-test \ -e MCP_MODE=http \ -e AUTH_TOKEN=test-token \ -e LOG_LEVEL=error \ n8n-mcp:test > /dev/null 2>&1 # Wait for startup sleep 3 # Check if running if docker ps | grep -q n8n-mcp-test; then echo "✅ Server started successfully" docker stop n8n-mcp-test > /dev/null 2>&1 else echo "❌ Server failed to start" docker logs n8n-mcp-test 2>&1 exit 1 fi # Clean up docker rmi n8n-mcp:test > /dev/null 2>&1 echo "" echo "🎉 All tests passed! Docker optimization is working correctly." echo "" echo "📈 Benefits:" echo " - No n8n dependencies in runtime image" echo " - Image size: ~200MB (vs ~1.5GB with n8n)" echo " - Build time: ~1-2 minutes (vs ~12 minutes)" echo " - No version conflicts at runtime" ``` -------------------------------------------------------------------------------- /.github/BENCHMARK_THRESHOLDS.md: -------------------------------------------------------------------------------- ```markdown # Performance Benchmark Thresholds This file defines the expected performance thresholds for n8n-mcp operations. ## Critical Operations | Operation | Expected Time | Warning Threshold | Error Threshold | |-----------|---------------|-------------------|-----------------| | Node Loading (per package) | <100ms | 150ms | 200ms | | Database Query (simple) | <5ms | 10ms | 20ms | | Search (simple word) | <10ms | 20ms | 50ms | | Search (complex query) | <50ms | 100ms | 200ms | | Validation (simple config) | <1ms | 2ms | 5ms | | Validation (complex config) | <10ms | 20ms | 50ms | | MCP Tool Execution | <50ms | 100ms | 200ms | ## Benchmark Categories ### Node Loading Performance - **loadPackage**: Should handle large packages efficiently - **loadNodesFromPath**: Individual file loading should be fast - **parsePackageJson**: JSON parsing overhead should be minimal ### Database Query Performance - **getNodeByType**: Direct lookups should be instant - **searchNodes**: Full-text search should scale well - **getAllNodes**: Pagination should prevent performance issues ### Search Operations - **OR mode**: Should handle multiple terms efficiently - **AND mode**: More restrictive but still performant - **FUZZY mode**: Slower but acceptable for typo tolerance ### Validation Performance - **minimal profile**: Fastest, only required fields - **ai-friendly profile**: Balanced performance - **strict profile**: Comprehensive but slower ### MCP Tool Execution - Tools should respond quickly for interactive use - Complex operations may take longer but should remain responsive ## Regression Detection Performance regressions are detected when: 1. Any operation exceeds its warning threshold by 10% 2. Multiple operations show degradation in the same category 3. Average performance across all benchmarks degrades by 5% ## Optimization Targets Future optimization efforts should focus on: 1. **Search performance**: Implement FTS5 for better full-text search 2. **Caching**: Add intelligent caching for frequently accessed nodes 3. **Lazy loading**: Defer loading of large property schemas 4. **Batch operations**: Optimize bulk inserts and updates ``` -------------------------------------------------------------------------------- /scripts/test-fuzzy-fix.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { N8NDocumentationMCPServer } from '../src/mcp/server'; async function testFuzzyFix() { console.log('Testing FUZZY mode fix...\n'); const server = new N8NDocumentationMCPServer(); // Wait for initialization await new Promise(resolve => setTimeout(resolve, 1000)); // Test 1: FUZZY mode with typo console.log('Test 1: FUZZY mode with "slak" (typo for "slack")'); const fuzzyResult = await server.executeTool('search_nodes', { query: 'slak', mode: 'FUZZY', limit: 5 }); console.log(`Results: ${fuzzyResult.results.length} found`); if (fuzzyResult.results.length > 0) { console.log('✅ FUZZY mode now finds results!'); fuzzyResult.results.forEach((node: any, i: number) => { console.log(` ${i + 1}. ${node.nodeType} - ${node.displayName}`); }); } else { console.log('❌ FUZZY mode still not working'); } // Test 2: AND mode with explanation console.log('\n\nTest 2: AND mode with "send message"'); const andResult = await server.executeTool('search_nodes', { query: 'send message', mode: 'AND', limit: 5 }); console.log(`Results: ${andResult.results.length} found`); if (andResult.searchInfo) { console.log('✅ AND mode now includes search info:'); console.log(` ${andResult.searchInfo.message}`); console.log(` Tip: ${andResult.searchInfo.tip}`); } console.log('\nFirst 5 results:'); andResult.results.slice(0, 5).forEach((node: any, i: number) => { console.log(` ${i + 1}. ${node.nodeType} - ${node.displayName}`); }); // Test 3: More typos console.log('\n\nTest 3: More FUZZY tests'); const typos = ['htpp', 'webook', 'slck', 'emial']; for (const typo of typos) { const result = await server.executeTool('search_nodes', { query: typo, mode: 'FUZZY', limit: 1 }); if (result.results.length > 0) { console.log(`✅ "${typo}" → ${result.results[0].displayName}`); } else { console.log(`❌ "${typo}" → No results`); } } process.exit(0); } // Run tests testFuzzyFix().catch(error => { console.error('Test failed:', error); process.exit(1); }); ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/workflow_management/n8n-get-workflow-structure.ts: -------------------------------------------------------------------------------- ```typescript import { ToolDocumentation } from '../types'; export const n8nGetWorkflowStructureDoc: ToolDocumentation = { name: 'n8n_get_workflow_structure', category: 'workflow_management', essentials: { description: 'Get workflow structure: nodes and connections only. No parameter details.', keyParameters: ['id'], example: 'n8n_get_workflow_structure({id: "workflow_123"})', performance: 'Fast (75-150ms)', tips: [ 'Shows workflow topology', 'Node types without parameters', 'Perfect for visualization' ] }, full: { 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.', parameters: { id: { type: 'string', required: true, description: 'Workflow ID to retrieve structure for' } }, returns: 'Workflow structure with: id, name, nodes array (id, name, type, position only), connections object. No node parameters, credentials, or settings included.', examples: [ 'n8n_get_workflow_structure({id: "abc123"}) - Visualize workflow', 'const structure = n8n_get_workflow_structure({id: "xyz789"}); // Analyze complexity' ], useCases: [ 'Generate workflow visualizations', 'Analyze workflow complexity', 'Understand node relationships', 'Create workflow diagrams', 'Quick topology validation' ], performance: 'Fast retrieval - typically 75-150ms. Faster than get_workflow as parameters are stripped.', bestPractices: [ 'Use for visualization tools', 'Ideal for workflow analysis', 'Good for connection validation', 'Cache for UI diagram rendering' ], pitfalls: [ 'Requires N8N_API_URL and N8N_API_KEY configured', 'No parameter data for configuration', 'Cannot validate node settings', 'Must use get_workflow for editing' ], relatedTools: ['n8n_get_workflow', 'n8n_validate_workflow_connections', 'n8n_get_workflow_minimal', 'validate_workflow_connections'] } }; ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/workflow_management/n8n-get-workflow.ts: -------------------------------------------------------------------------------- ```typescript import { ToolDocumentation } from '../types'; export const n8nGetWorkflowDoc: ToolDocumentation = { name: 'n8n_get_workflow', category: 'workflow_management', essentials: { description: 'Get a workflow by ID. Returns the complete workflow including nodes, connections, and settings.', keyParameters: ['id'], example: 'n8n_get_workflow({id: "workflow_123"})', performance: 'Fast (50-200ms)', tips: [ 'Returns complete workflow JSON', 'Includes all node parameters', 'Use get_workflow_minimal for faster listings' ] }, full: { 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.', parameters: { id: { type: 'string', required: true, description: 'Workflow ID to retrieve' } }, returns: 'Complete workflow object containing: id, name, active status, nodes array (with full parameters), connections object, settings, createdAt, updatedAt', examples: [ 'n8n_get_workflow({id: "abc123"}) - Get workflow for editing', 'const wf = n8n_get_workflow({id: "xyz789"}); // Clone workflow structure' ], useCases: [ 'View workflow configuration', 'Export workflow for backup', 'Clone workflow structure', 'Debug workflow issues', 'Prepare for updates' ], performance: 'Fast retrieval - typically 50-200ms depending on workflow size. Cached by n8n for performance.', bestPractices: [ 'Check workflow exists before updating', 'Use for complete workflow data needs', 'Cache results when making multiple operations', 'Validate after retrieving if modifying' ], pitfalls: [ 'Requires N8N_API_URL and N8N_API_KEY configured', 'Returns all data - use minimal/structure for performance', 'Workflow must exist or returns 404', 'Credentials are referenced but not included' ], relatedTools: ['n8n_get_workflow_minimal', 'n8n_get_workflow_structure', 'n8n_update_full_workflow', 'n8n_validate_workflow'] } }; ``` -------------------------------------------------------------------------------- /tests/unit/database/__mocks__/better-sqlite3.ts: -------------------------------------------------------------------------------- ```typescript import { vi } from 'vitest'; export class MockDatabase { private data = new Map<string, any[]>(); private prepared = new Map<string, any>(); public inTransaction = false; constructor() { this.data.set('nodes', []); this.data.set('templates', []); this.data.set('tools_documentation', []); } prepare(sql: string) { const key = this.extractTableName(sql); const self = this; return { all: vi.fn(() => self.data.get(key) || []), get: vi.fn((id: string) => { const items = self.data.get(key) || []; return items.find(item => item.id === id); }), run: vi.fn((params: any) => { const items = self.data.get(key) || []; items.push(params); self.data.set(key, items); return { changes: 1, lastInsertRowid: items.length }; }), iterate: vi.fn(function* () { const items = self.data.get(key) || []; for (const item of items) { yield item; } }), pluck: vi.fn(function(this: any) { return this; }), expand: vi.fn(function(this: any) { return this; }), raw: vi.fn(function(this: any) { return this; }), columns: vi.fn(() => []), bind: vi.fn(function(this: any) { return this; }) }; } exec(sql: string) { // Mock schema creation return true; } close() { // Mock close return true; } pragma(key: string, value?: any) { // Mock pragma if (key === 'journal_mode' && value === 'WAL') { return 'wal'; } return null; } transaction<T>(fn: () => T): T { this.inTransaction = true; try { const result = fn(); this.inTransaction = false; return result; } catch (error) { this.inTransaction = false; throw error; } } // Helper to extract table name from SQL private extractTableName(sql: string): string { const match = sql.match(/FROM\s+(\w+)|INTO\s+(\w+)|UPDATE\s+(\w+)/i); return match ? (match[1] || match[2] || match[3]) : 'nodes'; } // Test helper to seed data _seedData(table: string, data: any[]) { this.data.set(table, data); } } export default vi.fn(() => new MockDatabase()); ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/workflow_management/n8n-get-workflow-details.ts: -------------------------------------------------------------------------------- ```typescript import { ToolDocumentation } from '../types'; export const n8nGetWorkflowDetailsDoc: ToolDocumentation = { name: 'n8n_get_workflow_details', category: 'workflow_management', essentials: { description: 'Get workflow details with metadata, version, execution stats. More info than get_workflow.', keyParameters: ['id'], example: 'n8n_get_workflow_details({id: "workflow_123"})', performance: 'Fast (100-300ms)', tips: [ 'Includes execution statistics', 'Shows version history info', 'Contains metadata like tags' ] }, full: { 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.', parameters: { id: { type: 'string', required: true, description: 'Workflow ID to retrieve details for' } }, 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', examples: [ 'n8n_get_workflow_details({id: "abc123"}) - Get workflow with stats', 'const details = n8n_get_workflow_details({id: "xyz789"}); // Analyze performance' ], useCases: [ 'Monitor workflow performance', 'Analyze execution patterns', 'View workflow metadata', 'Check version information', 'Audit workflow usage' ], performance: 'Slightly slower than get_workflow due to additional metadata - typically 100-300ms. Stats may be cached.', bestPractices: [ 'Use for monitoring and analysis', 'Check execution stats before optimization', 'Review error counts for debugging', 'Monitor average execution times' ], pitfalls: [ 'Requires N8N_API_URL and N8N_API_KEY configured', 'More data than needed for simple edits', 'Stats may have slight delay', 'Not all n8n versions support all fields' ], relatedTools: ['n8n_get_workflow', 'n8n_list_executions', 'n8n_get_execution', 'n8n_list_workflows'] } }; ``` -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- ```typescript import { defineConfig } from 'vitest/config'; import path from 'path'; export default defineConfig({ test: { globals: true, environment: 'node', // Only include global-setup.ts, remove msw-setup.ts from global setup setupFiles: ['./tests/setup/global-setup.ts'], // Load environment variables from .env.test env: { NODE_ENV: 'test' }, // Test execution settings pool: 'threads', poolOptions: { threads: { singleThread: process.env.TEST_PARALLEL !== 'true', maxThreads: parseInt(process.env.TEST_MAX_WORKERS || '4', 10), minThreads: 1 } }, // Retry configuration retry: parseInt(process.env.TEST_RETRY_ATTEMPTS || '2', 10), // Test reporter - reduce reporters in CI to prevent hanging reporters: process.env.CI ? ['default', 'junit'] : ['default'], outputFile: { junit: './test-results/junit.xml' }, coverage: { provider: 'v8', enabled: process.env.FEATURE_TEST_COVERAGE !== 'false', reporter: process.env.CI ? ['lcov', 'text-summary'] : (process.env.COVERAGE_REPORTER || 'lcov,html,text-summary').split(','), reportsDirectory: process.env.COVERAGE_DIR || './coverage', exclude: [ 'node_modules/', 'tests/', '**/*.d.ts', '**/*.test.ts', '**/*.spec.ts', 'scripts/', 'dist/', '**/test-*.ts', '**/mock-*.ts', '**/__mocks__/**' ], thresholds: { lines: 80, functions: 80, branches: 75, statements: 80 }, // Add coverage-specific settings to prevent hanging all: false, // Don't collect coverage for untested files skipFull: true // Skip files with 100% coverage }, // Test isolation isolate: true, // Force exit after tests complete in CI to prevent hanging forceRerunTriggers: ['**/tests/**/*.ts'], teardownTimeout: 1000 }, resolve: { alias: { '@': path.resolve(__dirname, './src'), '@tests': path.resolve(__dirname, './tests') } }, // TypeScript configuration esbuild: { target: 'node18' }, // Define global constants define: { 'process.env.TEST_ENVIRONMENT': JSON.stringify('true') } }); ``` -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- ```markdown # Privacy Policy for n8n-mcp Telemetry ## Overview 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. ## What We Collect - **Anonymous User ID**: A hashed identifier derived from your machine characteristics (no personal information) - **Tool Usage**: Which MCP tools are used and their performance metrics - **Workflow Patterns**: Sanitized workflow structures (all sensitive data removed) - **Error Types**: Categories of errors encountered (no error messages with user data) - **System Information**: Platform, architecture, Node.js version, and n8n-mcp version ## What We DON'T Collect - Personal information or usernames - API keys, tokens, or credentials - URLs, endpoints, or hostnames - Email addresses or contact information - File paths or directory structures - Actual workflow data or parameters - Database connection strings - Any authentication information ## Data Sanitization All collected data undergoes automatic sanitization: - URLs are replaced with `[URL]` or `[REDACTED]` - Long alphanumeric strings (potential keys) are replaced with `[KEY]` - Email addresses are replaced with `[EMAIL]` - Authentication-related fields are completely removed ## Data Storage - Data is stored securely using Supabase - Anonymous users have write-only access (cannot read data back) - Row Level Security (RLS) policies prevent data access by anonymous users ## Opt-Out You can disable telemetry at any time: ```bash npx n8n-mcp telemetry disable ``` To re-enable: ```bash npx n8n-mcp telemetry enable ``` To check status: ```bash npx n8n-mcp telemetry status ``` ## Data Usage Collected data is used solely to: - Understand which features are most used - Identify common error patterns - Improve tool performance and reliability - Guide development priorities ## Data Retention - Data is retained for analysis purposes - No personal identification is possible from the collected data ## Changes to This Policy We may update this privacy policy from time to time. Updates will be reflected in this document. ## Contact For questions about telemetry or privacy, please open an issue on GitHub: https://github.com/czlonkowski/n8n-mcp/issues Last updated: 2025-09-25 ``` -------------------------------------------------------------------------------- /scripts/extract-changelog.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node /** * Extract changelog content for a specific version * Used by GitHub Actions to extract release notes */ const fs = require('fs'); const path = require('path'); function extractChangelog(version, changelogPath) { try { if (!fs.existsSync(changelogPath)) { console.error(`Changelog file not found at ${changelogPath}`); process.exit(1); } const content = fs.readFileSync(changelogPath, 'utf8'); const lines = content.split('\n'); // Find the start of this version's section const versionHeaderRegex = new RegExp(`^## \\[${version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]`); let startIndex = -1; let endIndex = -1; for (let i = 0; i < lines.length; i++) { if (versionHeaderRegex.test(lines[i])) { startIndex = i; break; } } if (startIndex === -1) { console.error(`No changelog entries found for version ${version}`); process.exit(1); } // Find the end of this version's section (next version or end of file) for (let i = startIndex + 1; i < lines.length; i++) { if (lines[i].startsWith('## [') && !lines[i].includes('Unreleased')) { endIndex = i; break; } } if (endIndex === -1) { endIndex = lines.length; } // Extract the section content const sectionLines = lines.slice(startIndex, endIndex); // Remove the version header and any trailing empty lines let contentLines = sectionLines.slice(1); while (contentLines.length > 0 && contentLines[contentLines.length - 1].trim() === '') { contentLines.pop(); } if (contentLines.length === 0) { console.error(`No content found for version ${version}`); process.exit(1); } const releaseNotes = contentLines.join('\n').trim(); // Write to stdout for GitHub Actions console.log(releaseNotes); } catch (error) { console.error(`Error extracting changelog: ${error.message}`); process.exit(1); } } // Parse command line arguments const version = process.argv[2]; const changelogPath = process.argv[3]; if (!version || !changelogPath) { console.error('Usage: extract-changelog.js <version> <changelog-path>'); process.exit(1); } extractChangelog(version, changelogPath); ``` -------------------------------------------------------------------------------- /scripts/test-helpers-validation.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env npx tsx import { EnhancedConfigValidator } from '../src/services/enhanced-config-validator.js'; console.log('🧪 Testing $helpers Validation\n'); const testCases = [ { name: 'Incorrect $helpers.getWorkflowStaticData', config: { language: 'javaScript', jsCode: `const data = $helpers.getWorkflowStaticData('global'); data.counter = 1; return [{json: {counter: data.counter}}];` } }, { name: 'Correct $getWorkflowStaticData', config: { language: 'javaScript', jsCode: `const data = $getWorkflowStaticData('global'); data.counter = 1; return [{json: {counter: data.counter}}];` } }, { name: '$helpers without check', config: { language: 'javaScript', jsCode: `const response = await $helpers.httpRequest({ method: 'GET', url: 'https://api.example.com' }); return [{json: response}];` } }, { name: '$helpers with proper check', config: { language: 'javaScript', jsCode: `if (typeof $helpers !== 'undefined' && $helpers.httpRequest) { const response = await $helpers.httpRequest({ method: 'GET', url: 'https://api.example.com' }); return [{json: response}]; } return [{json: {error: 'HTTP not available'}}];` } }, { name: 'Crypto without require', config: { language: 'javaScript', jsCode: `const token = crypto.randomBytes(32).toString('hex'); return [{json: {token}}];` } }, { name: 'Crypto with require', config: { language: 'javaScript', jsCode: `const crypto = require('crypto'); const token = crypto.randomBytes(32).toString('hex'); return [{json: {token}}];` } } ]; for (const test of testCases) { console.log(`Test: ${test.name}`); const result = EnhancedConfigValidator.validateWithMode( 'nodes-base.code', test.config, [ { name: 'language', type: 'options', options: ['javaScript', 'python'] }, { name: 'jsCode', type: 'string' } ], 'operation', 'ai-friendly' ); console.log(` Valid: ${result.valid}`); if (result.errors.length > 0) { console.log(` Errors: ${result.errors.map(e => e.message).join(', ')}`); } if (result.warnings.length > 0) { console.log(` Warnings: ${result.warnings.map(w => w.message).join(', ')}`); } console.log(); } console.log('✅ $helpers validation tests completed!'); ``` -------------------------------------------------------------------------------- /scripts/nginx-n8n-mcp.conf: -------------------------------------------------------------------------------- ``` server { listen 80; server_name n8ndocumentation.aiservices.pl; # Redirect HTTP to HTTPS location / { return 301 https://$server_name$request_uri; } } server { listen 443 ssl http2; server_name n8ndocumentation.aiservices.pl; # SSL configuration (managed by Certbot) ssl_certificate /etc/letsencrypt/live/n8ndocumentation.aiservices.pl/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/n8ndocumentation.aiservices.pl/privkey.pem; # SSL security settings ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; # Security headers add_header X-Content-Type-Options nosniff; add_header X-Frame-Options DENY; add_header X-XSS-Protection "1; mode=block"; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # Logging access_log /var/log/nginx/n8n-mcp-access.log; error_log /var/log/nginx/n8n-mcp-error.log; # Proxy settings location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; # Timeouts for MCP operations proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; # Increase buffer sizes for large responses proxy_buffer_size 16k; proxy_buffers 8 16k; proxy_busy_buffers_size 32k; } # Rate limiting for API endpoints location /mcp { limit_req zone=mcp_limit burst=10 nodelay; proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # Larger timeouts for MCP proxy_connect_timeout 120s; proxy_send_timeout 120s; proxy_read_timeout 120s; } } # Rate limiting zone limit_req_zone $binary_remote_addr zone=mcp_limit:10m rate=10r/s; ``` -------------------------------------------------------------------------------- /src/utils/error-handler.ts: -------------------------------------------------------------------------------- ```typescript import { logger } from './logger'; export class MCPError extends Error { public code: string; public statusCode?: number; public data?: any; constructor(message: string, code: string, statusCode?: number, data?: any) { super(message); this.name = 'MCPError'; this.code = code; this.statusCode = statusCode; this.data = data; } } export class N8NConnectionError extends MCPError { constructor(message: string, data?: any) { super(message, 'N8N_CONNECTION_ERROR', 503, data); this.name = 'N8NConnectionError'; } } export class AuthenticationError extends MCPError { constructor(message: string = 'Authentication failed') { super(message, 'AUTH_ERROR', 401); this.name = 'AuthenticationError'; } } export class ValidationError extends MCPError { constructor(message: string, data?: any) { super(message, 'VALIDATION_ERROR', 400, data); this.name = 'ValidationError'; } } export class ToolNotFoundError extends MCPError { constructor(toolName: string) { super(`Tool '${toolName}' not found`, 'TOOL_NOT_FOUND', 404); this.name = 'ToolNotFoundError'; } } export class ResourceNotFoundError extends MCPError { constructor(resourceUri: string) { super(`Resource '${resourceUri}' not found`, 'RESOURCE_NOT_FOUND', 404); this.name = 'ResourceNotFoundError'; } } export function handleError(error: any): MCPError { if (error instanceof MCPError) { return error; } if (error.response) { // HTTP error from n8n API const status = error.response.status; const message = error.response.data?.message || error.message; if (status === 401) { return new AuthenticationError(message); } else if (status === 404) { return new MCPError(message, 'NOT_FOUND', 404); } else if (status >= 500) { return new N8NConnectionError(message); } return new MCPError(message, 'API_ERROR', status); } if (error.code === 'ECONNREFUSED') { return new N8NConnectionError('Cannot connect to n8n API'); } // Generic error return new MCPError( error.message || 'An unexpected error occurred', 'UNKNOWN_ERROR', 500 ); } export async function withErrorHandling<T>( operation: () => Promise<T>, context: string ): Promise<T> { try { return await operation(); } catch (error) { logger.error(`Error in ${context}:`, error); throw handleError(error); } } ``` -------------------------------------------------------------------------------- /scripts/test-telemetry-integration.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env npx tsx /** * Integration test for the telemetry manager */ import { telemetry } from '../src/telemetry/telemetry-manager'; async function testIntegration() { console.log('🧪 Testing Telemetry Manager Integration\n'); // Check status console.log('Status:', telemetry.getStatus()); // Track session start console.log('\nTracking session start...'); telemetry.trackSessionStart(); // Track tool usage console.log('Tracking tool usage...'); telemetry.trackToolUsage('search_nodes', true, 150); telemetry.trackToolUsage('get_node_info', true, 75); telemetry.trackToolUsage('validate_workflow', false, 200); // Track errors console.log('Tracking errors...'); telemetry.trackError('ValidationError', 'workflow_validation', 'validate_workflow', 'Required field missing: nodes array is empty'); // Track a test workflow console.log('Tracking workflow creation...'); const testWorkflow = { nodes: [ { id: '1', type: 'n8n-nodes-base.webhook', name: 'Webhook', position: [0, 0], parameters: { path: '/test-webhook', httpMethod: 'POST' } }, { id: '2', type: 'n8n-nodes-base.httpRequest', name: 'HTTP Request', position: [250, 0], parameters: { url: 'https://api.example.com/endpoint', method: 'POST', authentication: 'genericCredentialType', genericAuthType: 'httpHeaderAuth', sendHeaders: true, headerParameters: { parameters: [ { name: 'Authorization', value: 'Bearer sk-1234567890abcdef' } ] } } }, { id: '3', type: 'n8n-nodes-base.slack', name: 'Slack', position: [500, 0], parameters: { channel: '#notifications', text: 'Workflow completed!' } } ], connections: { '1': { main: [[{ node: '2', type: 'main', index: 0 }]] }, '2': { main: [[{ node: '3', type: 'main', index: 0 }]] } } }; telemetry.trackWorkflowCreation(testWorkflow, true); // Force flush console.log('\nFlushing telemetry data...'); await telemetry.flush(); console.log('\n✅ Telemetry integration test completed!'); console.log('Check your Supabase dashboard for the telemetry data.'); } testIntegration().catch(console.error); ``` -------------------------------------------------------------------------------- /scripts/test-security.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import axios from 'axios'; import { spawn } from 'child_process'; async function testMaliciousHeaders() { console.log('🔒 Testing Security Fixes...\n'); // Start server with TRUST_PROXY enabled const serverProcess = spawn('node', ['dist/mcp/index.js'], { env: { ...process.env, MCP_MODE: 'http', AUTH_TOKEN: 'test-security-token-32-characters-long', PORT: '3999', TRUST_PROXY: '1' } }); // Wait for server to start await new Promise(resolve => { serverProcess.stdout.on('data', (data) => { if (data.toString().includes('Press Ctrl+C to stop')) { resolve(undefined); } }); }); const testCases = [ { name: 'Valid proxy headers', headers: { 'X-Forwarded-Host': 'example.com', 'X-Forwarded-Proto': 'https' } }, { name: 'Malicious host header (with path)', headers: { 'X-Forwarded-Host': 'evil.com/path/to/evil', 'X-Forwarded-Proto': 'https' } }, { name: 'Malicious host header (with @)', headers: { 'X-Forwarded-Host': '[email protected]', 'X-Forwarded-Proto': 'https' } }, { name: 'Invalid hostname (multiple dots)', headers: { 'X-Forwarded-Host': '.....', 'X-Forwarded-Proto': 'https' } }, { name: 'IPv6 address', headers: { 'X-Forwarded-Host': '[::1]:3000', 'X-Forwarded-Proto': 'https' } } ]; for (const testCase of testCases) { try { const response = await axios.get('http://localhost:3999/', { headers: testCase.headers, timeout: 2000 }); const endpoints = response.data.endpoints; const healthUrl = endpoints?.health?.url || 'N/A'; console.log(`✅ ${testCase.name}`); console.log(` Response: ${healthUrl}`); // Check if malicious headers were blocked if (testCase.name.includes('Malicious') || testCase.name.includes('Invalid')) { if (healthUrl.includes('evil.com') || healthUrl.includes('@') || healthUrl.includes('.....')) { console.log(' ❌ SECURITY ISSUE: Malicious header was not blocked!'); } else { console.log(' ✅ Malicious header was blocked'); } } } catch (error) { console.log(`❌ ${testCase.name} - Request failed`); } console.log(''); } serverProcess.kill(); } testMaliciousHeaders().catch(console.error); ``` -------------------------------------------------------------------------------- /src/services/sqlite-storage-service.ts: -------------------------------------------------------------------------------- ```typescript /** * SQLiteStorageService - A simple wrapper around DatabaseAdapter for benchmarks */ import { DatabaseAdapter, createDatabaseAdapter } from '../database/database-adapter'; export class SQLiteStorageService { private adapter: DatabaseAdapter | null = null; private dbPath: string; constructor(dbPath: string = ':memory:') { this.dbPath = dbPath; this.initSync(); } private initSync() { // For benchmarks, we'll use synchronous initialization // In real usage, this should be async const Database = require('better-sqlite3'); const db = new Database(this.dbPath); // Create a simple adapter this.adapter = { prepare: (sql: string) => db.prepare(sql), exec: (sql: string) => db.exec(sql), close: () => db.close(), pragma: (key: string, value?: any) => db.pragma(`${key}${value !== undefined ? ` = ${value}` : ''}`), inTransaction: db.inTransaction, transaction: (fn: () => any) => db.transaction(fn)(), checkFTS5Support: () => { try { db.exec("CREATE VIRTUAL TABLE test_fts USING fts5(content)"); db.exec("DROP TABLE test_fts"); return true; } catch { return false; } } }; // Initialize schema this.initializeSchema(); } private initializeSchema() { const schema = ` CREATE TABLE IF NOT EXISTS nodes ( node_type TEXT PRIMARY KEY, package_name TEXT NOT NULL, display_name TEXT NOT NULL, description TEXT, category TEXT, development_style TEXT CHECK(development_style IN ('declarative', 'programmatic')), is_ai_tool INTEGER DEFAULT 0, is_trigger INTEGER DEFAULT 0, is_webhook INTEGER DEFAULT 0, is_versioned INTEGER DEFAULT 0, version TEXT, documentation TEXT, properties_schema TEXT, operations TEXT, credentials_required TEXT, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_package ON nodes(package_name); CREATE INDEX IF NOT EXISTS idx_ai_tool ON nodes(is_ai_tool); CREATE INDEX IF NOT EXISTS idx_category ON nodes(category); `; this.adapter!.exec(schema); } get db(): DatabaseAdapter { if (!this.adapter) { throw new Error('Database not initialized'); } return this.adapter; } close() { if (this.adapter) { this.adapter.close(); this.adapter = null; } } } ``` -------------------------------------------------------------------------------- /tests/test-parsing-operations.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node const markdown = ` ## Operations * **Channel** * **Archive** a channel. * **Close** a direct message or multi-person direct message. * **Create** a public or private channel-based conversation. * **Get** information about a channel. * **Get Many**: Get a list of channels in Slack. * **File** * **Get** a file. * **Get Many**: Get and filter team files. * **Upload**: Create or upload an existing file. ## Templates and examples `; function extractOperations(markdown) { const operations = []; // Find operations section const operationsMatch = markdown.match(/##\s+Operations\s*\n([\s\S]*?)(?=\n##|\n#|$)/i); if (!operationsMatch) { console.log('No operations section found'); return operations; } const operationsText = operationsMatch[1]; console.log('Operations text:', operationsText.substring(0, 200)); // Parse operation structure let currentResource = null; const lines = operationsText.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmedLine = line.trim(); // Resource level (e.g., "* **Channel**") if (trimmedLine.match(/^\*\s+\*\*([^*]+)\*\*/)) { currentResource = trimmedLine.match(/^\*\s+\*\*([^*]+)\*\*/)[1].trim(); console.log(`Found resource: ${currentResource}`); continue; } // Skip if we don't have a current resource if (!currentResource) continue; // Operation level - look for indented bullets (4 spaces + *) if (line.match(/^\s{4}\*\s+/)) { console.log(`Found operation line: "${line}"`); // Extract operation name and description const operationMatch = trimmedLine.match(/^\*\s+\*\*([^*]+)\*\*(.*)$/); if (operationMatch) { const operation = operationMatch[1].trim(); let description = operationMatch[2].trim(); // Clean up description description = description.replace(/^:\s*/, '').replace(/\.$/, '').trim(); operations.push({ resource: currentResource, operation, description: description || operation, }); console.log(` Parsed: ${operation} - ${description}`); } } } return operations; } const operations = extractOperations(markdown); console.log('\nTotal operations found:', operations.length); console.log('\nOperations:'); operations.forEach(op => { console.log(`- ${op.resource}.${op.operation}: ${op.description}`); }); ``` -------------------------------------------------------------------------------- /docs/CLAUDE_CODE_SETUP.md: -------------------------------------------------------------------------------- ```markdown # Claude Code Setup Connect n8n-MCP to Claude Code CLI for enhanced n8n workflow development from the command line. ## Quick Setup via CLI ### Basic configuration (documentation tools only): ```bash claude mcp add n8n-mcp \ -e MCP_MODE=stdio \ -e LOG_LEVEL=error \ -e DISABLE_CONSOLE_OUTPUT=true \ -- npx n8n-mcp ```  ### Full configuration (with n8n management tools): ```bash claude mcp add n8n-mcp \ -e MCP_MODE=stdio \ -e LOG_LEVEL=error \ -e DISABLE_CONSOLE_OUTPUT=true \ -e N8N_API_URL=https://your-n8n-instance.com \ -e N8N_API_KEY=your-api-key \ -- npx n8n-mcp ``` Make sure to replace `https://your-n8n-instance.com` with your actual n8n URL and `your-api-key` with your n8n API key. ## Alternative Setup Methods ### Option 1: Import from Claude Desktop If you already have n8n-MCP configured in Claude Desktop: ```bash claude mcp add-from-claude-desktop ``` ### Option 2: Project Configuration For team sharing, add to `.mcp.json` in your project root: ```json { "mcpServers": { "n8n-mcp": { "command": "npx", "args": ["n8n-mcp"], "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" } } } } ``` Then use with scope flag: ```bash claude mcp add n8n-mcp --scope project ``` ## Managing Your MCP Server Check server status: ```bash claude mcp list claude mcp get n8n-mcp ``` During a conversation, use the `/mcp` command to see server status and available tools.  Remove the server: ```bash claude mcp remove n8n-mcp ``` ## Project Instructions For optimal results, create a `CLAUDE.md` file in your project root with the instructions from the [main README's Claude Project Setup section](../README.md#-claude-project-setup). ## Tips - If you're running n8n locally, use `http://localhost:5678` as the N8N_API_URL - The n8n API credentials are optional - without them, you'll have documentation and validation tools only - With API credentials, you'll get full workflow management capabilities - Use `--scope local` (default) to keep your API credentials private - Use `--scope project` to share configuration with your team (put credentials in environment variables) - Claude Code will automatically start the MCP server when you begin a conversation ``` -------------------------------------------------------------------------------- /scripts/test-telemetry-security.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env npx tsx /** * Test that RLS properly protects data */ import { createClient } from '@supabase/supabase-js'; import dotenv from 'dotenv'; dotenv.config(); async function testSecurity() { const supabaseUrl = process.env.SUPABASE_URL!; const supabaseAnonKey = process.env.SUPABASE_ANON_KEY!; console.log('🔒 Testing Telemetry Security (RLS)\n'); const supabase = createClient(supabaseUrl, supabaseAnonKey, { auth: { persistSession: false, autoRefreshToken: false, } }); // Test 1: Verify anon can INSERT console.log('Test 1: Anonymous INSERT (should succeed)...'); const testData = { user_id: 'security-test-' + Date.now(), event: 'security_test', properties: { test: true } }; const { error: insertError } = await supabase .from('telemetry_events') .insert([testData]); if (insertError) { console.error('❌ Insert failed:', insertError.message); } else { console.log('✅ Insert succeeded (as expected)'); } // Test 2: Verify anon CANNOT SELECT console.log('\nTest 2: Anonymous SELECT (should fail)...'); const { data, error: selectError } = await supabase .from('telemetry_events') .select('*') .limit(1); if (selectError) { console.log('✅ Select blocked by RLS (as expected):', selectError.message); } else if (data && data.length > 0) { console.error('❌ SECURITY ISSUE: Anon can read data!', data); } else if (data && data.length === 0) { console.log('⚠️ Select returned empty array (might be RLS working)'); } // Test 3: Verify anon CANNOT UPDATE console.log('\nTest 3: Anonymous UPDATE (should fail)...'); const { error: updateError } = await supabase .from('telemetry_events') .update({ event: 'hacked' }) .eq('user_id', 'test'); if (updateError) { console.log('✅ Update blocked (as expected):', updateError.message); } else { console.error('❌ SECURITY ISSUE: Anon can update data!'); } // Test 4: Verify anon CANNOT DELETE console.log('\nTest 4: Anonymous DELETE (should fail)...'); const { error: deleteError } = await supabase .from('telemetry_events') .delete() .eq('user_id', 'test'); if (deleteError) { console.log('✅ Delete blocked (as expected):', deleteError.message); } else { console.error('❌ SECURITY ISSUE: Anon can delete data!'); } console.log('\n✨ Security test completed!'); console.log('Summary: Anonymous users can INSERT (for telemetry) but cannot READ/UPDATE/DELETE'); } testSecurity().catch(console.error); ``` -------------------------------------------------------------------------------- /scripts/format-benchmark-results.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node const fs = require('fs'); const path = require('path'); /** * Formats Vitest benchmark results for github-action-benchmark * Converts from Vitest format to the expected format */ function formatBenchmarkResults() { const resultsPath = path.join(process.cwd(), 'benchmark-results.json'); if (!fs.existsSync(resultsPath)) { console.error('benchmark-results.json not found'); process.exit(1); } const vitestResults = JSON.parse(fs.readFileSync(resultsPath, 'utf8')); // Convert to github-action-benchmark format const formattedResults = []; // Vitest benchmark JSON reporter format if (vitestResults.files) { for (const file of vitestResults.files) { const suiteName = path.basename(file.filepath, '.bench.ts'); // Process each suite in the file if (file.groups) { for (const group of file.groups) { for (const benchmark of group.benchmarks || []) { if (benchmark.result) { formattedResults.push({ name: `${suiteName} - ${benchmark.name}`, unit: 'ms', value: benchmark.result.mean || 0, range: (benchmark.result.max - benchmark.result.min) || 0, extra: `${benchmark.result.hz?.toFixed(0) || 0} ops/sec` }); } } } } } } else if (Array.isArray(vitestResults)) { // Alternative format handling for (const result of vitestResults) { if (result.name && result.result) { formattedResults.push({ name: result.name, unit: 'ms', value: result.result.mean || 0, range: (result.result.max - result.result.min) || 0, extra: `${result.result.hz?.toFixed(0) || 0} ops/sec` }); } } } // Write formatted results const outputPath = path.join(process.cwd(), 'benchmark-results-formatted.json'); fs.writeFileSync(outputPath, JSON.stringify(formattedResults, null, 2)); // Also create a summary for PR comments const summary = { timestamp: new Date().toISOString(), benchmarks: formattedResults.map(b => ({ name: b.name, time: `${b.value.toFixed(3)}ms`, opsPerSec: b.extra, range: `±${(b.range / 2).toFixed(3)}ms` })) }; fs.writeFileSync( path.join(process.cwd(), 'benchmark-summary.json'), JSON.stringify(summary, null, 2) ); console.log(`Formatted ${formattedResults.length} benchmark results`); } // Run if called directly if (require.main === module) { formatBenchmarkResults(); } ``` -------------------------------------------------------------------------------- /scripts/vitest-benchmark-reporter.ts: -------------------------------------------------------------------------------- ```typescript import type { Task, TaskResult, BenchmarkResult } from 'vitest'; import { writeFileSync } from 'fs'; import { resolve } from 'path'; interface BenchmarkJsonResult { timestamp: string; files: Array<{ filepath: string; groups: Array<{ name: string; benchmarks: Array<{ name: string; result: { mean: number; min: number; max: number; hz: number; p75: number; p99: number; p995: number; p999: number; rme: number; samples: number; }; }>; }>; }>; } export class BenchmarkJsonReporter { private results: BenchmarkJsonResult = { timestamp: new Date().toISOString(), files: [] }; onInit() { console.log('[BenchmarkJsonReporter] Initialized'); } onFinished(files?: Task[]) { console.log('[BenchmarkJsonReporter] onFinished called'); if (!files) { console.log('[BenchmarkJsonReporter] No files provided'); return; } for (const file of files) { const fileResult = { filepath: file.filepath || 'unknown', groups: [] as any[] }; this.processTask(file, fileResult); if (fileResult.groups.length > 0) { this.results.files.push(fileResult); } } // Write results const outputPath = resolve(process.cwd(), 'benchmark-results.json'); writeFileSync(outputPath, JSON.stringify(this.results, null, 2)); console.log(`[BenchmarkJsonReporter] Results written to ${outputPath}`); } private processTask(task: Task, fileResult: any) { if (task.type === 'suite' && task.tasks) { const group = { name: task.name, benchmarks: [] as any[] }; for (const benchmark of task.tasks) { const result = benchmark.result as TaskResult & { benchmark?: BenchmarkResult }; if (result?.benchmark) { group.benchmarks.push({ name: benchmark.name, result: { mean: result.benchmark.mean || 0, min: result.benchmark.min || 0, max: result.benchmark.max || 0, hz: result.benchmark.hz || 0, p75: result.benchmark.p75 || 0, p99: result.benchmark.p99 || 0, p995: result.benchmark.p995 || 0, p999: result.benchmark.p999 || 0, rme: result.benchmark.rme || 0, samples: result.benchmark.samples?.length || 0 } }); } } if (group.benchmarks.length > 0) { fileResult.groups.push(group); } } } } ``` -------------------------------------------------------------------------------- /tests/fixtures/factories/node.factory.ts: -------------------------------------------------------------------------------- ```typescript import { Factory } from 'fishery'; import { faker } from '@faker-js/faker'; interface NodeDefinition { name: string; displayName: string; description: string; version: number; defaults: { name: string }; inputs: string[]; outputs: string[]; properties: any[]; credentials?: any[]; group?: string[]; } export const nodeFactory = Factory.define<NodeDefinition>(() => ({ name: faker.helpers.slugify(faker.word.noun()), displayName: faker.company.name(), description: faker.lorem.sentence(), version: faker.number.int({ min: 1, max: 5 }), defaults: { name: faker.word.noun() }, inputs: ['main'], outputs: ['main'], group: [faker.helpers.arrayElement(['transform', 'trigger', 'output'])], properties: [ { displayName: 'Resource', name: 'resource', type: 'options', default: 'user', options: [ { name: 'User', value: 'user' }, { name: 'Post', value: 'post' } ] } ], credentials: [] })); // Specific node factories export const webhookNodeFactory = nodeFactory.params({ name: 'webhook', displayName: 'Webhook', description: 'Starts the workflow when a webhook is called', group: ['trigger'], properties: [ { displayName: 'Path', name: 'path', type: 'string', default: 'webhook', required: true }, { displayName: 'Method', name: 'method', type: 'options', default: 'GET', options: [ { name: 'GET', value: 'GET' }, { name: 'POST', value: 'POST' } ] } ] }); export const slackNodeFactory = nodeFactory.params({ name: 'slack', displayName: 'Slack', description: 'Send messages to Slack', group: ['output'], credentials: [ { name: 'slackApi', required: true } ], properties: [ { displayName: 'Resource', name: 'resource', type: 'options', default: 'message', options: [ { name: 'Message', value: 'message' }, { name: 'Channel', value: 'channel' } ] }, { displayName: 'Operation', name: 'operation', type: 'options', displayOptions: { show: { resource: ['message'] } }, default: 'post', options: [ { name: 'Post', value: 'post' }, { name: 'Update', value: 'update' } ] }, { displayName: 'Channel', name: 'channel', type: 'string', required: true, displayOptions: { show: { resource: ['message'], operation: ['post'] } }, default: '' } ] }); ``` -------------------------------------------------------------------------------- /types/test-env.d.ts: -------------------------------------------------------------------------------- ```typescript /** * Type definitions for test environment variables */ declare global { namespace NodeJS { interface ProcessEnv { // Core Environment NODE_ENV: 'test' | 'development' | 'production'; MCP_MODE?: 'test' | 'http' | 'stdio'; TEST_ENVIRONMENT?: string; // Database Configuration NODE_DB_PATH?: string; REBUILD_ON_START?: string; TEST_SEED_DATABASE?: string; TEST_SEED_TEMPLATES?: string; // API Configuration N8N_API_URL?: string; N8N_API_KEY?: string; N8N_WEBHOOK_BASE_URL?: string; N8N_WEBHOOK_TEST_URL?: string; // Server Configuration PORT?: string; HOST?: string; CORS_ORIGIN?: string; // Authentication AUTH_TOKEN?: string; MCP_AUTH_TOKEN?: string; // Logging LOG_LEVEL?: 'debug' | 'info' | 'warn' | 'error'; DEBUG?: string; TEST_LOG_VERBOSE?: string; ERROR_SHOW_STACK?: string; ERROR_SHOW_DETAILS?: string; // Test Timeouts TEST_TIMEOUT_UNIT?: string; TEST_TIMEOUT_INTEGRATION?: string; TEST_TIMEOUT_E2E?: string; TEST_TIMEOUT_GLOBAL?: string; // Test Execution TEST_RETRY_ATTEMPTS?: string; TEST_RETRY_DELAY?: string; TEST_PARALLEL?: string; TEST_MAX_WORKERS?: string; // Feature Flags FEATURE_TEST_COVERAGE?: string; FEATURE_TEST_SCREENSHOTS?: string; FEATURE_TEST_VIDEOS?: string; FEATURE_TEST_TRACE?: string; FEATURE_MOCK_EXTERNAL_APIS?: string; FEATURE_USE_TEST_CONTAINERS?: string; // Mock Services MSW_ENABLED?: string; MSW_API_DELAY?: string; REDIS_MOCK_ENABLED?: string; REDIS_MOCK_PORT?: string; ELASTICSEARCH_MOCK_ENABLED?: string; ELASTICSEARCH_MOCK_PORT?: string; // Test Paths TEST_FIXTURES_PATH?: string; TEST_DATA_PATH?: string; TEST_SNAPSHOTS_PATH?: string; // Performance Thresholds PERF_THRESHOLD_API_RESPONSE?: string; PERF_THRESHOLD_DB_QUERY?: string; PERF_THRESHOLD_NODE_PARSE?: string; // Rate Limiting RATE_LIMIT_MAX?: string; RATE_LIMIT_WINDOW?: string; // Caching CACHE_TTL?: string; CACHE_ENABLED?: string; // Cleanup TEST_CLEANUP_ENABLED?: string; TEST_CLEANUP_ON_FAILURE?: string; // Network NETWORK_TIMEOUT?: string; NETWORK_RETRY_COUNT?: string; // Memory TEST_MEMORY_LIMIT?: string; // Coverage COVERAGE_DIR?: string; COVERAGE_REPORTER?: string; } } } // Export empty object to make this a module export {}; ``` -------------------------------------------------------------------------------- /scripts/http-bridge.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node /** * HTTP-to-stdio bridge for n8n-MCP * Connects to n8n-MCP HTTP server and bridges stdio communication */ const http = require('http'); const readline = require('readline'); // Use MCP_URL from environment or construct from HOST/PORT if available const defaultHost = process.env.HOST || 'localhost'; const defaultPort = process.env.PORT || '3000'; const MCP_URL = process.env.MCP_URL || `http://${defaultHost}:${defaultPort}/mcp`; const AUTH_TOKEN = process.env.AUTH_TOKEN || process.argv[2]; if (!AUTH_TOKEN) { console.error('Error: AUTH_TOKEN environment variable or first argument required'); process.exit(1); } // Parse URL const url = new URL(MCP_URL); // Create readline interface for stdio const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false }); // Buffer for incomplete JSON messages let buffer = ''; // Handle incoming stdio messages rl.on('line', async (line) => { try { const message = JSON.parse(line); // Forward to HTTP server const options = { hostname: url.hostname, port: url.port || 80, path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${AUTH_TOKEN}`, 'Accept': 'application/json, text/event-stream' } }; const req = http.request(options, (res) => { let responseData = ''; res.on('data', (chunk) => { responseData += chunk; }); res.on('end', () => { try { // Try to parse as JSON const response = JSON.parse(responseData); console.log(JSON.stringify(response)); } catch (e) { // Handle SSE format const lines = responseData.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { try { const data = JSON.parse(line.substring(6)); console.log(JSON.stringify(data)); } catch (e) { // Ignore parse errors } } } } }); }); req.on('error', (error) => { console.error(JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: `HTTP request failed: ${error.message}` }, id: message.id || null })); }); req.write(JSON.stringify(message)); req.end(); } catch (error) { // Not valid JSON, ignore } }); // Handle process termination process.on('SIGTERM', () => { process.exit(0); }); process.on('SIGINT', () => { process.exit(0); }); ``` -------------------------------------------------------------------------------- /tests/integration/setup/integration-setup.ts: -------------------------------------------------------------------------------- ```typescript import { beforeAll, afterAll, afterEach } from 'vitest'; import { setupServer } from 'msw/node'; import { handlers as defaultHandlers } from '../../mocks/n8n-api/handlers'; // Create the MSW server instance with default handlers export const server = setupServer(...defaultHandlers); // Enable request logging in development/debugging if (process.env.MSW_DEBUG === 'true' || process.env.TEST_DEBUG === 'true') { server.events.on('request:start', ({ request }) => { console.log('[MSW] %s %s', request.method, request.url); }); server.events.on('request:match', ({ request }) => { console.log('[MSW] Request matched:', request.method, request.url); }); server.events.on('request:unhandled', ({ request }) => { console.warn('[MSW] Unhandled request:', request.method, request.url); }); server.events.on('response:mocked', ({ request, response }) => { console.log('[MSW] Mocked response for %s %s: %d', request.method, request.url, response.status ); }); } // Start server before all tests beforeAll(() => { server.listen({ onUnhandledRequest: process.env.CI === 'true' ? 'error' : 'warn', }); }); // Reset handlers after each test (important for test isolation) afterEach(() => { server.resetHandlers(); }); // Clean up after all tests afterAll(() => { // Remove all event listeners to prevent memory leaks server.events.removeAllListeners(); // Close the server server.close(); }); // Export the server and utility functions for use in integration tests export { server as integrationServer }; export { http, HttpResponse } from 'msw'; /** * Utility function to add temporary handlers for specific tests * @param handlers Array of MSW request handlers */ export function useHandlers(...handlers: any[]) { server.use(...handlers); } /** * Utility to wait for a specific request to be made * Useful for testing async operations */ export function waitForRequest(method: string, url: string | RegExp, timeout = 5000): Promise<Request> { return new Promise((resolve, reject) => { let timeoutId: NodeJS.Timeout; const handler = ({ request }: { request: Request }) => { if (request.method === method && (typeof url === 'string' ? request.url === url : url.test(request.url))) { clearTimeout(timeoutId); server.events.removeListener('request:match', handler); resolve(request); } }; // Set timeout timeoutId = setTimeout(() => { server.events.removeListener('request:match', handler); reject(new Error(`Timeout waiting for ${method} request to ${url}`)); }, timeout); server.events.on('request:match', handler); }); } ``` -------------------------------------------------------------------------------- /tests/test-direct-extraction.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node const fs = require('fs'); const path = require('path'); // Import the NodeSourceExtractor const { NodeSourceExtractor } = require('../dist/utils/node-source-extractor'); async function testExtraction() { console.log('=== Direct Node Extraction Test ===\n'); const extractor = new NodeSourceExtractor(); // Test extraction of AI Agent node const nodeType = '@n8n/n8n-nodes-langchain.Agent'; console.log(`Testing extraction of: ${nodeType}`); // First, let's debug what paths are being searched console.log('\nSearching in paths:'); const searchPaths = [ '/usr/local/lib/node_modules/n8n/node_modules', '/app/node_modules', '/home/node/.n8n/custom/nodes', './node_modules' ]; for (const basePath of searchPaths) { console.log(`- ${basePath}`); try { const exists = fs.existsSync(basePath); console.log(` Exists: ${exists}`); if (exists) { const items = fs.readdirSync(basePath).slice(0, 5); console.log(` Sample items: ${items.join(', ')}...`); } } catch (e) { console.log(` Error: ${e.message}`); } } try { const result = await extractor.extractNodeSource(nodeType, true); console.log('\n✅ Extraction successful!'); console.log(`Source file: ${result.location}`); console.log(`Code length: ${result.sourceCode.length} characters`); console.log(`Credential code found: ${result.credentialCode ? 'Yes' : 'No'}`); console.log(`Package.json found: ${result.packageInfo ? 'Yes' : 'No'}`); // Show first 500 characters of the code console.log('\nFirst 500 characters of code:'); console.log('=' .repeat(60)); console.log(result.sourceCode.substring(0, 500) + '...'); console.log('=' .repeat(60)); // Show credential code if found if (result.credentialCode) { console.log('\nCredential code found!'); console.log('First 200 characters of credential code:'); console.log(result.credentialCode.substring(0, 200) + '...'); } // Check if we can find it in Docker volume const dockerPath = '/usr/local/lib/node_modules/n8n/node_modules/.pnpm/@n8n+n8n-nodes-langchain@file+packages+@n8n+nodes-langchain_f35e7d377a7fe4d08dc2766706b5dbff/node_modules/@n8n/n8n-nodes-langchain/dist/nodes/agents/Agent/Agent.node.js'; if (fs.existsSync(dockerPath)) { console.log('\n✅ File also found in expected Docker path'); const dockerContent = fs.readFileSync(dockerPath, 'utf8'); console.log(`Docker file size: ${dockerContent.length} bytes`); } } catch (error) { console.error('\n❌ Extraction failed:', error.message); console.error('Stack trace:', error.stack); } } testExtraction().catch(console.error); ``` -------------------------------------------------------------------------------- /tests/integration/mcp-protocol/basic-connection.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, it, expect } from 'vitest'; import { N8NDocumentationMCPServer } from '../../../src/mcp/server'; describe('Basic MCP Connection', () => { it('should initialize MCP server', async () => { const server = new N8NDocumentationMCPServer(); // Test executeTool directly - it returns raw data const result = await server.executeTool('get_database_statistics', {}); expect(result).toBeDefined(); expect(typeof result).toBe('object'); expect(result.totalNodes).toBeDefined(); expect(result.statistics).toBeDefined(); await server.shutdown(); }); it('should execute list_nodes tool', async () => { const server = new N8NDocumentationMCPServer(); // First check if we have any nodes in the database const stats = await server.executeTool('get_database_statistics', {}); const hasNodes = stats.totalNodes > 0; const result = await server.executeTool('list_nodes', { limit: 5 }); expect(result).toBeDefined(); expect(typeof result).toBe('object'); expect(result.nodes).toBeDefined(); expect(Array.isArray(result.nodes)).toBe(true); if (hasNodes) { // If database has nodes, we should get up to 5 expect(result.nodes.length).toBeLessThanOrEqual(5); expect(result.nodes.length).toBeGreaterThan(0); expect(result.nodes[0]).toHaveProperty('nodeType'); expect(result.nodes[0]).toHaveProperty('displayName'); } else { // In test environment with empty database, we expect empty results expect(result.nodes).toHaveLength(0); } await server.shutdown(); }); it('should search nodes', async () => { const server = new N8NDocumentationMCPServer(); // First check if we have any nodes in the database const stats = await server.executeTool('get_database_statistics', {}); const hasNodes = stats.totalNodes > 0; const result = await server.executeTool('search_nodes', { query: 'webhook' }); expect(result).toBeDefined(); expect(typeof result).toBe('object'); expect(result.results).toBeDefined(); expect(Array.isArray(result.results)).toBe(true); // Only expect results if the database has nodes if (hasNodes) { expect(result.results.length).toBeGreaterThan(0); expect(result.totalCount).toBeGreaterThan(0); // Should find webhook node const webhookNode = result.results.find((n: any) => n.nodeType === 'nodes-base.webhook'); expect(webhookNode).toBeDefined(); expect(webhookNode.displayName).toContain('Webhook'); } else { // In test environment with empty database, we expect empty results expect(result.results).toHaveLength(0); expect(result.totalCount).toBe(0); } await server.shutdown(); }); }); ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/workflow_management/n8n-update-full-workflow.ts: -------------------------------------------------------------------------------- ```typescript import { ToolDocumentation } from '../types'; export const n8nUpdateFullWorkflowDoc: ToolDocumentation = { name: 'n8n_update_full_workflow', category: 'workflow_management', essentials: { description: 'Full workflow update. Requires complete nodes[] and connections{}. For incremental use n8n_update_partial_workflow.', keyParameters: ['id', 'nodes', 'connections'], example: 'n8n_update_full_workflow({id: "wf_123", nodes: [...], connections: {...}})', performance: 'Network-dependent', tips: [ 'Must provide complete workflow', 'Use update_partial for small changes', 'Validate before updating' ] }, full: { description: 'Performs a complete workflow update by replacing the entire workflow definition. Requires providing the complete nodes array and connections object, even for small changes. This is a full replacement operation - any nodes or connections not included will be removed.', parameters: { id: { type: 'string', required: true, description: 'Workflow ID to update' }, name: { type: 'string', description: 'New workflow name (optional)' }, nodes: { type: 'array', description: 'Complete array of workflow nodes (required if modifying structure)' }, connections: { type: 'object', description: 'Complete connections object (required if modifying structure)' }, settings: { type: 'object', description: 'Workflow settings to update (timezone, error handling, etc.)' } }, returns: 'Updated workflow object with all fields including the changes applied', examples: [ 'n8n_update_full_workflow({id: "abc", name: "New Name"}) - Rename only', 'n8n_update_full_workflow({id: "xyz", nodes: [...], connections: {...}}) - Full structure update', 'const wf = n8n_get_workflow({id}); wf.nodes.push(newNode); n8n_update_full_workflow(wf); // Add node' ], useCases: [ 'Major workflow restructuring', 'Bulk node updates', 'Workflow imports/cloning', 'Complete workflow replacement', 'Settings changes' ], performance: 'Network-dependent - typically 200-500ms. Larger workflows take longer. Consider update_partial for better performance.', bestPractices: [ 'Get workflow first, modify, then update', 'Validate with validate_workflow before updating', 'Use update_partial for small changes', 'Test updates in non-production first' ], pitfalls: [ 'Requires N8N_API_URL and N8N_API_KEY configured', 'Must include ALL nodes/connections', 'Missing nodes will be deleted', 'Can break active workflows', 'No partial updates - use update_partial instead' ], relatedTools: ['n8n_get_workflow', 'n8n_update_partial_workflow', 'validate_workflow', 'n8n_create_workflow'] } }; ``` -------------------------------------------------------------------------------- /src/telemetry/error-sanitization-utils.ts: -------------------------------------------------------------------------------- ```typescript /** * Shared Error Sanitization Utilities * Used by both error-sanitizer.ts and event-tracker.ts to avoid code duplication * * Security patterns from v2.15.3 with ReDoS fix from v2.18.3 */ import { logger } from '../utils/logger'; /** * Core error message sanitization with security-focused patterns * * Sanitization order (critical for preventing leakage): * 1. Early truncation (ReDoS prevention) * 2. Stack trace limitation * 3. URLs (most encompassing) - fully redact * 4. Specific credentials (AWS, GitHub, JWT, Bearer) * 5. Emails (after URLs) * 6. Long keys and tokens * 7. Generic credential patterns * 8. Final truncation * * @param errorMessage - Raw error message to sanitize * @returns Sanitized error message safe for telemetry */ export function sanitizeErrorMessageCore(errorMessage: string): string { try { // Early truncate to prevent ReDoS and performance issues const maxLength = 1500; const trimmed = errorMessage.length > maxLength ? errorMessage.substring(0, maxLength) : errorMessage; // Handle stack traces - keep only first 3 lines (message + top stack frames) const lines = trimmed.split('\n'); let sanitized = lines.slice(0, 3).join('\n'); // Sanitize sensitive data in correct order to prevent leakage // 1. URLs first (most encompassing) - fully redact to prevent path leakage sanitized = sanitized.replace(/https?:\/\/\S+/gi, '[URL]'); // 2. Specific credential patterns (before generic patterns) sanitized = sanitized .replace(/AKIA[A-Z0-9]{16}/g, '[AWS_KEY]') .replace(/ghp_[a-zA-Z0-9]{36,}/g, '[GITHUB_TOKEN]') .replace(/eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g, '[JWT]') .replace(/Bearer\s+[^\s]+/gi, 'Bearer [TOKEN]'); // 3. Emails (after URLs to avoid partial matches) sanitized = sanitized.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[EMAIL]'); // 4. Long keys and quoted tokens sanitized = sanitized .replace(/\b[a-zA-Z0-9_-]{32,}\b/g, '[KEY]') .replace(/(['"])[a-zA-Z0-9_-]{16,}\1/g, '$1[TOKEN]$1'); // 5. Generic credential patterns (after specific ones to avoid conflicts) // FIX (v2.18.3): Replaced negative lookbehind with simpler regex to prevent ReDoS sanitized = sanitized .replace(/password\s*[=:]\s*\S+/gi, 'password=[REDACTED]') .replace(/api[_-]?key\s*[=:]\s*\S+/gi, 'api_key=[REDACTED]') .replace(/\btoken\s*[=:]\s*[^\s;,)]+/gi, 'token=[REDACTED]'); // Simplified regex (no negative lookbehind) // Final truncate to 500 chars if (sanitized.length > 500) { sanitized = sanitized.substring(0, 500) + '...'; } return sanitized; } catch (error) { logger.debug('Error message sanitization failed:', error); return '[SANITIZATION_FAILED]'; } } ``` -------------------------------------------------------------------------------- /src/scripts/rebuild-database.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import * as dotenv from 'dotenv'; import { NodeDocumentationService } from '../services/node-documentation-service'; import { logger } from '../utils/logger'; // Load environment variables dotenv.config(); /** * Rebuild the enhanced documentation database */ async function rebuildDocumentationDatabase() { console.log('🔄 Starting enhanced documentation database rebuild...\n'); const startTime = Date.now(); const service = new NodeDocumentationService(); try { // Run the rebuild const results = await service.rebuildDatabase(); const duration = ((Date.now() - startTime) / 1000).toFixed(2); console.log('\n✅ Enhanced documentation database rebuild completed!\n'); console.log('📊 Results:'); console.log(` Total nodes found: ${results.total}`); console.log(` Successfully processed: ${results.successful}`); console.log(` Failed: ${results.failed}`); console.log(` Duration: ${duration}s`); if (results.errors.length > 0) { console.log(`\n⚠️ First ${Math.min(5, results.errors.length)} errors:`); results.errors.slice(0, 5).forEach(err => { console.log(` - ${err}`); }); if (results.errors.length > 5) { console.log(` ... and ${results.errors.length - 5} more errors`); } } // Get and display statistics const stats = await service.getStatistics(); console.log('\n📈 Database Statistics:'); console.log(` Total nodes: ${stats.totalNodes}`); console.log(` Nodes with documentation: ${stats.nodesWithDocs}`); console.log(` Nodes with examples: ${stats.nodesWithExamples}`); console.log(` Nodes with credentials: ${stats.nodesWithCredentials}`); console.log(` Trigger nodes: ${stats.triggerNodes}`); console.log(` Webhook nodes: ${stats.webhookNodes}`); console.log('\n📦 Package distribution:'); stats.packageDistribution.slice(0, 10).forEach((pkg: any) => { console.log(` ${pkg.package}: ${pkg.count} nodes`); }); // Close database connection await service.close(); console.log('\n✨ Enhanced documentation database is ready!'); console.log('💡 The database now includes:'); console.log(' - Complete node source code'); console.log(' - Enhanced documentation with operations and API methods'); console.log(' - Code examples and templates'); console.log(' - Related resources and required scopes'); } catch (error) { console.error('\n❌ Documentation database rebuild failed:', error); service.close(); process.exit(1); } } // Run if called directly if (require.main === module) { rebuildDocumentationDatabase().catch(error => { console.error('Fatal error:', error); process.exit(1); }); } export { rebuildDocumentationDatabase }; ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/discovery/get-database-statistics.ts: -------------------------------------------------------------------------------- ```typescript import { ToolDocumentation } from '../types'; export const getDatabaseStatisticsDoc: ToolDocumentation = { name: 'get_database_statistics', category: 'discovery', essentials: { description: 'Returns database health metrics and node inventory. Shows 525 total nodes, 263 AI-capable nodes, 104 triggers, with 87% documentation coverage. Primary use: verify MCP connection is working correctly.', keyParameters: [], example: 'get_database_statistics()', performance: 'Instant', tips: [ 'First tool to call when testing MCP connection', 'Shows exact counts for all node categories', 'Documentation coverage indicates data quality' ] }, full: { description: 'Returns comprehensive database statistics showing the complete inventory of n8n nodes, their categories, documentation coverage, and package distribution. Essential for verifying MCP connectivity and understanding available resources.', parameters: {}, returns: `Object containing: { "total_nodes": 525, // All nodes in database "nodes_with_properties": 520, // Nodes with extracted properties (99%) "nodes_with_operations": 334, // Nodes with multiple operations (64%) "ai_tools": 263, // AI-capable nodes "triggers": 104, // Workflow trigger nodes "documentation_coverage": "87%", // Nodes with official docs "packages": { "n8n-nodes-base": 456, // Core n8n nodes "@n8n/n8n-nodes-langchain": 69 // AI/LangChain nodes }, "categories": { "trigger": 104, "transform": 250, "output": 45, "input": 38, "AI": 88 } }`, examples: [ 'get_database_statistics() - Returns complete statistics object', '// Common check:', 'const stats = get_database_statistics();', 'if (stats.total_nodes < 500) console.error("Database incomplete!");' ], useCases: [ 'Verify MCP server is connected and responding', 'Check if database rebuild is needed (low node count)', 'Monitor documentation coverage improvements', 'Validate AI tools availability for workflows', 'Audit node distribution across packages' ], performance: 'Instant (<1ms) - Statistics are pre-calculated and cached', bestPractices: [ 'Call this first to verify MCP connection before other operations', 'Check total_nodes >= 500 to ensure complete database', 'Monitor documentation_coverage for data quality', 'Use ai_tools count to verify AI capabilities' ], pitfalls: [ 'Statistics are cached at database build time, not real-time', 'Won\'t reflect changes until database is rebuilt', 'Package counts may vary with n8n version updates' ], relatedTools: ['list_nodes for detailed node listing', 'list_ai_tools for AI nodes', 'n8n_health_check for API connectivity'] } }; ``` -------------------------------------------------------------------------------- /src/scripts/test-summary.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env npx tsx import { createDatabaseAdapter } from '../database/database-adapter'; import { NodeRepository } from '../database/node-repository'; import { NodeSimilarityService } from '../services/node-similarity-service'; import path from 'path'; async function testSummary() { const dbPath = path.join(process.cwd(), 'data/nodes.db'); const db = await createDatabaseAdapter(dbPath); const repository = new NodeRepository(db); const similarityService = new NodeSimilarityService(repository); const testCases = [ { invalid: 'HttpRequest', expected: 'nodes-base.httpRequest' }, { invalid: 'HTTPRequest', expected: 'nodes-base.httpRequest' }, { invalid: 'Webhook', expected: 'nodes-base.webhook' }, { invalid: 'WebHook', expected: 'nodes-base.webhook' }, { invalid: 'slack', expected: 'nodes-base.slack' }, { invalid: 'googleSheets', expected: 'nodes-base.googleSheets' }, { invalid: 'telegram', expected: 'nodes-base.telegram' }, { invalid: 'htpRequest', expected: 'nodes-base.httpRequest' }, { invalid: 'webook', expected: 'nodes-base.webhook' }, { invalid: 'slak', expected: 'nodes-base.slack' }, { invalid: 'http', expected: 'nodes-base.httpRequest' }, { invalid: 'sheet', expected: 'nodes-base.googleSheets' }, { invalid: 'nodes-base.openai', expected: 'nodes-langchain.openAi' }, { invalid: 'n8n-nodes-base.httpRequest', expected: 'nodes-base.httpRequest' }, { invalid: 'foobar', expected: null }, { invalid: 'xyz123', expected: null }, ]; let passed = 0; let failed = 0; console.log('Test Results Summary:'); console.log('='.repeat(60)); for (const testCase of testCases) { const suggestions = await similarityService.findSimilarNodes(testCase.invalid, 3); let result = '❌'; let status = 'FAILED'; if (testCase.expected === null) { // Should have no suggestions if (suggestions.length === 0) { result = '✅'; status = 'PASSED'; passed++; } else { failed++; } } else { // Should have the expected suggestion const found = suggestions.some(s => s.nodeType === testCase.expected); if (found) { const suggestion = suggestions.find(s => s.nodeType === testCase.expected); const isAutoFixable = suggestion && suggestion.confidence >= 0.9; result = '✅'; status = isAutoFixable ? 'PASSED (auto-fixable)' : 'PASSED'; passed++; } else { failed++; } } console.log(`${result} "${testCase.invalid}" → ${testCase.expected || 'no suggestions'}: ${status}`); } console.log('='.repeat(60)); console.log(`\nTotal: ${passed}/${testCases.length} tests passed`); if (failed === 0) { console.log('🎉 All tests passed!'); } else { console.log(`⚠️ ${failed} tests failed`); } } testSummary().catch(console.error); ``` -------------------------------------------------------------------------------- /tests/extracted-nodes-db/insert-nodes.sql: -------------------------------------------------------------------------------- ```sql -- Auto-generated SQL for n8n nodes -- Node: n8n-nodes-base.Function INSERT INTO nodes (node_type, name, package_name, code_hash, code_length, source_location, has_credentials) VALUES ('n8n-nodes-base.Function', 'Function', 'n8n-nodes-base', 'd68f1ab94b190161e2ec2c56ec6631f6c3992826557c100ec578efff5de96a70', 7449, 'node_modules/n8n-nodes-base/dist/nodes/Function/Function.node.js', false); -- Node: n8n-nodes-base.Webhook INSERT INTO nodes (node_type, name, package_name, code_hash, code_length, source_location, has_credentials) VALUES ('n8n-nodes-base.Webhook', 'Webhook', 'n8n-nodes-base', '143d6bbdce335c5a9204112b2c1e8b92e4061d75ba3cb23301845f6fed9e6c71', 10667, 'node_modules/n8n-nodes-base/dist/nodes/Webhook/Webhook.node.js', false); -- Node: n8n-nodes-base.HttpRequest INSERT INTO nodes (node_type, name, package_name, code_hash, code_length, source_location, has_credentials) VALUES ('n8n-nodes-base.HttpRequest', 'HttpRequest', 'n8n-nodes-base', '5b5e2328474b7e85361c940dfe942e167b3f0057f38062f56d6b693f0a7ffe7e', 1343, 'node_modules/n8n-nodes-base/dist/nodes/HttpRequest/HttpRequest.node.js', false); -- Node: n8n-nodes-base.If INSERT INTO nodes (node_type, name, package_name, code_hash, code_length, source_location, has_credentials) VALUES ('n8n-nodes-base.If', 'If', 'n8n-nodes-base', '7910ed9177a946b76f04ca847defb81226c37c698e4cdb63913f038c6c257ee1', 20533, 'node_modules/n8n-nodes-base/dist/nodes/If/If.node.js', false); -- Node: n8n-nodes-base.SplitInBatches INSERT INTO nodes (node_type, name, package_name, code_hash, code_length, source_location, has_credentials) VALUES ('n8n-nodes-base.SplitInBatches', 'SplitInBatches', 'n8n-nodes-base', 'c751422a11e30bf361a6c4803376289740a40434aeb77f90e18cd4dd7ba5c019', 1135, 'node_modules/n8n-nodes-base/dist/nodes/SplitInBatches/SplitInBatches.node.js', false); -- Node: n8n-nodes-base.Airtable INSERT INTO nodes (node_type, name, package_name, code_hash, code_length, source_location, has_credentials) VALUES ('n8n-nodes-base.Airtable', 'Airtable', 'n8n-nodes-base', '2d67e72931697178946f5127b43e954649c4c5e7ad9e29764796404ae96e7db5', 936, 'node_modules/n8n-nodes-base/dist/nodes/Airtable/Airtable.node.js', false); -- Node: n8n-nodes-base.Slack INSERT INTO nodes (node_type, name, package_name, code_hash, code_length, source_location, has_credentials) VALUES ('n8n-nodes-base.Slack', 'Slack', 'n8n-nodes-base', '0ed10d0646f3c595406359edfa2c293dac41991cee59ad4fb3ccf2bb70eca6fc', 1007, 'node_modules/n8n-nodes-base/dist/nodes/Slack/Slack.node.js', false); -- Node: n8n-nodes-base.Discord INSERT INTO nodes (node_type, name, package_name, code_hash, code_length, source_location, has_credentials) VALUES ('n8n-nodes-base.Discord', 'Discord', 'n8n-nodes-base', '4995f9ca5c5b57d2486c2e320cc7505238e7f2260861f7e321b44b45ccabeb00', 10049, 'node_modules/n8n-nodes-base/dist/nodes/Discord/Discord.node.js', false); ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/validation/validate-workflow-connections.ts: -------------------------------------------------------------------------------- ```typescript import { ToolDocumentation } from '../types'; export const validateWorkflowConnectionsDoc: ToolDocumentation = { name: 'validate_workflow_connections', category: 'validation', essentials: { description: 'Check workflow connections only: valid nodes, no cycles, proper triggers, AI tool links. Fast structure validation.', keyParameters: ['workflow'], example: 'validate_workflow_connections({workflow: {nodes: [...], connections: {...}}})', performance: 'Fast (<100ms)', tips: [ 'Use for quick structure checks when editing connections', 'Detects orphaned nodes and circular dependencies', 'Validates AI Agent tool connections to ensure proper node references' ] }, full: { description: 'Validates only the connection structure of a workflow without checking node configurations or expressions. This focused validation checks that all referenced nodes exist, detects circular dependencies, ensures proper trigger node placement, validates AI tool connections, and identifies orphaned or unreachable nodes.', parameters: { workflow: { type: 'object', required: true, description: 'The workflow JSON with nodes array and connections object.' } }, returns: 'Object with valid (boolean), errors (array), warnings (array), and statistics about connections', examples: [ 'validate_workflow_connections({workflow: myWorkflow}) - Check all connections', 'validate_workflow_connections({workflow: {nodes: [...], connections: {...}}}) - Validate structure only' ], useCases: [ 'Quick validation when modifying workflow connections', 'Ensure all node references in connections are valid', 'Detect circular dependencies that would cause infinite loops', 'Validate AI Agent nodes have proper tool connections', 'Check workflow has at least one trigger node', 'Find orphaned nodes not connected to any flow' ], performance: 'Fast (<100ms). Only validates structure, not node content. Scales linearly with connection count.', bestPractices: [ 'Run after adding or removing connections', 'Use before validate_workflow for quick structural checks', 'Check for warnings about orphaned nodes', 'Ensure trigger nodes are properly positioned', 'Validate after using n8n_update_partial_workflow with connection operations' ], pitfalls: [ 'Does not validate node configurations - use validate_workflow for full validation', 'Cannot detect logical errors in connection flow', 'Some valid workflows may have intentionally disconnected nodes', 'Circular dependency detection only catches direct loops', 'Does not validate connection types match node capabilities' ], relatedTools: ['validate_workflow', 'validate_workflow_expressions', 'n8n_update_partial_workflow'] } }; ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/workflow_management/n8n-list-workflows.ts: -------------------------------------------------------------------------------- ```typescript import { ToolDocumentation } from '../types'; export const n8nListWorkflowsDoc: ToolDocumentation = { name: 'n8n_list_workflows', category: 'workflow_management', essentials: { description: 'List workflows (minimal metadata only - no nodes/connections). Supports pagination via cursor.', keyParameters: ['limit', 'active', 'tags'], example: 'n8n_list_workflows({limit: 20, active: true})', performance: 'Fast (100-300ms)', tips: [ 'Use cursor for pagination', 'Filter by active status', 'Tag filtering for organization' ] }, full: { description: 'Lists workflows from n8n with powerful filtering options. Returns ONLY minimal metadata (id, name, active, dates, tags, nodeCount) - no workflow structure, nodes, or connections. Use n8n_get_workflow to fetch full workflow details.', parameters: { limit: { type: 'number', description: 'Number of workflows to return (1-100, default: 100)' }, cursor: { type: 'string', description: 'Pagination cursor from previous response for next page' }, active: { type: 'boolean', description: 'Filter by active/inactive status' }, tags: { type: 'array', description: 'Filter by exact tag matches (AND logic)' }, projectId: { type: 'string', description: 'Filter by project ID (enterprise feature)' }, excludePinnedData: { type: 'boolean', description: 'Exclude pinned data from response (default: true)' } }, returns: 'Object with: workflows array (minimal fields: id, name, active, createdAt, updatedAt, tags, nodeCount), returned (count in this response), hasMore (boolean), nextCursor (for pagination), and _note (guidance when more data exists)', examples: [ 'n8n_list_workflows({limit: 20}) - First 20 workflows', 'n8n_list_workflows({active: true, tags: ["production"]}) - Active production workflows', 'n8n_list_workflows({cursor: "abc123", limit: 50}) - Next page of results' ], useCases: [ 'Build workflow dashboards', 'Find workflows by status', 'Organize by tags', 'Bulk workflow operations', 'Generate workflow reports' ], performance: 'Very fast - typically 50-200ms. Returns only minimal metadata without workflow structure.', bestPractices: [ 'Always check hasMore flag to determine if pagination is needed', 'Use cursor from previous response to get next page', 'The returned count is NOT the total in the system', 'Iterate with cursor until hasMore is false for complete list' ], pitfalls: [ 'Requires N8N_API_URL and N8N_API_KEY configured', 'Maximum 100 workflows per request', 'Server may return fewer than requested limit', 'returned field is count of current page only, not system total' ], relatedTools: ['n8n_get_workflow_minimal', 'n8n_get_workflow', 'n8n_update_partial_workflow', 'n8n_list_executions'] } }; ``` -------------------------------------------------------------------------------- /test-reinit-fix.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # Test script to verify re-initialization fix works echo "Starting n8n MCP server..." AUTH_TOKEN=test123456789012345678901234567890 npm run start:http & SERVER_PID=$! # Wait for server to start sleep 3 echo "Testing multiple initialize requests..." # First initialize request echo "1. First initialize request:" RESPONSE1=$(curl -s -X POST http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "Authorization: Bearer test123456789012345678901234567890" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": { "roots": { "listChanged": false } }, "clientInfo": { "name": "test-client-1", "version": "1.0.0" } } }') if echo "$RESPONSE1" | grep -q '"result"'; then echo "✅ First initialize request succeeded" else echo "❌ First initialize request failed: $RESPONSE1" fi # Second initialize request (this was failing before) echo "2. Second initialize request (this was failing before the fix):" RESPONSE2=$(curl -s -X POST http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "Authorization: Bearer test123456789012345678901234567890" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": { "roots": { "listChanged": false } }, "clientInfo": { "name": "test-client-2", "version": "1.0.0" } } }') if echo "$RESPONSE2" | grep -q '"result"'; then echo "✅ Second initialize request succeeded - FIX WORKING!" else echo "❌ Second initialize request failed: $RESPONSE2" fi # Third initialize request to be sure echo "3. Third initialize request:" RESPONSE3=$(curl -s -X POST http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "Authorization: Bearer test123456789012345678901234567890" \ -d '{ "jsonrpc": "2.0", "id": 3, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": { "roots": { "listChanged": false } }, "clientInfo": { "name": "test-client-3", "version": "1.0.0" } } }') if echo "$RESPONSE3" | grep -q '"result"'; then echo "✅ Third initialize request succeeded" else echo "❌ Third initialize request failed: $RESPONSE3" fi # Check health to see active transports echo "4. Checking server health for active transports:" HEALTH=$(curl -s -X GET http://localhost:3000/health) echo "$HEALTH" | python3 -m json.tool # Cleanup echo "Stopping server..." kill $SERVER_PID wait $SERVER_PID 2>/dev/null echo "Test completed!" ``` -------------------------------------------------------------------------------- /src/mcp/tool-docs/validation/validate-workflow-expressions.ts: -------------------------------------------------------------------------------- ```typescript import { ToolDocumentation } from '../types'; export const validateWorkflowExpressionsDoc: ToolDocumentation = { name: 'validate_workflow_expressions', category: 'validation', essentials: { description: 'Validate n8n expressions: syntax {{}}, variables ($json/$node), references. Returns errors with locations.', keyParameters: ['workflow'], example: 'validate_workflow_expressions({workflow: {nodes: [...], connections: {...}}})', performance: 'Fast (<100ms)', tips: [ 'Catches syntax errors in {{}} expressions before runtime', 'Validates $json, $node, and other n8n variables', 'Shows exact location of expression errors in node parameters' ] }, full: { description: 'Validates all n8n expressions within a workflow for syntax correctness and reference validity. This tool scans all node parameters for n8n expressions (enclosed in {{}}), checks expression syntax, validates variable references like $json and $node("NodeName"), ensures referenced nodes exist in the workflow, and provides detailed error locations for debugging.', parameters: { workflow: { type: 'object', required: true, description: 'The workflow JSON to check for expression errors.' } }, returns: 'Object with valid (boolean), errors (array with node ID, parameter path, and error details), and expression count', examples: [ 'validate_workflow_expressions({workflow: myWorkflow}) - Check all expressions', 'validate_workflow_expressions({workflow: {nodes: [...], connections: {...}}}) - Validate expression syntax' ], useCases: [ 'Catch expression syntax errors before workflow execution', 'Validate node references in $node() expressions exist', 'Find typos in variable names like $json or $input', 'Ensure complex expressions are properly formatted', 'Debug expression errors with exact parameter locations', 'Validate expressions after workflow modifications' ], performance: 'Fast (<100ms). Scans all string parameters in all nodes. Performance scales with workflow size and expression count.', bestPractices: [ 'Run after modifying any expressions in node parameters', 'Check all $node() references when renaming nodes', 'Validate expressions before workflow deployment', 'Pay attention to nested object paths in expressions', 'Use with validate_workflow for comprehensive validation' ], pitfalls: [ 'Cannot validate expression logic, only syntax', 'Runtime data availability not checked (e.g., if $json.field exists)', 'Complex JavaScript in expressions may need runtime testing', 'Does not validate expression return types', 'Some valid expressions may use advanced features not fully parsed' ], relatedTools: ['validate_workflow', 'validate_workflow_connections', 'validate_node_operation'] } }; ``` -------------------------------------------------------------------------------- /scripts/test-optimized-docker.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # Test script for optimized Docker build set -e echo "🧪 Testing Optimized Docker Build" echo "=================================" # Colors for output GREEN='\033[0;32m' RED='\033[0;31m' NC='\033[0m' # No Color # Build the optimized image echo -e "\n📦 Building optimized Docker image..." docker build -f Dockerfile.optimized -t n8n-mcp:optimized . # Check image size echo -e "\n📊 Checking image size..." SIZE=$(docker images n8n-mcp:optimized --format "{{.Size}}") echo "Image size: $SIZE" # Extract size in MB for comparison SIZE_MB=$(docker images n8n-mcp:optimized --format "{{.Size}}" | sed 's/MB//' | sed 's/GB/*1024/' | bc 2>/dev/null || echo "0") if [ "$SIZE_MB" != "0" ] && [ "$SIZE_MB" -lt "300" ]; then echo -e "${GREEN}✅ Image size is optimized (<300MB)${NC}" else echo -e "${RED}⚠️ Image might be larger than expected${NC}" fi # Test stdio mode echo -e "\n🔍 Testing stdio mode..." TEST_RESULT=$(echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | \ docker run --rm -i -e MCP_MODE=stdio n8n-mcp:optimized 2>/dev/null | \ grep -o '"name":"[^"]*"' | head -1) if [ -n "$TEST_RESULT" ]; then echo -e "${GREEN}✅ Stdio mode working${NC}" else echo -e "${RED}❌ Stdio mode failed${NC}" fi # Test HTTP mode echo -e "\n🌐 Testing HTTP mode..." docker run -d --name test-optimized \ -e MCP_MODE=http \ -e AUTH_TOKEN=test-token \ -p 3002:3000 \ n8n-mcp:optimized # Wait for startup sleep 5 # Test health endpoint HEALTH=$(curl -s http://localhost:3002/health | grep -o '"status":"healthy"' || echo "") if [ -n "$HEALTH" ]; then echo -e "${GREEN}✅ Health endpoint working${NC}" else echo -e "${RED}❌ Health endpoint failed${NC}" fi # Test MCP endpoint MCP_TEST=$(curl -s -H "Authorization: Bearer test-token" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' \ http://localhost:3002/mcp | grep -o '"tools":\[' || echo "") if [ -n "$MCP_TEST" ]; then echo -e "${GREEN}✅ MCP endpoint working${NC}" else echo -e "${RED}❌ MCP endpoint failed${NC}" fi # Test database statistics tool STATS_TEST=$(curl -s -H "Authorization: Bearer test-token" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"get_database_statistics","arguments":{}},"id":2}' \ http://localhost:3002/mcp | grep -o '"totalNodes":[0-9]*' || echo "") if [ -n "$STATS_TEST" ]; then echo -e "${GREEN}✅ Database statistics tool working${NC}" echo "Database stats: $STATS_TEST" else echo -e "${RED}❌ Database statistics tool failed${NC}" fi # Cleanup docker stop test-optimized >/dev/null 2>&1 docker rm test-optimized >/dev/null 2>&1 # Compare with original image echo -e "\n📊 Size Comparison:" echo "Original image: $(docker images n8n-mcp:latest --format "{{.Size}}" 2>/dev/null || echo "Not built")" echo "Optimized image: $SIZE" echo -e "\n✨ Testing complete!" ``` -------------------------------------------------------------------------------- /tests/test-mcp-server-extraction.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node /** * Test MCP Server extraction functionality * Simulates an MCP client calling the get_node_source_code tool */ const { spawn } = require('child_process'); const path = require('path'); // MCP request to get AI Agent node source code const mcpRequest = { jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'get_node_source_code', arguments: { nodeType: '@n8n/n8n-nodes-langchain.Agent', includeCredentials: true } } }; async function testMCPExtraction() { console.log('=== MCP Server Node Extraction Test ===\n'); console.log('Starting MCP server...'); // Start the MCP server const serverPath = path.join(__dirname, '../dist/index.js'); const server = spawn('node', [serverPath], { env: { ...process.env, MCP_SERVER_PORT: '3000', MCP_SERVER_HOST: '0.0.0.0', N8N_API_URL: 'http://n8n:5678', N8N_API_KEY: 'test-api-key', MCP_AUTH_TOKEN: 'test-token', LOG_LEVEL: 'info' }, stdio: ['pipe', 'pipe', 'pipe'] }); let responseBuffer = ''; let errorBuffer = ''; server.stdout.on('data', (data) => { responseBuffer += data.toString(); }); server.stderr.on('data', (data) => { errorBuffer += data.toString(); }); // Give server time to start await new Promise(resolve => setTimeout(resolve, 2000)); console.log('Sending MCP request...'); console.log(JSON.stringify(mcpRequest, null, 2)); // Send the request via stdin (MCP uses stdio transport) server.stdin.write(JSON.stringify(mcpRequest) + '\n'); // Wait for response await new Promise(resolve => setTimeout(resolve, 3000)); // Kill the server server.kill(); console.log('\n=== Server Output ==='); console.log(responseBuffer); if (errorBuffer) { console.log('\n=== Server Errors ==='); console.log(errorBuffer); } // Try to parse any JSON responses const lines = responseBuffer.split('\n').filter(line => line.trim()); for (const line of lines) { try { const data = JSON.parse(line); if (data.id === 1 && data.result) { console.log('\n✅ MCP Response received!'); console.log(`Node type: ${data.result.nodeType}`); console.log(`Source code length: ${data.result.sourceCode ? data.result.sourceCode.length : 0} characters`); console.log(`Location: ${data.result.location}`); console.log(`Has credentials: ${data.result.credentialCode ? 'Yes' : 'No'}`); console.log(`Has package info: ${data.result.packageInfo ? 'Yes' : 'No'}`); if (data.result.sourceCode) { console.log('\nFirst 300 characters of extracted code:'); console.log('='.repeat(60)); console.log(data.result.sourceCode.substring(0, 300) + '...'); console.log('='.repeat(60)); } } } catch (e) { // Not JSON, skip } } } testMCPExtraction().catch(console.error); ``` -------------------------------------------------------------------------------- /scripts/audit-schema-coverage.ts: -------------------------------------------------------------------------------- ```typescript /** * Database Schema Coverage Audit Script * * Audits the database to determine how many nodes have complete schema information * for resourceLocator mode validation. This helps assess the coverage of our * schema-driven validation approach. */ import Database from 'better-sqlite3'; import path from 'path'; const dbPath = path.join(__dirname, '../data/nodes.db'); const db = new Database(dbPath, { readonly: true }); console.log('=== Schema Coverage Audit ===\n'); // Query 1: How many nodes have resourceLocator properties? const totalResourceLocator = db.prepare(` SELECT COUNT(*) as count FROM nodes WHERE properties_schema LIKE '%resourceLocator%' `).get() as { count: number }; console.log(`Nodes with resourceLocator properties: ${totalResourceLocator.count}`); // Query 2: Of those, how many have modes defined? const withModes = db.prepare(` SELECT COUNT(*) as count FROM nodes WHERE properties_schema LIKE '%resourceLocator%' AND properties_schema LIKE '%modes%' `).get() as { count: number }; console.log(`Nodes with modes defined: ${withModes.count}`); // Query 3: Which nodes have resourceLocator but NO modes? const withoutModes = db.prepare(` SELECT node_type, display_name FROM nodes WHERE properties_schema LIKE '%resourceLocator%' AND properties_schema NOT LIKE '%modes%' LIMIT 10 `).all() as Array<{ node_type: string; display_name: string }>; console.log(`\nSample nodes WITHOUT modes (showing 10):`); withoutModes.forEach(node => { console.log(` - ${node.display_name} (${node.node_type})`); }); // Calculate coverage percentage const coverage = totalResourceLocator.count > 0 ? (withModes.count / totalResourceLocator.count) * 100 : 0; console.log(`\nSchema coverage: ${coverage.toFixed(1)}% of resourceLocator nodes have modes defined`); // Query 4: Get some examples of nodes WITH modes for verification console.log('\nSample nodes WITH modes (showing 5):'); const withModesExamples = db.prepare(` SELECT node_type, display_name FROM nodes WHERE properties_schema LIKE '%resourceLocator%' AND properties_schema LIKE '%modes%' LIMIT 5 `).all() as Array<{ node_type: string; display_name: string }>; withModesExamples.forEach(node => { console.log(` - ${node.display_name} (${node.node_type})`); }); // Summary console.log('\n=== Summary ==='); console.log(`Total nodes in database: ${db.prepare('SELECT COUNT(*) as count FROM nodes').get() as any as { count: number }.count}`); console.log(`Nodes with resourceLocator: ${totalResourceLocator.count}`); console.log(`Nodes with complete mode schemas: ${withModes.count}`); console.log(`Nodes without mode schemas: ${totalResourceLocator.count - withModes.count}`); console.log(`\nImplication: Schema-driven validation will apply to ${withModes.count} nodes.`); console.log(`For the remaining ${totalResourceLocator.count - withModes.count} nodes, validation will be skipped (graceful degradation).`); db.close(); ``` -------------------------------------------------------------------------------- /src/scripts/debug-http-search.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env npx tsx import { createDatabaseAdapter } from '../database/database-adapter'; import { NodeRepository } from '../database/node-repository'; import { NodeSimilarityService } from '../services/node-similarity-service'; import path from 'path'; async function debugHttpSearch() { const dbPath = path.join(process.cwd(), 'data/nodes.db'); const db = await createDatabaseAdapter(dbPath); const repository = new NodeRepository(db); const service = new NodeSimilarityService(repository); console.log('Testing "http" search...\n'); // Check if httpRequest exists const httpNode = repository.getNode('nodes-base.httpRequest'); console.log('HTTP Request node exists:', httpNode ? 'Yes' : 'No'); if (httpNode) { console.log(' Display name:', httpNode.displayName); } // Test the search with internal details const suggestions = await service.findSimilarNodes('http', 5); console.log('\nSuggestions for "http":', suggestions.length); suggestions.forEach(s => { console.log(` - ${s.nodeType} (${Math.round(s.confidence * 100)}%)`); }); // Manually calculate score for httpRequest console.log('\nManual score calculation for httpRequest:'); const testNode = { nodeType: 'nodes-base.httpRequest', displayName: 'HTTP Request', category: 'Core Nodes' }; const cleanInvalid = 'http'; const cleanValid = 'nodesbasehttprequest'; const displayNameClean = 'httprequest'; // Check substring const hasSubstring = cleanValid.includes(cleanInvalid) || displayNameClean.includes(cleanInvalid); console.log(` Substring match: ${hasSubstring}`); // This should give us pattern match score const patternScore = hasSubstring ? 35 : 0; // Using 35 for short searches console.log(` Pattern score: ${patternScore}`); // Name similarity would be low console.log(` Total score would need to be >= 50 to appear`); // Get all nodes and check which ones contain 'http' const allNodes = repository.getAllNodes(); const httpNodes = allNodes.filter(n => n.nodeType.toLowerCase().includes('http') || (n.displayName && n.displayName.toLowerCase().includes('http')) ); console.log('\n\nNodes containing "http" in name:'); httpNodes.slice(0, 5).forEach(n => { console.log(` - ${n.nodeType} (${n.displayName})`); // Calculate score for this node const normalizedSearch = 'http'; const normalizedType = n.nodeType.toLowerCase().replace(/[^a-z0-9]/g, ''); const normalizedDisplay = (n.displayName || '').toLowerCase().replace(/[^a-z0-9]/g, ''); const containsInType = normalizedType.includes(normalizedSearch); const containsInDisplay = normalizedDisplay.includes(normalizedSearch); console.log(` Type check: "${normalizedType}" includes "${normalizedSearch}" = ${containsInType}`); console.log(` Display check: "${normalizedDisplay}" includes "${normalizedSearch}" = ${containsInDisplay}`); }); } debugHttpSearch().catch(console.error); ```