Use with caution. Address findings before production.
Scanned 5/3/2026, 6:47:07 PMยทCached resultยทFast Scanยท45 rulesยทHow we decide โ
AIVSS Score
Medium
Severity Breakdown
0
critical
1
high
98
medium
10
low
MCP Server Information
Findings
This package receives a C grade with a safety score of 55/100 due to 98 medium-severity issues concentrated in server configuration (67 findings) and ANSI escape injection vulnerabilities (16 findings), alongside 11 resource exhaustion risks and 10 readiness concerns. While no critical or high-severity flaws were identified, the volume of medium-severity configuration and injection issues suggests the server may be vulnerable to input manipulation and operational instability under certain conditions. Installation should be reconsidered unless these configuration gaps can be addressed or mitigated in your deployment environment.
No known CVEs found for this package or its dependencies.
Scan Details
Want deeper analysis?
Fast scan found 21 findings using rule-based analysis. Upgrade for LLM consensus across 5 judges, AI-generated remediation, and cross-file taint analysis.
Building your own MCP server?
Same rules, same LLM judges, same grade. Private scans stay isolated to your account and never appear in the public registry. Required for code your team hasnโt shipped yet.
21 of 21 findings
21 findings
File mounts an HTTP route that handles MCP `tools/list` (Express / Fastify / FastAPI / Flask) but the route โ and the router it sits behind โ has no auth middleware applied. An anonymous client can enumerate every tool the server exposes, scope the attack surface, and (if `tools/call` shares the route) invoke them. Apply auth at the route or router level: Express `passport.authenticate(...)` / a `requireAuth`-style middleware, FastAPI `Depends(get_current_user)` or `Depends(verify_jwt)`, Flask
Evidence
| 1 | /** |
| 2 | * N8N MCP Engine - Clean interface for service integration |
| 3 | * |
| 4 | * This class provides a simple API for integrating the n8n-MCP server |
| 5 | * into larger services. The wrapping service handles authentication, |
| 6 | * multi-tenancy, rate limiting, etc. |
| 7 | */ |
| 8 | import { Request, Response } from 'express'; |
| 9 | import { SingleSessionHTTPServer } from './http-server-single-session'; |
| 10 | import { logger } from './utils/logger'; |
| 11 | import { InstanceContext } from './types/instance-context'; |
| 12 | import { SessionState } from './ |
Remediation
Apply auth middleware at the route or router level: - Express / Fastify / Koa: `passport.authenticate(...)`, `requireAuth`, `verifyToken`, or an equivalent JWT middleware applied via `router.use(authMw)` or as a per-route handler. - FastAPI: `Depends(get_current_user)`, `OAuth2PasswordBearer`, `HTTPBearer`, or `verify_jwt` dependency. - Flask: `@login_required`, `@auth_required`, `@jwt_required`, or call `verify_jwt_in_request()` in the handler. Mounting MCP behind a s
`.env` file contains a credential-like variable name (API_KEY / TOKEN / SECRET / PASSWORD / PRIVATE_KEY / BEARER) assigned to what looks like a real value. `.env` files ship inside `git archive`, `docker build` contexts, and install tarballs โ any secret here leaks downstream (MCP Top-10 R9). Replace the value with a placeholder, rename the file to `.env.example`, and load the real value from a secret manager at runtime. If the credential was already committed, revoke it now (it is still in git
Evidence
| 3 | # Copy to .env and fill in values |
| 4 | |
| 5 | # Required for HTTP mode |
| 6 | AUTH_TOKEN= |
| 7 | |
| 8 | # Server configuration |
| 9 | PORT=3000 |
| 10 | HTTP_PORT=80 |
| 11 | HTTPS_PORT=443 |
Remediation
Remove the credential from the `.env` file and replace with a template placeholder: OPENAI_API_KEY=<your-openai-key> OPENAI_API_KEY=${OPENAI_API_KEY} Rename the file to `.env.example` so humans know it is a template. Store the real value in a secret manager and inject it at runtime. If the credential has already been committed, revoke it immediately (git history still contains it).
`.env` file contains a credential-like variable name (API_KEY / TOKEN / SECRET / PASSWORD / PRIVATE_KEY / BEARER) assigned to what looks like a real value. `.env` files ship inside `git archive`, `docker build` contexts, and install tarballs โ any secret here leaks downstream (MCP Top-10 R9). Replace the value with a placeholder, rename the file to `.env.example`, and load the real value from a secret manager at runtime. If the credential was already committed, revoke it now (it is still in git
Evidence
| 17 | # === API Configuration for Mocking === |
| 18 | # Mock API endpoints |
| 19 | N8N_API_URL=http://localhost:3001/mock-api |
| 20 | N8N_API_KEY=test-api-key-12345 |
| 21 | N8N_WEBHOOK_BASE_URL=http://localhost:3001/webhook |
| 22 | N8N_WEBHOOK_TEST_URL=http://localhost:3001/webhook-test |
Remediation
Remove the credential from the `.env` file and replace with a template placeholder: OPENAI_API_KEY=<your-openai-key> OPENAI_API_KEY=${OPENAI_API_KEY} Rename the file to `.env.example` so humans know it is a template. Store the real value in a secret manager and inject it at runtime. If the credential has already been committed, revoke it immediately (git history still contains it).
`.env` file contains a credential-like variable name (API_KEY / TOKEN / SECRET / PASSWORD / PRIVATE_KEY / BEARER) assigned to what looks like a real value. `.env` files ship inside `git archive`, `docker build` contexts, and install tarballs โ any secret here leaks downstream (MCP Top-10 R9). Replace the value with a placeholder, rename the file to `.env.example`, and load the real value from a secret manager at runtime. If the credential was already committed, revoke it now (it is still in git
Evidence
| 27 | CORS_ORIGIN=http://localhost:3000,http://localhost:5678 |
| 28 | |
| 29 | # === Authentication === |
| 30 | AUTH_TOKEN=test-auth-token |
| 31 | MCP_AUTH_TOKEN=test-mcp-auth-token |
| 32 | |
| 33 | # === Logging Configuration === |
Remediation
Remove the credential from the `.env` file and replace with a template placeholder: OPENAI_API_KEY=<your-openai-key> OPENAI_API_KEY=${OPENAI_API_KEY} Rename the file to `.env.example` so humans know it is a template. Store the real value in a secret manager and inject it at runtime. If the credential has already been committed, revoke it immediately (git history still contains it).
`.env` file contains a credential-like variable name (API_KEY / TOKEN / SECRET / PASSWORD / PRIVATE_KEY / BEARER) assigned to what looks like a real value. `.env` files ship inside `git archive`, `docker build` contexts, and install tarballs โ any secret here leaks downstream (MCP Top-10 R9). Replace the value with a placeholder, rename the file to `.env.example`, and load the real value from a secret manager at runtime. If the credential was already committed, revoke it now (it is still in git
Evidence
| 28 | # === Authentication === |
| 29 | AUTH_TOKEN=test-auth-token |
| 30 | MCP_AUTH_TOKEN=test-mcp-auth-token |
| 31 | |
| 32 | # === Logging Configuration === |
| 33 | # Set to 'debug' for verbose test output |
Remediation
Remove the credential from the `.env` file and replace with a template placeholder: OPENAI_API_KEY=<your-openai-key> OPENAI_API_KEY=${OPENAI_API_KEY} Rename the file to `.env.example` so humans know it is a template. Store the real value in a secret manager and inject it at runtime. If the credential has already been committed, revoke it immediately (git history still contains it).
Dockerfile never sets a non-root `USER` directive, so the CMD runs as root by default. Any RCE or library-level vulnerability exploited inside this container gets full privileges (MCP Top-10 R3). Add `USER <non-root>` before CMD / ENTRYPOINT in the final stage โ e.g. `USER 1000`, `USER nobody`, or `USER nonroot` on distroless.
Evidence
| 1 | # Quick test Dockerfile using pre-built files |
| 2 | FROM node:22-alpine |
| 3 | |
| 4 | WORKDIR /app |
| 5 | |
| 6 | # Copy only the essentials |
| 7 | COPY package*.json ./ |
| 8 | COPY dist ./dist |
| 9 | COPY data ./data |
| 10 | COPY docker/docker-entrypoint.sh /usr/local/bin/ |
| 11 | COPY .env.example ./ |
| 12 | |
| 13 | # Install only runtime dependencies |
| 14 | RUN npm install --production @modelcontextprotocol/sdk better-sqlite3 express dotenv |
| 15 | |
| 16 | # Make entrypoint executable |
| 17 | RUN chmod +x /usr/local/bin/docker-entrypoint.sh |
| 18 | |
| 19 | # Set environment |
| 20 | ENV IS_DOCKER=true |
| 21 | ENV MCP_MODE=stdio |
| 22 | |
| 23 | ENTRYPO |
Remediation
Create and switch to a non-root user before the CMD / ENTRYPOINT: RUN adduser --system --uid 1000 app USER 1000 Or reuse the base image's shipped non-root account (e.g. `USER nobody`, `USER nonroot` on distroless). Multi-stage builds only need the USER directive in the final stage.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 116 | } |
| 117 | |
| 118 | // Test http call query |
| 119 | console.log('\n2. Testing "http call" query (was not finding HTTP Request easily):'); |
| 120 | const httpCallResult = await server.executeTool('search_nodes', { query: 'http call', limit: 10 }); |
| 121 | const httpCallFirst = httpCallResult.results[0]; |
| 122 | if (httpCallFirst.nodeType === 'nodes-base.httpRequest') { |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 214 | console.log('Input Data Result:'); |
| 215 | console.log('- Has input data:', !!inputDataResult.nodes?.['HTTP Request']?.data?.input); |
| 216 | console.log('- Has output data:', !!inputDataResult.nodes?.['HTTP Request']?.data?.output); |
| 217 | console.log('\nโ Can include input data for debugging\n'); |
| 218 | |
| 219 | /** |
| 220 | * Test 9: itemsLimit Validation |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 105 | console.log('=====================================\n'); |
| 106 | |
| 107 | // Test webhook query that was problematic |
| 108 | console.log('1. Testing "webhook" query (was returning service-specific webhooks first):'); |
| 109 | const webhookResult = await server.executeTool('search_nodes', { query: 'webhook', limit: 10 }); |
| 110 | const webhookFirst = webhookResult.results[0]; |
| 111 | if (webhookFirst.nodeType === 'nodes-base.webhook') { |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 29 | } |
| 30 | }; |
| 31 | |
| 32 | console.log('Sending event:', testEvent); |
| 33 | |
| 34 | const { data, error } = await supabase |
| 35 | .from('telemetry_events') |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 143 | console.log('\nโ Successfully seeded', totalInserted, 'canonical AI tool examples'); |
| 144 | console.log('\nExamples are now available via:'); |
| 145 | console.log(' โข search_nodes({query: "HTTP Request Tool", includeExamples: true})'); |
| 146 | console.log(' โข get_node_essentials({nodeType: "nodes-langchain.toolCode", includeExamples: true})'); |
| 147 | |
| 148 | } catch (error) { |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 44 | queue.forEach((event, i) => { |
| 45 | console.log(`\n Event ${i + 1}:`); |
| 46 | console.log(` - Type: ${event.event}`); |
| 47 | console.log(` - Properties:`, JSON.stringify(event.properties, null, 6)); |
| 48 | }); |
| 49 | |
| 50 | // Flush to database |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 68 | for (const test of tests) { |
| 69 | console.log(`\n${test.description}`); |
| 70 | console.log(`Query: "${test.query}" (Mode: ${test.mode || 'OR'})`); |
| 71 | console.log('-'.repeat(40)); |
| 72 | |
| 73 | try { |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 69 | console.log(JSON.stringify(response)); |
| 70 | } else { |
| 71 | // Wrap non-JSON-RPC responses |
| 72 | console.log(JSON.stringify({ |
| 73 | jsonrpc: '2.0', |
| 74 | id: request.id || null, |
| 75 | error: { |
| 76 | code: -32603, |
| 77 | message: 'Internal error', |
| 78 | data: response |
| 79 | } |
| 80 | })); |
| 81 | } |
| 82 | } catch (err) { |
| 83 | console.log(JSON.stringify({ |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 52 | await telemetry.flush(); |
| 53 | |
| 54 | console.log('\n7. Done! Check Supabase for error events with "error" field.'); |
| 55 | console.log(' Query: SELECT * FROM telemetry_events WHERE event = \'error_occurred\' ORDER BY created_at DESC LIMIT 5;'); |
| 56 | } |
| 57 | |
| 58 | testErrorTracking().catch(console.error); |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
User-controlled value printed to terminal without ANSI escape sanitization. Malicious input can inject cursor-control sequences, rewrite earlier output, or hide shell commands from the operator.
Evidence
| 212 | }); |
| 213 | |
| 214 | console.log('Input Data Result:'); |
| 215 | console.log('- Has input data:', !!inputDataResult.nodes?.['HTTP Request']?.data?.input); |
| 216 | console.log('- Has output data:', !!inputDataResult.nodes?.['HTTP Request']?.data?.output); |
| 217 | console.log('\nโ Can include input data for debugging\n'); |
Remediation
Strip C0/C1 control codes before printing user-controlled values. Python: re.sub(r"[\x00-\x08\x0b-\x1f\x7f]", "", s). Prefer a structured logger (json/logfmt) over raw print to stdout.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 157 | ### Fixed |
| 158 | |
| 159 | - **`n8n_create_workflow` / `n8n_update_full_workflow` failures from JSON-stringified array parameters (Issue #611, reported by @Mte90).** VS Code + GitHub Copilot and some other MCP clients serialize array/object tool arguments as JSON strings rather than native JSON types. This reliably affected workflows with 3+ nodes or complex nested parameters (e.g. `__rl` resource-locator objects, filter conditions), producing the error `"nodes must be an array, got string"` while 1-2 node payl |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 93 | packagePath = testPath; |
| 94 | break; |
| 95 | } |
| 96 | } catch {} |
| 97 | } |
| 98 | |
| 99 | if (packagePath) break; |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 154 | // Cleanup on error |
| 155 | try { |
| 156 | fs.unlinkSync(inputFile); |
| 157 | } catch {} |
| 158 | throw error; |
| 159 | } |
| 160 | } |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 85 | await fs.access(fullPath); |
| 86 | packagePath = fullPath; |
| 87 | break; |
| 88 | } catch {} |
| 89 | } |
| 90 | } |
| 91 | } else { |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.
Silent error swallowing detected. An except clause that does pass or ... discards the exception with no log, no metric, and no trace. This blinds incident response and hides real failures.
Evidence
| 187 | // Cleanup on error |
| 188 | try { |
| 189 | fs.unlinkSync(inputFile); |
| 190 | } catch {} |
| 191 | throw error; |
| 192 | } |
| 193 | } |
Remediation
Log the exception at minimum (`logger.exception(e)`), emit a metric, or re-raise if the error is not recoverable. If you genuinely want to ignore an exception, say so with a comment.