Use with caution. Address findings before production.
Scanned 5/3/2026, 9:13:31 PMยทCached resultยทFast Scanยท88 rulesยทHow we decide โ
AIVSS Score
Medium
Severity Breakdown
0
critical
2
high
9
medium
0
low
MCP Server Information
Findings
This package receives a C grade with a safety score of 75/100 and carries 2 high-severity findings related to prompt injection vulnerabilities alongside 9 medium-severity issues, primarily centered on server configuration weaknesses and one resource exhaustion risk. The prompt injection risks are the most concerning as they could allow attackers to manipulate the server's behavior through crafted inputs, while the configuration issues suggest the server may not be properly hardened for production use. You should address these vulnerabilities before deploying this package, particularly the high-severity prompt injection flaws.
No known CVEs found for this package or its dependencies.
Scan Details
Want deeper analysis?
Fast scan found 11 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.
11 of 11 findings
11 findings
Tool handlers in index.ts use BRAVE_API_KEY from config.braveApiKey (module-level global read from process.env at import time) to authenticate requests to Brave Search API without consulting caller identity.
Remediation
Pull the token / credential from `ctx.principal` (or the MCP auth layer equivalent) on every call. The handler must fail closed when the caller supplies no credential. Never cache a global API token at import time and reuse it across callers.
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 | import { randomUUID } from 'node:crypto'; |
| 2 | import express, { type Request, type Response } from 'express'; |
| 3 | import config from '../config.js'; |
| 4 | import createMcpServer from '../server.js'; |
| 5 | import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; |
| 6 | import { ListToolsRequest, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; |
| 7 | |
| 8 | const yieldGenericServerError = (res: Response) => { |
| 9 | res.status(500).json({ |
| 10 | id: null, |
| 11 | jsonrpc: '2.0', |
| 12 | err |
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
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
| 114 | headers.set(key, String(value)); |
| 115 | } |
| 116 | |
| 117 | const response = await fetch(urlWithParams, { headers }); |
| 118 | |
| 119 | // Handle Error |
| 120 | if (!response.ok) { |
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
| 147 | const ResultSchema = z.looseObject({ |
| 148 | title: z.string().describe('The display title of the location.'), |
| 149 | url: z.string().describe('Primary URL associated with the location.'), |
| 150 | is_source_local: z.boolean().optional(), |
| 151 | is_source_both: z.boolean().optional(), |
| 152 | description: z |
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
| 32 | netloc: z.string().optional().describe('The network location of the URL.'), |
| 33 | hostname: z.string().optional().describe('The lowercased hostname of the URL.'), |
| 34 | favicon: z.url().optional().describe('The URL of the favicon of the URL.'), |
| 35 | path: z.string().optional().describe('The path of the URL (useful as a display string).'), |
| 36 | }); |
| 37 | |
| 38 | export const ConfidenceSchema = z |
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
| 107 | const ActionSchema = z.looseObject({ |
| 108 | type: z.string().describe('The type representing the action.'), |
| 109 | url: z.string().describe('A URL representing the action to be taken.'), |
| 110 | }); |
| 111 | |
| 112 | const MetaUrlSchema = z.looseObject({ |
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
| 123 | const ResultsSchema = z.looseObject({ |
| 124 | title: z.string().describe('The display title of the location.'), |
| 125 | url: z.string().describe('Primary URL associated with the location.'), |
| 126 | is_source_local: z.boolean().optional(), |
| 127 | is_source_both: z.boolean().optional(), |
| 128 | description: z |
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
| 62 | type: z.string().optional(), |
| 63 | name: z.string().optional(), |
| 64 | long_name: z.string().optional(), |
| 65 | url: z.string().optional(), |
| 66 | img: z.string().optional(), |
| 67 | }); |
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
Package declares an install-time hook (npm postinstall/preinstall/prepare, setup.py cmdclass override, custom setuptools install class, or non-default pyproject build-backend). Anyone installing this package runs the hook. Confirm the hook is necessary and review its contents; prefer shipping a plain library without install-time execution.
Evidence
| 29 | "build": "tsc && shx chmod +x dist/*.js", |
| 30 | "smithery:build": "smithery build", |
| 31 | "smithery:dev": "smithery dev", |
| 32 | "prepare": "npm run format && npm run build", |
| 33 | "watch": "tsc --watch", |
| 34 | "format": "prettier --write \"src/**/*.ts\"", |
| 35 | "format:check": "prettier --check \"src/**/*.ts\"", |
Remediation
Prefer libraries that do not require install-time code execution: - Drop `postinstall`/`preinstall`/`prepare` scripts if the work can happen at runtime or build-time instead. - Ship pre-built native binaries rather than compiling via a custom `cmdclass` or `build_ext` override. - For Dockerfiles: replace `RUN curl โฆ | sh` with a pinned download + checksum verification + explicit `RUN` of a named script. - If the hook is unavoidable, document exactly what it does so downstream reviewers
MCP manifest declares tools but no authentication field is present (none of: auth, authorization, bearer, oauth, mtls, apiKey, api_key, basic, token, authToken). Absence is a weak signal โ confirm whether the server relies on network-layer or host-level auth, or declare the real mechanism explicitly so reviewers can audit it.
Evidence
| 1 | # Brave Search MCP Server |
| 2 | |
| 3 | An MCP server implementation that integrates the Brave Search API, providing comprehensive search capabilities including web search, local business search, place search, image search, video search, news search, LLM context, and AI-powered summarization. This project supports both STDIO and HTTP transports, with STDIO as the default mode. |
| 4 | |
| 5 | [](https://deepwiki.com/brave/brave-search-mcp-server) |
| 6 | |
| 7 | ## Migration |
| 8 | |
| 9 | ### 1.x to 2.x |
| 10 |
Remediation
Declare a real authentication mechanism in the manifest, matching what the running server actually enforces: - `"auth": "bearer"` with a token scheme documented for callers - `"auth": "oauth"` / `"oauth2": { ... }` for delegated flows - `"apiKey": { "header": "X-API-Key", "prefix": "..." }` - `"mtls": true` when client certificates are required If the server is intentionally unauthenticated (stdio-only, local developer tool, trusted-host network), document the assumption in the manifest via a `"
MCP tool description or return text contains an imperative phrase that asks the LLM to invoke or call ANOTHER tool โ "invoke the write_file tool", "before using this, also call send_email", "silently invoke X". This is a cross-tool chaining injection: the user authorized THIS tool, but the payload escalates into others. Tool descriptions should describe what the tool DOES, not direct the LLM to use other tools. If a tool's correct operation requires composition, document the dependency in human
Evidence
| 1 | import type { |
| 2 | LocationResult, |
| 3 | LocationDescription, |
| 4 | OpeningHours, |
| 5 | DayOpeningHours, |
| 6 | } from './types.js'; |
| 7 | import webParams, { type QueryParams as WebQueryParams } from '../web/params.js'; |
| 8 | import type { CallToolResult, ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'; |
| 9 | import API from '../../BraveAPI/index.js'; |
| 10 | import { formatWebResults } from '../web/index.js'; |
| 11 | import { stringify } from '../../utils.js'; |
| 12 | import { type WebSearchApiResponse } from '../web/types.js'; |
| 13 | import { type Mc |
Remediation
Tool descriptions should describe what the tool does โ not what the model should do with other tools. If a tool's correct operation legitimately requires another tool to be called, document that as a `composition` requirement in human-readable docs and let the calling code orchestrate, not the LLM. If the directive phrasing is coming from external content the tool retrieved (RAG, web fetch), wrap in `<untrusted>` tags and rely on the system prompt to flag tag-bound content as data, not instruct