GCA · INTEGRATIONS

    MCP Tools Reference

    Per-tool reference for the Gadriel Code MCP server.

    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):

    MethodPurposeResponse
    initializeCapability negotiationserverInfo + protocolVersion + capabilities
    notifications/initializedClient acknowledges the handshakenone (no-op notification)
    tools/listEnumerate registered tools{ "tools": [ … ] }
    tools/callInvoke a tool by name{ "content": [ … ] }

    1.1 initialize

    { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {} }

    Response:

    {
    "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):

    { "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {} }

    1.3 tools/call

    params carries the tool name and its arguments:

    {
    "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:

    {
    "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

    ToolPurposeWrites diskCalls LLMMutates state
    validate_bufferScan an in-memory buffer (SAST + Secrets)nonoopens ephemeral Tier-2 session (in-process)
    validate_fileScan a saved path (SAST + Secrets)nonoopens ephemeral Tier-2 session (in-process)
    findings_for_pathRead persisted findings for a filenonono
    fix_findingLLM-assisted fix → returns a diffaudit log¹yesaudit log¹ (never source)
    dismiss_false_positiveWaive a finding + record feedbackyesnowaiver + audit + Bayesian prior
    submit_ai_findingsTier-2 fusion callbacknonoconsumes the Tier-2 session
    start_remediation_campaignBegin a repo-scale fix campaignyes (state only)nocampaign state + lock
    campaign_advanceStep the fix→verify loopyes (state/waivers)yescampaign 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

    { "content": "<source>", "file_path": "src/auth.py" }
    FieldTypeNotes
    contentstringThe unsaved buffer. Hard cap of 5 MiB (MAX_BUFFER_BYTES); over-size requests fail with ToolFailed.
    file_pathstringLogical label used as code_context.file; consumed verbatim, never read or written on disk.

    Result

    { "findings": [], "count": 3, "language_coverage": null, "tier2_request": null }
    FieldTypeNotes
    findingsarrayFindings from SAST + Secrets.
    countintfindings.length.
    language_coveragestring | nullnull for a Python buffer (SAST ran); "unsupported(<ext>)" for any other extension (SAST skipped, Secrets still ran).
    tier2_requestobject | nullPresent 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

    {
    "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

    { "file_path": "src/auth.py" }
    FieldTypeNotes
    file_pathstringPath to scan, read as UTF-8. A non-existent or binary file surfaces as ToolFailed.

    Result

    { "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

    {
    "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

    { "path": "src/auth.py", "repo_root": "/abs/path/to/repo" }
    FieldTypeNotes
    pathstringMatched 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_rootstring (optional)Defaults to the current working directory.

    Result

    { "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

    {
    "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

    { "finding_id": "CODE-W1-AI-001", "repo_root": "/abs/path", "runner_id": "claude" }
    FieldTypeNotes
    finding_idstringFinding ID. Loaded from .security/findings.json.
    repo_rootstring (optional)Defaults to the current working directory.
    runner_idstring (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):

    {
    "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:

    ResponseWhen
    { "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

    {
    "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

    {
    "finding_id": "CODE-W2-CONFIG-MCP-004",
    "reason": "legitimate filesystem MCP with allow-list",
    "repo_root": "/abs/path"
    }
    FieldTypeNotes
    finding_idstringFinding ID.
    reasonstringWritten to the audit note (truncated to 280 chars) and the waiver's reason field.
    repo_rootstring (optional)Defaults to the current working directory.

    Result

    { "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:

    1. Durable waiver appended to .gadriel-waivers.toml, keyed by (rule_id, path, line) — identical in shape to gadriel code fix --false-positive. The path + line are resolved by looking finding_id up in findings.json; if that lookup fails it falls back to rule-id-only keying (never silently drops the dismissal).
    2. Audit event appended to .security/audit-log.jsonl: confirmed_false_positive, agent_runner: "human", notes: <reason>.
    3. 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 weaker DismissAcceptedRisk signal (β += 0.3); everything else maps to McpDismiss (β += 0.5) — deliberately down-weighted versus the CLI's full-weight DismissNotReal (β += 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

    {
    "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

    {
    "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"
    }
    FieldTypeNotes
    session_idstringThe session_id returned in a prior tier2_request. One-shot; 120-second TTL.
    ai_findingsarrayEach entry: cwe, severity, file, line, rationale, confidence, kind (new_finding / triage_note / false_positive_override / refutation). Malformed entries are skipped, not fatal.
    modestring (optional)"advisory" (default) adds unmatched AI findings as advisory; "corroborate_only" keeps only findings that match a deterministic one.

    Result

    {
    "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

    {
    "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):

    {
    "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
    }
    FieldTypeNotes
    severity / pillar / scan_type / rule_id / confidence_tierarrays (optional)Each present field is an OR-set the finding must intersect; the fields AND together. All-absent matches every finding.
    finding_idsarray (optional)Explicit ID allowlist — when present, only these IDs pass.
    repo_rootstring (required)Absolute repo path.
    runner_idstring (optional)Carried for parity with fix_finding; campaign_advance defaults to claude.
    forcebool (optional)Reclaim a live lock held by an abandoned campaign.

    Result

    { "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

    {
    "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

    { "campaign_id": "campaign-1719…", "repo_root": "/abs/path", "applied": true, "after_hash": null, "reason": null }
    FieldTypeNotes
    campaign_idstringThe campaign to advance.
    repo_rootstring (required)Absolute repo path.
    appliedbool (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_hashstring (optional)Reserved; unused — verification re-derives the file's findings directly rather than trusting a caller hash.
    reasonstring (optional)Reason written into the waiver on an explicit decline (applied: false).

    Result — one of two shapes.

    Next fix to apply:

    {
    "campaign_id": "campaign-1719…",
    "finding_id": "CODE-W1-AI-001",
    "status": "in_progress",
    "proposed_fix": { "status": "proposed", "diff": "…", "rationale": "…",}
    }

    Campaign finished:

    {
    "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

    {
    "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.