#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Servidor MCP do Maternidades Abertas. Dá a um assistente de IA (Claude, ChatGPT, …) acesso ao estado das urgências de obstetrícia e ginecologia das maternidades portuguesas, a partir da API pública em https://maternidadesabertas.pt/api Duas ferramentas: - estado_das_maternidades(dia, filtro) - maternidade_aberta_mais_proxima(latitude, longitude, dia, limite) PRINCÍPIO: este servidor **não interpreta nem aconselha**. Serve os dados tal como a API os dá, sempre com a hora da recolha e com o aviso de que a informação é informal — porque quem a vai ler em voz alta é um modelo, e o que não vier no texto não é dito. Pelo mesmo motivo, uma falha devolve um erro claro e NUNCA uma lista parcial: na dúvida, não mostrar. Instalar e correr (ficheiro único, não precisa de mais nada do projeto): pip install "mcp>=2" python servidor.py No Claude Desktop (Definições -> Developer -> Edit Config), acrescentar: {"mcpServers": {"maternidades-abertas": { "command": "python", "args": ["/caminho/para/servidor.py"]}}} Config: MATERNIDADES_API (por omissão https://maternidadesabertas.pt) MATERNIDADES_TIMEOUT (por omissão 20 segundos) Documentação da API e desta ferramenta: https://maternidadesabertas.pt/api """ import json import math import os import urllib.error import urllib.parse import urllib.request # SDK oficial, versão 2.x (no 1.x esta classe chamava-se FastMCP e vivia em # `mcp.server.fastmcp`). O `requirements.txt` desta pasta fixa `mcp>=2`. from mcp.server.mcpserver import MCPServer BASE = os.environ.get("MATERNIDADES_API", "https://maternidadesabertas.pt").rstrip("/") TIMEOUT = float(os.environ.get("MATERNIDADES_TIMEOUT", "20")) AGENTE = "maternidades-abertas-mcp/1.0 (+https://maternidadesabertas.pt/api)" # Repetido em cada resposta de propósito: é a única forma de garantir que o # modelo que lê isto passa o aviso ao utilizador. AVISO = ("IMPORTANTE: informacao informal e NAO oficial, pode estar desatualizada — " "confirmar sempre antes de se deslocar. Emergencia: 112. " "Duvidas ou encaminhamento: SNS 24 — 808 24 24 24.") # `referenciada` (CODU/INEM) NAO conta como a funcionar: so recebe quem o INEM # encaminha, e ninguem se deve deslocar la por iniciativa propria. `info` e # desconhecido e tambem nao conta — ver as regras de seguranca do projeto. A_FUNCIONAR = ("aberta", "condicionada") # As `instructions` são lidas pelo modelo ANTES de ele usar as ferramentas — é o # sítio mais eficaz para pôr as regras, melhor do que esperar que leia os avisos # só depois, no meio do JSON. _INSTRUCOES = """Dados sobre as urgencias de obstetricia e ginecologia das maternidades portuguesas, do portal do SNS, via maternidadesabertas.pt. Ao usar estas ferramentas: - Indica SEMPRE a hora da ultima atualizacao (campo `atualizado`) e que a informacao e informal e deve ser confirmada antes de alguem se deslocar. - NUNCA apresentes um estado como garantido, nem adivinhes o estado de uma maternidade que nao venha nos dados. - `classe: info` significa DESCONHECIDO — nunca o leias como aberta. `classe: referenciada` (CODU/INEM) tambem NAO e aberta: so recebe quem o INEM encaminha. - `classe: condicionada` exige ligar primeiro ao SNS 24. - Se uma ferramenta devolver `erro`, nao tentes compensar com conhecimento teu: encaminha para o SNS 24 (808 24 24 24) ou 112. - Perante sinais de urgencia (trabalho de parto, perda de liquido, hemorragia, reducao de movimentos do bebe), a resposta e ligar ao SNS 24 ou 112 — nao e uma lista de maternidades. - Nao fornecas aconselhamento clinico: estas ferramentas dao estado de servicos, nao orientacao medica. - Ao citar, atribui a maternidadesabertas.pt (CC BY 4.0).""" servidor = MCPServer("maternidades-abertas", version="1.0.0", title="Maternidades Abertas", instructions=_INSTRUCOES, website_url="https://maternidadesabertas.pt/api") def _buscar(caminho: str, params: dict | None = None) -> dict: url = BASE + caminho + (("?" + urllib.parse.urlencode(params)) if params else "") pedido = urllib.request.Request(url, headers={"User-Agent": AGENTE, "Accept": "application/json"}) with urllib.request.urlopen(pedido, timeout=TIMEOUT) as r: return json.loads(r.read().decode("utf-8")) def _erro(msg: str) -> dict: """Erro explícito. Nunca devolver dados parciais quando algo falhou.""" return {"erro": msg, "o_que_fazer": ("Nao inventar nem adivinhar o estado de nenhuma maternidade. " "Encaminhar para o SNS 24 (808 24 24 24), para o 112 em emergencia, " "ou para https://maternidadesabertas.pt"), "aviso": AVISO} def _estado_efetivo(h: dict) -> tuple: """(estado, classe) do hospital. Usa a 1.ª faixa horária quando o dia tem várias — o detalhe completo vai no campo `faixas` da resposta.""" return h.get("estado") or "sem informacao", h.get("classe") or "info" def _haversine_km(lat1, lng1, lat2, lng2) -> float: r = 6371.0 p1, p2 = math.radians(lat1), math.radians(lat2) dp, dl = math.radians(lat2 - lat1), math.radians(lng2 - lng1) a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2 return 2 * r * math.asin(math.sqrt(a)) def _dia_normalizado(dia: str) -> str: d = (dia or "").strip().lower() if d in ("", "hoje", "today"): return "Hoje" if d in ("amanha", "amanhã", "tomorrow"): return "Amanhã" return dia.strip() @servidor.tool() def estado_das_maternidades(dia: str = "hoje", filtro: str = "") -> dict: """Estado das urgências de obstetrícia/ginecologia das maternidades portuguesas. Args: dia: "hoje", "amanha" ou uma data "dd/mm/aaaa". Dias passados devolvem um registo histórico, que NAO diz nada sobre o estado atual. filtro: texto opcional para filtrar por nome de hospital ou região (ex.: "Porto", "Santa Maria"). Vazio devolve todas. Devolve o estado por maternidade, a hora da recolha e o aviso obrigatório. """ alvo = _dia_normalizado(dia) try: d = _buscar("/api/urgencias", {"dia": alvo}) except urllib.error.HTTPError as e: if e.code == 503: return _erro("A fonte de dados (portal do SNS) esta indisponivel neste momento. " "Nao ha informacao fiavel para dar sobre " + alvo + ".") return _erro(f"A API respondeu {e.code} ao pedir o dia {alvo}.") except (urllib.error.URLError, TimeoutError, ValueError) as e: return _erro(f"Nao foi possivel contactar a API do Maternidades Abertas ({e}).") if d.get("erro"): return _erro(str(d["erro"])) hospitais = d.get("hospitais") or [] if filtro: f = filtro.strip().lower() hospitais = [h for h in hospitais if f in (h.get("hospital") or "").lower() or f in (h.get("uls") or "").lower()] itens = [] for h in hospitais: estado, classe = _estado_efetivo(h) item = {"hospital": h.get("hospital"), "regiao": h.get("uls"), "tipo": h.get("tipo"), "estado": estado, "classe": classe, "horario": h.get("horario"), "telefone": h.get("telefone") or None} if len(h.get("faixas") or []) > 1: item["faixas"] = h["faixas"] # o estado muda ao longo do dia if h.get("sem_partos"): item["nao_admite_partos"] = h["sem_partos"] itens.append(item) out = {"dia": d.get("dia", alvo), "total": len(itens), "maternidades": itens, "atualizado": d.get("atualizado_iso") or d.get("atualizado") or "desconhecido", "significado_das_classes": { "aberta": "a funcionar", "condicionada": "aberta, mas e preciso ligar primeiro ao SNS 24", "referenciada": "so recebe quem o INEM encaminha; NAO ir por iniciativa propria", "fechada": "nao esta a receber", "info": "DESCONHECIDO; nunca assumir que esta aberta"}, "fonte": d.get("fonte"), "aviso": AVISO} if d.get("historico"): out["atencao"] = ("Este dia JA PASSOU. E um registo historico e nao diz nada sobre o " "estado atual de nenhuma maternidade.") if d.get("nota"): out["nota"] = d["nota"] if not itens: out["nota"] = d.get("nota") or "Sem maternidades a devolver para este dia/filtro." return out @servidor.tool() def maternidade_aberta_mais_proxima(latitude: float, longitude: float, dia: str = "hoje", limite: int = 3) -> dict: """Maternidades A FUNCIONAR mais perto de umas coordenadas, por distância em linha reta. Args: latitude: latitude de quem procura (graus decimais). longitude: longitude de quem procura (graus decimais). dia: "hoje" (por omissão), "amanha" ou "dd/mm/aaaa". limite: quantas devolver (1 a 10). Só conta as que estão abertas ou em contacto prévio. As que estão "referenciada" (CODU/INEM) ficam de fora de propósito: ninguém se deve deslocar a essas por iniciativa própria. """ try: lat, lng = float(latitude), float(longitude) except (TypeError, ValueError): return _erro("Coordenadas invalidas.") if not (-90 <= lat <= 90 and -180 <= lng <= 180): return _erro("Coordenadas fora dos limites validos.") limite = max(1, min(10, int(limite or 3))) estado = estado_das_maternidades(dia=dia) if estado.get("erro"): return estado if estado.get("atencao"): # dia passado: nao serve para encaminhar return _erro("O dia pedido ja passou; um registo historico nao serve para " "escolher para onde ir. Perguntar pelo dia de hoje.") try: dirs = _buscar("/api/maternidades").get("maternidades") or [] except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, ValueError) as e: return _erro(f"Nao foi possivel obter as coordenadas das maternidades ({e}).") coords = {m["hospital"]: (m.get("lat"), m.get("lng")) for m in dirs} fichas = {m["hospital"]: m.get("ficha") for m in dirs} candidatas = [] sem_coords = 0 for m in estado["maternidades"]: if m["classe"] not in A_FUNCIONAR: continue la, lo = coords.get(m["hospital"], (None, None)) if la is None or lo is None: sem_coords += 1 continue d = _haversine_km(lat, lng, la, lo) candidatas.append({**m, "distancia_km": round(d, 1), "ficha": fichas.get(m["hospital"])}) candidatas.sort(key=lambda x: x["distancia_km"]) out = {"dia": estado["dia"], "a_partir_de": {"latitude": lat, "longitude": lng}, "maternidades": candidatas[:limite], "atualizado": estado["atualizado"], "como_foi_calculado": ("Distancia em LINHA RETA, nao de estrada: o tempo real de " "viagem pode ser bastante maior."), "atencao": ("Antes de se deslocar: ligar ao SNS 24 (808 24 24 24), que encaminha para a " "maternidade que esta a receber. As marcadas como 'condicionada' EXIGEM " "contacto previo. Em emergencia, 112."), "fonte": estado.get("fonte"), "aviso": AVISO} if sem_coords: out["nota"] = (f"{sem_coords} maternidades a funcionar ficaram de fora por nao terem " "coordenadas registadas.") if not candidatas: out["nota"] = ("Nenhuma maternidade a funcionar com coordenadas conhecidas para este dia. " "Ligar ao SNS 24 (808 24 24 24).") return out if __name__ == "__main__": servidor.run()