The MCP Remediation Loop
isitsecure ships a local MCP server so the AI coding tool you already use can scan, explain, fix, verify and export without leaving the editor. The server supplies data and verbs. The host LLM supplies the conversation, the plan and the judgment.
Three actors, one principle
The bar is four needs: scan, converse about the report, plan the fixes, run individual fixes. Only the first and the last are functions. The two in the middle are the host LLM reasoning and talking, and the MCP's job is to make that reasoning grounded instead of guessed.
1You work in Cursor, Claude Code or Claude Desktop and ask about your code in your own words. You never have to name isitsecure.
2The host LLM is the one that converses, prioritises and plans. Of the four needs (scan, converse, plan, run fixes), the two in the middle are its reasoning, not something you call.
3Your tool spawns isitsecure mcp as a local stdio process, one per user. It exposes seven tools and returns typed, self-describing payloads. Nothing runs on a server anywhere.
4The principle: the MCP supplies data + verbs; the host LLM supplies judgment. Building "conversation" or "planning" as tools fights the grain. Instead the scan and fix verbs are made excellent and their payloads rich enough that the LLM never has to guess.
Asked "what gets me to a C grade?", an agent once gave a perfect answer, but only because it could read the grader in plain_english.py. A real user's agent, scanning their own repo, has no access to isitsecure internals and would guess the thresholds. The fix was not a "grading tool". It was putting the grade model into the scan payload as path_to_next_grade.
The loop, one turn at a time
A sequence diagram of a real session. Solid arrows are calls, dashed arrows are what comes back. The panel on the right shows the payload at each step, which is what the host LLM reasons over.
1You ask "is this safe to ship?" in your editor. isitsecure is not mentioned anywhere in the question.
2The host LLM reaches for scan(path, min_severity) on its own. Discoverability comes from three things, in priority order: the server instructions in the initialize response, the tool description that leads with the trigger phrases, and the tool name. It maximises the odds; it cannot guarantee the call.
3scan returns a typed ScanResult: a scan_id, the grade, the launch verdict, severity counts, path_to_next_grade, root-cause themes and trimmed findings with a plain-English explanation and a priority each.
4You ask "what gets me to a C?". No tool runs. The LLM reads the rung for C from the payload (requires 0 critical, 0 high; clear at least 4) and answers from the grade model instead of guessing.
5explain(scan_id, finding_id) resolves the finding from the scan cache and returns a deep dive: its own description, technical detail, evidence and vulnerable snippet, then business impact, stack-tailored remediation and a step-by-step walkthrough.
6fix(scan_id, finding_id) reads the finding's source file and returns a proposed patch: a unified diff, the full fixed_file, an explanation, and applied: false.
7The host applies it with its own editor, inside your normal review flow. The MCP writes no files by default; the four reasons are in the panel.
8verify(scan_id) re-scans the same path and reports what cleared, what is still there, the grade before and after, and a fresh scan_id. Only rule-based findings are verifiable; LLM and DAST findings are reported as unverifiable and held constant.
9export(scan_id, format) renders the cached scan as html, sarif, json or markdown and returns the content. The host writes the file, same stance as fix.
Who applies the fix
Mutating the user's code is the riskiest capability here, so the decision is explicit. In the MCP context the host LLM holds the pen, and the quality gate is verify, not the act of applying.
Whoever writes the file, verify(scan_id) re-scans and confirms the finding is actually gone for rule-based findings, and reports grade movement attributable to that change. This decouples the quality gate from who does the writing, which is what makes "let the host apply" safe. Success is never assumed.
For headless callers, or a user who explicitly wants the tool to do it, fix(…, apply=true) would write through the same safety-net pipeline the CLI uses: back up the working tree, write one finding's fix, re-scan. Still one finding at a time, never "fix everything" over MCP. This fallback is still a proposal in docs/mcp.md.
Live-URL scans: a job handle, not a long call
A DAST scan of a running app takes minutes, far longer than a tool call should block. So scan_url starts the scan in the background and returns a job id at once; scan_status is polled until the result is ready.
1scan_url(url) validates that the URL is http(s), starts a url-only DAST scan as a background task and returns a job_id immediately. The URL must be reachable from the machine running the server.
2The host polls scan_status(job_id). It gets running, a 0 to 1 progress, a message and result: null.
3Still running. Finished jobs are the only ones evicted when the job table is over its cap, so a running scan's handle never vanishes mid-flight.
4done. The full result has the same shape as scan, and its scan_id works with explain and export. Authenticated crawls and full SAST+DAST runs stay CLI-side for now.
Stable finding ids across turns
"Explain the SQLi one" and "fix #3" only work if a finding from an earlier turn can still be resolved. The server keeps the full report of each scan in an in-process cache keyed by scan_id; every later tool resolves its finding from there.
1scan stores the full report under a fresh 12-character scan_id together with the path that was scanned. The findings it returns carry ids that stay valid for the session.
2Turns later you say "fix #3". get_finding(scan_id, finding_id) resolves the finding from the cached report, and the cached path lets fix read the actual source file. Every later tool builds on this one lookup.
3verify caches its re-scan as a new entry and returns the new scan_id, so the next fix targets the current state of the code.
4The cache is bounded to the 20 most recent scans (oldest evicted first) and lives only in the server process. On-disk persistence under ~/.isitsecure/scans/ is a possible later add; in-process is enough for one session's loop.
Why the CLI applies and the MCP does not
Same fix generator, opposite right answer. The divergence is intentional and recorded so it is not mistaken for an inconsistency.
1Both paths start from the same FixGenerator: the finding plus its full source file go to the LLM, and a fixed file and unified diff come back.
2The CLI has no host LLM, so it must apply the fix itself. It snapshots the working tree as a safety net, writes one finding's fix, then re-scans to confirm the finding is gone.
3The MCP always has a capable agent, so it defers the write. It returns the diff with applied: false, the host applies it in your review flow, and verify closes the loop with the same re-scan proof.
The tool surface
Seven tools, as registered in isitsecure/mcp_server.py. Every tool after scan takes a scan_id, which is what makes the conversation stateful.
| Tool | Parameters | Returns | Who writes |
|---|---|---|---|
scan | path, min_severity="medium" | scan_id, grade, launch verdict, counts, path_to_next_grade, themes, trimmed findings | nobody (code-only SAST, read only) |
explain | scan_id, finding_id | the finding's own description, technical detail, evidence, snippet; business impact; remediation; walkthrough | nobody |
fix | scan_id, finding_id | unified diff, fixed_file, explanation, applied: false | the host LLM |
verify | scan_id | resolved / still present / unverifiable, grade before → after, fresh scan_id | nobody (re-scan only) |
export | scan_id, format="markdown" | content as html, sarif, json or markdown, suggested filename | the host LLM |
scan_url | url, min_severity="medium" | job_id, status "running" | nobody |
scan_status | job_id | status running | done | error, progress 0–1, result once done | nobody |
docs/mcp.md now lists scan(path, min_severity), matching the registered tool: there is no mode argument and the scan is always code-only. The apply=true fallback on fix is still only a proposal in the doc and is not implemented.
Open questions from the design doc
- Scan-cache lifetime. In-process only (simplest, lost on restart) or on disk under
~/.isitsecure/scans/(survives, but needs cleanup and expiry). - verify cost. A full re-scan with LLM review is slow. Could verification be scoped to changed files, or run rule-based only for speed?
- Multi-file fixes. Some findings span files (adding RLS across tables). How does
fixpresent and apply those safely under the one-finding-at-a-time rule? - Grade-path shape. The exact schema of
path_to_next_gradethat the LLM reads to answer "what gets me to a C?".
No hosted or multi-user MCP: this is a local stdio server spawned per user by their own tool. No authenticated or full DAST over MCP: scan_url is url-only; authenticated crawls and SAST+DAST runs stay on the CLI.