simlock
병렬 코딩 에이전트가 동일 시뮬레이터를 놓고 충돌하지 않도록 제어 평면 제공
Anvisa 규제 조회용 오픈소스 MCP 서버로 약품 등록 상태와 SaMD AI 탐지를 제공
Servidor MCP open source para consulta regulatória da Anvisa: status de registro de medicamentos e detecção heurística de SaMD com IA, pelo conector hospedado ou instalado na sua máquina. Um projeto I
개발 도구Python · JinjaGitHub @fabianofilho온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Servidor MCP para consulta regulatória da Anvisa: status de registro de medicamentos e dispositivos médicos recentes com classificação heurística de uso de IA
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Servidor MCP que expõe duas consultas à base de dados abertos da Anvisa a um LLM: status de registro de medicamentos e busca de dispositivos médicos recentes, com uma tentativa de classificar quais deles usam IA. Roda local, contra um LLM que você mesmo hospeda, ou pelo connector público descrito abaixo.
⚠️ Não é fonte oficial
- Não substitui a consulta ao portal da Anvisa. Os dados vêm dos arquivos abertos publicados pela agência e podem estar defasados em relação ao sistema oficial.
- A classificação de uso de IA é heurística, feita por um LLM lendo texto livre. A Anvisa não publica campo estruturado sobre isso. Cada resposta traz confiança e justificativa justamente porque o veredito pode estar errado.
- Não houve validação clínica ou regulatória deste software. Ele não foi avaliado por nenhum órgão e não é um dispositivo médico.
- Para qualquer decisão regulatória, use o registro oficial no portal da Anvisa.
Há uma instância rodando este código, aberta, que dá para adicionar ao Claude como custom connector sem instalar nada:
https://mcp.iamed.cc/anvisa/mcp
Condições de uso:
coletado_em, a hora da última coleta, e avisa quando ela passou de 48
horas. Não substitui o portal da Anvisa (ver o aviso acima).limite de 1 a 200 nas duas tools e dias de 1 a 3650.
Valores fora da faixa são recusados.nao_classificado.warning); o que
vai para o log é a origem da requisição quando ela passa do limite de taxa.| O quê | Versão | Para quê |
|---|---|---|
| Python | 3.12+ | runtime |
| uv | recente | dependências e venv |
| Um LLM local com API OpenAI-compatible | - | classificar dispositivos (llama.cpp, Ollama, LM Studio) |
| Espaço em disco | ~500 MB | base DuckDB (~42 MB) + dependências |
Não é preciso servidor de banco: o DuckDB é um arquivo.
git clone https://github.com/fabianofilho/anvisa-mcp.git
cd anvisa-mcp
uv sync
cp .env.example .env
Edite o .env com o endereço do seu LLM:
| Variável | Padrão | Observação |
|---|---|---|
QWEN_ENDPOINT | http://127.0.0.1:8080/v1 | llama.cpp. Ollama: :11434/v1. LM Studio: :1234/v1 |
QWEN_MODEL | local-model | llama.cpp e LM Studio aceitam qualquer nome; o Ollama exige o exato |
DUCKDB_PATH | ./data/anvisa.duckdb | onde a base local fica |
QWEN_TIMEOUT_SEGUNDOS | 120 | timeout por chamada ao LLM |
Confirme que o LLM responde e baixe os dados:
uv run anvisa-cli llm # deve imprimir a resposta do modelo
uv run anvisa-cli sync # ~1 min: baixa os dois CSVs da Anvisa
uv run anvisa-cli schema # contagens da base
O sync é idempotente: rode de novo quando quiser atualizar. A Anvisa republica os
arquivos diariamente (D-1). O código não agenda nada sozinho: para rodar todo dia, use o
timer systemd de deploy/ (ver Rodar como serviço) ou o agendador
que preferir chamando anvisa-cli sync.
Para classificar os dispositivos e gravar os vereditos na base:
uv run anvisa-cli classificar --dias 365 # só o que ainda não tem veredito
Troque /caminho/para/anvisa-mcp pelo caminho real do clone. Os caminhos precisam ser
absolutos: o servidor é lançado de qualquer diretório, então ./data/anvisa.duckdb
relativo não resolveria.
claude mcp add anvisa --scope user \
-e DUCKDB_PATH=/caminho/para/anvisa-mcp/data/anvisa.duckdb \
-e QWEN_ENDPOINT=http://127.0.0.1:8080/v1 \
-e QWEN_MODEL=local-model \
-- uv --directory /caminho/para/anvisa-mcp run anvisa-mcp
consultar_status_medicamento(termo: str, limite: int = 20)Busca por trecho do nome comercial ou do princípio ativo, ignorando acento e caixa, ou
pelo número de registro completo, com ou sem pontos (1.1819.0006 e 118190006 dão o
mesmo resultado; o número de 13 dígitos da apresentação também serve). Devolve todos os
registros que casam: grafias variam e a ambiguidade é de quem pergunta. Registros ativos
vêm primeiro, porque dois terços da base são inativos.
limite vai de 1 a 200. total é quantos casam na base inteira, e truncado=true diz
que há mais do que veio: "dipirona" casa com 557 registros, um por detentor e
apresentação. situacao vem como a Anvisa publica, Ativo ou Inativo, e
data_situacao é a data de vencimento do registro.
Saída real, de 24/09/2026:
{
"termo_consultado": "1.1819.0006",
"fonte": "duckdb",
"coletado_em": "2026-09-24T03:22:04.857697",
"total": 1,
"retornados": 1,
"truncado": false,
"resultados": [
{
"numero_registro": "118190006",
"nome_produto": "DELTALAB",
"principio_ativo": "deltametrina",
"empresa_detentora": "92265552000905 - MULTILAB INDUSTRIA E COMERCIO DE PRODUTOS FARMACEUTICOS LTDA",
"situacao": "Ativo",
"data_situacao": "2029-02-01",
"categoria": "Similar",
"visto_na_ultima_coleta": true
}
],
"aviso": null
}
Termo sem correspondência devolve resultados: [], total: 0 e um aviso. Dados de
exemplo, com fonte: "mock" e nomes terminados em "(EXEMPLO MOCK)", só aparecem quando a
base não existe, está vazia ou não pôde ser aberta.
buscar_samd_recentes(dias: int = 90, apenas_com_ia: bool = True, apenas_software: bool = True, limite: int = 50)Dispositivos Classe III/IV registrados na janela, com um veredito heurístico sobre uso de
IA. dias vai de 1 a 3650 e limite de 1 a 200. Trecho de uma saída real, de
24/09/2026, no connector (dias=3650, limite=200):
{
"dias": 3650,
"apenas_com_ia": true,
"fonte": "duckdb",
"coletado_em": "2026-09-24T03:22:40.940760",
"total": 310,
"analisados": 200,
"retornados": 1,
"truncado": true,
"indeterminados": 158,
"resultados": [
{
"numero_registro": "80102513702",
"nome_produto": "CAD4TB",
"classe_risco": "III",
"situacao": "29/06/2036",
"validade_registro": "2036-06-29",
"data_registro": "2026-06-29",
"classificacao": {
"usa_ia": true,
"confianca": 0.6,
"justificativa": "O nome 'CAD4TB' sugere uso de CAD (Computer-Aided Diagnosis) para tuberculose, e o fabricante 'DELFT AI B.V.' indica forte probabilidade de uso de IA, mas a documentação fornecida é insuficiente para confirmar o funcionamento técnico.",
"evidencia_confere": null,
"origem": "cache",
"heuristica": true
}
}
]
}
total é o universo da janela depois do filtro de software, analisados é quantos esta
chamada olhou e indeterminados conta os que ficaram de fora por confiança baixa ou por
não terem veredito, que não é o mesmo que "não usa IA". evidencia_confere é false
quando o modelo citou como evidência algo que não está no registro, e null nos
vereditos gravados antes dessa checagem existir.
O servidor MCP nunca grava na base, em nenhum modo. Em stdio, com o LLM no ar, ele
classifica na hora o que não está no cache e avisa que o veredito não ficou gravado; para
gravar, rode anvisa-cli classificar. Assim um cliente stdio apontado para a mesma base de
um connector não disputa o lock de escrita com ele.
Por padrão o servidor fala stdio: o cliente sobe o processo na máquina de quem usa.
Com TRANSPORTE=streamable-http, ele vira um servidor alcançável pela rede, que é o que
o Claude aceita como custom connector.
TRANSPORTE=streamable-http HTTP_HOST=127.0.0.1 HTTP_PORTA=8000 uv run anvisa-mcp
Ele escuta no loopback. Para expor, ponha um proxy reverso com TLS ou um túnel na frente (ver abaixo).
| Variável | Padrão | Observação |
|---|---|---|
TRANSPORTE | stdio | streamable-http liga o modo connector |
HTTP_HOST / HTTP_PORTA / HTTP_PATH | 127.0.0.1 / 8000 / /mcp | |
HTTP_STATELESS | true | cada requisição independente; escala melhor |
HTTP_LIMITE_GLOBAL_POR_MINUTO | 1200 | o teto que protege a máquina |
HTTP_LIMITE_POR_MINUTO | 600 | por origem, contra chamada direta |
Por que dois limites. Quando o Claude chama um connector remoto, as requisições chegam dos IPs da Anthropic, não do usuário final. Limitar só por IP colocaria todos os usuários no mesmo balde: ou derruba todo mundo junto, ou não protege nada. O teto global é o que vale para esse tráfego; o por origem serve contra quem chama o servidor direto.
buscar_samd_recentes normalmente pede a um LLM local que leia o texto do registro e
julgue se o produto usa IA. Num connector isso não se sustenta: cada usuário pagaria a
espera de uma fila de GPU compartilhada, e a classificação grava no DuckDB, que recusa
abrir para escrita enquanto houver leitor.
Então, com TRANSPORTE=streamable-http, o servidor serve só o que já está no cache e
diz isso na resposta. Quem classifica é a coleta, fora do processo do servidor:
uv run anvisa-cli sync --publicar --classificar --dias-classificacao 3650
--classificar é o único momento em que o LLM entra num deploy de connector. Sem ele a
coleta atualiza os registros mas não julga os novos, e eles chegam ao servidor como
nao_classificado. A janela padrão de --dias-classificacao é 365 dias; o deploy oficial
usa 3650 para que as consultas de até 10 anos venham classificadas. Fora da janela, o
esperado é nao_classificado.
Cada item traz de onde veio o veredito, em origem_classificacao:
| Valor | Significado |
|---|---|
llm | classificado agora (só acontece em stdio, e não fica gravado) |
cache | classificado numa coleta anterior |
nao_classificado | connector sem o item no cache: indeterminado, não é "não usa IA" |
indisponivel | LLM configurado mas fora do ar |
A diferença importa. Um registro que ninguém classificou ainda não é um registro sem IA, e a resposta nunca conta os dois juntos.
O SDK do MCP valida o cabeçalho Host e responde 421 Invalid Host ao que não
reconhece. É proteção contra DNS rebinding, um ataque em que um site qualquer faz o
navegador da vítima conversar com um servidor que só deveria ser local.
Quando o servidor fica atrás de um túnel, o Host que chega é o nome público, não
127.0.0.1, e toda requisição legítima leva 421. A saída certa é declarar o nome, não
desligar a checagem:
HTTP_HOSTS_PUBLICOS=mcp.exemplo.ts.net
Aceita vários separados por vírgula. O loopback continua valendo junto, porque é assim que se testa o servidor de dentro da máquina.
deploy/ traz as três units de usuário que rodam o connector oficial:
| Unit | O que faz |
|---|---|
anvisa-connector.service | servidor HTTP na porta 8101, só loopback, Restart=always |
anvisa-sync.service | sync --publicar --classificar --dias-classificacao 3650 --reclassificar |
anvisa-sync.timer | dispara o sync todo dia às 03:20, com até 30 min de espalhamento |
Elas esperam o repositório em ~/anvisa-mcp e o uv em ~/.local/bin; ajuste os caminhos
se o seu layout for outro. Em anvisa-connector.service, troque HTTP_HOSTS_PUBLICOS pelo
nome público da sua instância. Para instalar:
cp deploy/anvisa-connector.service deploy/anvisa-sync.service deploy/anvisa-sync.timer \
~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now anvisa-connector.service anvisa-sync.timer
O connector escuta só em 127.0.0.1. Expor para fora é uma camada à parte, um proxy reverso com TLS ou um túnel, que aponta para essa porta. Manter assim deixa a decisão de expor num lugar só, em vez de espalhada em variável de ambiente. Se o processo morrer, o systemd sobe de novo em 5 segundos.
O connector só lê. Quem escreve é o sync, que roda separado e troca o arquivo por rename.
--reclassificar refaz uma vez os vereditos gravados antes da checagem de evidência
(evidencia_confere nulo), inclusive os que estão fora da janela ou do filtro de software;
depois que todos passaram por ela, a opção não custa nada.
Não há aviso externo quando o sync falha: o connector segue servindo a base anterior, e a
falha aparece em journalctl --user -u anvisa-sync e no coletado_em das respostas, que
ganham aviso quando a coleta passa de 48 horas.
Risco conhecido: o connector roda o código da árvore de trabalho. O ExecStart sobe o
que estiver em ~/anvisa-mcp no momento de cada restart, inclusive o automático. Se essa
pasta também for onde se desenvolve, trocar de branch ali muda o que está em produção no
próximo restart. Para isolar, aponte a unit para um checkout dedicado, por exemplo um git worktree fixo na main, mantendo DUCKDB_PATH na base que o sync publica.
O DuckDB recusa abrir para escrita enquanto houver um leitor, e no modo connector o servidor abre a base a cada requisição. Escrever direto no arquivo servido falharia sempre que a coleta caísse em cima de uma consulta.
Por isso o sync usa --publicar: constrói a base ao lado e troca por os.replace, que é
atômico no POSIX. Quem já abriu continua no arquivo antigo até fechar, o tempo de uma
requisição; quem abrir depois pega o novo.
A base ao lado começa como cópia da que está sendo servida, não vazia. Nem tudo na base
vem do dataset: o cache de classificações custou uma chamada de LLM por linha, e os
registros que sumiram do arquivo da Anvisa continuam lá, marcados como não vistos na última
coleta. Construir do zero jogaria os dois fora. Medido uma vez, sem a cópia: 109
classificações e 13 registros a menos, publicados sem um aviso. A cópia é feita pelo próprio
DuckDB (COPY FROM DATABASE), porque um cp pegaria o arquivo sem o WAL pendente.
uv run anvisa-cli sync --publicar --classificar # coleta, julga os novos e troca no fim
uv run anvisa-cli classificar --publicar --dias 365 # só enche o cache, sem recoletar
uv run anvisa-cli sync --publicar --forcar # aceita base menor que a servida
Com um connector lendo a base, use sempre --publicar, também no classificar: sem ele
a CLI tenta escrever direto no arquivo servido, o DuckDB recusa, e ela sai com código 1
dizendo que a classificação não foi gravada.
A publicação é recusada quando a base nova encolhe mais de 10%. Com a cópia acima, uma
coleta interrompida já não produz base pequena: ela só deixa de atualizar. A checagem fica
como rede de segurança para o que a cópia não cobre, como um clone que falhou pela metade
ou uma remoção em massa vinda da fonte. A versão trocada fica como .anterior, e
store.troca.reverter() volta atrás.
O repositório não traz a base pronta. Quem clona roda o próprio sync; quem hospeda um connector serve a sua. Os dados vêm do portal de dados abertos da Anvisa, que é público, então qualquer pessoa consegue montar a sua, mas a base construída e classificada é trabalho de organização, não faz parte do código.
O registro de dispositivos não tem campo de descrição. O texto que alimenta a
classificação é nome técnico + nome comercial + fabricante. É pouco. Um produto cujo nome
não diga o que ele faz será classificado com confiança baixa, e confiança baixa significa
"não dá para saber", não "não usa IA". Por isso a resposta traz indeterminados.
apenas_software=True troca recall por custo. No último ano há 1.832 registros Classe
III/IV e só 13 mencionam software. Sem esse pré-filtro por palavra-chave, uma varredura
dos 60 registros mais recentes encontra zero software, são cânulas, parafusos e testes
rápidos. Com ele, um produto que use IA sem dizer "software", "algoritmo" ou "CAD" fica de
fora. Use apenas_software=False para varrer tudo, ao custo de uma chamada de LLM por
registro.
A calibração de confiança do LLM é parcial. Modelos pequenos tendem a responder com confiança alta mesmo quando o texto não sustenta. O prompt fixa faixas de confiança por situação, o que melhorou bastante (reagentes saem com 0,9, nomes ambíguos com 0,2), mas não resolve por completo.
A base guarda o último estado visto, e não remove nada. O sync é upsert: um registro
que a Anvisa tira da publicação continua na base com a situação da última vez que apareceu.
Registro que sai da publicação costuma ter sido cancelado, então cada resultado traz
visto_na_ultima_coleta, e a resposta ganha um aviso quando algum vier false. Aconteceu
de verdade: entre 20 e 21/09/2026, 13 dispositivos sumiram do arquivo publicado.
anvisa-cli schema mostra a contagem, e o sync loga um aviso quando há registros assim.
Medicamentos sem número de registro não entram. O arquivo inclui produtos notificados (baixo risco), que não têm registro, e a pergunta "qual o status do registro" não se aplica a eles.
As URLs dos datasets podem mudar. Elas estão em data/sources.py, confirmadas em
20/09/2026 baixando os arquivos. Se uma sair do ar, o sync falha com mensagem explicando
como reconfirmar, em vez de baixar o arquivo errado em silêncio.
O DuckDB aceita um escritor por vez. Com --publicar, o sync e o classificar
escrevem numa cópia e as consultas seguem lendo a base servida o tempo todo. Sem
--publicar, enquanto a CLI escreve na base, as tools não conseguem abri-la e caem para
dados de exemplo, com um aviso que diz que a base está ocupada, não vazia.
A classificação cobre uma janela. Só os dispositivos dentro de --dias-classificacao
que passam no filtro de software recebem veredito na coleta. No connector oficial a janela
é de 10 anos; numa instância com o padrão, 365 dias.
Parte dos vereditos antigos não passou pela checagem de evidência. Eles vêm com
evidencia_confere: null até o sync com --reclassificar refazê-los. Essa passada parte
do próprio cache, sem janela nem filtro de software, então alcança também os vereditos
gravados por consultas com apenas_software=False. Se o LLM estiver fora nessa hora, o
veredito antigo continua servindo e volta na coleta seguinte.
Rodando local:
dados.anvisa.gov.br, para baixar os CSVs
públicos. Nada mais.Pelo connector público, os termos e parâmetros de cada consulta chegam ao servidor, como em qualquer serviço remoto.
Veja CONTRIBUTING.md. Em resumo: rode pytest, ruff e mypy antes do
PR, e não rode sincronizações em loop contra os dados abertos da Anvisa.
Apache License 2.0: escolhida por tocar em regulação de dispositivo médico, onde a cláusula explícita de patente é mais protetiva.
Construído no contexto do IA.med.