Skip to main content
Glama
willianmarcel

medical-mcp-fake

README.md
# medical-mcp-fake

Servidor **MCP (Model Context Protocol)** com transporte **HTTP streamable** e base clínica **fictícia**, para desenvolver e testar agentes enquanto a integração real com o TASI não existe.

> ⚠️ Todos os dados são inventados. Nenhum paciente, CPF, profissional ou resultado de exame aqui corresponde a pessoas ou registros reais.

## Início rápido

```bash
npm install && npm run seed && npm run dev
```

O servidor sobe em `http://localhost:4000/mcp`. Confira com:

```bash
curl -s localhost:4000/health
```

## Tools

| Tool | Entrada | O que devolve |
|---|---|---|
| `search_patients` | `query`, `context?`, `limit?` | Pacientes cujo nome casa com a busca, com os IDs de cada contexto. **Ponto de entrada** das demais tools. |
| `get_inpatient_full` | `inpatient_id` \| `patient_id`, `sections?` | Prontuário completo da internação. |
| `get_patient_profile` | `patient_id` | Cadastro + linha do tempo de todos os atendimentos. |
| `get_encounter_details` | `encounter_id` | Blocos clínicos de um atendimento ambulatorial. |
| `get_ps_patient` | `ps_visit_id` \| `patient_id` | Ficha completa de uma passagem pelo pronto-socorro. |

### Fluxo típico de um agente

```
search_patients("maria")                    → PAC-0001, internado, INT-0001
  ├─ get_patient_profile("PAC-0001")        → linha do tempo cruzando PS, internação e ambulatório
  ├─ get_inpatient_full("INT-0001")         → prontuário completo da UTI
  └─ get_ps_patient("PS-0001")              → como o caso chegou ao hospital
```

### Detalhes de comportamento

**`search_patients`** ignora acentos e maiúsculas (`jose` acha `José Carlos Ferreira`) e exige que **todos** os termos apareçam no nome — `maria souza` não traz todas as Marias. Ranqueia nome exato > palavra exata > prefixo > substring. `context` filtra por `internacao`, `ambulatorial`, `ps` ou `todos`.

**`get_inpatient_full`** aceita `sections` para trazer só os blocos que interessam e economizar contexto do agente:

```json
{ "inpatient_id": "INT-0001", "sections": ["analises_ia", "intercorrencias"] }
```

Seções disponíveis: `sinais_vitais`, `laboratorio`, `exames_imagem`, `intercorrencias`, `enfermagem`, `evolucoes_medicas`, `plano_terapeutico`, `notas_round`, `analises_ia`. O cabeçalho da internação e os dados do paciente vêm sempre.

**`get_inpatient_full`** e **`get_ps_patient`** aceitam `patient_id` no lugar do ID específico e resolvem para o registro **ativo** (internação em curso / passagem em andamento) ou, na ausência dele, para o mais recente.

**Erros** — ID inexistente devolve resultado com `isError: true` e mensagem explícita em português, nunca um objeto vazio. O agente consegue distinguir "não encontrei" de "encontrei e está vazio".

## A base fictícia

18 pacientes, 7 internações, 23 atendimentos ambulatoriais e 8 passagens pelo PS. Nomes brasileiros, CID-10, medicações, exames e séries temporais de sinais vitais plausíveis.

A base cobre casos deliberadamente variados: choque séptico em deterioração na UTI, pós-infarto em melhora, ICC compensando, pancreatite pré-cirúrgica, pós-operatório de fratura com delirium, pé diabético infectado, crise asmática em atendimento agora, dor torácica em investigação, e seguimentos ambulatoriais longos (artrite reumatoide, hipotireoidismo, DPOC, demência).

**Pacientes aparecem em mais de um contexto.** `PAC-0001` e `PAC-0006` têm a trilha completa PS → internação (`ps_visit_id` na internação, `internacao_gerada` na ficha do PS); `PAC-0002` tem consultas ambulatoriais anteriores ao infarto que o internou — inclusive um retorno em que faltou, com o teste ergométrico positivo anexado. São os casos que exercitam o cruzamento de histórico pelo agente.

