Skip to main content
Glama

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, 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

Related MCP server: Claims Data MCP Server

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

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

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

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Let LLMs analyze sensitive data safely by querying a tokenized, join-preserving copy of the database, with fail-closed PII scanning and provable numeric equivalence.
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables natural language querying of healthcare claims data by exposing a SQLite database with read-only SQL tools, allowing users to ask questions in plain English and get answers backed by real database queries.
    1
    -
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables AI agents to query governed data warehouses through plain language, returning grounded answers with SQL, confidence grades, and signed receipts while enforcing access, testing, and audit controls.
    279 PyPI
    17
    Apache 2.0
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI assistants to securely query a synthetic health-insurance claims database through read-only, parameterized tools for claims, member histories, coverage rules, and year-to-date summaries. Includes API-key authentication, per-tool authorization, rate limiting, untrusted-text sanitization, and fail-closed audit logging.
    MIT