{
  "markdown": "<!-- mcp-name: io.github.Rinava/umbryn-mcp -->\n\n# umbryn-mcp\n\n**An MCP server that redacts PII/PHI from text before it ever reaches an LLM — self-hosted, fail-closed, and HIPAA-aware.**\n\n[![PyPI version](https://img.shields.io/pypi/v/umbryn-mcp.svg?v=2)](https://pypi.org/project/umbryn-mcp/) \n[![Tests](https://img.shields.io/github/actions/workflow/status/Rinava/umbryn-mcp/ci.yml?branch=main&label=tests)](https://github.com/Rinava/umbryn-mcp/actions/workflows/ci.yml)\n[![Python versions](https://img.shields.io/pypi/pyversions/umbryn-mcp.svg?v=2)](https://pypi.org/project/umbryn-mcp/)\n[![License: MIT](https://img.shields.io/github/license/Rinava/umbryn-mcp?v=2)](LICENSE)\n[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)\n[![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)\n\nTeams building LLM and agent pipelines in regulated domains have no clean, drop-in way to strip PHI/PII from a payload *before* it crosses into a model provider's infrastructure. `umbryn-mcp` is that boundary: three MCP tools — `redact`, `restore`, `detect` — that scrub sensitive values into reversible placeholders, run entirely inside infrastructure you control, and **block the request if detection is uncertain instead of leaking data**.\n\n```text\nredact(\"Patient MRN: 1234567, provider NPI 1234567893, ssn 078-05-1120, john.doe@example.com\")\n\n  redacted_text  (safe to send to the model):\n    \"Patient MRN: [MEDICAL_RECORD_NUMBER_1], provider NPI [NPI_1], ssn [US_SSN_1], [EMAIL_ADDRESS_1]\"\n\n  token_map      (kept local, never sent to the model):\n    [MEDICAL_RECORD_NUMBER_1] → 1234567\n    [NPI_1]                   → 1234567893\n    [US_SSN_1]                → 078-05-1120\n    [EMAIL_ADDRESS_1]         → john.doe@example.com\n```\n\nSend the redacted text to the model; keep the `token_map` local; call `restore` afterward to rehydrate the result. Round-trips are **byte-exact** and proven with property-based tests.\n\n---\n\n## Why this exists\n\nThe PHI/PII-redaction MCP niche is real but underserved — the existing options are thin Presidio wrappers with no HIPAA-specific detection and, critically, **no guarantee that a detection failure blocks the request** instead of silently passing raw data through. So teams either roll their own boundary or ship sensitive data to a provider and lean on a BAA to cover it — the design-time mistake that causes real compliance incidents.\n\n| | Naive Presidio wrapper | Regex-in-your-app | Cloud DLP API | **umbryn-mcp** |\n|---|:---:|:---:|:---:|:---:|\n| Drop-in MCP tools | sometimes | ❌ | ❌ | ✅ |\n| **Fail-closed** on uncertain detection | ❌ | ❌ | ❌ | ✅ |\n| HIPAA identifiers (NPI, DEA, MBI, MRN, CLIA) | ❌ | partial | partial | ✅ |\n| Reversible (restore original) | rarely | DIY | some | ✅ |\n| Runs self-hosted, **zero egress** | ✅ | ✅ | ❌ (sends data out) | ✅ |\n| Works with **zero heavy deps** | ❌ (needs spaCy) | ✅ | n/a | ✅ (regex engine) |\n| Optional ML NER (names, addresses) | ✅ | ❌ | ✅ | ✅ (`[presidio]` extra) |\n\n**Why it was built:** MCP went mainstream fast — it's now first-class in Claude, Cursor, and ChatGPT, across thousands of servers — but the PHI/PII-redaction corner was left to a few unmaintained wrappers. This fills that gap with a single honest, auditable, fail-closed boundary, kept open source so the redaction logic you depend on is fully inspectable rather than a black box.\n\n## Features\n\n- **Three tools, one boundary** — `redact` (→ scrubbed text + reversible token map), `restore` (→ original), `detect` (→ entities found, no mutation).\n- **Fail-closed by construction** — if detection errors *or any detection lands below the confidence threshold*, the call returns a typed error. Uncertainty blocks; it never redacts-what-it-can and passes the rest.\n- **HIPAA-aware detection** — checksum-validated NPI and DEA, position-typed Medicare MBI, context-anchored MRN, CLIA lab IDs, plus standard PII (email, phone, SSN, credit card, IBAN, IP, URL).\n- **Zero-egress, self-hosted** — the default engine is pure regex + checksums with **no network calls and no heavy dependencies**. It installs anywhere Python does.\n- **Optional ML upgrade** — `pip install \"umbryn-mcp[presidio]\"` adds Microsoft Presidio + spaCy for `PERSON`/`LOCATION` NER, transparently.\n- **Reversible & deterministic** — collision-proof typed placeholders make `restore(redact(x)) == x` for *arbitrary* input; same input + config always yields the same output.\n\n## When to use it (and when not to)\n\n**Reach for `umbryn-mcp` when:**\n\n- You send healthcare, clinical, financial, or user-generated text to a **third-party LLM API** and need PHI/PII kept out of that provider's infrastructure and logs.\n- You're building an **agent or MCP pipeline** in a regulated domain and want a drop-in scrubbing boundary you wire in with one tool call.\n- You need **reversible** redaction so downstream steps still work: `redact` → send to model → `restore`.\n- You want a **self-hosted, no-egress** detector you can audit line by line.\n- You need **HIPAA-specific identifiers** (NPI, DEA, Medicare MBI, MRN, CLIA), not just names and emails.\n\n**Reach for something else when:**\n\n- You need **irreversible** de-identification / anonymization (tokenization, k-anonymity) — redaction here is reversible by design.\n- You need to redact **non-text** data (images, audio, PDFs, database rows) — scope is text.\n- You want a **certified compliance** product — this is one technical control, not a compliance program (see [Scope & honest limitations](#scope--honest-limitations)).\n- You want a **transparent proxy** that auto-scrubs everything in the request path — v1 is explicit tool calls; proxy mode is on the [roadmap](ROADMAP.md).\n- You require **guaranteed 100% recall** — no detector, this one included, can promise that.\n\n## Quickstart (< 60 seconds)\n\n```bash\npip install umbryn-mcp        # zero heavy deps; runs immediately\n```\n\nThen register it with your MCP client.\n\n**Claude Desktop / Claude Code** (`claude_desktop_config.json`, or `claude mcp add umbryn-mcp -- umbryn-mcp`):\n\n```json\n{\n  \"mcpServers\": {\n    \"umbryn-mcp\": {\n      \"command\": \"umbryn-mcp\"\n    }\n  }\n}\n```\n\n**Cursor** (`.cursor/mcp.json`) and **VS Code** use the same shape — see [`examples/`](examples/) for ready-to-paste configs.\n\nWant name/address detection too?\n\n```bash\npip install \"umbryn-mcp[presidio]\"\npython -m spacy download en_core_web_lg\n```\n\nThe server auto-detects Presidio and upgrades — no config change needed. (Set `UMBRYN_ENGINE=regex` to force the dependency-free engine, or `=presidio` to require the ML one.)\n\n## How it works\n\nA tool call comes in over stdio; the `Redactor` core runs the configured detection engine, resolves overlaps deterministically, applies the fail-closed threshold check, and swaps detected spans for reversible typed placeholders. Only scrubbed text is meant to leave the boundary you run.\n\n```mermaid\nflowchart LR\n    A[MCP client<br/>Claude · Cursor · agent] -- redact / restore / detect --> B[umbryn-mcp<br/>stdio server]\n    B --> C[Redactor core<br/>fail-closed · reversible]\n    C --> D{Detection engine}\n    D -->|default, zero deps| E[Regex + checksums]\n    D -->|optional| F[Presidio + spaCy NER]\n    C -. scrubbed text .-> A\n    A -- scrubbed text only --> G[(LLM / downstream)]\n```\n\nThe `Redactor` core depends only on a small `DetectionEngine` interface — never on Presidio or MCP directly. Raw data and the detection engine stay inside the boundary you run; only scrubbed text leaves it. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) and [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md).\n\n## The tools\n\n### `redact(text) → { redacted_text, token_map, entities }`\nReplaces detected PHI/PII with typed placeholders like `[NPI_1]`. `token_map` maps each placeholder back to its original value — **keep it local; never send it to the model.** `entities` lists what was redacted (type/span/score) for auditing.\n\n### `restore(redacted_text, token_map) → { text }`\nReverses a redaction, recovering the original text exactly. Safe to call on model output that still contains the placeholders.\n\n### `detect(text) → { entities, count }`\nReports the entities found — type, span, confidence — **without** modifying the text. Unlike `redact`, it surfaces low-confidence hits rather than blocking, so you can inspect coverage before trusting the boundary in a pipeline.\n\n## How to use it (a real pipeline)\n\nThe pattern is **redact → model → restore**, with the token map never leaving your side:\n\n1. **Scrub before the model.** Call `redact(user_text)`. Send only `redacted_text` to the LLM. Keep `token_map` in your process — treat it as sensitively as the raw input, and never pass it to the model.\n2. **Let the model work on placeholders.** It sees `[NPI_1]`, `[US_SSN_1]`, etc. — semantically neutral tokens it can reason about and echo back.\n3. **Rehydrate after.** Call `restore(model_output, token_map)` to swap the real values back into the model's response before it reaches your user or database.\n4. **Handle the block.** If `redact` returns a `[LOW_CONFIDENCE]` or `[DETECTION_ERROR]` tool error, the boundary refused to leak — surface it, tighten input, or lower the risk, but don't send the raw text onward.\n\nBefore trusting it in a pipeline, call `detect(sample_text)` on representative (synthetic) data to see exactly what is and isn't caught, and tune the thresholds (below) to your risk tolerance.\n\n## Fail-closed, precisely\n\nTwo thresholds govern every `redact` call:\n\n- **`detection_floor`** (default `0.35`) — the sensitivity boundary. Signals below it are treated as noise.\n- **`min_confidence`** (default `0.5`) — the *trust* threshold.\n\nAny candidate that survives the floor but scores **below `min_confidence`** puts the call into fail-closed mode: it returns a `[LOW_CONFIDENCE]` error rather than redacting the confident spans and passing the uncertain one through. Engine errors return `[DETECTION_ERROR]`. **On any error, no redacted text is returned.** Both thresholds are configurable (see below).\n\n## Configuration\n\nAll optional; sane defaults mean it runs with zero config. Set via the client's `env` block.\n\n| Variable | Default | Meaning |\n|---|---|---|\n| `UMBRYN_ENGINE` | `auto` | `auto` (Presidio if installed, else regex), `regex`, or `presidio` |\n| `UMBRYN_MIN_CONFIDENCE` | `0.5` | Trust threshold; detections below it fail closed |\n| `UMBRYN_DETECTION_FLOOR` | `0.35` | Below this, a signal is treated as noise |\n| `UMBRYN_MAX_INPUT_CHARS` | `100000` | Reject larger input with a typed error |\n| `UMBRYN_SPACY_MODEL` | `en_core_web_lg` | spaCy model for the Presidio engine |\n| `UMBRYN_AUDIT_LOG` | `false` | Emit a structured audit record per `redact` call (counts and types only) |\n| `UMBRYN_CONFIG` | *(unset)* | Path to a JSON config file (below) |\n\n### Config file\n\nFor settings that don't fit a flat environment variable, point `UMBRYN_CONFIG` at a JSON file. Environment variables still win over the file for the scalar values above, so you can ship one file and tweak per launch. A malformed file (bad JSON, unknown threshold, un-compilable regex) fails **closed** at startup rather than degrading silently.\n\n```jsonc\n{\n  // Per-entity trust thresholds override min_confidence for that type.\n  \"entity_thresholds\": { \"PHONE_NUMBER\": 0.7, \"IP_ADDRESS\": 0.9 },\n\n  // Entity types to drop entirely — never detected, never redacted.\n  // (A privacy trade-off you're opting into: a disabled type can leak.)\n  \"disabled_entities\": [\"URL\"],\n\n  // Your own recognizers, no fork required. `validator` names a built-in\n  // check-digit function (luhn, npi, dea, iban, nhs) — config supplies data,\n  // never code.\n  \"recognizers\": [\n    {\n      \"entity_type\": \"EMPLOYEE_ID\",\n      \"regex\": \"\\\\bEMP-\\\\d{6}\\\\b\",\n      \"base_score\": 0.85,\n      \"context\": [\"employee\", \"badge\"],\n      \"context_required\": false\n    }\n  ],\n\n  \"audit_log\": true\n}\n```\n\nA ready-to-copy example lives at [`examples/umbryn_config.json`](examples/umbryn_config.json).\n\n## Entity coverage\n\n| Entity | Regex engine (default) | Presidio engine (`[presidio]`) |\n|---|:---:|:---:|\n| Email, Phone, SSN, Credit card, IP, URL | ✅ | ✅ |\n| **NPI** (Luhn + 80840 check digit) | ✅ | ✅ |\n| **DEA** (check digit) | ✅ | ✅ |\n| **Medicare MBI** (position-typed) | ✅ | ✅ |\n| **MRN** (context-anchored) | ✅ | ✅ |\n| **Medicare HICN** (SSN + beneficiary code) | ✅ | ✅ |\n| **CLIA** lab number | ✅ | ✅ |\n| **US ITIN** (9XX-range structure) | ✅ | ✅ |\n| **UK NHS number** (mod-11 check) | ✅ | ✅ |\n| **Canadian SIN** (Luhn check) | ✅ | ✅ |\n| **US driver's license** (context-anchored) | ✅ | ✅ |\n| **IBAN** (mod-97 / ISO 7064 check) | ✅ | ✅ |\n| **Person names** | ❌ | ✅ (spaCy NER) |\n| **Addresses / locations** | ❌ | ✅ (spaCy NER) |\n| **Custom recognizers** (your regex + check digit, via config) | ✅ | ✅ |\n\n## Benchmark\n\nDetection quality is **measured, not asserted**. The numbers below are the default (zero-dependency) engine scored against the [synthetic eval corpus](eval/) — 200 generated documents, ~1,800 labeled spans, with checksum-failing look-alikes woven in as distractors to keep precision honest. Reproduce them with `python eval/run_eval.py --markdown`.\n\n| Entity | Precision | Recall | F1 | TP | FP | FN |\n|---|--:|--:|--:|--:|--:|--:|\n| `CANADA_SIN` | 1.00 | 1.00 | 1.00 | 87 | 0 | 0 |\n| `CLIA_NUMBER` \\* | 1.00 | 1.00 | 1.00 | 105 | 0 | 0 |\n| `CREDIT_CARD` | 1.00 | 1.00 | 1.00 | 72 | 0 | 0 |\n| `DEA_NUMBER` \\* | 1.00 | 1.00 | 1.00 | 119 | 0 | 0 |\n| `EMAIL_ADDRESS` | 1.00 | 1.00 | 1.00 | 144 | 0 | 0 |\n| `IBAN_CODE` | 1.00 | 1.00 | 1.00 | 87 | 0 | 0 |\n| `IP_ADDRESS` | 1.00 | 1.00 | 1.00 | 62 | 0 | 0 |\n| `MEDICAL_RECORD_NUMBER` \\* | 1.00 | 1.00 | 1.00 | 200 | 0 | 0 |\n| `MEDICARE_BENEFICIARY_ID` \\* | 1.00 | 1.00 | 1.00 | 126 | 0 | 0 |\n| `MEDICARE_HICN` \\* | 1.00 | 1.00 | 1.00 | 78 | 0 | 0 |\n| `NPI` \\* | 0.94 | 1.00 | 0.97 | 200 | 12 | 0 |\n| `PHONE_NUMBER` | 1.00 | 1.00 | 1.00 | 144 | 0 | 0 |\n| `UK_NHS_NUMBER` | 1.00 | 1.00 | 1.00 | 95 | 0 | 0 |\n| `US_DRIVERS_LICENSE` \\* | 1.00 | 1.00 | 1.00 | 81 | 0 | 0 |\n| `US_ITIN` | 1.00 | 1.00 | 1.00 | 97 | 0 | 0 |\n| `US_SSN` \\* | 1.00 | 1.00 | 1.00 | 136 | 0 | 0 |\n\n`\\*` = HIPAA-relevant identifier, subject to the CI quality gate. **Aggregate over the gated set: precision 0.99, recall 1.00.** The gate fails the build if recall drops below 0.90 or precision below 0.80. (`NPI`'s 12 false positives are look-alike 10-digit numbers that happen to pass the Luhn/80840 check digit — a deliberate, fail-safe bias toward over-redaction.)\n\nThese are synthetic, best-case conditions with clean formatting and nearby context words; **real-world text is messier**. Treat this as a regression guardrail and a sanity check, not a guarantee — always evaluate on your own representative data.\n\n## Scope & honest limitations\n\n**This tool reduces PHI/PII exposure at one boundary. It does not make a system \"HIPAA compliant.\"** Compliance is a property of an entire system and organization — its policies, contracts, access controls, audit posture, and people — not of any single library. Running `umbryn-mcp` can be *part* of a compliant design, but it is not a certification, a guarantee, or a substitute for a Business Associate Agreement, a risk assessment, or legal counsel.\n\nConcretely, this project **does not**: guarantee 100% detection (no detector does), de-identify beyond reversible redaction, cover non-text data, or act as a transparent proxy in v1 (redaction is via explicit tool calls you wire in). No detector is perfect — evaluate on your own representative data before relying on it. See [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md) for the full boundary, assumptions, and residual risks, and [SECURITY.md](SECURITY.md) to report issues.\n\n## How to contribute\n\nContributions are very welcome — this is a deliberately friendly place to make your first open-source PR, and the maintainer tries to respond quickly.\n\n**The easiest high-value contribution:** add a detection recognizer for a new identifier (a regex + an optional check-digit validator + a test). The [add-a-recognizer issue form](https://github.com/Rinava/umbryn-mcp/issues/new?template=add_recognizer.yml) doubles as the spec, and [CONTRIBUTING.md](CONTRIBUTING.md#how-to-add-a-recognizer-the-classic-good-first-issue) walks through the six steps.\n\nOther good ways to help: improve docs, add test cases or example client configs, or pick up something from the [roadmap](ROADMAP.md). Browse [good first issues](https://github.com/Rinava/umbryn-mcp/contribute) or open an issue to propose something.\n\n```bash\ngit clone https://github.com/Rinava/umbryn-mcp && cd umbryn-mcp\npip install -e \".[dev]\"\npytest                 # fast invariant suite (Presidio faked, sub-second)\nruff check . && mypy src/umbryn_mcp\npython eval/run_eval.py\n```\n\nThe full guide — dev setup, conventions, and the **no-real-PHI rule for fixtures** — is in [CONTRIBUTING.md](CONTRIBUTING.md). By contributing you agree your work is MIT-licensed.\n\n## License\n\n[MIT](LICENSE) — matches Presidio and maximizes reuse. Built with [Microsoft Presidio](https://github.com/microsoft/presidio) (optional) and the [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk).\n\numbryn-mcp is built by the team behind [Kenda](https://www.kenda.app).\n",
  "bytes": 17311,
  "sha": "59400777084b74c058ffb34e9949c28774c56bf944ef65c13e9828a74ca46b99",
  "repo_slug": "rinava/umbryn-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_rinava_umbryn_mcp_60f540a8/readme"
}