Skip to content

AI Security · 5 min

Your scanner's LLM triage isn't dumb, it's underfed

Mohib Uddin · July 13, 2026

Point a static scanner at a bunch of MCP servers and you get a big pile of maybes. The problem isn't that it misses things. It's that a person still has to read every single flag to figure out which ones actually matter. If your triage step just forwards almost all of them to a human anyway, it hasn't really saved anyone any work.

Sift is a thin layer on top of SARIF. For each finding it asks a model one question: is this real, is it designed_behavior, or does it need a human? It also gives you a dry-run cost estimate and a default limit, so it can't quietly run up 500 API calls behind your back. That's the whole thing.

The run

I took four well-known MCP servers: server-memory, server-everything, server-sequential-thinking, and server-filesystem. That's 37 tools between them. I scanned all of them with Snyk's real snyk-agent-scan engine, which gave me nine findings. Then I ran those nine through sift twice on the same day. First on the raw SARIF, then again with each finding carrying the actual tool descriptions from the server it came from.

designed_behavior needs_human real
bare SARIF 4 5 0
enriched 6 1 2

Both runs together cost about thirteen cents.

0 → 2real risks that were invisible until the finding carried context

Here's what's going on. Snyk's W0xx codes are capability flags. They say "this tool can delete files," not "this tool did something bad." And Snyk flags at the server level, so the SARIF message is a generic template with none of the actual tool text in it. On the bare run, sift threw out the obviously fine capabilities (a filesystem server modifying files is kind of the point) and deferred everything else. Its reasoning kept saying the same thing: not enough evidence, a human should go read the real description. That's honestly the correct answer to the question it was given. It just wasn't a useful one.

The fix isn't clever at all. mcp-scan inspect already spits out signature.tools, so the descriptions were sitting right there in the scan output the whole time, one join away from the findings. So I attached them, bumped the message cap so the longer findings don't get cut off, and ran it again.

The one that mattered

server-everything ships a tool called get-env. Its description says it "returns all environment variables, helpful for debugging MCP server configuration." It takes no arguments, and the handler is literally one line: JSON.stringify(process.env, null, 2).

On the bare SARIF, Snyk's W016 (untrusted content retrieval) and W017 (sensitive data exposure) for that server both came back as needs_human at 55 to 60 percent confidence. Nothing in the finding said what the tools actually did, so there was nothing to judge.

Once the descriptions were attached, both flipped to real: secret and credential exposure. I didn't just trust the verdict, I went and read the source. No allowlist, no redaction, it dumps the entire process environment and hands it back. So this wasn't a hallucination. It was a correct catch that the bare pipeline could never have made, because the one fact that made it a real problem was never in the finding to begin with.

What this doesn't prove

Nine findings, four servers, and all of them are reputable. In an earlier round I used a dumb keyword filter instead of Snyk. It flagged 9 of 37 tools and sift correctly dismissed all 9. That's good specificity on real, benign tool text, but a sample with zero actual threats in it only tells you about true negatives. Two real verdicts is not a recall number, and I'm not going to pretend it is.

The idea also isn't new. Cisco's mcp-scanner already has an LLM judge for contextual triage. MCP-Guard ends in LLM arbitration. Datadog runs false-positive filtering on SAST in production. The whole industry is moving toward scanners cleaning up their own output, which leaves a standalone triage tool sitting on the side with less and less to do.

So the takeaway isn't the tool. It's this: "the LLM triage is miscalibrated, it defers too much" and "the LLM triage is being handed a keyword and told to guess the intent" look exactly the same from the outside, and only one of them gets fixed by rewriting the prompt. Before you go tuning the judge, check what you're actually feeding it. In my case it was a rule ID and a server name, and that was the whole problem.