PHI Guard MCP
Escanea el código fuente en busca de fugas de PHI en prompts de LLM, registros y llamadas de analítica: local-primero, sin llamadas de red.
Documentación
phi-guard-mcp
Un servidor MCP local-first que detecta PHI (información de salud protegida) que fluye hacia prompts de LLM, declaraciones de registro y llamadas de analítica — en tu código fuente, antes de que se publique.
El servidor en sí no realiza llamadas de red: se ejecuta como un proceso stdio local y nunca sube tu código. Lo que devuelve es otro asunto. Los hallazgos vuelven al cliente MCP que lo lanzó, así que si ese cliente es un asistente alojado, los hallazgos entran en el contexto de ese modelo. Los valores PHI coincidentes se enmascaran de los resultados por defecto precisamente por esta razón — consulta Límite de seguridad.
Por qué
El momento de riesgo en un código de atención médica rara vez es la base de datos. Es la línea
donde un registro de paciente se interpola en un prompt, un console.log, o un
evento de analítica. Esas líneas parecen inofensivas en la revisión y nunca aparecen en
el escaneo de infraestructura, porque nada está mal configurado — el código simplemente
hace lo que dice.
Demo

Once segundos del recorrido: scan_code marca la línea 15 — un nombre de paciente
y diagnóstico interpolados en una llamada a openai.responses.create — y
redact_suggest enmascara los mismos valores de un prompt crudo. Cada valor mostrado es
sintético.
Un linter para una clase de error. No es una certificación HIPAA, no es una atestación de cumplimiento, y no es un analizador de flujo de datos — consulta Lo que esto NO es.
Instalación
Requiere Node.js 22 o más reciente. El punto de entrada usa atributos de importación JSON
(with { type: "json" }), así que los runtimes más antiguos no lo iniciarán. Verificado en
Node 22.18.0, 24.2.0 y 26.8.2; Node 20 y anteriores no son compatibles y
no están probados.
Desde npm (recomendado)
Nada que clonar o compilar. Tu cliente MCP lo ejecuta bajo demanda:
npx -y phi-guard-mcp --version
Desde el código fuente
git clone https://github.com/Abidit/phi-guard-mcp.git
cd phi-guard-mcp
npm ci
npm ci ejecuta el script prepare, que compila dist/. No hay un paso
de compilación separado que olvidar. npm install también funciona; npm ci es el reproducible
porque instala exactamente lo que package-lock.json fija.
Configuración de MCP
Copia y pega uno de los siguientes. La forma npm no necesita rutas y es la que se debe entregar a otra persona.
Claude Code
Un comando, alcance de proyecto:
claude mcp add phi-guard -- npx -y phi-guard-mcp
O confirma un .mcp.json en la raíz de tu proyecto:
{
"mcpServers": {
"phi-guard": {
"command": "npx",
"args": ["-y", "phi-guard-mcp"]
}
}
}
Claude Desktop
~/Library/Application Support/Claude/claude_desktop_config.json en macOS,
%APPDATA%\Claude\claude_desktop_config.json en Windows:
{
"mcpServers": {
"phi-guard": {
"command": "npx",
"args": ["-y", "phi-guard-mcp"]
}
}
}
Cursor
.cursor/mcp.json en el proyecto, o ~/.cursor/mcp.json globalmente:
{
"mcpServers": {
"phi-guard": {
"command": "npx",
"args": ["-y", "phi-guard-mcp"]
}
}
}
Ejecutar un clon local en lugar de la versión npm
Apunta el cliente a la salida de compilación. Usa una ruta absoluta a menos que estés seguro de que tu cliente lanza el servidor con la raíz del proyecto como su directorio de trabajo:
{
"mcpServers": {
"phi-guard": {
"command": "node",
"args": ["/absolute/path/to/phi-guard-mcp/dist/index.js"]
}
}
}
El .mcp.json confirmado en este repositorio usa la forma relativa (dist/index.js)
para que el repositorio pueda usar su propio servidor después de npm ci.
Cualquiera que sea la forma que uses: reinicia el cliente, o ejecuta /mcp en Claude Code y
reconecta phi-guard. Una recompilación por sí sola no alcanzará un proceso stdio
ya en ejecución.
Herramientas
redact_suggest
Toma un fragmento de texto crudo — una línea de registro, un prompt, un mensaje de error — detecta valores con forma de PHI y devuelve una versión redactada junto con lo que encontró.
Entrada
{ "text": "Patient John Doe (MRN-12345), DOB: 01/01/1980" }
Salida
{
"redacted": "Patient [NAME] ([MRN]), [DOB]",
"detected": [
{ "type": "mrn", "confidence": 0.9, "start": 18, "end": 27 },
{ "type": "dob", "confidence": 0.85, "start": 30, "end": 45 },
{ "type": "name", "confidence": 0.8, "start": 8, "end": 16 }
]
}
Los valores coincidentes no se repiten por defecto, y tampoco el original sin redactar.
Un resultado de herramienta fluye directamente al contexto de cualquier
modelo que lo llamó, así que repetir el PHI crudo allí anularía el propósito
de la herramienta. start/end son desplazamientos en el texto original, que es suficiente
para localizar una coincidencia sin repetirla.
detected está ordenado por patrón, no por posición.
Pasa includeMatchedValues: true cuando realmente necesites los valores crudos (un
CLI local, un entorno de pruebas) y detected[].value más original vuelven:
{ "text": "Patient John Doe (MRN-12345)", "includeMatchedValues": true }
Patrones y sus puntuaciones de confianza:
| Tipo | Confianza | Coincidencias |
|---|---|---|
ssn | 0.95 | 123-45-6789 |
mrn | 0.90 | MRN-12345, MRN: 12345 |
dob | 0.85 | DOB: 01/01/1980, born 3/14/75 |
name | 0.80 | Patient John Doe (captura John Doe) |
phone | 0.75 | 555-867-5309, (555) 867 5309 |
email | 0.70 | jane.roe@example.com |
Esa tabla es la lista completa. Los patrones comienzan deliberadamente estrechos — un falso positivo que entrena a alguien para ignorar la herramienta es peor que una coincidencia perdida.
scan_code
Recorre un directorio y marca líneas donde un identificador de aspecto sensible aparece en la misma línea que un sumidero de riesgo.
Identificadores sensibles — la lista completa:
patient, diagnosis, dob, ssn, mrn, birthdate, medicalrecord
La coincidencia no distingue mayúsculas y minúsculas y está delimitada por no-letras, así que patient_name
coincide mientras que outpatient y inpatient no.
Categorías de sumideros — la lista completa:
| Categoría | Coincidencias |
|---|---|
| Proveedores de LLM | openai, anthropic, bedrock |
| Consola | console.log, console.error, console.warn |
| Registradores | logger., winston, pino |
| Analítica | .track( |
| Reporte de errores | capture(, captureException(, captureMessage( |
Lenguajes escaneados — la lista completa:
| Extensión | Lenguaje |
|---|---|
.ts, .tsx | TypeScript |
.js, .jsx | JavaScript |
.py | Python |
.go | Go |
La detección es puramente léxica, así que el soporte de lenguaje significa "estas extensiones de archivo se leen". No hay analizador ni información de tipos para ninguno de ellos.
Omite node_modules, dist, build, coverage, out, .next, .turbo,
y cualquier archivo o directorio con punto. Los comentarios de línea completa // y # se omiten,
así que un archivo que discute el manejo de PHI en prosa no activa el escáner en su
propia documentación.
Dadas las líneas operativas de
test/fixtures/leaky-example.ts:
const prompt = await openai.responses.create({ input: `Patient: ${patient.name}, diagnosis: ${patient.diagnosis}` });
console.log("Sending patient prompt to LLM:", prompt);
Entrada
{ "path": "/abs/path/to/repo/test/fixtures" }
Salida — extracto. Ese directorio devuelve 9 hallazgos en total: 2 de este archivo y 7 de los fixtures positivos descritos en Evaluación.
[
{
"file": "/abs/path/to/repo/test/fixtures/leaky-example.ts",
"line": 7,
"severity": "high",
"issue": "Sensitive-looking identifier passed to a risky sink (LLM call, logger, or analytics)",
"snippet": "const prompt = await openai.responses.create({ input: `Patient: ${patient.name}, diagnosis: ${patient.diagnosis}` });"
},
{
"file": "/abs/path/to/repo/test/fixtures/leaky-example.ts",
"line": 8,
"severity": "high",
"issue": "Sensitive-looking identifier passed to a risky sink (LLM call, logger, or analytics)",
"snippet": "console.log(\"Sending patient prompt to LLM:\", prompt);"
}
]
file se construye uniendo path con el nombre de la entrada, así que vuelve en
la forma que pasaste: absoluta adentro, absoluta afuera. severity siempre es
"high" — hay una regla, así que hay una severidad.
snippet es la línea ofensiva con cualquier PHI literal enmascarado, por la misma
razón que redact_suggest retiene los valores coincidentes: el hallazgo va a
el contexto de un modelo. Nombres de identificadores como patient.diagnosis no son valores
literales, no coinciden con ningún patrón de PHI y permanecen visibles — son la parte accionable.
Un array vacío significa que cada archivo fuente elegible descubierto bajo path se
leyó con éxito y ninguna línea coincidió con ambas condiciones. "Elegible" y
"descubierto" son importantes: los archivos con una extensión no compatible, y
cualquier cosa bajo un directorio omitido o con prefijo de punto, nunca se abren.
Un escaneo que no puede leer un directorio o un archivo falla con un error nombrado en lugar de devolver una lista más corta, porque un resultado parcial se lee como un resultado limpio:
No such path: "/nope". Pass an absolute path to a directory that exists on the
machine running this server. (ENOENT: no such file or directory, stat '/nope')
La regla de la misma línea
Ambas condiciones deben cumplirse en la misma línea física. Esto es lo más importante que hay que entender sobre el escáner, en ambas direcciones.
Es lo que lo mantiene silencioso. En el propio src/ de este repositorio — que está denso con las
palabras patient, diagnosis, mrn y ssn dentro de sus definiciones de patrones —
reporta cero hallazgos.
También es la razón principal por la que pierde cosas. Esto se filtra y no se marca:
const value = patient.diagnosis; // sensitive identifier, no sink
console.log("audit", value); // sink, no sensitive identifier
No hay análisis de flujo de datos, no hay seguimiento de variables y no hay
correlación entre líneas. Un ejemplo trabajado vive en
test/fixtures/known-limitations/split-across-lines.ts,
y el conjunto de pruebas lo escanea en cada ejecución para que la brecha permanezca visible en lugar de
olvidada.
Evaluación
npm run verify
Un comando: verificación de tipos (src/ y test/ bajo modo estricto), compilación, luego el
conjunto de fixtures contra el servidor real a través de stdio.
El resultado actual:
Detectó los 7 fixtures de fugas representativos y no marcó ninguno de los 5 fixtures limpios.
Esos fixtures están todos en test/fixtures/, así que puedes leerlos
en lugar de tomarlo por fe.
Los 7 fixtures de fugas cubren 5 categorías de sumideros (OpenAI, Anthropic, Sentry,
Winston, analítica) y 2 lenguajes (TypeScript, Python). Incluyen
identificadores snake_case (patient_name, patient_diagnosis), que una regex
de límite de palabra ingenua pierde y que es la convención de nomenclatura dominante en
Python y Go, y un fixture de literal codificado que verifica que scan_code enmascara
PHI literal fuera del snippet que devuelve.
Los 5 fixtures limpios incluyen código que discute la política de PHI en comentarios y prosa sin filtrarlo nunca, y código que maneja legítimamente registros de pacientes sin enviarlos a ningún lugar riesgoso.
El conjunto también afirma:
- el ejemplo trabajado del README todavía produce exactamente 2 hallazgos, en las líneas 7 y 8
- ningún PHI literal crudo aparece en ningún lugar de un resultado
scan_code redact_suggestomite cada valor coincidente y el original por defectoincludeMatchedValues: truerealmente los devuelve- una ruta faltante y un archivo-en-lugar-de-directorio devuelven cada uno un error nombrado
Lo que este número no es. Es un conjunto de fixtures de 12 archivos escritos a mano que este proyecto escribió para ejercitar sus propias reglas. No es una tasa de detección, no es un benchmark, no es una tasa de falsos positivos y no es validación externa. No dice nada sobre cómo se desempeña el escáner en un código de atención médica real, donde la tasa de falsos negativos es desconocida y, dada la regla de la misma línea anterior, ciertamente no es cero. Medir eso requeriría un corpus etiquetado que este proyecto no tiene.
Otras verificaciones:
npm test # fixture suite only
npm run typecheck # src/ and test/ under strict mode
npm run test:smoke # minimal stdio round-trip
O conduce a través del Inspector oficial sin un navegador:
npx @modelcontextprotocol/inspector --cli node dist/index.js --method tools/list
npx @modelcontextprotocol/inspector --cli node dist/index.js \
--method tools/call --tool-name redact_suggest \
--tool-arg text="Patient John Doe (MRN-12345)"
El servidor declara solo la capacidad tools, así que a nivel de protocolo
resources/list y prompts/list devuelven -32601 Method not found. El
CLI del Inspector los normaliza en {"resources": []} y {"prompts": []},
y la interfaz del Inspector puede mostrarlos en rojo. Ambos son esperados, no una falla.
Solución de problemas
El servidor parece colgarse cuando lo ejecuto. Ese es un comportamiento correcto. Es
un servidor stdio: espera JSON-RPC en stdin y no imprime nada por sí mismo. Ejecuta
npx -y phi-guard-mcp --help o node dist/index.js --version si solo
quieres confirmar que está instalado.
El cliente muestra phi-guard como fallido o desconectado. Ejecuta el comando
de tu configuración a mano y mira stderr — npx -y phi-guard-mcp --version,
o node /absolute/path/to/dist/index.js --version. Si eso imprime una versión,
el problema es la ruta de configuración, no el servidor.
Cannot find module '.../dist/index.js'. Estás ejecutando desde un clon y
dist/ no se ha compilado. dist/ está en gitignore a propósito. Ejecuta npm ci.
SyntaxError: Unexpected identifier 'assert' o un error de atributo de importación al
iniciar. Tu Node es más antiguo que 22. Verifica con node --version. Ten en cuenta
que tu cliente MCP puede lanzar un Node diferente al que tu shell usa.
Los cambios de código no aparecen. Un proceso stdio ya en ejecución mantiene la compilación
antigua. Recompila, luego reinicia el cliente o reconecta el servidor (/mcp en
Claude Code).
scan_code devuelve [] en un repositorio que sé que tiene fugas. Tres causas probables, en orden: el identificador y el sumidero están en líneas diferentes (consulta la regla de la misma línea); la extensión del archivo no está en la lista de extensiones admitidas; o el directorio se omite (dist, build, cualquier cosa con prefijo de punto). Confirma que el escáner está activo apuntándolo al directorio test/fixtures de este repositorio, que debe devolver 9 hallazgos.
scan_code falla con No such path. La ruta se resuelve en la máquina donde se ejecuta el servidor, y una ruta relativa se resuelve contra el directorio de trabajo que tu cliente MCP le haya dado al proceso. Pasa una ruta absoluta.
Qué NO es esto
- No es un servicio alojado. Es un proceso stdio local. No hay backend, ni cuenta, ni telemetría, y el servidor no realiza llamadas de red propias. Eso no es lo mismo que "nada sale de tu máquina": los hallazgos se devuelven al cliente MCP, que puede ser un asistente alojado. Consulta Límite de seguridad.
- No es una certificación HIPAA, auditoría o atestación de cumplimiento. Pasar una ejecución de
scan_codeno prueba nada ante un regulador. Es un linter para una clase específica de error, no evidencia de cumplimiento. Trata un resultado limpio como "estos patrones particulares no se activaron", nunca como "este código es seguro para HIPAA". - No está validado por nadie más que su autor. Los fixtures fueron escritos por este proyecto para probar este proyecto. No ha habido revisión externa, ni auditoría de terceros, ni evaluación contra un código de atención médica real.
- No es un competidor de Prowler, AWS Config o herramientas de postura en la nube. Esos escanean infraestructura y configuración. Esto lee código fuente y encuentra una clase diferente de problema. Son complementarios; esto no reemplaza a ninguno.
- No es exhaustivo. La detección basada en regex tiene una tasa real y no medida de falsos negativos. No capturará PHI en una variable que no pueda coincidir por nombre, ni valores que lleguen desde una llamada externa.
- No puede seguir un valor a través de líneas. Consulta la regla de la misma línea. El análisis real de flujo de datos está fuera de alcance; este es un límite deliberado.
- No es totalmente consciente de los comentarios. Solo se omiten los comentarios de línea completa
//y#. Los comentarios de bloque (/* ... */) y los comentarios finales de línea todavía se escanean, por lo que una palabra clave de sumidero dentro de uno de esos puede producir un hallazgo aunque no se ejecute nada. - No es un escáner de secretos, ni un sustituto de la revisión de código. Busca un patrón: un nombre de aspecto sensible junto a una llamada riesgosa.
Límite de seguridad
El servidor lee cualquier directorio que se le entregue, en la máquina donde se ejecuta, con los permisos del usuario que lo inició. No tiene sandbox ni lista de permitidos. Apúntalo a código que tengas derecho a leer.
El servidor no abre sockets ni realiza solicitudes salientes. Eso no implica que tus datos permanezcan locales. Un resultado de herramienta se devuelve al cliente MCP que lanzó el servidor, y si ese cliente es un asistente alojado, el resultado se transmite y procesa por ese proveedor bajo sus términos, no los de este proyecto. Trata cada resultado de scan_code y redact_suggest como contenido que estás enviando a tu proveedor de modelos.
Los resultados de snippet y redact_suggest están enmascarados por defecto específicamente para que lo que se transmita sean nombres de identificadores y posiciones en lugar de valores de PHI. El enmascaramiento es el mismo conjunto de regex descrito anteriormente, por lo que conlleva la misma tasa de falsos negativos: un valor que esos patrones no reconozcan se pasa sin enmascarar. No trates "fue enmascarado" como una garantía, y no ejecutes esto contra PHI de producción a través de un cliente alojado sin antes asegurarte sobre el manejo de datos de ese cliente.
Contribuciones
npm run verify debe pasar. CI lo ejecuta en Node 22 y 24.
Licencia
MIT — consulta LICENSE.