{
  "markdown": "# phi-guard-mcp\n\n[![npm version](https://img.shields.io/npm/v/phi-guard-mcp.svg)](https://www.npmjs.com/package/phi-guard-mcp)\n[![npm downloads](https://img.shields.io/npm/dt/phi-guard-mcp.svg)](https://www.npmjs.com/package/phi-guard-mcp)\n[![License: MIT](https://img.shields.io/npm/l/phi-guard-mcp.svg)](./LICENSE)\n\nA local-first [MCP](https://modelcontextprotocol.io) server that catches PHI\n(protected health information) flowing into LLM prompts, log statements, and\nanalytics calls — in your source code, before it ships.\n\nIt runs entirely on your machine over stdio. No code, no snippets, and no\ndetected values are ever sent anywhere.\n\n## Why\n\nThe risky moment in a healthcare codebase is rarely the database. It's the line\nwhere a patient record gets interpolated into a prompt, a `console.log`, or an\nanalytics event. Those lines look harmless in review and never show up in\ninfrastructure scanning, because nothing is misconfigured — the code is just\ndoing what it says.\n\n## Tools\n\n### `redact_suggest`\n\nTakes a raw text snippet — a log line, a prompt, an error message — detects\nPHI-shaped values, and returns a redacted version alongside what it found.\n\n**Input**\n\n```json\n{ \"text\": \"Patient John Doe (MRN-12345), DOB: 01/01/1980\" }\n```\n\n**Output**\n\n```json\n{\n  \"redacted\": \"Patient [NAME] ([MRN]), [DOB]\",\n  \"detected\": [\n    { \"type\": \"mrn\",  \"confidence\": 0.9,  \"start\": 18, \"end\": 27 },\n    { \"type\": \"dob\",  \"confidence\": 0.85, \"start\": 30, \"end\": 45 },\n    { \"type\": \"name\", \"confidence\": 0.8,  \"start\": 8,  \"end\": 16 }\n  ]\n}\n```\n\nThe matched values are **not** echoed back by default, and neither is the\nunredacted `original`. A tool result flows straight into the context of\nwhatever model called it, so repeating the raw PHI there would undo the point\nof the tool. `start`/`end` are offsets into the original text, which is enough\nto locate a match without restating it.\n\nPass `includeMatchedValues: true` when you genuinely need the raw values (a\nlocal CLI, a test harness) and `detected[].value` plus `original` come back:\n\n```json\n{ \"text\": \"Patient John Doe (MRN-12345)\", \"includeMatchedValues\": true }\n```\n\nPatterns and their confidence scores:\n\n| Type    | Confidence | Matches |\n| ------- | ---------- | ------- |\n| `ssn`   | 0.95 | `123-45-6789` |\n| `mrn`   | 0.90 | `MRN-12345`, `MRN: 12345` |\n| `dob`   | 0.85 | `DOB: 01/01/1980`, `born 3/14/75` |\n| `name`  | 0.80 | `Patient John Doe` (captures `John Doe`) |\n| `phone` | 0.75 | `555-867-5309`, `(555) 867 5309` |\n| `email` | 0.70 | `jane.roe@example.com` |\n\nThe patterns start deliberately narrow. A false positive that trains someone to\nignore the tool is worse than a missed match.\n\n### `scan_code`\n\nWalks a directory and flags lines where a sensitive-looking identifier\n(`patient`, `diagnosis`, `dob`, `ssn`, `mrn`, `birthdate`, `medicalrecord`)\nappears on the same line as a risky sink (`openai`, `anthropic`, `bedrock`,\n`console.log/error/warn`, `logger.`, `winston`, `pino`, `.track(`, and\n`capture(` / `captureException(` / `captureMessage(`).\n\nWhole-line `//` and `#` comments are skipped, so a file that discusses PHI\nhandling in prose doesn't trip the scanner on its own documentation.\n\nGiven the operative lines of\n[`test/fixtures/leaky-example.ts`](test/fixtures/leaky-example.ts):\n\n```ts\nconst prompt = await openai.responses.create({ input: `Patient: ${patient.name}, diagnosis: ${patient.diagnosis}` });\nconsole.log(\"Sending patient prompt to LLM:\", prompt);\n```\n\n**Input**\n\n```json\n{ \"path\": \"/abs/path/to/repo/test/fixtures\" }\n```\n\n**Output** — excerpt. The full fixtures directory returns 8 findings, because\nit also holds the positive fixtures described under\n[Tested against](#tested-against).\n\n```json\n[\n  {\n    \"file\": \"test/fixtures/leaky-example.ts\",\n    \"line\": 7,\n    \"severity\": \"high\",\n    \"issue\": \"Sensitive-looking identifier passed to a risky sink (LLM call, logger, or analytics)\",\n    \"snippet\": \"const prompt = await openai.responses.create({ input: `Patient: ${patient.name}, diagnosis: ${patient.diagnosis}` });\"\n  },\n  {\n    \"file\": \"test/fixtures/leaky-example.ts\",\n    \"line\": 8,\n    \"severity\": \"high\",\n    \"issue\": \"Sensitive-looking identifier passed to a risky sink (LLM call, logger, or analytics)\",\n    \"snippet\": \"console.log(\\\"Sending patient prompt to LLM:\\\", prompt);\"\n  }\n]\n```\n\nScans `.ts`, `.js`, `.tsx`, `.jsx`, `.py`, `.go`. Skips `node_modules`, `dist`,\n`build`, `coverage`, `out`, `.next`, `.turbo`, and dotfiles.\n\n`snippet` is the offending line with any literal PHI masked, for the same\nreason `redact_suggest` withholds matched values: the finding is going into a\nmodel's context. Identifier names like `patient.diagnosis` are not literal\nvalues, match no PHI pattern, and stay visible — they are the actionable part.\n\nBoth conditions must hold **on the same line**. That is what keeps it quiet: on\nthis repo's own source — which is dense with the words `patient`, `diagnosis`,\n`mrn`, and `ssn` inside its pattern definitions — it reports zero findings.\n\n## Tested against\n\n**7 out of 7 real leak patterns detected**, across 5 different sinks (OpenAI,\nAnthropic, Sentry, Winston, PostHog/analytics) and 2 languages (TypeScript,\nPython) — including snake_case identifiers (`patient_name`,\n`patient_diagnosis`), which a naive word-boundary regex misses and which is the\ndominant naming convention in Python and Go, and a hardcoded-literal fixture\nthat verifies `scan_code` masks literal PHI out of the `snippet` it returns.\n\n**0 false positives across 5 clean-code fixtures**, including code that\ndiscusses PHI policy in comments and prose without ever leaking it, and code\nthat legitimately handles patient records without sending them anywhere risky.\n\n**1 documented limitation:** detection is line-based, so a sensitive value\nassigned on one line and used in a risky call several lines later isn't\ncurrently caught. This is a known scope boundary, not a bug — see\n[What this is NOT](#what-this-is-not) below.\n\nFull test fixtures live in [`test/fixtures/`](test/fixtures/) if you want to\nverify any of this yourself rather than take it on faith:\n\n```bash\nnpm test\n```\n\nThe suite asserts both directions: every file under `positive/` must produce at\nleast one finding, and `negative/` must produce exactly zero. A miss on either\nside fails the run.\n\n## What this is NOT\n\n- **Not a hosted service.** It is a local stdio process. There is no backend, no\n  account, and no telemetry. Your code never leaves your machine.\n- **Not a HIPAA certification, audit, or compliance attestation.** Passing a\n  `scan_code` run proves nothing to a regulator. It is a linter for a specific\n  class of mistake, not evidence of compliance. Treat a clean result as \"these\n  particular patterns didn't fire\", never as \"this codebase is HIPAA-safe\".\n- **Not a competitor to Prowler, AWS Config, or cloud posture tools.** Those\n  scan infrastructure and configuration. This reads source code and finds a\n  different class of problem. They are complementary; this replaces neither.\n- **Not exhaustive.** Regex-based detection has a real false-negative rate. It\n  will not catch PHI in a variable it can't name-match, or values arriving from\n  an external call.\n- **Not able to follow a value across lines.** The identifier and the sink have\n  to appear on the same line. Assigning `patient.diagnosis` to a local variable\n  and logging that variable three lines later produces no finding — there is a\n  worked example in\n  [`test/fixtures/known-limitations/`](test/fixtures/known-limitations/). Real\n  dataflow analysis is out of scope for v1; this is a deliberate boundary, and\n  the fixture exists so the gap stays visible rather than forgotten.\n- **Not fully comment-aware.** Only whole-line `//` and `#` comments are\n  skipped. Block comments (`/* ... */`) and trailing end-of-line comments are\n  still scanned, so a sink keyword sitting inside one of those can produce a\n  finding even though nothing executes.\n\n## Setup\n\nRequires Node.js 18+.\n\n```bash\ngit clone https://github.com/Abidit/phi-guard-mcp.git\ncd phi-guard-mcp\nnpm install\nnpm run build\n```\n\n`dist/` is gitignored, so `npm run build` is required after cloning — the MCP\nconfig below points at the compiled output.\n\n### Claude Code\n\nAdd `.mcp.json` to your project root, using the **absolute path** to your clone:\n\n```json\n{\n  \"mcpServers\": {\n    \"phi-guard\": {\n      \"command\": \"node\",\n      \"args\": [\"/absolute/path/to/phi-guard-mcp/dist/index.js\"]\n    }\n  }\n}\n```\n\nRestart Claude Code, or run `/mcp` and reconnect `phi-guard`. A rebuild alone\nwill not reach an already-running stdio process.\n\n### Verifying\n\n```bash\nnpm test          # fixture suite: positive, negative, known limitations\nnpm run typecheck # src/ and test/ under strict mode\nnpx tsx test/smoke.ts\n```\n\nOr drive it through the official Inspector without a browser:\n\n```bash\nnpx @modelcontextprotocol/inspector --cli node dist/index.js --method tools/list\nnpx @modelcontextprotocol/inspector --cli node dist/index.js \\\n  --method tools/call --tool-name redact_suggest \\\n  --tool-arg text=\"Patient John Doe (MRN-12345)\"\n```\n\nThe server declares only the `tools` capability, so `resources/list` and\n`prompts/list` correctly return `-32601 Method not found`. The Inspector UI\nprobes all three regardless and shows those two in red — expected, not a fault.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n\n## Mcp Server Approved\n[![Listed on mcpservers.org](https://mcpservers.org/badge.svg)](https://mcpservers.org/servers/abidit/phi-guard-mcp)\n",
  "bytes": 9490,
  "sha": "775c5b80963402ccc57ed3eb7c24beb0712b0247d275114bffe7268e4a12aab8",
  "repo_slug": "abidit/phi-guard-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_abidit_phi_guard_mcp_f4c25824/readme"
}