SafeHarbor MCP
by sanjeevk264
README.md
# SafeHarbor MCP
**A guarded Model Context Protocol server that lets AI agents analyze healthcare claims without ever touching PHI.**
Every health plan wants analysts to ask Claude "what's our lumbar MRI denial rate by plan?" Almost none can hand an agent a connection to a claims warehouse full of names, SSNs and clinical notes. SafeHarbor is the layer in between: a zero-dependency MCP server that exposes a de-identified mart and enforces privacy **inside the database engine**, not with SQL regexes an agent can talk its way around.
```
Claude / any MCP client
│ JSON-RPC over stdio
▼
┌──────────────────────────── SafeHarbor ────────────────────────────┐
│ query(sql, purpose) ──► read-only conn ──► authorizer ──► engine │
│ query_only tables, k-supp. │
│ functions, aggregates│
│ statements VM budget │
│ hash-chained audit log ◄── every call │
└─────────────────────────────────┬──────────────────────────────────┘
▼
de-identified mart (mart.db)
▲ build_mart(): pseudonymize, generalize, drop free text
raw warehouse (PHI) — never opened by the server
```
## Results
From [`reports/REDTEAM.md`](reports/REDTEAM.md), generated by `python -m safeharbor.redteam`:
| | |
|---|---|
| Attacks neutralized | **33 / 33** (PHI access, schema recon, sandbox escape, writes/DDL, function abuse, resource exhaustion, small-cell inference, CTE shadowing, prompt-injection follow-through) |
| Legitimate analyst queries answered | **14 / 14** (0 false positives) |
| PHI canary scan | **0 leaks**: 9,676 real identifiers from the raw warehouse checked against every byte of every allowed response |
| Median query latency | **~5 ms** |
| Known gaps | 3, run and documented in the report rather than hidden |
## How it works
**1. Physical separation, not just views.** `build_mart()` materializes de-identification views (pseudonymous `member_token`, 10-year age bands, 3-digit ZIP, service *month*, no names/SSN/DOB/address/contact/notes) into a separate file. The server only ever opens that file.
> **Why not guard views over the raw tables?** That was the first design. The red-team found the bypass: SQLite resolves a CTE before a view of the same name, so
> `WITH v_members AS (SELECT first_name, last_name, ssn FROM members) SELECT * FROM v_members`
> runs with the authorizer believing every read came from the approved view. View-level column policies can't distinguish the two. Case A32 keeps that attack in the suite as a regression test.
**2. Engine-level authorization.** SQLite's authorizer callback is consulted for every table read, column read, function call and statement type *while the query is compiled*. SafeHarbor allows reads of the three mart tables only (CTE references are recognized by their null database name; virtual tables like `pragma_table_info` and `dbstat` are not), an allow-list of functions, and `SELECT`. Everything else (PRAGMA, ATTACH, writes, DDL, transactions, `load_extension`, `group_concat`, window functions) is denied before a single row is read. The connection is also opened `mode=ro` with `PRAGMA query_only`.
**3. Small-cell suppression inside the engine.** `count`, `sum`, `total`, `avg`, `min` and `max` are replaced with implementations that return `NULL` when computed over fewer than *k* = 11 rows (the CMS cell-size threshold). Because it happens at the source, derived values are covered too: `count(*) + 0`, `sum(1)`, `HAVING count(*) < 11` probes and single-member averages all come back empty.
**4. Resource limits.** A VM-instruction budget and wall-clock deadline (via the progress handler) kill runaway recursion and cartesian joins; results are capped at 500 rows.
**5. Purpose + tamper-evident audit.** The `query` tool requires a stated purpose (HIPAA minimum-necessary). Every call (client name, purpose, SQL, normalized fingerprint, decision, reason, rows, suppressed cells, latency) is appended to a SHA-256 hash-chained JSONL log. Editing, deleting or reordering any line is detected by `python -m safeharbor.audit logs/audit.jsonl`.
**6. Prompt-injection hygiene.** One synthetic provider's `practice_name` contains an injected instruction to dump SSNs. Responses mark free-text columns as `untrusted_columns`, and the injected query is blocked regardless (A33): defense doesn't depend on the model behaving.
**Agent-friendly errors.** Denials explain themselves ("read of sqlite_master.type is not permitted; available tables: [...]", "query exceeded the compute budget; add filters or aggregate") so an agent can correct course instead of retrying blindly.
## Quick start
```bash
pip install -e ".[dev]"
python -m safeharbor.data # builds data/raw_warehouse.db and data/mart.db (synthetic, seeded)
python -m safeharbor.redteam # runs the suite, writes reports/REDTEAM.md
pytest -q
```
### Use it from Claude Desktop or Claude Code
```json
{
"mcpServers": {
"safeharbor": {
"command": "python",
"args": ["-m", "safeharbor.server", "--mart", "data/mart.db",
"--policy", "policy.toml", "--audit", "logs/audit.jsonl"],
"cwd": "/absolute/path/to/safeharbor-mcp"
}
}
}
```
Claude Code: `claude mcp add safeharbor -- python -m safeharbor.server --mart data/mart.db` from the repo directory.
Then ask: *"What's the denial rate by provider specialty, and how does out-of-network allowed cost compare to in-network?"*
## Policy
All rules live in [`policy.toml`](policy.toml): allowed tables, allowed functions, *k*, row cap, compute budget, untrusted text columns. Anything not listed is denied.
## Known gaps
Documented in the report and kept in the suite:
- **Differencing (G01).** Two large, allowed counts that differ by one person can be subtracted. Per-query suppression can't stop this; it needs cross-query auditing or differential privacy.
- **Quasi-identifiers (G02).** Row-level pseudonymous records are allowed for drill-down. A rare ZIP3 + sex + age-band combination can still single someone out.
- **Utility cost of fail-closed suppression (G03).** Per-member aggregates inside a CTE are nulled before the outer query sees them. The mitigation is the FDE one: precompute common member-level features into the mart (`members.claim_count`).
## Layout
```
safeharbor/
data.py synthetic raw warehouse (PHI) + de-identification views + build_mart()
guard.py authorizer, k-suppressed aggregates, budgets, row cap
server.py MCP stdio server (JSON-RPC 2.0, protocol 2025-06-18 / 2025-03-26 / 2024-11-05)
audit.py hash-chained audit log + verifier
redteam.py attack / utility / gap suites + PHI canary scan
policy.toml the access policy
```
All data is synthetic and generated from a fixed seed. Python 3.11+, no runtime dependencies.
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues