Mostly safe — a couple of notes worth reading.
Scanned 5/3/2026, 6:59:19 PM·Cached result·Fast Scan·45 rules·How we decide ↗
AIVSS Score
Low
Severity Breakdown
0
critical
0
high
115
medium
1
low
MCP Server Information
Findings
This package has a B grade with a safety score of 55/100 and carries moderate risk primarily from 72 resource exhaustion vulnerabilities, 33 server configuration issues, and 8 ANSI escape injection flaws across 115 medium-severity findings. While no critical or high-severity issues were detected, the resource exhaustion risks could allow denial-of-service attacks, and the configuration gaps may expose the server to unauthorized access or misuse. You should address the resource exhaustion and server configuration issues before deployment, particularly if this will run in a production environment.
No known CVEs found for this package or its dependencies.
Scan Details
Want deeper analysis?
Fast scan found 16 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.
16 of 16 findings
16 findings
Container base image uses the floating :latest tag. This provides no reproducibility — the image content can change under you, and there is no signature to verify.
Evidence
| 113 | ### Single Container |
| 114 | |
| 115 | ```dockerfile |
| 116 | FROM ghcr.io/arabold/docs-mcp-server:latest |
| 117 | EXPOSE 6280 |
| 118 | CMD ["--protocol", "http", "--port", "6280"] |
| 119 | ``` |
Remediation
Pin the base image by digest (e.g. python@sha256:...) or at minimum an exact semver tag (e.g. python:3.12.4-slim-bookworm). Never use :latest in production Dockerfiles.
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 | # Base stage with build dependencies |
| 2 | FROM node:22-slim AS base |
| 3 | |
| 4 | WORKDIR /app |
| 5 | |
| 6 | # Install build dependencies for native modules (better-sqlite3, tree-sitter, etc.) |
| 7 | RUN apt-get update \ |
| 8 | && apt-get install -y --no-install-recommends \ |
| 9 | python3 \ |
| 10 | make \ |
| 11 | g++ \ |
| 12 | && rm -rf /var/lib/apt/lists/* |
| 13 | |
| 14 | # Build stage |
| 15 | FROM base AS builder |
| 16 | |
| 17 | # Accept build argument for PostHog API key |
| 18 | ARG POSTHOG_API_KEY |
| 19 | ENV POSTHOG_API_KEY=$POSTHOG_API_KEY |
| 20 | |
| 21 | # Copy package files |
| 22 | COPY package*.json ./ |
| 23 | |
| 24 | # Install all dependen |
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
| 160 | // Subscribe to events and dispatch them as DOM events for HTMX |
| 161 | eventClient.subscribe((event) => { |
| 162 | console.log(`📋 Received event: ${event.type}`, event.payload); |
| 163 | // Dispatch custom event with payload that HTMX can listen to |
| 164 | document.body.dispatchEvent( |
| 165 | new CustomEvent(event.type, { |
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
| 49 | // Listen for all event types defined in ServerEventName |
| 50 | const eventTypes = Object.values(ServerEventName) as SseEventName[]; |
| 51 | |
| 52 | console.log("🔧 Setting up event listeners", eventTypes); |
| 53 | |
| 54 | for (const eventType of eventTypes) { |
| 55 | this.eventSource.addEventListener(eventType, (event: MessageEvent) => { |
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
| 41 | }); |
| 42 | |
| 43 | this.eventSource.addEventListener("error", (event) => { |
| 44 | console.error("❌ SSE connection error:", event); |
| 45 | this.isConnected = false; |
| 46 | // EventSource will automatically attempt to reconnect |
| 47 | }); |
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
| 67 | // Add a generic message listener as a fallback to catch any events |
| 68 | this.eventSource.addEventListener("message", (event: MessageEvent) => { |
| 69 | console.log("📨 Generic message event received:", event.data); |
| 70 | }); |
| 71 | } |
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
| 48 | // 2. The rest is the query |
| 49 | const query = args.join(" ").trim(); |
| 50 | // console.error(`DEBUG: Executing search for query: "${query}"`); |
| 51 | |
| 52 | // 3. Determine Library (Context -> Env -> Default) |
| 53 | const library = context.vars?.library || process.env.LIBRARY || "react"; |
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
| 108 | try { |
| 109 | callback(event); |
| 110 | } catch (error) { |
| 111 | console.error("Error in event callback:", error); |
| 112 | } |
| 113 | } |
| 114 | } |
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
| 60 | payload, |
| 61 | }); |
| 62 | } catch (error) { |
| 63 | console.error(`Failed to parse SSE event data: ${error}`); |
| 64 | } |
| 65 | }); |
| 66 | } |
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
| 54 | const library = context.vars?.library || process.env.LIBRARY || "react"; |
| 55 | |
| 56 | if (!query) { |
| 57 | console.error("Error: No query provided"); |
| 58 | console.error("Args received:", process.argv); |
| 59 | process.exit(1); |
| 60 | } |
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.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 22 | * Uses enhanced MIME type detection for better source code file recognition. |
| 23 | * Supports conditional fetching via ETag comparison for efficient refresh operations. |
| 24 | */ |
| 25 | async fetch(source: string, options?: FetchOptions): Promise<RawContent> { |
| 26 | // Remove the file:// protocol prefix and handle both file:// and file:/// formats |
| 27 | let filePath = source.replace(/^file:\/\/\/?/, ""); |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 214 | [filePath]: "initial", |
| 215 | }); |
| 216 | |
| 217 | const result1 = await fetcher.fetch(`file://${filePath}`); |
| 218 | const oldEtag = result1.etag; |
| 219 | |
| 220 | // Wait and modify file |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 384 | let repoContent: Awaited<ReturnType<typeof this.httpFetcher.fetch>>; |
| 385 | try { |
| 386 | repoContent = await this.httpFetcher.fetch(repoUrl, { signal, headers }); |
| 387 | } catch (error) { |
| 388 | // Convert HTTP auth errors to user-friendly messages |
| 389 | if (error instanceof ScraperError) { |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 111 | }; |
| 112 | |
| 113 | // Use AutoDetectFetcher which handles fallbacks automatically |
| 114 | const rawContent: RawContent = await this.fetcher.fetch(url, fetchOptions); |
| 115 | const effectiveSource = options.preserveHashes |
| 116 | ? this.restorePreservedHash(url, rawContent.source) |
| 117 | : rawContent.source; |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
Network / IO / subprocess call without an explicit timeout. A malicious or hung upstream (HTTP host, socket peer, child process) can pin threads, exhaust connection/process pools, and make the MCP server unresponsive. Always pass a bounded timeout. v2 extends v1 with subprocess coverage (R03 from the legacy readiness audit).
Evidence
| 547 | }; |
| 548 | mockedAxios.get.mockResolvedValue(mockResponse); |
| 549 | |
| 550 | await fetcher.fetch("https://example.com"); |
| 551 | |
| 552 | expect(mockedAxios.get).toHaveBeenCalledWith( |
| 553 | "https://example.com", |
Remediation
Pass timeout= on every call: - HTTP: `requests.get(url, timeout=5)`, `httpx.get(url, timeout=5.0)` - Node fetch: `AbortSignal.timeout(5000)` - Subprocess: `subprocess.run(["cmd"], timeout=30, check=True)` Pick a value short enough to fail fast and retry.
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
| 96 | try { |
| 97 | const u = new URL(url); |
| 98 | basename = u.pathname ? u.pathname.split("/").pop() : undefined; |
| 99 | } catch {} |
| 100 | } |
| 101 | // Helper to strip leading slash from patterns for basename matching |
| 102 | const stripSlash = (patterns?: string[]) => |
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.