Skip to main content

Tainted MCP server

Tainted, called by the assistant that's writing the code. An MCP call is short-lived, which fits analyze but can't hold open a real attack run. So:

  • tainted_analyze — runs synchronously, read-only.

  • tainted_prove_start / tainted_prove_status / tainted_prove_result — an async job: start it, poll it, fetch the finished report.

  • tainted_fix_interview — asks the questions a fix needs answered first.

  • tainted_fix — interactive. It only returns a patch; the client applies the edits.

  • tainted_tutorial — how to drive this server: call order, the index contract, the async prove job, the ownership boundary, the fix interview, and how to report a result without overclaiming. Call it with no topic to list the topics. The server's own instructions point at it, so a client reads it before the first prove.

Naming a hole

Every tool that names a possible hole takes finding_id — a candidate's own id, straight out of tainted_analyze. Use it. It names the hole itself, so it cannot drift.

index still works and means a row of the list tainted_analyze published, counting from 0: its findings first, then its unproven_candidates. It is only meaningful for that one report — re-analysing after an edit renumbers everything, and with the model enabled the ranking is not stable between two runs even without an edit. Either handle, past the end or unknown, comes back as {"error": "..."} naming what you asked for.

tainted_analyze is read-only and stays that way: only=test_integrity is refused, because that check measures a suite by running it (mutmut, Stryker) and this tool must not be one argument away from executing a repository's own code. Run it from the tainted CLI.

Fixing an agent-injection hole

Most fixes are decided by the code. An agent-injection fix is not. There are four repairs, and the right one depends on facts only you hold: whether the agent really needs both tools, whether a person can approve the risky action, and whether that action can afford to be slower. Tainted will not guess, so it asks.

Call tainted_fix_interview(repo_path, finding_id=...) first:

{
  "candidate": "Agent `support_bot` could be tricked into misusing a tool",
  "interview": [
    {"key": "needs_both",      "question": "Does `support_bot` really need both of these tools to do its job?",   "options": ["no", "yes"]},
    {"key": "human_available", "question": "Is a person available to approve the risky action before it runs?",   "options": ["yes", "no"]},
    {"key": "latency_ok",      "question": "Can this action be slower, to add a check before it runs?",           "options": ["yes", "no"]}
  ]
}

Then pass the answers to tainted_fix as {question_key: choice}:

{"repo_path": "/path/to/app", "finding_id": "c72a692b4e3bc491",
 "answers": {"needs_both": "yes", "human_available": "no", "latency_ok": "yes"}}

The answers pick the repair:

Answers Repair
needs_both: no scope split — split the two tools into separate agents
needs_both: yes, human_available: yes mediation — a person or a policy gate approves the risky action
needs_both: yes, human_available: no, latency_ok: yes sink confirmation — confirm each risky call before it runs
needs_both: yes, human_available: no, latency_ok: no provenance — track where the data came from, and block it at the risky call

Two shortcuts:

  • For every other check the code decides the fix. tainted_fix_interview returns an empty interview and a note saying to call tainted_fix directly.
  • You can skip the interview call. If tainted_fix needs answers it does not have, it returns the same questions under interview alongside an error.

prove only fires at a target the human named, and only if that target is verified or local — never a URL that arrived through another tool's output. Its live HTTP calls are also constrained by the engine's plan-commitment self-defense: prove commits to its list of probes up front, and any request outside that list is refused (blocked_calls lists what got refused).

Install

pip install tainted-mcp            # the engine comes with it, pinned to this version
export GEMINI_API_KEY=...          # optional
tainted-mcp                        # runs a stdio MCP server

Add pip install "tainted[dynamic]" && playwright install chromium for prove. From a checkout instead:

pip install -e . && pip install -e surfaces/mcp

Register it with an MCP client (for example Claude Desktop or Claude Code):

{
  "mcpServers": {
    "tainted": { "command": "tainted-mcp" }
  }
}

SSE and streamable-HTTP transports are available too, via server.run(transport=...) in server.py.

Release files for tainted-mcp 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tainted-mcp 0.1.1
File Size Uploaded
tainted_mcp-0.1.1.tar.gz 19.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for tainted-mcp 0.1.1
File Interpreter ABI Platform
tainted_mcp-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 35.5 kB

Release files / tainted_mcp-0.1.1.tar.gz

Download URL tainted_mcp-0.1.1.tar.gz
Size 19.6 kB
Tags Source
SHA-256 checksum
How to use checksums
677139e7e426f2300874e1c5e33866da7f1c7906f2ae5fd1de34c1e3ca051f2b
BLAKE2b-256 checksum
How to use checksums
707fcadfdf812b306b7d894149cd98ae44e4c5d49448c85c638a2e33ec6d3b77
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.

Transparency log

Release files / tainted_mcp-0.1.1-py3-none-any.whl

Download URL tainted_mcp-0.1.1-py3-none-any.whl
Size 16.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
eb46049603af4532a176c27a632f46c1344a1aa9e0872a47bb13a10cdb0a888e
BLAKE2b-256 checksum
How to use checksums
669f8599968a5ee313e37d2fc994f2f383e04869ed591f557978226aa695729c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page