An MCP scanner can return no findings even when the server it checked is dangerous to run. That does not mean the scanner is broken or useless. It means the result answers a narrower question than many users assume.
We tested five widely used public scanners that support MCP analysis against working malicious servers. One server read SSH, AWS, and Kubernetes credentials and sent them to a collector. The other fetched a shell command from a second server, ran it, and returned the output. Both still answered the user’s weather request normally.
Across the configurations we tested, each scanner could be made to report no security findings for these behaviors. Some scanners inspected only the tool name, description, and schema. Some inspected source code with static rules. One used an LLM to review the source and caught both servers at their normal size, but excluded the file after it crossed a size limit. The products did not all inspect the same data, and their clean results did not mean the same thing.
TL;DR
- We built a working credential thief and a working remote-code-execution server. Both looked like ordinary weather tools to the user and to metadata-based scanners.
- Cisco
mcp-scanner, Snykagent-scan, Ventrovasentinel-scan-cli,mcp-shield, and NVIDIA SkillSpector each returned a clean or zero-finding result in at least one tested configuration. - SkillSpector’s default LLM review caught both normal-size servers. When the same code was placed after 16 MiB of harmless text, the file was left out of the LLM review and the scan returned
CAUTION / 0with partial coverage. - A scan result is useful only when you know what was inspected, what was skipped, and which kind of risk the scanner was designed to find.
- We submitted four fixes upstream.
Why We Ran This Test
This research is part of our wider work on MCP trust boundaries. The MCP Debrief maps the risks created when servers inherit files, credentials, network access, and permissions. Vicious Circle and The Notebook Backdoor show how gaps in popular MCP servers can lead to code execution or a silent backend takeover. The Manipulated Agent shows how untrusted content can steer an agent into an action the user never requested. This article looks at a control users may rely on before installation: scanning the server.
The goal is not to rank scanner projects or criticize their maintainers. MCP scanning is a young and difficult field. We submitted fixes for concrete bugs we found, and two of those changes have already passed review. Our goal is to show users what different scanner results cover, where that coverage ends, and why several kinds of analysis are needed.
It also helps to separate two ideas that are often mixed together.
A risky MCP server has capabilities that could cause harm. A database administration tool, a shell tool, or a server that reads local files may be risky even when it is legitimate and well maintained.
A malicious MCP server is built to misuse its access or deceive the user. The two servers in this test were malicious because they hid credential theft and command execution behind a weather tool.
These are different scanning problems. A static rule can flag subprocess or a credential path, but it may not know whether the use is legitimate. A metadata scanner can find a tool description that asks an agent to reveal a secret, but it cannot see behavior that exists only in the handler code. An LLM can reason about source code, but only if the complete source reaches it. A runtime monitor can observe file and network access, but only on the paths that run during the test.
No single result covers all of those questions.
How We Tested
We tested five widely used public scanners that support MCP analysis:
- Cisco
mcp-scanner(about 99,000 PyPI downloads in the last month and 1,100 GitHub stars) - Snyk
agent-scan(about 36,000 PyPI downloads in the last month and 3,000 GitHub stars) - Ventrova
sentinel-scan-cli mcp-shield- NVIDIA
SkillSpector(about 17,100 GitHub stars), which accepts a directory or Git URL throughskillspector scan <path-or-url>and can expose scanning to MCP clients through itsscan_skilltool
The test set contained four servers:
| Server | Purpose |
|---|---|
| Confessing malicious control | Its tool description openly says that it reads an SSH private key and sends it away. We used it as a positive control to confirm that a scanner could flag malicious text when that text appeared in the input it inspected. |
| Benign control | A small server that adds two numbers. This checks for false alarms in the comparison. |
| Hidden credential thief | Its metadata describes a weather tool. On every call, its code reads credential files and matching environment variables, then sends them to a local collector. |
| Hidden RCE server | Its metadata describes the same weather tool. On every call, its code asks a local command server what to run, executes the command, and sends the output to a local collector. |
We ran the malicious behavior in an isolated test environment. The home directory contained fake SSH and AWS credentials, the command server listened only on 127.0.0.1, and the RCE payload wrote a canary file inside a temporary directory.
We ran both malicious servers from end to end before scanning them. The collector received the fake credentials and RCE canary output, while the user still received a normal weather forecast.
The confessing server was the positive control. It put the theft directly in the tool description. When a scanner flagged it, we knew that detection path was running and could recognize the malicious text it received. That result did not tell us whether the scanner inspected the handler code or could find behavior hidden there.
The Gap Between Scan Time and Run Time
Most MCP scanners make a decision before a tool is called. They may connect to the server, request its tool list, and inspect the returned names, descriptions, and schemas. The handler code runs later, after an agent chooses the tool.
How to Read the Results
The scanners use different labels. They should not be treated as one shared severity scale.
- Cisco
SAFEmeans the selected analyzer returnedis_safe=true, severitySAFE, and no findings. - Sentinel
0 findingsmeans none of its manifest heuristics matched. Its own output says that this static check is not a guarantee and does not execute the server. - Snyk’s empty
risk_indexesmeans its cloud scan returned no risk category for the server. - MCP-Shield uses
Verifiedfor its clean result. - SkillSpector
SAFE / 0means a score of zero with aSAFErecommendation. - SkillSpector
CAUTION / 0means zero findings with incomplete analysis. It should not be read as a safety guarantee.
The table keeps the original product labels and shows what each check inspected.
| Scanner and check | What it inspected | Confessing control | Credential thief | RCE server | Benign control |
|---|---|---|---|---|---|
| Cisco YARA | Tool metadata | HIGH DETECTED |
SAFE MISSED |
SAFE MISSED |
SAFE EXPECTED |
| Cisco metadata LLM | Tool metadata | HIGH DETECTED |
SAFE MISSED |
SAFE MISSED |
SAFE EXPECTED |
Cisco behavioral |
Extracted source evidence | SAFE MISSED |
SAFE MISSED |
SAFE MISSED |
NOT RUN |
| Sentinel | Static manifest | 1 HIGH DETECTED |
0 findings MISSED |
0 findings MISSED |
0 findings EXPECTED |
Snyk cloud scan |
Live tool metadata | PI 1000 DETECTED |
Empty risk indexes MISSED |
Empty risk indexes MISSED |
Empty risk indexes EXPECTED |
| MCP-Shield | Live tool metadata | HIGH DETECTED |
Verified MISSED |
Verified MISSED |
Verified EXPECTED |
| SkillSpector static | Source files and static rules | DO_NOT_INSTALL / 100 DETECTED |
SAFE / 0 MISSED |
SAFE / 0 MISSED |
SAFE / 0 EXPECTED |
| SkillSpector default | Static rules and LLM source review | DO_NOT_INSTALL / 100 DETECTED |
Normal size: DO_NOT_INSTALL / 100 DETECTED Oversized variant: CAUTION / 0 MISSED |
Normal size: DO_NOT_INSTALL / 100 DETECTED Oversized variant: CAUTION / 0 MISSED |
SAFE / 0 EXPECTED |
SkillSpector’s result was different from the other rows. Its default LLM review was the only check in this comparison that found both hidden servers at normal size. The zero-finding result came from separate 17 MiB variants with the same working payloads. Those files crossed SkillSpector’s per-file limit and were excluded from the LLM input.
Root Causes of the Scanner Gaps
The tests covered both broad limits of pre-install scanning and specific implementation bugs.
Cisco’s Behavioral Review Started With Too Little Evidence
Cisco’s review prompt was already larger than its own prompt budget, leaving only a short fragment for evidence from the server.
The tested behavioral analyzer used a 72,798-character instruction template with a configured 68,000-character limit. Its prompt builder kept a 500-character minimum for extracted analysis. In our captured prompt, that fragment ended before the relevant file and network behavior.
The captured prompt measurements showed the mismatch directly:
The extractor also summarized helper calls by name without including their full bodies in the default batch view. Names such as _load_preferences, _sync_telemetry, and _apply_service_update fit the handler’s docstring, while the credential reads, HTTP request, and command execution were inside the helper bodies.
This check therefore reported SAFE even for the confessing control. The analyzer never received the code that showed the credential reads, HTTP request, or command execution. Its result was based on missing evidence, not a clean review of the relevant behavior.
An Oversized File Did Not Reach SkillSpector’s LLM
Adding harmless text caused SkillSpector’s strongest review stage to receive no server.py at all.
At normal size, SkillSpector’s default LLM review found the credential theft and RCE. We then placed the same handler logic after about 16.2 MiB of harmless weather reference text.
The file was arranged like this:
SkillSpector 2.11.0 read only the start of an oversized file for static analysis. More importantly, it did not add a truncated file to the cache used by its semantic LLM checks. The LLM reviewed the manifest, but not server.py.
The scan recorded partial coverage in its JSON metadata and returned CAUTION / 0. The same result was reproduced with three model backends. Paired tests confirmed that those models found the payload when they received the normal-size file. The bypass happened before model judgment.
A Server Could Show a Scanner Different Metadata
An MCP server can identify the connecting client and choose which tool description to return.
During initialization, an MCP client reports a clientInfo.name. The PoC used that value like this:
Cisco’s live stdio scan and Snyk used the MCP Python SDK’s generic mcp name in our tests, so the PoC returned the weather description to them. It returned the confessing description to clients whose names were not on its scanner list.
Cisco’s live scan therefore returned SAFE. MCP-Shield connected as mcp-shield, received the malicious description, and returned HIGH.
Changing or hiding a scanner’s client name would make this exact fingerprint less reliable, but it would not solve the full problem. A server can use timing, environment, network, or request patterns to return different data to different observers.
Equivalent Python Calls Produced Different Static Results
The static rule recognized one Python syntax for os.popen, but not another syntax that returns the same function.
SkillSpector’s AST rules detected this form:
The RCE server used this form instead:
Python modules store their attributes in __dict__, so both forms resolve to os.popen. The second form is a dictionary subscript, not a getattr call. The tested AST rule did not classify that subscript as reflective access to a dangerous function, and the static score fell to zero.
The credential server used the same idea for paths. It joined "~", "/.ssh", and "/id_rsa" at runtime, so no complete sensitive path appeared as one source string.
A separate blind review showed that capable models could follow this indirection when they received the full source. The gap was in the static pattern, not a general inability to understand the code.
Related Fields Were Scanned Separately
Cisco’s YARA rule needed two clues in one string, so we placed one clue in the description and the other in the parameter schema.
The credential-harvesting rule required a credential indicator and an action verb in the same scanned value. Cisco scanned the tool description first, removed it, and then scanned the parameter JSON separately.
The PoC put reads and sends in the description, with no credential path. It put ~/.aws/credentials in a parameter description, with no action verb. A person reading the complete tool definition could connect the two. Neither YARA input contained both parts, so the rule did not match.
The RCE Command Arrived After the Scan
The server did not contain the attacker’s command until it was already running.
The RCE server contacted 127.0.0.1:9998 during the weather call and received {"cmd": "..."}. It then ran that value and sent the output to a second local endpoint. Changing the command did not require changing or reinstalling the MCP server.
A pre-call metadata scan cannot match a command that does not yet exist in the package or tool definition. Source analysis can still flag the code path that downloads and executes a value, but only if that path is inspected and understood.
Encoding Hid Strings but Left Another Signal
Encoded code can hide a credential path, but the code that decodes and runs it may still be suspicious.
One PoC stored its payload as a hex string, decoded it with XOR, and passed the result to exec during the tool call. Metadata scanners still saw only the weather description. A simple search for ~/.ssh/id_rsa in the source would not find the decoded path.
SkillSpector did not miss this version. Its static rules flagged the exec call and returned CAUTION / 27. Encoding removed one clue, the literal credential path, but exec remained visible. Hiding one pattern can still leave other suspicious behavior in the code.
The Metadata Did Not Describe the Handler
Several scanners checked what the tool said it did, not the code that ran when the tool was called.
Cisco’s YARA and metadata-LLM checks, Sentinel, Snyk’s cloud scan, and MCP-Shield inspected the tool name, description, schema, or manifest. The malicious servers described a weather lookup in those fields. The credential reads and command execution existed only in server.py.
The tool list returned this description:
The credential server’s handler called code that never appeared in that description:
This was enough to keep those checks clean. It required no unusual wording because the malicious behavior was absent from the inspected text.
Closed Wordlists Missed Wording They Did Not Contain
Exact keyword rules can miss a dangerous sentence when it uses a different verb or form of the same verb.
One Cisco rule looked for a credential term together with a verb from a fixed list. The PoC used populate, which was not in that list. It also included a parameter named config_file, which matched a false-positive suppression pattern in the tested rules. Cisco YARA and Sentinel returned no findings, while MCP-Shield and SkillSpector still flagged the content.
A narrow Sentinel test showed a similar boundary. Its human-confirmation rule matched the word send, but not the third-person form sends. Changing send it to to sends it to removed that specific finding. Other scanners and a human reader still saw the dangerous sentence.
Tool Results Were a Separate Input Channel
A scanner that only lists tools never sees text returned after a tool is called.
One PoC returned a normal forecast followed by an instruction telling the agent to read ~/.ssh/id_rsa. None of the tested MCP-server scanners called the tool during its normal scan, so this result text was outside their view.
An agent that follows this instruction could expose the private key in a later tool call or send it to another service. The server does not need permission to read the file itself. It can use its result to steer the agent toward a more privileged tool that has that access. A scan that stops at the tool list never sees the instruction that starts this chain.
Status of the Submitted Fixes
We submitted four changes for the concrete implementation bugs:
- NVIDIA/SkillSpector #509 sends a bounded excerpt of oversized files to the LLM stage and adds a high-severity coverage finding when a file could not be reviewed completely.
- NVIDIA/SkillSpector #517 detects dangerous module access through
__dict__andvars()subscripts, including dynamically assembled keys. - cisco-ai-defense/mcp-scanner #255 increases the behavioral prompt budget, reserves space for evidence, and rejects configurations in which the template cannot fit.
- cisco-ai-defense/mcp-scanner #256 scans the complete tool definition so a rule can connect evidence across the description and parameters.
At the time of writing, none of these changes has been merged or released. Both SkillSpector pull requests are approved and pass their current CI checks. The two Cisco pull requests are open and awaiting maintainer review.
These patches address specific bugs. They do not remove the larger limits of pre-install analysis, such as behavior fetched at call time, different responses for different clients, or malicious intent hidden inside a capability that also has legitimate uses.
What MCP Users Should Do
Read the scope of the result. Check whether the scanner inspected metadata, source files, dependencies, tool results, or runtime behavior. Do not turn “no matching findings” into “safe to install.”
Treat incomplete analysis as a failed gate. A skipped file, size limit, timeout, unavailable model, or degraded analyzer means the scan did not finish. Do not accept a zero-finding count from an incomplete run.
Check where the server came from. Prefer reviewed and curated sources. Pin the exact version or commit, record its hash, and review updates before they replace an approved build.
Review the complete code that will run. Use static rules, dependency checks, and semantic review as complementary tools. Confirm that every executable file reached the review, including generated files, archives, binaries, and code loaded at runtime.
Test unknown servers in isolation. Use fake credentials and a temporary home directory. Watch which files, processes, and network destinations the server touches when tools are called.
Limit access even after review. Give each server only the files, environment variables, credentials, and network destinations it needs. A weather server should not inherit SSH keys or unrestricted outbound network access.
Monitor behavior after installation. Tool definitions, packages, remote content, and command responses can change. Record tool-definition changes and watch process-level file and network activity, not only MCP requests and responses.
Use several signals, not several badges. Running more scanners can improve coverage when they inspect different surfaces. Agreement between metadata-only scanners still does not establish what the handler code will do.
A Scan Is Evidence, Not Trust
The five scanners in this test were not interchangeable. Some looked for dangerous text. Some checked schemas and known patterns. Some inspected source. One sent source to a model. Those checks can all provide useful evidence.
The mistake is treating a clean result as a complete trust decision. MCP risk spans provenance, code, tool metadata, client identity, permissions, call-time data, and runtime side effects. A scanner sees only the parts it is built and configured to inspect.
Before running an MCP server, ask what the scan actually covered, what it could not cover, and what access the process will have if the result is wrong.
Pluto’s platform starts with discovery: every connector, skill, plugin, and MCP server already running across an organization’s AI stack, sanctioned or not. From there, risk assessment surfaces exactly the kind of gap covered in this piece, and enforcement closes it before an attacker finds it first. MCP servers are one surface in that stack. The same blind spot shows up in browser extensions, coding agents, and the plugins builders install without asking anyone first.
If your team is using MCP servers, self-hosted or otherwise, and wants someone to verify everything’s under control, book time with us and we’ll show you what we’d find in yours.
