# @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`](../../MCP.md).

## Configure

Get a key (one command, returns a live token):

```bash
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:

```json
{
  "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:

| action | meaning |
|---|---|
| `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

```bash
npm run build  --workspace @hiddencontent/mcp   # tsc
npm run bundle --workspace @hiddencontent/mcp   # single-file bundle + dist/hiddencontent.mcpb
npx vitest run packages/mcp/test/
```

Against the live service:

```bash
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.
