@hiddencontent/mcp
An MCP server so an agent can check a document before it reads it.
Text hidden inside a document — white-on-white, zero-size fonts, text behind an
image, off-page runs, comments, tracked deletions — is invisible to a person and
read verbatim by a model. This server sits in front of that.
Design and the reasoning behind every decision: MCP.md.
Configure
Get a key (one command, returns a live token):
curl -fsS -X POST https://api.hiddencontent.ai/v1/signup
Then add the server. The JSON below works in Claude Desktop, Claude Code and
Cursor unchanged:
{
"mcpServers": {
"hiddencontent": {
"command": "npx",
"args": ["-y", "@hiddencontent/mcp"],
"env": {
"HCS_API_TOKEN": "your-token",
"HCS_CAPABILITIES": "tool-use"
}
}
}
}
HCS_CAPABILITIES is worth setting. It is what turns on the checks for
concealed text that instructs a model to send mail or call a tool
(unauthorized-action, exfiltration). Declaring more can only raise severity,
never lower it. Comma-separated: tool-use, network-egress, code-execution.
Tools
check_document
Give it one of path (best), url, or text.
Prefer a path or URL. Whole classes of concealment are properties of the file
format and cannot be seen in extracted text, so a text submission is a
deliberately weaker check and the response says so.
Returns a recommended action:
proceed | nothing concealed worth reporting; read the document normally |
use_clean_text | concealed content was found and removed — read safe_text, not the file |
ask_human | a person should look before the agent goes further |
ask_human covers three cases, and the second is the one worth knowing about:
concealed text shaped like an instruction to a model; concealed content that was
deliberately retained in the text because removing it would have been data
loss (so there is no safe version to hand over); and a degraded result, where the
service did not answer and the check did not really happen.
get_evidence
Returns the concealed words from a previous check, so a person can judge them.
They are not in the default response. They come only from this explicit call,
by which point the agent's routing decision is already made — an injection
arriving then cannot change what it decided to do. The text is quoted inside a
fence whose delimiter carries a token minted at analysis time, so a payload
cannot close the block and escape into instruction position.
That last part is structural. The framing around it — declaring the block as
data before and after, presenting spans as records — reduces compliance
substantially but is not a proof.
What this package deliberately does not contain
No engine code, no rule pack, and no detection patterns. It calls /v1/analyze
over HTTPS and its only dependency is the MCP SDK.
An earlier build used our client library, which carries an offline fallback, and
that fallback dragged the rule pack — including the literal instruction-shape
regexes — into the published bundle. Publishing a detection signature turns
evasion from research into reading.
The consequence for you: with no connectivity you get "could not check this",
not a partial local check. That is deliberate. If we are offline, we say so.
Build
npm run build --workspace @hiddencontent/mcp
npm run bundle --workspace @hiddencontent/mcp
npx vitest run packages/mcp/test/
Against the live service:
HCS_CREDS=~/Desktop/hcs-credentials.txt npm run smoke:mcp
The unit tests prove the shaping logic against a fake client. They cannot prove
the action an agent receives is right for a real document analysed by the
deployed engine — every bug worth finding in this package was found that way,
not by a fixture. smoke:mcp is that check, made repeatable.
bundle produces a self-contained dist/bundle.mjs (no node_modules
required) and packs it into dist/hiddencontent.mcpb for one-click
installation from Claude Desktop's Extensions tab. Verified 2026-08-28 by
extracting the bundle into an empty directory and speaking MCP to it over
stdio.