Content
# Confidentiality MCP
An MCP server that keeps sensitive names, codenames, and identifiers out
of an agent's output.
You define what's protected in a YAML file; the
server exposes tools that redact protected entities from files the agent
reads and check the agent's output for leaks before it's returned.
## Philosophy
Working in the world of enterprise tech, I've found that navigating legal
agreements and sensitive information in the context of AI development can
be tricky. I believe that in the future, we will see more concrete
standards and tools for controlling information flow between parties and
their associated agents. This repo is an actualization of one component
of this philosophy foremost as an MCP, along with a pre-commit hook.
Sometimes you want your agent to work with client-sensitive or internal
information, but be sure that this doesn't bleed into the output. This is
useful for writing documentation or public-facing documents where internal
context is helpful, especially in larger interconnected organizations.
We can categorize how three main parties — the agent, the developer, and
the end-user / reader — interact with a document:
| Case | Agent | Dev | Reader | Name | How it's enforced | Example |
|------|:-----:|:---:|:------:|------|-------------------|---------|
| 1 | ✓ | ✓ | ✓ | Public | Nothing needed | Docs from a public spec |
| 2 | ✓ | ✓ | ✗ | Working context | Agent sees the real info but its output is scrubbed before it's returned | Case study: client's real name in context, published as "a Fortune-500 client" |
| 3 | ✗ | ✓ | ✗ | Secret-blind | Agent only ever sees a placeholder; the real value is swapped back in outside the agent | Runbook using `$DB_PASSWORD`; dev holds real creds |
| 4 | ✓ | ✗ | ✗ | Dev-blind context | The data owner (not the dev) controls access; the server holds the data so the dev never sees raw context | Compliance agent reads salaries, drafts anonymized report |
| 5 | ✗ | ✓ | ✓ | Internal-only routing | Sensitive part is added by a human/the server into an internal-only destination the agent never touches | Detail lands only in internal wiki; agent never sees it |
| 6 | ✗ | ✗ | ✓ | Transclusion | The server stamps the value straight into the finished file after the agent is done | Per-recipient license key in a generated contract |
| 7 | ✓ | ✗ | ✓ | User-private | Agent reads and writes for one user; the operator is contractually blinded (no logging/training) | Agent reads a user's medical data, reports back to that same user |
| 8 | ✗ | ✗ | ✗ | Degenerate | n/a | Data plays no role |
**This MCP targets cases 2 and 3.** `verify_output` / `verify_file` cover
case 2 (agent sees real info, output is scrubbed); `read_clean` covers
case 3 (agent only ever sees placeholders). Both live in one server so a
single config can span both workflows.
## Features
- **`read_clean`** — the agent reads a file with protected entities
replaced by placeholders; secrets never enter its context.
- **`verify_output`** / **`verify_file`** — scan text or a file for
protected entities before the agent sends its response.
- Case-insensitive matching with automatic variant expansion for common
code shapes (`client-one`, `client_one`, `ClientOne`, `clientone`).
- spaCy named-entity recognition catches related PERSON / ORG / GPE /
PRODUCT spans and filters them against a bundled + user-extensible
allowlist.
- Boot-time config validation.
- CLI (`confidentiality verify`) and git pre-commit hook for
non-agent workflows.
## Installation
Requires [uv](https://github.com/astral-sh/uv).
```bash
git clone https://github.com/Otto-Cline/confidentiality-mcp.git
cd confidentiality-mcp
uv sync
uv tool install .
```
`uv sync` sets up the dev environment (used by the MCP server). `uv tool
install .` puts the `confidentiality` CLI on your PATH.
## Configuration
Ready-made configs live in [`examples/configs/`](examples/configs/) — copy
one and adapt. Or create `configs/protected.yaml` from scratch:
```yaml
protected:
- entity: "Client One"
granularity: concept # identifier | concept
output_mode: anonymize # anonymize | omit
placeholder: "the client" # optional, used by read_clean
allowlist:
- lucille # names spaCy should ignore
on_violation: reprompt # reprompt | report
preverification: enabled # enabled | disabled
default_placeholder: "<<redacted>>"
nlp_model: en_core_web_sm # or en_core_web_lg
```
### Fields
| Field | Values | Effect |
|---|---|---|
| `protected[].entity` | string | The literal text to protect |
| `protected[].granularity` | `identifier` \| `concept` | `identifier` runs literal + variant matching; `concept` adds a spaCy NER pass for related entities |
| `protected[].output_mode` | `anonymize` \| `omit` | How the agent should rewrite a leak |
| `protected[].placeholder` | string | Read-time substitute for this entity |
| `allowlist[]` | list of strings | Names to exclude from NER flagging (merged with the built-in list) |
| `on_violation` | `reprompt` \| `report` | `reprompt`: agent silently rewrites and re-verifies. `report`: agent surfaces the violation list and stops. |
| `preverification` | `enabled` \| `disabled` | Whether the `read_clean` tool is registered |
| `default_placeholder` | string | Fallback for entries without `placeholder` |
| `nlp_model` | spaCy model name | Model for NER (`en_core_web_sm` ships with the package) |
## Connect to Claude Code
```bash
claude mcp add confidentiality-mcp -- \
uv --directory /absolute/path/to/confidentiality-mcp run \
python -m confidentiality.server \
--config /absolute/path/to/confidentiality-mcp/configs/protected.yaml
```
Start a session; `/mcp` shows `confidentiality-mcp` as connected.
## Tools
### `read_clean(path)`
Returns the file at `path` with every protected entity (and its variants)
replaced by the configured placeholder. Files with no matches are
returned unchanged.
### `verify_output(text)`
Scans `text` and returns:
```json
{
"violations": [{"entity": "...", "match": "...", "span": [start, end], ...}],
"suspicious_entities": [{"text": "...", "label": "PERSON", ...}],
"action": "reprompt"
}
```
`violations` are confirmed literal matches. `suspicious_entities` are
NER-flagged spans the agent should review (populated only when at least
one `concept` entry exists). `action` reflects the configured
`on_violation` mode.
### `verify_file(path)`
Same as `verify_output` but reads from disk; use for large drafts to
avoid sending the file body through the tool call. Adds a `path` key to
the response.
## CLI
```bash
confidentiality verify --config protected.yaml file.md
confidentiality verify --show-suspicious file.md
confidentiality verify --quiet file.md
```
Config resolution: `--config` → `$CONFIDENTIALITY_CONFIG` → `./protected.yaml`.
Exit codes: `0` clean, `1` violation, `2` config error.
## Pre-commit hook
```yaml
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: confidentiality-verify
name: Confidentiality leak check
entry: confidentiality verify --config /absolute/path/to/protected.yaml
language: system
files: '.+'
```
Then `pre-commit install`.
## Programmatic API
```python
from confidentiality import (
verify_text, verify_path,
obfuscate_text, obfuscate_path,
load_config,
)
```
## Examples
- [`examples/configs/`](examples/configs/) — starter configs for the two
main workflows (minimal, case-2 output scrubbing, case-3 read-time).
- [`examples/case-study/`](examples/case-study/) — a full walkthrough with
a fabricated engineering brief, a matching config, and a prompt to feed
Claude Code.
## Development
```bash
uv sync
uv run pytest
uv tool install --editable . # live source for CLI iteration
```
## Troubleshooting
- `confidentiality: command not found` — run `uv tool update-shell`.
- Server won't boot — run the same config through `confidentiality
verify --config <path> some-file` to see the lint errors.
- `nlp_model` mismatch — install other spaCy models with `uv run python
-m spacy download <model_name>`.