PHI Guard MCP

Examina o código-fonte em busca de vazamento de PHI em prompts de LLM, logs e chamadas de analytics - local-first, sem chamadas de rede.

Documentação

phi-guard-mcp

npm version npm downloads License: MIT

Um servidor MCP local-first que detecta PHI (informações de saúde protegidas) fluindo para prompts de LLM, declarações de log e chamadas de analytics — no seu código-fonte, antes de ser publicado.

O servidor em si não faz chamadas de rede: ele roda como um processo stdio local e nunca envia seu código para fora. O que ele retorna é outra história. Os achados vão de volta para qualquer cliente MCP que o iniciou; portanto, se esse cliente for um assistente hospedado, os achados entram no contexto do modelo. Valores de PHI correspondentes são mascarados dos resultados por padrão exatamente por esse motivo — veja Limite de segurança.

Por quê

O momento de risco em uma base de código de saúde raramente é o banco de dados. É a linha onde um registro de paciente é interpolado em um prompt, um console.log, ou um evento de analytics. Essas linhas parecem inofensivas na revisão e nunca aparecem em varreduras de infraestrutura, porque nada está mal configurado — o código está apenas fazendo o que diz.

Demonstração

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

Onze segundos do passo a passo: scan_code sinaliza a linha 15 — um nome de paciente e diagnóstico interpolados em uma chamada de openai.responses.create — e redact_suggest mascara os mesmos valores de um prompt bruto. Todo valor exibido é sintético.

Um linter para uma classe de erro. Não é uma certificação HIPAA, não é uma atestação de conformidade, e não é um analisador de fluxo de dados — veja O que isto NÃO é.

Instalação

Requer Node.js 22 ou mais recente. O ponto de entrada usa atributos de importação JSON (with { type: "json" }), então runtimes mais antigos não o iniciarão. Verificado em Node 22.18.0, 24.2.0 e 26.8.2; Node 20 e versões anteriores não são suportados e não foram testados.

Via npm (recomendado)

Nada para clonar ou compilar. Seu cliente MCP o executa sob demanda:

npx -y phi-guard-mcp --version

A partir do código-fonte

git clone https://github.com/Abidit/phi-guard-mcp.git
cd phi-guard-mcp
npm ci

npm ci executa o script prepare, que compila dist/. Não há etapa de compilação separada para esquecer. npm install também funciona; npm ci é o reproduzível porque instala exatamente o que package-lock.json fixa.

Configuração do MCP

Copie e cole uma das opções a seguir. A forma npm não precisa de caminhos e é a que se entrega a outra pessoa.

Claude Code

Um comando, escopo do projeto:

claude mcp add phi-guard -- npx -y phi-guard-mcp

Ou faça commit de um .mcp.json na raiz do seu projeto:

{
  "mcpServers": {
    "phi-guard": {
      "command": "npx",
      "args": ["-y", "phi-guard-mcp"]
    }
  }
}

Claude Desktop

~/Library/Application Support/Claude/claude_desktop_config.json no macOS, %APPDATA%\Claude\claude_desktop_config.json no Windows:

{
  "mcpServers": {
    "phi-guard": {
      "command": "npx",
      "args": ["-y", "phi-guard-mcp"]
    }
  }
}

Cursor

.cursor/mcp.json no projeto, ou ~/.cursor/mcp.json globalmente:

{
  "mcpServers": {
    "phi-guard": {
      "command": "npx",
      "args": ["-y", "phi-guard-mcp"]
    }
  }
}

Executando um clone local em vez do lançamento npm

Aponte o cliente para a saída da compilação. Use um caminho absoluto a menos que tenha certeza de que seu cliente inicia o servidor com a raiz do projeto como diretório de trabalho:

{
  "mcpServers": {
    "phi-guard": {
      "command": "node",
      "args": ["/absolute/path/to/phi-guard-mcp/dist/index.js"]
    }
  }
}

O .mcp.json commitado neste repositório usa a forma relativa (dist/index.js) para que o repositório possa usar seu próprio servidor após npm ci.

Qualquer forma que use: reinicie o cliente, ou execute /mcp no Claude Code e reconecte phi-guard. Apenas recompilar não alcançará um processo stdio já em execução.

