Most MCP security writeups (including a few of mine) focus on a single server: does this one tool description contain a hidden instruction, does this one manifest request too many scopes. That's the easy case to scan for because everything you need is in one file.
The case that's harder to catch, and that I haven't seen a scanner actually check for, is what happens when a model has two or more MCP servers connected at once and neither one is individually malicious. You get a confused-deputy problem: server A holds a capability (say, "send email" or "write to this repo"), server B holds untrusted content (say, "read this webpage" or "read this issue"), and the model happily uses A's capability on data it just pulled from B, because nothing in either server's config told it not to.
Say you've got a "fetch a URL and summarize it" MCP server and a "send a Slack message" MCP server both wired into the same agent. Individually both are boring, useful tools. Chained, the failure mode looks like this:
This is the classic confused-deputy pattern (a component with more authority than the data it's acting on trusts that data implicitly), just wearing MCP's clothes. It's also exactly why single-server manifest scanning, however thorough, has a ceiling: the risk surface is the set of connected servers, not any one of them.
A tool that reads manifests and static config (this is what sentinel-scan-cli does, and what most of the current MCP scanners do) can tell you:
What it can't tell you is whether a given combination will actually get exploited in a live conversation, because that depends on runtime behavior and the specific content an agent happens to fetch. Static analysis gets you "here is your blast radius if this combination goes wrong," not "this will go wrong."
WRITE_CAPABLE = {"send_email", "post_message", "write_file", "create_pr", "execute_command"}
INGESTS_UNTRUSTED = {"fetch_url", "read_webpage", "read_issue", "read_email", "search_web"}
def flag_confused_deputy_risk(session_tools, requires_confirmation):
write_tools = [t for t in session_tools if t.name in WRITE_CAPABLE]
ingest_tools = [t for t in session_tools if t.name in INGESTS_UNTRUSTED]
if write_tools and ingest_tools:
unguarded = [t for t in write_tools if t.name not in requires_confirmation]
if unguarded:
return {
"risk": "confused_deputy",
"write_tools": [t.name for t in unguarded],
"ingest_tools": [t.name for t in ingest_tools],
"note": "untrusted content and unguarded write capability in same session",
}
return None
That's a session-topology check, not a content check. It won't catch the specific injected instruction. It will tell you, before anything bad happens, "you've wired a tool that reads the open internet directly into a tool that can post to Slack with no human in the loop, that pairing is worth a second look."
None of this is exotic once you name it:
If you're auditing your own MCP setup and want the "which sessions have both a write-capable server and a content-ingesting server" check without writing it yourself: sentinel-scan-cli is open source and does static manifest/config scanning including this kind of cross-server topology flag. Full findings from scanning it against real server configs are here: sample report.
Curious whether anyone's seen this actually exploited in the wild versus just theorized, most of the writeups I've found (including some of my own) are proof-of-concept, not incident reports.