Concepts
OWASP MCP Top 10, scoring, incomplete scans, target types, and credits.
OWASP MCP Top 10
Every finding carries both a rule id (MCP-EXEC-001, MCP-POISON-001) and one of ten OWASP MCP categories,
so a rule result and a scanner result read the same way, and the same categories appear
identically in the CLI and the dashboard.
| Code | Category |
|---|---|
| MCP01 | Token Mismanagement & Secret Exposure |
| MCP02 | Tool Poisoning (Hidden Instructions) |
| MCP03 | Cross-Origin Escalation / Tool Shadowing |
| MCP04 | Rug Pull (Tool Drift After Install) |
| MCP05 | Command Injection, Path Traversal, SSRF, File Access |
| MCP06 | Missing/Weak Authentication |
| MCP07 | Supply Chain / Malicious or Typosquatted Dependencies |
| MCP08 | Prompt Injection via Live Tool Responses |
| MCP09 | Excessive Agency / Overprivileged Scope |
| MCP10 | Weak/Missing Audit Logging |
MCP08 is never tested by a static scan. It requires dynamic, adversarial testing against a server's actual runtime tool responses, which is out of scope for this version. Every report includes an explicit note saying so, rather than silently omitting it. It is a documented gap, not a clean result.
Risk score and grade
Risk counts up. 0 is clean; 100 is "do not use". The engine grades the whole server, not each tool: it gives each finding risk points and works out one score for the server. Aevrin shows that score and grade as the engine gave them and never recomputes either. Grouping findings into one card changes how the report reads, not the score.
The letter comes from the number:
| Risk | Grade | Meaning | Suggested policy |
|---|---|---|---|
| 0 | A | Trusted | ALLOW |
| below 15 | B | Generally safe | ALLOW |
| below 35 | C | Caution | REQUIRE_APPROVAL |
| below 60 | D | High risk | REQUIRE_APPROVAL |
| 60 or more | F | Do not use | BLOCK |
An F also needs at least one validated high or critical finding. A high score made of lesser findings is not enough on its own.
A scan with no grade (see below) is given REQUIRE_APPROVAL. The suggested policy is a recommendation, never an automatic action.
The score changed direction
If you used Aevrin before this change, the score counted down from 100, where higher was better. It now counts up, where higher is worse. Scans from before the change show no score rather than a converted one: the two numbers are not convertible, and inventing a conversion would have been a lie about a security result.
No grade is a real result
A scan gets no letter at all when any of these is true:
- The scan engine could not run, or could not finish (Docker down, the engine image missing, no network).
- No MCP tools could be enumerated.
- The scan itself failed before it finished.
The second is the common case and the more dangerous one. Zero findings from a server whose
tools nobody could read looks exactly like a perfect server. Rather than print an A, Aevrin
prints ?, labels it Not graded, and says why:
Scan Incomplete
The server started but returned no tool definitions. The findings below are real, but they
do not add up to a complete security assessment, and no grade is given because there is
not enough evidence to justify one.The first sentence names the stage that stopped the scan, because the three common ways a scan ends early need three different answers from you:
| Stopped at | What it means | What to do |
|---|---|---|
| Resolving | No single MCP server could be identified to start. The message says which: the code holds no MCP server at all; it uses an MCP SDK but publishes no package Aevrin can start; it holds several servers; or it could not be fetched or read. | If it is a server, scan the command that starts it: aevrin scan mcp "npx -y <package>". |
| Launching | The server was identified but would not start, so its tools were never read. | Run the same command yourself. A server that needs a credential or a config file before it completes the MCP handshake cannot be enumerated: the sandbox is given no environment of its own, so a scanned server never inherits one. |
| Enumerating | The server started and returned nothing. | Tools are probably registered through indirection that cannot be read. Registering them with literal names and descriptions, or publishing a manifest, makes them reviewable. |
Only the third is a statement about the server's tools. Reporting the other two that way was a claim about a target the scan never reached.
When the recorded failure says exactly what happened, the summary says that instead of the stage wording. A hosted server that answers HTTP 401 or 403 is reported as requiring sign-in - that is the server working as built, not a fault, and a scan deliberately carries no credentials, so the result means unassessed, never clean. The same goes for a handshake timeout, a server that is unreachable, an address that answers with something other than MCP, an error the server itself returned, and a package that would not install: each gets its own explanation and its own next step, with the raw error recorded on the stage that stopped.
A scan that failed is reported separately, and it says nothing about the target:
Scan Failed
This scan did not finish, so it is not an assessment of this target. Any findings listed
below were recorded before it stopped and are real, but the checks that never ran could
have found more.The distinction is deliberate. A failed run leaves no recorded tools behind, and reporting that emptiness as "no tool definitions were found" would be a claim about the server that the scan never established.
A public report from an earlier scan
When a scan fails or finds no tools, Aevrin looks for a public report from an earlier scan of
the same npm package or GitHub repository, published by someone else (the MIT-licensed ToolTrust
directory). If there is one, the scan page shows it in its own card, A public report from an
earlier scan, with that report's grade, date, version and findings, and the API returns it as
fallback_report.
It is not this scan's result. It never changes this scan's grade, score, stages or findings, and this scan still says it did not finish. It may describe a different version of the server than the one you tried to scan. Use it as a hint about what to look at, never as a pass. Looking it up tells GitHub the name of the package or repository, and only happens for a scan that could not finish.
The full report
Every scan keeps the scan engine's own complete JSON report: every tool it read with its input
schema, each finding with its evidence, the score and any errors, and the public report above when
there is one. Download it from the scan page (Download full report (JSON)), from
GET /scans/{id}/engine-report, or write it locally with aevrin scan --engine-report <path>.
A report over 1 MB is not kept rather than cut short. The grade, the stages and the findings on
the page are the result; the report is the engine's record behind them.
Risk summary
Every report opens with the five things you actually need, generated from the findings that drove the score rather than from a template:
Needs Approval
Arbitrary Code Execution and Excessive Permissions raise enough risk that this server
should not be trusted automatically.
Potential impact: Anything that can influence this tool's arguments can run code with
the MCP process's own permissions.
Recommended action: Require approval for the tools named below, and re-scan once their
permissions are narrowed.
Suggested policy: REQUIRE_APPROVALRanking is by the risk each rule actually contributed, not by how many times it fired: a server with one critical execution hole and twenty-four informational notes is about the execution hole.
Incomplete scans
A scan stage is marked unreliable when it could not do its job. Not when it runs and finds nothing, but when it did not finish. Common causes: Docker isn't running, the engine image is missing, the network is unreachable, or the engine reports that it only scanned part of the server. The CLI runs one version-pinned engine image in one container with a 2 GB memory ceiling; Docker Desktop should have at least 4 GB available. On Windows it must be using Linux containers and permit bind mounts from the system temporary directory.
Runtime MCP inspection accepts only public HTTPS endpoints. Submitted stdio commands are never executed by Aevrin, and private/loopback/link-local/metadata targets are rejected. When no safe remote endpoint exists, tool-description coverage is shown as skipped rather than failed or silently treated as clean.
If any category is unreliable, the whole scan is flagged incomplete and that status follows it
everywhere:
- The CLI prints a red warning, shows
?instead of a letter, and exits with code3, regardless of--fail-on. - The dashboard shows a destructive-styled banner naming the specific stages that
couldn't finish, and the grade tile reads
?/ Not graded rather than a letter.
This is a deliberate, security-first design choice: a scanner that can be made to say "clean" just by breaking its own tools isn't trustworthy. An incomplete result is always treated as inconclusive, never as a pass.
Self-reported results
The CLI runs the scan on your own machine and reports the result to Aevrin's backend. Aevrin does not re-run the scan server-side (that is not feasible for a local filesystem path, and would be redundant for a public repo). This has one real security implication worth being explicit about: neither the grade nor the findings list is independently verified.
- Risk score and grade: the engine's verdict, as the CLI reported it. The API cannot grade an upload itself, because grading needs the sandbox and the exact package the CLI ran, and working a letter out again from the findings would be a second grader. It does refuse an upload that contradicts itself: a grade for a target whose tools were never enumerated, or an A or B next to a critical or high finding in the same upload.
- Findings: not independently re-verified. A valid API key can, in principle, submit fabricated or incomplete findings. This is an inherent limitation of any tool that runs locally and reports centrally, the same shape as, for example, a CI badge driven by a self-reported test result. It's also why every CLI scan on the dashboard carries a note saying it ran on your own computer and that Aevrin did not scan the target again, rather than being presented as an unconditional guarantee.
Every scan result carries this framing consistently: a real signal worth trusting for its stated scope, not a cryptographic attestation.
Target types
| Type | What it means | What runs |
|---|---|---|
| GitHub repo | A github.com URL | Clone it to find the command that starts the server, then start the server and run the rules over what it offers |
| Local path | A directory on your machine (CLI only) | Same as above, minus cloning |
| Live server URL | A running MCP server's URL | The same rules, against what the server answers with. Its package is never installed, so rules that read a package or its code have nothing to read |
| Pasted config | A {"mcpServers": {...}} JSON blob | Nothing yet: Aevrin cannot start a server from a config, so such a scan stops at resolving. The dashboard no longer offers it; scan the server's code or its live URL instead |
For repo and local-path targets, Aevrin first works out which published package starts the
server, from the project's own manifests: package.json (or one workspace package that depends
on an @modelcontextprotocol/* SDK) or pyproject.toml. Naming a repository mcp-something is
not evidence and does not count. When there is nothing to start, the scan stops at resolving and
says which of these is true:
- The code holds no MCP server: no package to start, and no MCP SDK (no
@modelcontextprotocol/*dependency, and nomcporfastmcpinpyproject.tomlorrequirements.txt). Only then does the result say "This is not an MCP server." Aevrin does not audit general source code, so there is nothing further for it to report. - The code uses an MCP SDK but publishes no package with an entry point, so Aevrin cannot tell how to start it. It is an MCP server; scan it by the command that starts it.
- The code holds several MCP servers, named in the message; scan the one you want by its command.
- The code could not be fetched or read (the clone failed, or a manifest is unreadable). That says nothing about the code.
Credits and tiers
Usage is measured in credits: one monthly pool per account, refilled on a rolling monthly window from your signup date, not the calendar month. Each action spends a fixed number of credits, so you decide what your month goes on - more scans, more capability runs, or a mix.
| Free | Pro | |
|---|---|---|
| Credits per month | 1,500 | 50,000 |
| Action | Credits |
|---|---|
Capability read (get_capability) | 1 |
| Agent posture upload | 5 |
| Capability run | 10 |
| MCP scan (CLI, dashboard or through an agent) | 50 |
| AI code review (Pro plans only) | 50 |
Searching Aevrin MCP is free and is never counted, and so is reading a scan report. A capability
read is one get_capability, and a run spends its credits on top of the read that retrieved the
capability. A few things are fixed per plan rather than priced: AI code review (Pro only, then 50
credits a review), monitored devices (1 Free, 10 Pro), report export (Pro), concurrent capability
runs (1 Free, 2 Pro), and Aevrin MCP's compute time and per-run memory and time; see
Aevrin MCP limits.
Top-ups. You can buy 5,000 extra credits as a one-time payment on the billing page. Top-up credits never expire and are spent only after the monthly allowance runs out, oldest pack first.
Every pool is a cap, Pro's included. A plan belongs to one account and applies only to that account: there is no shared workspace, and nobody else's plan changes your credits. Pro is bought one monthly or annual cycle at a time. Buying another cycle applies once the payment is confirmed and adds one cycle after the current paid-until date, with no proration.
Running out of credits returns a two-part message everywhere it can happen (CLI and dashboard): what happened and what it costs, when the pool refills, and where to upgrade or top up. Never a bare refusal.