Ferramentas

redact_suggest

Recebe um trecho de texto bruto — uma linha de log, um prompt, uma mensagem de erro — detecta valores com formato de PHI e retorna uma versão editada junto com o que encontrou.

Entrada

{ "text": "Patient John Doe (MRN-12345), DOB: 01/01/1980" }

Saída

{
  "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 }
  ]
}

Os valores correspondentes não são ecoados de volta por padrão, e nem o original não editado. O resultado de uma ferramenta flui diretamente para o contexto de qualquer modelo que a chamou, então repetir o PHI bruto ali anularia o propósito da ferramenta. start/end são deslocamentos no texto original, o que é suficiente para localizar uma correspondência sem repeti-la.

detected é ordenado por padrão, não por posição.

Passe includeMatchedValues: true quando você realmente precisar dos valores brutos (um CLI local, um harness de teste) e detected[].value mais original voltam:

{ "text": "Patient John Doe (MRN-12345)", "includeMatchedValues": true }

Padrões e suas pontuações de confiança:

TipoConfiançaCorrespondências
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

Essa tabela é a lista completa. Os padrões começam deliberadamente estreitos — um falso positivo que treina alguém a ignorar a ferramenta é pior do que uma correspondência perdida.

scan_code

Percorre um diretório e sinaliza linhas onde um identificador de aparência sensível aparece na mesma linha de um destino arriscado.

Identificadores sensíveis — a lista completa:

patient, diagnosis, dob, ssn, mrn, birthdate, medicalrecord

A correspondência não diferencia maiúsculas de minúsculas e é limitada por não-letras, então patient_name corresponde enquanto outpatient e inpatient não correspondem.

Categorias de destino — a lista completa:

