FHIR-backed MCP server powering the LookCloser closed-loop safety net for incidental imaging findings. Stateless tool layer over HAPI FHIR R4. The four LookCloser agents (Significance Classifier, Recipient Resolver, Closure Auditor, Escalator) all share these tools.
uv sync # or: python -m venv .venv && pip install -e ".[dev]"
cp .env.example .envDefaults to https://hapi.fhir.org/baseR4 — override FHIR_BASE_URL in .env.
python -m lookcloser_mcp # stdio (for local MCP clients)
python -m lookcloser_mcp --http --port 8080 # streamable HTTP (for Prompt Opinion)
mcp-inspector python -m lookcloser_mcp # interactive tool explorationSeeds two patients onto HAPI sandbox: Diane Suryanto (adrenal incidentaloma, open loop) and Hari Wibowo (pulmonary nodule, closed loop). Each run produces fresh patients with run-unique identifiers — no clean-up required between runs.
python scripts/seed_fhir.py # phases 1+2 (patients + post-state)
python scripts/seed_fhir.py --phase 1 # just the patients
python scripts/seed_fhir.py --phase 2 # post-state for Diane (run after platform creates Task)Resource IDs are written to demo-data/patient_ids.json. Hand the relevant IDs to your MCP client / Prompt Opinion agents.
uv run pytest # 11 unit tests, mocked httpx (~8s)
uv run pytest -m integration # live HAPI E2E for Diane audit flow| Tool | Purpose |
|---|---|
fhir_get_patient_summary |
Demographics + active conditions/meds/allergies — call first |
fhir_get_diagnostic_report |
Full report body (decoded from base64 presentedForm) |
fhir_search_diagnostic_reports |
Recent reports, metadata + conclusion only |
fhir_get_care_team |
PCP + active specialists; falls back to encounter participants |
fhir_search_service_requests |
Orders by code/intent/status/category/date |
fhir_search_clinical_notes |
DocumentReference text — load-bearing for the Auditor |
fhir_search_encounters |
Past or planned encounters |
fhir_create_task |
POST a tracked Task (with structured_recommendation extension) |
fhir_search_tasks |
Find Tasks (decodes structured_recommendation extension back to dict) |
fhir_update_task_status |
Read-modify-write businessStatus + append note |
finding_followup_guidance |
Naive lookup against an embedded ~7-entry corpus |
All tool returns include a top-level provenance: {fhir_server, fetched_at}. FHIR errors return {"error": "fhir_unavailable", "detail": "...", "provenance": {...}} instead of raising. patient_id accepts either "Patient/123" or bare "123".
The MCP must be reachable from Prompt Opinion. Three options:
# Terminal 1 — run the MCP over HTTP
python -m lookcloser_mcp --http --port 8080
# Terminal 2 — expose it
cloudflared tunnel --url http://localhost:8080Use the printed https://*.trycloudflare.com/mcp URL in Prompt Opinion.
fly launch --no-deploy # accepts fly.toml as-is; pick org/region
fly deployPublic URL: https://lookcloser-mcp.fly.dev/mcp
railway init && railway upSet start command to uv run python -m lookcloser_mcp --http --port $PORT --host 0.0.0.0 if Railway doesn't pick it from the Dockerfile.
The MCP correctly returns HTTP 400 {"error":{"code":-32600,"message":"Bad Request: Missing session ID"}} for an unauthenticated POST to /mcp — that's the protocol-compliant response before initialize. Use the MCP Inspector to actually call tools.
src/lookcloser_mcp/
server.py # FastMCP server, registers all 11 callables
fhir_client.py # async httpx wrapper
schemas.py # pydantic return models (documentation only)
tools/ # one module per tool group
guidance/corpus.json # 7 follow-up snippets
demo-data/ # 3 transaction Bundles + post-seed state
scripts/seed_fhir.py # POSTs bundles, harvests assigned IDs
tests/test_tools.py # 11 unit + 1 live integration