Mostly safe โ a couple of notes worth reading.
Scanned 5/3/2026, 6:39:08 PMยทCached resultยทFast Scanยท45 rulesยทHow we decide โ
AIVSS Score
Low
Severity Breakdown
0
critical
0
high
20
medium
0
low
MCP Server Information
Findings
This package has a moderate security risk with a B grade and a safety score of 75, primarily due to 20 medium-severity issuesโmostly vulnerable dependencies (8), potential ANSI escape injection (4), resource exhaustion risks (4), and server configuration concerns (4). While no critical or high-severity flaws were found, the volume of medium-risk findings suggests you should review dependencies and configuration before installing. The lack of top findings means no single issue stands out as severe, but the cumulative risk warrants caution.
Dependencies
mcp-handler (1)
oauth2-server (2)
@modelcontextprotocol/sdk (1)
Scan Details
Want deeper analysis?
Fast scan found 20 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.
20 of 20 findings
20 findings
axios==1.13.6 has 2 known CVEs [MEDIUM]: GHSA-3p68-rc4w-qgx5, GHSA-fvcv-3m26-pcqx. Upgrade to a patched version.
Remediation
Upgrade the pinned dependency to a patched version. Check the CVE's advisory URL for the recommended safe release, or use `npm audit fix` / `pip-audit --fix`. If no patched release is available yet, pin to a known-good prior version, vendor the fix, or remove the dependency.
@modelcontextprotocol/sdk==1.25.3 has 1 known CVE [HIGH]: GHSA-345p-7cg4-v4c7. Upgrade to a patched version.
Remediation
Upgrade the pinned dependency to a patched version. Check the CVE's advisory URL for the recommended safe release, or use `npm audit fix` / `pip-audit --fix`. If no patched release is available yet, pin to a known-good prior version, vendor the fix, or remove the dependency.
next==16.1.1 has 9 known CVEs [HIGH]: GHSA-3x4c-7xq6-9pq8, GHSA-5f7q-jpqc-wp7h, GHSA-9g9p-9gw9-jx7f (+6 more). Upgrade to a patched version.
Remediation
Upgrade the pinned dependency to a patched version. Check the CVE's advisory URL for the recommended safe release, or use `npm audit fix` / `pip-audit --fix`. If no patched release is available yet, pin to a known-good prior version, vendor the fix, or remove the dependency.
mcp-handler==1.0.6 has 1 known CVE [HIGH]: GHSA-w2fm-25vw-vh7f. Upgrade to a patched version.
Remediation
Upgrade the pinned dependency to a patched version. Check the CVE's advisory URL for the recommended safe release, or use `npm audit fix` / `pip-audit --fix`. If no patched release is available yet, pin to a known-good prior version, vendor the fix, or remove the dependency.
oauth2-server==3.1.1 has 2 known CVEs [HIGH]: GHSA-2fw4-mgq9-39cx, GHSA-4rg6-fm25-gc34. Upgrade to a patched version.
Remediation
Upgrade the pinned dependency to a patched version. Check the CVE's advisory URL for the recommended safe release, or use `npm audit fix` / `pip-audit --fix`. If no patched release is available yet, pin to a known-good prior version, vendor the fix, or remove the dependency.
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
| 18 | ```typescript |
| 19 | app.post('/articles', async (c) => { |
| 20 | console.log('Received POST /articles request'); |
| 21 | |
| 22 | const body = await c.req.json(); |
| 23 | console.log('Request body parsed', { title: body.title }); |
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
| 51 | ```typescript |
| 52 | // DON'T create new loggers in each file |
| 53 | const logger = new Logger(); // Each file creates its own |
| 54 | console.log('some event'); // Bypasses the logger entirely |
| 55 | ``` |
| 56 | |
| 57 | ### Use Middleware for Consistent Wide 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
| 21 | console.log('Received POST /articles request'); |
| 22 | |
| 23 | const body = await c.req.json(); |
| 24 | console.log('Request body parsed', { title: body.title }); |
| 25 | |
| 26 | const user = await getUser(c.get('userId')); |
| 27 | console.log('User fetched', { userId: user.id }); |
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
| 18 | ```typescript |
| 19 | app.post('/checkout', async (c) => { |
| 20 | console.log('Received checkout request'); // Line 1 |
| 21 | console.log(`User ID: ${c.get('userId')}`); // Line 2 |
| 22 | const user = await getUser(c.get('userId')); |
| 23 | console.log(`User fetched: ${user.email}`); // Line 3 |
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.
@types/he==1.2.3 last released 908 days ago (>730d) โ possible abandoned package
Remediation
Typosquat: verify you meant the popular package. If so, correct the spelling; if you truly intended the less-common name, suppress with an inline waiver. Stale release: check whether the package has a maintained fork or successor. If no patched release exists, vendor the code or migrate to an active alternative before the unmaintained code accrues unfixed CVEs.
oauth2-server==3.1.1 last released 1790 days ago (>730d) โ possible abandoned package
Remediation
Typosquat: verify you meant the popular package. If so, correct the spelling; if you truly intended the less-common name, suppress with an inline waiver. Stale release: check whether the package has a maintained fork or successor. If no patched release exists, vendor the code or migrate to an active alternative before the unmaintained code accrues unfixed CVEs.
he==1.2.0 last released 2779 days ago (>730d) โ possible abandoned package
Remediation
Typosquat: verify you meant the popular package. If so, correct the spelling; if you truly intended the less-common name, suppress with an inline waiver. Stale release: check whether the package has a maintained fork or successor. If no patched release exists, vendor the code or migrate to an active alternative before the unmaintained code accrues unfixed CVEs.
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
| 4 | export async function fetchRawGithubContent(rawPath: string) { |
| 5 | const path = rawPath.replace('/blob', ''); |
| 6 | |
| 7 | const response = await fetch(`https://raw.githubusercontent.com${path}`); |
| 8 | if (!response.ok) { |
| 9 | throw new Error( |
| 10 | `Failed to fetch GitHub content: ${response.status} ${response.statusText}`, |
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
| 1129 | Use the list_docs_resources tool first to discover available page slugs, then pass the slug to this tool. |
| 1130 | |
| 1131 | Use this tool when: |
| 1132 | - You have identified a specific docs page to fetch (from list_docs_resources results) |
| 1133 | - You need detailed guidance on a Neon feature, workflow, or configuration |
| 1134 | - The user needs step-by-step instructions for a Neon-related task |
| 1135 | </use_case> |
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
| 1067 | } |
| 1068 | |
| 1069 | async function handleListDocsResources() { |
| 1070 | const response = await fetch(NEON_DOCS_INDEX_URL); |
| 1071 | if (!response.ok) { |
| 1072 | if (response.status === 404) { |
| 1073 | throw new NotFoundError('Neon docs index not found'); |
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
| 1102 | // Ensure the slug ends with .md for the new docs format |
| 1103 | const mdSlug = slug.endsWith('.md') ? slug : `${slug}.md`; |
| 1104 | const url = `${NEON_DOCS_BASE_URL}/${mdSlug}`; |
| 1105 | const response = await fetch(url); |
| 1106 | if (!response.ok) { |
| 1107 | if (response.status === 404) { |
| 1108 | throw new NotFoundError(`Doc page not found: "${mdSlug}"`); |
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.
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 86 | }); |
| 87 | |
| 88 | export const explainSqlStatementInputSchema = z.object({ |
| 89 | sql: z.string().describe('The SQL statement to analyze'), |
| 90 | projectId: z |
| 91 | .string() |
| 92 | .describe('The ID of the project to execute the query against'), |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 287 | }); |
| 288 | |
| 289 | export const prepareQueryTuningInputSchema = z.object({ |
| 290 | sql: z.string().describe('The SQL statement to analyze and tune'), |
| 291 | databaseName: z |
| 292 | .string() |
| 293 | .describe('The name of the database to execute the query against'), |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP tool input schema exposes an unconstrained string/any field with a risky name (command/query/sql/code/script/url/path/expr/ eval). Any caller can pass arbitrary values, which typically widens the tool's blast radius well beyond its intent. Narrow the schema with `.enum()`, `.regex()`, `.max()`, `Literal[...]`, Pydantic `Field(max_length=..., pattern=...)`, or a JSON Schema `enum` / `pattern` / `maxLength`.
Evidence
| 58 | }); |
| 59 | |
| 60 | export const runSqlInputSchema = z.object({ |
| 61 | sql: z.string().describe('The SQL query to execute'), |
| 62 | projectId: z |
| 63 | .string() |
| 64 | .describe('The ID of the project to execute the query against'), |
Remediation
Shape the schema to the tool's actual intent: - Zod: chain `.enum([...])`, `.regex(/.../)`, or `.max(n)`; prefer `z.enum([...])` or `z.literal(...)` when the value set is small. - Pydantic: use `Literal["a", "b"]` or `Field(max_length=..., pattern=r"...")`. - JSON Schema: add `"enum"`, `"pattern"`, or `"maxLength"` to the property. An overbroad schema is an "overpowered tool" โ the model has nothing to prevent it from calling the tool with input far beyond what the tool's prose contract
MCP server declares an OAuth scope using a wildcard or omnibus token (`*`, `all`, `full-access`, `<resource>:*`). The official MCP security best practices forbid this โ broad scopes amplify the blast radius of a stolen token and obscure the audit trail. Replace with named, narrowly-defined scopes (e.g. `files:read`, `tools:call:safe-readers`).
Evidence
| 888 | return { |
| 889 | token: bearerToken, |
| 890 | scopes: ['*'], // API keys get all scopes |
| 891 | clientId: 'api-key', // Literal string |
| 892 | extra: { |
| 893 | account: apiKeyRecord.account, |
Remediation
Replace wildcard scopes with specific named scopes: # BAD SCOPES = ["files:*", "all", "*"] # GOOD SCOPES = [ "files:read", "files:write:owned", "tools:call:safe-readers", ] Issue least privilege at the authorization server, request the minimum at the client, and challenge for elevation only when a privileged operation is attempted.