CategoriaCorrespondências
Provedores de LLMopenai, anthropic, bedrock
Consoleconsole.log, console.error, console.warn
Loggerslogger., winston, pino
Analytics.track(
Relatório de erroscapture(, captureException(, captureMessage(

Linguagens varridas — a lista completa:

ExtensãoLinguagem
.ts, .tsxTypeScript
.js, .jsxJavaScript
.pyPython
.goGo

A detecção é puramente lexical, então suporte a linguagem significa "essas extensões de arquivo são lidas". Não há parser nem informações de tipo para nenhuma delas.

Ignora node_modules, dist, build, coverage, out, .next, .turbo, e qualquer dotfile ou dot-directory. Comentários de linha inteira // e # são ignorados, então um arquivo que discute o tratamento de PHI em prosa não dispara o scanner em sua própria documentação.

Dadas as linhas 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" }

Saída — trecho. Esse diretório retorna 9 achados no total: 2 deste arquivo e 7 dos fixtures positivos descritos em Avaliação.

[
  {
    "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 é construído juntando path com o nome da entrada, então volta na mesma forma que você passou: absoluto na entrada, absoluto na saída. severity é sempre "high" — há uma regra, então há uma severidade.

snippet é a linha ofensiva com qualquer PHI literal mascarado, pela mesma razão que redact_suggest retém valores correspondentes: o achado está indo para o contexto de um modelo. Nomes de identificadores como patient.diagnosis não são valores literais, não correspondem a nenhum padrão de PHI e permanecem visíveis — são a parte acionável.

Um array vazio significa que todo arquivo-fonte elegível descoberto sob path foi lido com sucesso e nenhuma linha correspondeu a ambas as condições. "Elegível" e "descoberto" são termos importantes: arquivos com extensão não suportada e qualquer coisa sob um diretório ignorado ou com prefixo de ponto nunca são abertos.

Uma varredura que não consegue ler um diretório ou um arquivo falha com um erro nomeado em vez de retornar uma lista mais curta, porque um resultado parcial é lido como um resultado limpo:

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')

A regra da mesma linha

Ambas as condições devem ser verdadeiras na mesma linha física. Esta é a coisa mais importante para entender sobre o scanner, em ambas as direções.

É isso que o mantém silencioso. No src/ deste próprio repositório — que é denso com as palavras patient, diagnosis, mrn e ssn dentro de suas definições de padrão — ele relata zero achados.

Também é o principal motivo pelo qual ele perde coisas. Isto vaza e não é sinalizado:

const value = patient.diagnosis;   // sensitive identifier, no sink
console.log("audit", value);       // sink, no sensitive identifier

Não há análise de fluxo de dados, rastreamento de variáveis ou correlação entre linhas. Um exemplo trabalhado vive em test/fixtures/known-limitations/split-across-lines.ts, e a suíte de testes o varre a cada execução para que a lacuna permaneça visível em vez de esquecida.

Avaliação

npm run verify

Um comando: verificação de tipos (src/ e test/ em modo estrito), compilação e então a suíte de fixtures contra o servidor real via stdio.

O resultado atual:

Detectou todos os 7 fixtures de vazamento representativos e não sinalizou nenhum dos 5 fixtures limpos.

Esses fixtures estão todos em test/fixtures/, então você pode lê-los em vez de aceitar isso por fé.

Os 7 fixtures de vazamento cobrem 5 categorias de destino (OpenAI, Anthropic, Sentry, Winston, analytics) e 2 linguagens (TypeScript, Python). Eles incluem identificadores snake_case (patient_name, patient_diagnosis), que uma regex ingênua de fronteira de palavra perde e que é a convenção de nomenclatura dominante em Python e Go, e um fixture de literal codificado que verifica se scan_code mascara PHI literal do snippet que retorna.

Os 5 fixtures limpos incluem código que discute política de PHI em comentários e prosa sem nunca vazar, e código que legitima o tratamento de registros de pacientes sem enviá-los a nenhum destino arriscado.

A suíte também afirma:

  • o exemplo trabalhado do README ainda produz exatamente 2 achados, nas linhas 7 e 8
  • nenhum PHI literal bruto aparece em qualquer lugar em um resultado de scan_code
  • redact_suggest omite todo valor correspondente e o original por padrão
  • includeMatchedValues: true realmente os retorna
  • um caminho ausente e um arquivo-em-vez-de-diretório cada um retorna um erro nomeado

O que este número não é. É uma suíte de fixtures de 12 arquivos escritos à mão que este projeto escreveu para exercitar suas próprias regras. Não é uma taxa de detecção, não é um benchmark, não é uma taxa de falsos positivos e não é validação externa. Não diz nada sobre como o scanner se comporta em uma base de código de saúde real, onde a taxa de falsos negativos é desconhecida e, dada a regra da mesma linha acima, certamente não é zero. Medir isso exigiria um corpus rotulado que este projeto não tem.

Outras verificações:

npm test          # fixture suite only
npm run typecheck # src/ and test/ under strict mode
npm run test:smoke  # minimal stdio round-trip

Ou conduza-o pelo Inspector oficial sem um 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)"

O servidor declara apenas a capacidade tools, então no nível do protocolo resources/list e prompts/list retornam -32601 Method not found. O CLI do Inspector normaliza-os em {"resources": []} e {"prompts": []}, e a UI do Inspector pode mostrá-los em vermelho. Ambos são esperados, não uma falha.

Solução de problemas

O servidor parece travar quando eu o executo. Esse é o comportamento correto. Ele é um servidor stdio: espera por JSON-RPC no stdin e não imprime nada por conta própria. Execute npx -y phi-guard-mcp --help ou node dist/index.js --version se você apenas quiser confirmar que está instalado.

O cliente mostra phi-guard como falho ou desconectado. Execute o comando da sua configuração manualmente e olhe o stderr — npx -y phi-guard-mcp --version, ou node /absolute/path/to/dist/index.js --version. Se isso imprimir uma versão, o problema é o caminho da configuração, não o servidor.

Cannot find module '.../dist/index.js'. Você está executando a partir de um clone e dist/ não foi compilado. dist/ é gitignorado de propósito. Execute npm ci.

SyntaxError: Unexpected identifier 'assert' ou um erro de atributo de importação na inicialização. Seu Node é mais antigo que 22. Verifique com node --version. Observe que seu cliente MCP pode iniciar um Node diferente do que seu shell usa.

Mudanças de código não aparecem. Um processo stdio já em execução mantém a compilação antiga. Recompile e então reinicie o cliente ou reconecte o servidor (/mcp no Claude Code). scan_code retorna [] em um repositório que sei que tem vazamentos. Três causas prováveis, em ordem: o identificador e o sink estão em linhas diferentes (veja a regra da mesma linha); a extensão do arquivo não está na lista de suportados; ou o diretório é ignorado (dist, build, qualquer coisa com prefixo de ponto). Confirme que o scanner está ativo apontando-o para o diretório test/fixtures deste repositório, que deve retornar 9 descobertas.

scan_code erros com No such path. O caminho é resolvido na máquina que executa o servidor, e um caminho relativo é resolvido em relação ao diretório de trabalho que seu cliente MCP deu ao processo. Passe um caminho absoluto.

O que isto NÃO é

  • Não é um serviço hospedado. É um processo stdio local. Não há backend, nenhuma conta e nenhuma telemetria, e o servidor não faz chamadas de rede próprias. Isso não é o mesmo que "nada sai da sua máquina": as descobertas são retornadas ao cliente MCP, que pode ser um assistente hospedado. Veja Limite de segurança.
  • Não é uma certificação HIPAA, auditoria ou atestado de conformidade. Passar em uma execução de scan_code não prova nada para um regulador. É um linter para uma classe específica de erro, não evidência de conformidade. Trate um resultado limpo como "esses padrões específicos não dispararam", nunca como "este código é seguro para HIPAA".
  • Não é validado por ninguém além de seu autor. Os fixtures foram escritos por este projeto para testar este projeto. Não houve revisão externa, nenhuma auditoria de terceiros e nenhuma avaliação contra um código de saúde real.
  • Não é um concorrente do Prowler, AWS Config ou ferramentas de postura de nuvem. Esses escaneiam infraestrutura e configuração. Este lê código-fonte e encontra uma classe diferente de problema. Eles são complementares; este não substitui nenhum deles.
  • Não é exaustivo. A detecção baseada em regex tem uma taxa real e não medida de falsos negativos. Não capturará PHI em uma variável que não consegue corresponder pelo nome, ou valores que chegam de uma chamada externa.
  • Não é capaz de seguir um valor entre linhas. Veja a regra da mesma linha. Análise de fluxo de dados real está fora do escopo; este é um limite deliberado.
  • Não é totalmente ciente de comentários. Apenas comentários // e # de linha inteira são ignorados. Comentários de bloco (/* ... */) e comentários de fim de linha ainda são escaneados, então uma palavra-chave de sink dentro de um deles pode produzir uma descoberta mesmo que nada seja executado.
  • Não é um scanner de segredos, e não é um substituto para revisão de código. Ele procura um padrão: um nome de aparência sensível próximo a uma chamada arriscada.

Limite de segurança

O servidor lê qualquer diretório que receber, na máquina em que executa, com as permissões do usuário que o iniciou. Não tem sandbox e nenhuma lista de permissões. Aponte-o para código que você tem o direito de ler.

O servidor não abre sockets e não faz solicitações de saída. Isso não significa que seus dados permanecem locais. Um resultado de ferramenta é retornado ao cliente MCP que iniciou o servidor, e se esse cliente for um assistente hospedado, o resultado é transmitido e processado por esse provedor sob os termos dele, não os deste projeto. Trate cada resultado de scan_code e redact_suggest como conteúdo que você está enviando ao seu provedor de modelo.

Resultados de snippet e redact_suggest são mascarados por padrão especificamente para que o que é transmitido sejam nomes de identificadores e posições, em vez de valores de PHI. O mascaramento é o mesmo conjunto de regex descrito acima, então carrega a mesma taxa de falsos negativos: um valor que esses padrões não reconhecem é passado sem mascaramento. Não trate "foi mascarado" como uma garantia, e não execute isso contra PHI de produção por meio de um cliente hospedado sem antes se certificar sobre o tratamento de dados desse cliente.

Contribuindo

npm run verify deve passar. CI executa isso no Node 22 e 24.

Licença

MIT — veja LICENSE.

Mcp Server Approved

Listed on mcpservers.org