Reference: API quickstart
An agent that routes intake asks a narrower question than a fraud reviewer: is this untrusted file worth a full inspection at all, and what will looking cost me? Engines 1.34.0 and 1.35.0 exist for that caller. The first makes the engine's measured I/O cost an output and adds a cheap triage scope; the second lets the MCP tools take a URL, fetched on the caller's own machine. Per the house rule, the triage was measured before it shipped, on 5,168 documents. The URL fetch got something better: an adversarial review that found two real holes.
The cost becomes an output
The parser has always spent decompression against a per-document budget; now
it keeps the receipt. Every report carries
summary.cost: physical bytes read, decompressed bytes produced,
and the ratio between them. Both counts are deterministic, which is what lets
them ride in a body that is otherwise byte-identical for identical input.
Wall-clock time is not deterministic, so it stays out of the default body and
appears only when asked for with ?timing=1. Byte counters in,
wall-clock out: the determinism claim survives the feature.
Triage: the one expensive pass, skipped and disclosed
?mode=triage on /api/v1/inspect, or the
triage_document MCP tool, reads a PDF for structure, metadata,
signatures and presence flags, and skips the per-page content walk: the glyph
geometry, the text runs, the arithmetic, the redaction paint order. The
measurement of what that trade costs and buys:
| measured over 5,168 documents | result |
|---|---|
| decompressed output, triage as a share of full | 9.18% |
| median per-document ratio | 0.0000 |
| routes the same as full (both clean, or both flagged) | 90.29% |
| of full-flagged documents, triage also flags | 70.90% |
| false clean: full flags, triage says clean | 502 (9.71%) |
The half of that table worth trusting is the structural half:
triage reproduces every structural, metadata and signature family
count-for-count. 1,105 incremental-updates firings at
medium or above under a full parse, 1,105 under triage. 851
id-inconsistency, 851. 151 signature-coverage, 151.
For the tier that answers "was this file edited, appended to, re-issued, or
signed", the cheap scope is not an approximation of the full one. It is the
full one, at 9% of the decompression.
The other half is a blind spot, and it is by construction, not by degree.
All 502 false cleans come from exactly three families:
hybrid-page (290), redaction-exposure (221) and
embedded-image-anomalies (23). Every one is a page-content
family, produced by the walk triage skips. Not a single false clean comes
from a structural, metadata or signature family.
Which is why the report does not leave the gap to be inferred from silence. A
triage report carries a triage block naming what did not run and
saying in words that a quiet triage is not a clean document; it is a document
whose cheap tells were absent. It is the same rule that stops a parse-failed
report from reading as reassurance, applied to a scope instead of a failure.
The URL fetch that deliberately is not an API feature
As of 1.35.0 the MCP tools accept an http or https URL instead of a local
path: inspect_document, check_redaction, and
compare_documents as originalUrl and
candidateUrl. The REST API deliberately did not grow the same
feature, because a server-side fetch of a caller-supplied URL is an SSRF
primitive, and the production box has neighbours that listen only on the
internal network. The MCP server runs on the caller's machine, so the fetch
leaves from there and reaches only what the caller can already reach.
That moves the risk. It does not zero it, because the caller's machine has a
localhost too, and an agent an attacker is steering is exactly
the kind of caller this server expects. So the fetch shipped guarded: http and
https only, re-checked at every redirect hop, a cap of five redirects, a 30
second timeout, and the API's own 10 MB limit enforced on the bytes as they
arrive, because Content-Length can lie.
Then an adversarial review of the new surface found the two holes worth a
post. First: the scheme check never looked at the destination
address, so http://127.0.0.1 and
http://169.254.169.254, the cloud metadata endpoint, sailed
through as perfectly valid http. The fetch now resolves the hostname, checks
the address against the non-routable ranges (loopback, link-local, private,
CGNAT, reserved, and their IPv4-mapped IPv6 forms), and then
pins the connection to the validated address, which closes
the rebinding hole where a hostname resolves public for the check and private
for the connect. Second: compare_documents was returning its
report raw, while every other tool elides attacker-authored strings. A prompt
injection planted in a /Title, which
inspect_document detects and elides, rode back out through
compare wearing a trusted label. It now passes through the same
instruction-shaped-string criterion the injection families use, and the
equality verdicts survive with the payloads replaced.
The path argument reads any local file the process can read and
sends its bytes to the configured deployment. That was always true, and it
is now documented instead of discoverable: run the server only with agents
and inputs you trust. triage_document stays path-only.
The numbers that will not travel
The 9.18% cost ratio is a property of the parser and should hold anywhere. The 70.90% routing recall is a property of this corpus, whose medium-plus base rate of 33% is far above a random-web population because it is deliberately weighted toward banking, contracts and templates. A caller whose fraud is mostly page-content tampering will see triage miss more; a caller whose fraud is mostly structural will see it miss almost nothing. That is precisely why triage is documented as a pre-filter and never as a verdict.
One call before the bytes reach your model
If your agent opens attachments, the useful order is inspect, then read:
triage before ingestion, a full inspect before trusting, and the injection
families checked before the document's text enters a context window. The
quickstart has the anonymous curl; the MCP server is
npx tamperlens-mcp, version-pinned to the engine.
The API quickstart: an anonymous call returns the full report, 10 documents an hour per IP, no account. Nothing is stored, the parse is in memory, and identical bytes return an identical report, which means you can regression-test everything this post claims.