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
evidenceand 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=payloadis 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.
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.