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

npm version npm downloads License: MIT

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

scan_code flagging a patient name and diagnosis passed into an openai.responses.create call, then redact_suggest returning a masked result

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:

TipoConfianzaCoincidencias
ssn0.95123-45-6789
mrn0.90MRN-12345, MRN: 12345
dob0.85DOB: 01/01/1980, born 3/14/75
name0.80Patient John Doe (captura John Doe)
phone0.75555-867-5309, (555) 867 5309
email0.70jane.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íaCoincidencias
Proveedores de LLMopenai, anthropic, bedrock
Consolaconsole.log, console.error, console.warn
Registradoreslogger., winston, pino
Analítica.track(
Reporte de errorescapture(, captureException(, captureMessage(

Lenguajes escaneados — la lista completa:

ExtensiónLenguaje
.ts, .tsxTypeScript
.js, .jsxJavaScript
.pyPython
.goGo

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_suggest omite cada valor coincidente y el original por defecto
  • includeMatchedValues: true realmente 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_code no 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.

Servidor MCP Aprobado

Listed on mcpservers.org