In April 2025, researchers at Invariant Labs disclosed a technique they called Tool Poisoning Attacks: a malicious or compromised Model Context Protocol (MCP) server embeds hidden instructions inside a tool's natural-language description — metadata that's never executed as code, only read by the LLM deciding how to use the tool. Their proof of concept got Cursor to quietly exfiltrate a user's SSH private key while returning a normal-looking result to the user. A second demo against a WhatsApp MCP integration showed a tool that altered its own description mid-session to reroute outgoing messages to an attacker-controlled number. The technique is now cataloged as MCP03:2025 in the OWASP MCP Top 10, and Microsoft's security team documented similar poisoned-description attacks causing data leakage in production agent deployments as recently as June 2026. None of this involves a single malicious function call, obfuscated string, or known-bad import — the exact signals static and signature-based scanners are built to catch. As AI agent skills and MCP tools become a new software supply chain of their own, it's worth being precise about where static analysis still helps, and where it structurally cannot see the threat at all.
What is MCP tool poisoning, and why can't a scanner see it?
Tool poisoning attacks work by hiding attacker instructions inside a tool's description field — the human-readable text an LLM reads to decide how and when to call that tool — rather than in its executable implementation. Invariant Labs' April 2025 disclosure showed a "add two numbers" tool whose description also instructed the model, in prose, to read and attach the contents of a user's SSH key when calling it. The code that runs is completely benign; the payload lives entirely in a string field a static analyzer has no reason to treat as a threat surface, because it isn't code at all — it's a natural-language instruction the LLM itself interprets and acts on. A signature scanner looking for eval(), hardcoded credentials, or known malicious byte patterns has nothing to match against. This is precisely why OWASP classifies it as a distinct MCP-specific risk (MCP03:2025) rather than folding it into existing injection categories: the vulnerable surface is the model's trust in tool metadata, not a line of vulnerable code.
Can attackers evade static scanners in the tool's actual code, too?
Yes, and the npm ecosystem has already demonstrated the playbook attackers reuse against agent skill and MCP tool code. Real supply-chain compromises have shipped payloads split across dozens of innocuous-looking variables that are concatenated only at runtime, base64 or eval chains assembled from helper functions with no single suspicious string present anywhere in the source, and multi-layer custom encodings — reversed base64 combined with XOR — specifically so that nothing a scanner reads at rest resembles a known-bad pattern. The 2021 ua-parser-js compromise shares the load-bearing part of that shape even though its specific payload wasn't base64/XOR string obfuscation: after an attacker hijacked the maintainer's npm account, the malicious code appeared only in later version bumps (0.7.29, 0.8.0, 1.0.0) of a package that had shipped clean and trusted for years, and it worked by dropping a preinstall script that fetched and ran an OS-specific payload (a credential-stealing trojan on Windows, a cryptominer on Linux) at install time — behavior a static read of the prior, benign source would never surface. An agent skill or MCP server package can use identical techniques — dynamic string assembly, delayed activation, remote payload fetch on first invocation — and a scanner reading static source sees nothing to flag, because the actual malicious behavior doesn't exist in the file yet; it's assembled or downloaded only when the code runs.
Why does execution matter when the payload isn't even in the file yet?
Any analysis technique that doesn't execute code is fundamentally blind to a class of payload that only materializes at runtime — a remotely fetched instruction set, a dynamically decoded string, or a tool description that rewrites itself mid-session, as in Invariant Labs' WhatsApp demo. Static analysis, however sophisticated its control-flow or taint tracing, operates on the file as written; it cannot observe a fetch() call's actual destination, a decoded payload's actual content, or a description field's state after a server-side edit. This is the same principle that separates SAST from DAST in conventional application security: SAST traces how untrusted input could theoretically flow to a dangerous sink across your source, while DAST exercises a running target and observes what actually happens — the responses, the outbound calls, the side effects. Agent skills need the equivalent of that DAST step, because the sink in a tool-poisoning or delayed-payload attack isn't a function call sitting in the source at all — it's a behavior that only exists once the skill executes in the context of a live agent.
What does behavioral analysis add that static scanning structurally can't?
Behavioral analysis observes what an artifact actually does when it runs — its network egress, file system access, and divergence from prior versions — rather than what its source text merely contains. That distinction matters because the indicators that catch real malicious packages are frequently runtime-only: an install script contacting an unexpected host, a process reading ~/.aws/credentials or ~/.ssh it has no legitimate reason to touch, or a new version of a previously-benign package suddenly making outbound calls it never made before. None of those signals exist in the source text of a well-obfuscated payload; they only appear once the code executes and its actual behavior can be measured against a baseline. For AI agent skills specifically, that baseline needs to extend to the tool description itself — flagging when a description changes between invocations, or when a tool's declared purpose ("format this JSON") doesn't match the data it's actually observed requesting, since a static read of the description text alone can't tell prose-based social engineering from a legitimate instruction to the model.
Does this mean static scanning for agent skills is worthless?
No — static scanning still catches real, common cases, particularly credential harvesting patterns, typosquatted package names, and obviously obfuscated code that hasn't bothered to hide itself, and it remains cheap enough to run on every publish. Safeguard's own Eagle classification model, for example, scores install-script behavior, obfuscation, egress patterns, credential-harvesting reads, and typosquat similarity across ecosystems including npm, PyPI, and Hugging Face, and explicitly treats "behavior divergence from prior versions" as one of its indicator classes precisely because static-only signals miss a large share of real incidents. The honest caveat is that neither Eagle nor Safeguard's broader platform today ships an MCP-tool-description-specific detector for the exact prose-injection pattern Invariant Labs disclosed — that's a genuine, fast-moving gap across the industry, not a solved problem anyone should claim coverage of. The practical takeaway for teams adopting AI agent skills is the same one application security teams already learned from SAST and DAST: static analysis tells you what code contains, behavioral analysis tells you what it does, and for a class of attack that lives entirely in natural-language metadata or runtime-assembled payloads, only the latter can see it at all.