### Regenerar ou editar

Os JSON em `data/` são **gerados** a partir dos casos curados em `scripts/cases/`:

```bash
npm run seed
```

A saída é determinística: os timestamps são ancorados numa data de referência fixa (`REF` em `scripts/seed-utils.ts`) e os valores usam PRNG com seed fixa. Rodar duas vezes produz exatamente o mesmo JSON.

Para adicionar um paciente ou caso, edite os arquivos em `scripts/cases/` e rode `npm run seed`. O gerador valida integridade referencial e IDs duplicados antes de escrever — referência quebrada falha o comando.

## Configuração

| Variável | Padrão | Descrição |
|---|---|---|
| `PORT` | `4000` | Porta HTTP. |
| `HOST` | `0.0.0.0` | Interface de bind. |
| `API_TOKEN` | *(vazio)* | Se definida, `/mcp` exige `Authorization: Bearer <token>`. Vazia = servidor aberto. |
| `DATA_DIR` | `<cwd>/data` | Diretório dos JSON da base. |

`/health` nunca exige token — é o endpoint que o Coolify usa para healthcheck.

## Docker

```bash
docker compose up --build
```

Ou direto:

```bash
docker build -t medical-mcp-fake . && docker run -p 4000:4000 -e API_TOKEN=seu-token medical-mcp-fake
```

### Deploy no Coolify

1. Nova aplicação → **Dockerfile** (ou Docker Compose) apontando para este repositório.
2. Porta exposta: **4000**.
3. Healthcheck: `GET /health`.
4. Variável de ambiente `API_TOKEN` — recomendado, já que o serviço fica exposto na internet.

O endpoint MCP fica em `https://<seu-dominio>/mcp`.

## Conectando um agente

O servidor roda em **modo stateless**: cada `POST /mcp` cria transporte e sessão próprios e os descarta ao final. Não há sessão a manter, porque a base é somente-leitura. `GET` e `DELETE` em `/mcp` respondem `405`.

Exemplo de configuração de cliente MCP:

```json
{
  "mcpServers": {
    "tasi-fake": {
      "type": "http",
      "url": "https://<seu-dominio>/mcp",
      "headers": { "Authorization": "Bearer <API_TOKEN>" }
    }
  }
}
```

Chamada crua, para depurar sem cliente MCP:

```bash
curl -s -X POST localhost:4000/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search_patients","arguments":{"query":"maria"}}}'
```

## Estrutura

```
src/
  index.ts          bootstrap HTTP, healthcheck, rotas MCP
  auth.ts           bearer token opcional
  server.ts         McpServer e registro das tools
  db/
    types.ts        modelo de domínio
    load.ts         carga dos JSON e índices em memória
    search.ts       normalização e pontuação de nomes
  tools/            uma tool por arquivo + helpers de resposta
scripts/
  seed-utils.ts     PRNG, séries de vitais, painéis laboratoriais
  cases/            casos clínicos curados
  generate-seed.ts  valida e escreve data/*.json
data/               a base gerada (versionada)
```

## Scripts

| Comando | Ação |
|---|---|
| `npm run seed` | Regenera `data/` a partir de `scripts/cases/`. |
| `npm run dev` | Servidor em modo watch. |
| `npm run build` | Compila para `dist/`. |
| `npm start` | Roda o build. |
| `npm run typecheck` | Checagem de tipos sem emitir. |

## Limitações conhecidas

- **Somente leitura.** Nenhuma tool grava; não há simulação de prescrição ou evolução criada pelo agente.
- **Sem testes automatizados** nesta versão, por decisão de escopo. O gerador valida integridade referencial, e os tipos cobrem o contrato das tools.
- **Datas ancoradas** em `REF` (`scripts/seed-utils.ts`). Com o passar dos meses os casos "de agora" envelhecem; ajuste `REF` e rode `npm run seed` para atualizá-los.
- **Campos aproximados.** A modelagem é plausível clinicamente, mas não replica o esquema real do TASI. Quando o contrato real aparecer, o ponto de ajuste é `src/db/types.ts` mais as tools.