MCP Tools Reference
A per-tool reference for the Gadriel Code MCP server (gadriel code mcp).
This page complements AI-coding integration,
which covers why the server exists and how the agentic loop uses it; here
we document what each tool takes, returns, and touches.
The server registers 8 tools. None of them ever writes source code — the
fix_finding and campaign_advance tools return a diff + rationale that the
host agent applies out of process. There is no
auto-commit and no auto-git add, ever.
Related: AI-coding integration · Tiered analysis model · False positives · Privacy boundary · CLI reference
1. The JSON-RPC handshake
The server speaks JSON-RPC 2.0 over stdio. Beyond direct tool invocation it
implements the three MCP protocol methods any compliant client expects
(verified in crates/code-security/gadriel-mcp/src/server.rs):
| Method | Purpose | Response |
|---|---|---|
initialize | Capability negotiation | serverInfo + protocolVersion + capabilities |
notifications/initialized | Client acknowledges the handshake | none (no-op notification) |
tools/list | Enumerate registered tools | { "tools": [ … ] } |
tools/call | Invoke a tool by name | { "content": [ … ] } |
1.1 initialize
json{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {} }
Response:
json{"jsonrpc": "2.0","id": 1,"result": {"protocolVersion": "2024-11-05","capabilities": { "tools": {} },"serverInfo": { "name": "gadriel", "version": "<crate version>" }}}
serverInfo.name is always gadriel; the advertised protocolVersion is
2024-11-05. stdio transport is unauthenticated by design — the auth_token
field in ServerConfig is reserved for a future HTTP/SSE transport and is
unused today.
1.2 tools/list
Returns every registered tool with its name, description, and a minimal
inputSchema stub ({ "type": "object", "properties": {}, "additionalProperties": true }
— the server does not publish a strict per-tool schema; each tool validates
its own params on invoke):
json{ "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {} }
1.3 tools/call
params carries the tool name and its arguments:
json{"jsonrpc": "2.0", "id": 3, "method": "tools/call","params": { "name": "validate_file", "arguments": { "file_path": "src/auth.py" } }}
The tool's own JSON result is wrapped into the MCP content envelope — the result value is serialized to a string inside a single text block:
json{"jsonrpc": "2.0", "id": 3,"result": { "content": [ { "type": "text", "text": "{\"findings\":[…],\"count\":0,\"tier2_request\":null}" } ] }}
Throughout the per-tool sections below, the JSON labelled Result is the
tool's own return value — i.e. the object that appears (stringified) inside
content[0].text on a tools/call, or verbatim as result on the legacy
direct-invocation path.
2. Tool surface at a glance
| Tool | Purpose | Writes disk | Calls LLM | Mutates state |
|---|---|---|---|---|
validate_buffer | Scan an in-memory buffer (SAST + Secrets) | no | no | opens ephemeral Tier-2 session (in-process) |
validate_file | Scan a saved path (SAST + Secrets) | no | no | opens ephemeral Tier-2 session (in-process) |
findings_for_path | Read persisted findings for a file | no | no | no |
fix_finding | LLM-assisted fix → returns a diff | audit log¹ | yes | audit log¹ (never source) |
dismiss_false_positive | Waive a finding + record feedback | yes | no | waiver + audit + Bayesian prior |
submit_ai_findings | Tier-2 fusion callback | no | no | consumes the Tier-2 session |
start_remediation_campaign | Begin a repo-scale fix campaign | yes (state only) | no | campaign state + lock |
campaign_advance | Step the fix→verify loop | yes (state/waivers) | yes | campaign state, waivers (never source) |
¹ fix_finding's audit-log writes happen on the back-compat AgentVerdict
path (no runner_id). See §3.4.
The two validate_* tools open a Tier-2 session only when Tier 2 is opted
in (ServerConfig::tier2_registry is Some). With Tier 2 off (the
default) they emit tier2_request: null and open nothing; submit_ai_findings
is still registered but backed by a private registry that never receives a
session, so it is inert. See tiers.md.
3. Per-tool reference
3.1 validate_buffer
Scan an unsaved code buffer so the host agent can catch a vulnerability before the file hits disk.
Input params
json{ "content": "<source>", "file_path": "src/auth.py" }
| Field | Type | Notes |
|---|---|---|
content | string | The unsaved buffer. Hard cap of 5 MiB (MAX_BUFFER_BYTES); over-size requests fail with ToolFailed. |
file_path | string | Logical label used as code_context.file; consumed verbatim, never read or written on disk. |
Result
json{ "findings": [ … ], "count": 3, "language_coverage": null, "tier2_request": null }
| Field | Type | Notes |
|---|---|---|
findings | array | Findings from SAST + Secrets. |
count | int | findings.length. |
language_coverage | string | null | null for a Python buffer (SAST ran); "unsupported(<ext>)" for any other extension (SAST skipped, Secrets still ran). |
tier2_request | object | null | Present only when Tier 2 is enabled; carries session_id, chunks, and expires_at_hint. For a buffer, candidate_files is empty (the agent already holds the content). |
Scope & side-effects. SAST covers Python only; Secrets runs on any buffer. No SCA / Container / API (those need disk / multi-file projects) and no graph amplifiers (the buffer is not part of the persisted graph yet). The tool writes nothing and calls no LLM; when Tier 2 is on it opens an in-process, one-shot, 120-second session in the registry.
JSON-RPC example
json{"jsonrpc": "2.0", "id": 10, "method": "tools/call","params": {"name": "validate_buffer","arguments": {"content": "import os\npassword = \"hunter2\"\n","file_path": "src/config.py"}}}
3.2 validate_file
Full single-file scan from disk — the common case, since most editor agents save before calling a tool.
Input params
json{ "file_path": "src/auth.py" }
| Field | Type | Notes |
|---|---|---|
file_path | string | Path to scan, read as UTF-8. A non-existent or binary file surfaces as ToolFailed. |
Result
json{ "findings": [ … ], "count": 1, "tier2_request": null }
Same shape as validate_buffer minus language_coverage. When Tier 2 is
enabled the tier2_request.candidate_files contains the scanned file, and the
host agent SHOULD call submit_ai_findings with the returned session_id
within 120 seconds.
Scope & side-effects. SAST covers Python (non-Python files yield zero SAST findings); Secrets runs on any file. No SCA / Container / API (project-scoped, not single-file). The tool reads the target file but writes nothing and calls no LLM. The read + parse + match runs on a blocking thread pool so the async runtime is not stalled.
JSON-RPC example
json{"jsonrpc": "2.0", "id": 11, "method": "tools/call","params": { "name": "validate_file", "arguments": { "file_path": "src/db.py" } }}
3.3 findings_for_path
The read-side of the loop: fetch the current persisted findings for a file the agent is editing.
Input params
json{ "path": "src/auth.py", "repo_root": "/abs/path/to/repo" }
| Field | Type | Notes |
|---|---|---|
path | string | Matched against code_context.file by exact-string equality and suffix match, so src/auth.py matches an absolute /abs/src/auth.py and vice-versa. |
repo_root | string (optional) | Defaults to the current working directory. |
Result
json{ "findings": [ … ], "count": 2, "source": ".security/findings.json" }
Side-effects. Reads <repo_root>/.security/findings.json (a missing file
is ToolFailed, an invalid-OCSF file is ToolFailed). No writes, no LLM.
JSON-RPC example
json{"jsonrpc": "2.0", "id": 12, "method": "tools/call","params": {"name": "findings_for_path","arguments": { "path": "src/auth.py", "repo_root": "/work/acme-api" }}}
3.4 fix_finding
LLM-assisted remediation. It invokes the per-pillar reasoning runner and returns a diff for the host to apply — it never edits source itself.
Input params
json{ "finding_id": "CODE-W1-AI-001", "repo_root": "/abs/path", "runner_id": "claude" }
| Field | Type | Notes |
|---|---|---|
finding_id | string | Finding ID. Loaded from .security/findings.json. |
repo_root | string (optional) | Defaults to the current working directory. |
runner_id | string (optional) | claude (default), copilot, codex, cursor, aider. The four non-Claude runners are roadmap stubs. |
The presence of runner_id selects one of two response shapes:
(a) With runner_id — a FixProposal envelope (the recommended path):
json{"status": "proposed","runner_id": "claude","finding_id": "CODE-W1-AI-001","diff": "--- a/src/db.py\n+++ b/src/db.py\n@@ …","rationale": "Parameterize the query to break the injection sink.","iterations_used": 1,"tool_calls": []}
Error / non-success variants on this path:
| Response | When |
|---|---|
{ "error": "unknown runner x", "available": [ … ] } | runner_id is not registered. |
{ "status": "not_available", "reason": …, "runner_id": …, "finding_id": … } | Runner is a stub (or refused offline). |
{ "status": "failed", "reason": …, "runner_id": …, "finding_id": … } | Runner timeout / internal error / privacy-boundary refusal. |
(b) Without runner_id — the back-compat AgentVerdict (confirmation +
explanation + optional fix), used by gadriel code fix smoke tests.
Side-effects. This tool calls an LLM (a multi-second round-trip). On
the back-compat AgentVerdict path it appends audit-log entries to
.security/audit-log.jsonl — a confirmation event (confirmed_real /
suppressed_by_agent / surfaced) and, when a fix is produced, a
fix_proposed event carrying the pre-fix before_hash. It never
writes source: the diff is returned for the host agent to apply
out-of-process, so no after_hash is captured here.
Source-content consent. Whether source excerpts are shipped to Anthropic
is enforced at the runner level, not by this tool. The MCP server binary
constructs the Claude runner with source_consent = false by default;
operators opt in via GADRIEL_MCP_SOURCE_CONSENT=true. See
privacy.md.
JSON-RPC example
json{"jsonrpc": "2.0", "id": 13, "method": "tools/call","params": {"name": "fix_finding","arguments": {"finding_id": "CODE-W1-AI-001","repo_root": "/work/acme-api","runner_id": "claude"}}}
3.5 dismiss_false_positive
Register a human "this is not a real issue" decision. This is the durable, learning-aware complement to a bare audit note.
Input params
json{"finding_id": "CODE-W2-CONFIG-MCP-004","reason": "legitimate filesystem MCP with allow-list","repo_root": "/abs/path"}
| Field | Type | Notes |
|---|---|---|
finding_id | string | Finding ID. |
reason | string | Written to the audit note (truncated to 280 chars) and the waiver's reason field. |
repo_root | string (optional) | Defaults to the current working directory. |
Result
json{ "logged": true, "audit_log_path": "/abs/path/.security/audit-log.jsonl" }
Side-effects — three writes. This is the one read/write-heavy tool that never touches source:
- Durable waiver appended to
.gadriel-waivers.toml, keyed by(rule_id, path, line)— identical in shape togadriel code fix --false-positive. The path + line are resolved by lookingfinding_idup infindings.json; if that lookup fails it falls back to rule-id-only keying (never silently drops the dismissal). - Audit event appended to
.security/audit-log.jsonl:confirmed_false_positive,agent_runner: "human",notes: <reason>. - Bayesian prior update (best-effort): folds a weighted learning
signal into the per-repo
PriorStore. Risk-acceptance language ("accepted risk", "wontfix", "won't fix", "acceptable for now") maps to a weakerDismissAcceptedRisksignal (β += 0.3); everything else maps toMcpDismiss(β += 0.5) — deliberately down-weighted versus the CLI's full-weightDismissNotReal(β += 1.0) because the MCP path cannot yet distinguish a human caller from an AI agent. A store hiccup logs and returns; the waiver + audit event still stand.
No LLM. See false-positives.md for how waivers and priors shape future scans.
JSON-RPC example
json{"jsonrpc": "2.0", "id": 14, "method": "tools/call","params": {"name": "dismiss_false_positive","arguments": {"finding_id": "CODE-W2-CONFIG-MCP-004","reason": "legitimate filesystem MCP with allow-list","repo_root": "/work/acme-api"}}}
3.6 submit_ai_findings
The Tier-2 fusion callback. The host agent calls it after a
validate_* response handed back a tier2_request — it submits the LLM's
semantic findings, and the server fuses them with the deterministic results
from that scan session.
Input params
json{"session_id": "tier2-session-1","ai_findings": [{"cwe": "CWE-89","severity": "high","file": "src/db.py","line": 42,"rationale": "User input flows from request.args into cursor.execute","confidence": "low","kind": "new_finding"}],"mode": "advisory"}
| Field | Type | Notes |
|---|---|---|
session_id | string | The session_id returned in a prior tier2_request. One-shot; 120-second TTL. |
ai_findings | array | Each entry: cwe, severity, file, line, rationale, confidence, kind (new_finding / triage_note / false_positive_override / refutation). Malformed entries are skipped, not fatal. |
mode | string (optional) | "advisory" (default) adds unmatched AI findings as advisory; "corroborate_only" keeps only findings that match a deterministic one. |
Result
json{"findings": [ … ],"tier2_applied": true,"report": {"corroborated": 0,"contested": 0,"advisory_added": 1,"rejected_count": 0,"malformed_skipped": 0}}
Errors. An unknown or expired session returns JSON-RPC -32602 invalid
params with message "session expired or unknown: <id>". A double-consume of
the same session fails the same way (sessions are one-shot).
Side-effects. Consumes the in-process session; no disk writes, no LLM
(the host already ran the LLM). Fusion can never suppress a deterministic
finding, and AI-only findings are non-gating (floored to Low). When Tier 2
is off, the tool is inert. See tiers.md for the full invariant.
JSON-RPC example
json{"jsonrpc": "2.0", "id": 15, "method": "tools/call","params": {"name": "submit_ai_findings","arguments": {"session_id": "tier2-session-1","ai_findings": [{ "cwe": "CWE-862", "severity": "high", "file": "src/orders.py","line": 88, "rationale": "No ownership check before mutating the order","confidence": "low", "kind": "new_finding" }],"mode": "advisory"}}}
3.7 start_remediation_campaign
Begin a stateful, repo-scale fix campaign. It filters and orders a worklist, persists campaign state, and takes a single-campaign-per-repo lock. See agentic-remediation.md for the full workflow.
Input params (the filter fields are flattened to the top level):
json{"severity": ["critical", "high"],"pillar": ["security"],"scan_type": ["sast"],"rule_id": ["CODE-W1-INJECT-SQL"],"confidence_tier": ["high", "medium"],"finding_ids": ["CODE-W1-AI-001"],"repo_root": "/abs/path","runner_id": "claude","force": false}
| Field | Type | Notes |
|---|---|---|
severity / pillar / scan_type / rule_id / confidence_tier | arrays (optional) | Each present field is an OR-set the finding must intersect; the fields AND together. All-absent matches every finding. |
finding_ids | array (optional) | Explicit ID allowlist — when present, only these IDs pass. |
repo_root | string (required) | Absolute repo path. |
runner_id | string (optional) | Carried for parity with fix_finding; campaign_advance defaults to claude. |
force | bool (optional) | Reclaim a live lock held by an abandoned campaign. |
Result
json{ "campaign_id": "campaign-1719…", "total": 40, "first_batch": [ "CODE-W1-…", … ] }
first_batch previews the (up to 5) highest-priority finding IDs. Ordering
by effective_risk(severity, confidence) descending, then by
attack-path presence, then by ascending path length; same-file findings stay
adjacent.
Side-effects. Reads .security/findings.json; writes the campaign state
file .security/campaigns/<campaign_id>.json and a .lock file under the
same directory. No LLM, no source writes. An empty (zero-match) campaign
releases the lock immediately so a no-op never wedges the repo.
JSON-RPC example
json{"jsonrpc": "2.0", "id": 16, "method": "tools/call","params": {"name": "start_remediation_campaign","arguments": {"scan_type": ["sast"], "severity": ["critical", "high"],"repo_root": "/work/acme-api"}}}
3.8 campaign_advance
Step the campaign's fix → apply → verify → next loop. The host
calls it repeatedly.
Input params
json{ "campaign_id": "campaign-1719…", "repo_root": "/abs/path", "applied": true, "after_hash": null, "reason": null }
| Field | Type | Notes |
|---|---|---|
campaign_id | string | The campaign to advance. |
repo_root | string (required) | Absolute repo path. |
applied | bool (optional) | true runs the re-scan verification on the current in-progress item; false is an explicit decline → mechanized waiver; absent leaves the current item untouched (first call for a fresh item, or a bare poll). |
after_hash | string (optional) | Reserved; unused — verification re-derives the file's findings directly rather than trusting a caller hash. |
reason | string (optional) | Reason written into the waiver on an explicit decline (applied: false). |
Result — one of two shapes.
Next fix to apply:
json{"campaign_id": "campaign-1719…","finding_id": "CODE-W1-AI-001","status": "in_progress","proposed_fix": { "status": "proposed", "diff": "…", "rationale": "…", … }}
Campaign finished:
json{"status": "campaign_complete","campaign_id": "campaign-1719…","summary": {"total": 40,"cost_usd": 1.23,"by_status": { "verified": 31, "verified_partial": 6, "waived": 2, "regressed": 1 },"regressed": [ "CODE-W1-AI-014" ],"verified_partial_note": "6 item(s) marked verified_partial: …"}}
Per-item terminal states: fixed, verified, verified_partial,
regressed, waived, skipped_no_agent.
Side-effects. Calls an LLM (via fix_finding's propose logic) to
produce each diff; re-scans the touched file to verify; writes campaign
state and may append a mechanized waiver to .gadriel-waivers.toml on a
decline or runner failure. It never writes source — the host applies every
diff. Verification is an instant re-scan and is only deterministic for
Secrets + Python-SAST; other scan types are honestly reported as
verified_partial.
JSON-RPC example
json{"jsonrpc": "2.0", "id": 17, "method": "tools/call","params": {"name": "campaign_advance","arguments": { "campaign_id": "campaign-1719…", "repo_root": "/work/acme-api", "applied": true }}}
4. The zero-source-write invariant
Worth restating because it is the load-bearing safety property of the whole
surface: no tool ever writes source code. fix_finding and
campaign_advance return diffs; the host agent applies them out of process
and self-reports applied: true. The only files the server writes are the
audit log, waivers, and campaign state — all under .security/ /
.gadriel-waivers.toml. There is no auto-commit and no auto-git add. A
buggy or malicious diff can never be silently committed by Gadriel.
See agentic-remediation.md for the end-to-end workflows built on these tools.
