The report your agent reads is part of the attack surface

A tool that inspects hostile documents and answers into a model's context has one rule above all others: describe what you found without repeating it.

Reference: Developer docs

The Tamperlens MCP server is deliberately boring. MCP is the Model Context Protocol, the standard way an AI agent plugs into an outside tool. Ours is a thin wrapper over the same REST API everyone else calls. It takes a file path or a URL, so the document's bytes go to the engine and never through the model's context. There is no second engine in it and nothing to keep in step. The interesting part runs the other way. Every tool result is text a model will read and act on. Some of the documents it describes were written to attack exactly that reader.

The echo-back contract

Engine 1.13.5 added the injection families: text placed in a document so that a language model reads it and a person does not. Info-dictionary values and XMP text nodes, which are the two places a PDF keeps its title, author and keywords. Annotation contents, embedded-file names, Office docProps, Word comments, hidden runs. The false-positive argument for the whole phase fits in one sentence: nothing legitimate writes an instruction to a language model into /Keywords.

Detection was the easy half. The hard rule is what the finding is allowed to say. A family that pasted the recovered payload into its own prose would ship the injection straight to the model, through Tamperlens's own tool, with a trusted label on it. So, the contract:

  • the payload lives in evidence and nowhere else: not in the title, not in the detail prose, not in message parameters. Prose names the kind of field from a closed set, because a custom Info key is a string the attacker chose too;
  • it is truncated to 200 characters and stripped of control, bidi-override and zero-width characters on the way in;
  • every such signal carries evidence.payloadIsUntrusted: true;
  • the MCP server elides it unconditionally. The tool result reports cue categories and counts instead. POST /api/v1/inspect?redact=payload is the same elision over REST, for an integrator who wants the finding and never the bytes.

The classifier behind it is a versioned lexicon, a fixed word list rather than a model, because the product's contract is that identical bytes produce an identical report. It needs two independent cue categories to fire, or one from a short list with no benign reading.

The fixture that earned its place is a PDF of an academic paper about prompt injection. Its title, keywords, abstract and a reviewer's annotation all use the vocabulary, and the engine stays silent. The first draft of the lexicon flagged it, on the phrase "system prompts" in the keyword list. That is how the two-category rule came to exist.

The contract held. The routes around it were the bugs.

Two adversarial reviews later, the elision itself has never been the finding. What the reviews found, twice, is report surfaces the contract did not yet cover.

Compare bodies, closed in 1.35.0. compare_documents returned its report raw. A compare body carries the raw values of /Title, /Creator, /Producer and the XMP fields. So an injection planted in /Title rode back out through compare with a trusted label, even though inspect_document detects and elides it. The fix applies the same classifier the injection families use, replacing any instruction-shaped metadata string with [redacted] and leaving the equality verdicts intact. The regression test drives a real ephemeral origin and proves the attack is blocked.

Recovered redaction text, closed in 1.36.0. redaction-exposure recovers text hidden under a drawn rectangle, and recovering it is the family's whole value: a lawyer needs the leaked name. But the recovered text was not flagged untrusted, so inspect_document, sold as a "safe for a model to read?" pre-check, echoed it verbatim. Hide your injection under a black box and the tool would read it out. The fix is a second lock, on the same classifier. It elides only instruction-shaped recovered text. A benign redaction failure, a name or a number, still comes back, so check_redaction keeps its value.

What this tool refuses to say

There is no "safe for AI" badge and there will not be one. The tool description says to call it before a document reaches your context. The report's usageCaution field is a constant: safeForAutomatedAdverseDecision: false. That goes on every report, clean ones included, because absence of signals is not proof of authenticity. A certification would be a liability the first time a pipeline is compromised by something not caught. It would be wrong even before then.

The registry listing is hygiene, and priced accordingly

tamperlens-mcp is on npm and in the official MCP Registry as com.tamperlens/mcp, verified by a DNS TXT record on the apex. The honest framing comes straight from the internal review that approved it. Registries are disconfirmed as a growth channel. The listing exists to remove a procurement objection, not to acquire anyone. It cost a day. The version is pinned to the engine version by a test, so a published package can never describe an engine other than the one answering.

The same discipline applies to the server's own risks, stated in its header rather than discovered by users. The path argument reads any local file the process can read, so the server is for trusted agents and trusted inputs. TAMPERLENS_ALLOWED_DIRS exists for operators who want reads confined to one intake directory. The URL fetch carries its own guards against server-side request forgery, which an earlier post covers in full.

Point an agent at it

The server installs with npx tamperlens-mcp and works without a key on the anonymous allowance. Feed it a document you do not trust and read what comes back. Signals, evidence, counts, and nowhere in it the text that was aimed at your model.

The developer docs carry the MCP setup, the REST quickstart and the ?redact=payload flag. The first ten documents an hour need no account at all.