Skip to main content
Glama
Roy-Wanyoike

Mstari MCP Server

by Roy-Wanyoike

Mstari — the Triage Desk

Mstari (Swahili: the queue) is an agentic triage desk for busy public outpatient departments. It reads each patient's intake card, turns vitals + the presenting complaint into a cited South African Triage Scale (adult chart) priority, and sequences the queue for the triage nurse — with every priority carrying the exact vitals behind it.

It scores and sequences the queue. It never diagnoses, never prescribes, never decides who gets seen. A named clinician confirms every priority before it reaches the queue — that gate is enforced in code, not in prose.

Built for the Healthcare: The Triage Desk track of the Agentic AI for Africa Innovation Challenge. Sub-theme: symptom-based priority scoring (grounded in the SATS adult chart, the scale behind Kenya's Kenya Triage Scale), with a deterioration-watch second-look flag. Serves the OPD triage desk of a Level 2–4 public facility; workflow: registration → vitals card → Mstari scores & sequences → nurse confirms → bay.

This repository is new work built during the submission period. It is a ground-up rebuild of the author's earlier "Health-advisor" prototype, which was retired because it violated the track's non-negotiable rule (it produced diagnosis-style advice). Mstari keeps the original idea — intake in, help out — and rebuilds it inside the track's guardrails.

Contents: 1. Problem statement · 2. Solution overview · 3. Target users · 4. Architecture · 5. Setup / installation · 6. Usage · 7. Technology stack · 8. Agent architecture · 9. MCP implementation · 10. MCP tools / servers · 11. Human-in-the-loop workflow · 12. Limitations · 13. Future improvements · 14. Other

1. Problem statement

In Level 2–4 public facilities across East Africa, the outpatient department (OPD) opens onto a queue that routinely runs to dozens or hundreds of patients, met by one or two triage nurses. Prioritisation on paper is fast to teach but slow to keep consistent: two nurses can read the same vitals differently, the sickest-looking patient can sit unlabelled behind a queue that was formed by order of arrival, and the depth of the coloured bands is invisible until someone physically counts the files. The most dangerous failure is quieter: a patient who was correctly scored at 09:40 but deteriorates at 11:15 while still waiting — nothing in a paper-based desk systematically looks again. The track brief frames the problem exactly: the bottleneck is ordering, visibility and deterioration-watch at the triage desk, not diagnosis. Clinicians diagnose; the desk needs an instrument that keeps the queue honestly sorted, cites why every patient sits where they sit, and re-checks the waiting.

Related MCP server: CardioTriage MCP Server

2. Solution overview

Mstari is a LangGraph-orchestrated agent that works behind the triage nurse, not instead of her. Each patient arrives as an intake card (vitals + free-text presenting complaint). The agent reads the complaint and maps it onto the SATS adult chart discriminator list using an open-weights model (Qwen 2.5 via Ollama, running locally — with an honest deterministic keyword fallback when no LLM is configured), then computes the Triage Early Warning Score with deterministic, cited code — the model never invents a priority. The output is a sequenced queue board: RED (immediate) → ORANGE (<10 min) → YELLOW (<60 min) → GREEN (<240 min), longer-waiting patients first within a colour, every entry citing the exact vitals and discriminator codes behind it. Before any priority reaches the live queue, a named clinician must approve it at a code-enforced gate. If re-check vitals show a waiting patient has worsened, the agent raises a second-look flag citing both vital sets — it never changes the score itself. Every tool call is written to a JSONL audit log and every gate decision to a gate ledger.

3. Target users

  • Primary — the OPD triage nurse at a Level 2–4 public facility (e.g. a county referral hospital outpatient department). Mstari is a queue instrument for her desk: it proposes, she confirms, the bay follows.

  • Duty clinicians who action the gated second-look flags on waiting patients whose re-check vitals have worsened.

  • Facility records / audit officers, who get a timestamped JSONL record of every tool call and every gate decision without touching the workflow.

  • County health managers and researchers, who can reuse the eval harness (15 tasks + 1 documented failure) and the synthetic data generator to adapt the desk to their own facilities.

  • Explicitly not a target user: the patient. Mstari is deliberately not a patient-facing self-diagnosis app; nothing in it gives advice to the public.

4. Architecture

Three layers: an MCP tool layer (our own FastMCP server over stdio, plus one borrowed official server), a LangGraph orchestration layer (the agent pipeline with the human gate as a first-class node), and an LLM layer with honest, labelled lanes (local open-weights Qwen by default; deterministic heuristic fallback; optional frontier lane for synthetic data only). All artifacts land on disk per run, so every decision is replayable.

Mstari system architecture — data sources → LangGraph agent → models / MCP tools / human gate → run artifacts

Diagram source: docs/architecture.html (editable HTML, rendered to docs/architecture.png). It maps the agents, the LLM lanes, the MCP client/server and its six tools, the external APIs, the data sources, the human-in-the-loop gate, and the overall data flow.

Full one-page design (division of labour, why-agent-not-a-script, MCP built-vs-borrowed rationale): see ARCHITECTURE.md.

5. Setup / installation

Prerequisites: Python 3.10+ and git. Optional: Ollama for the open-weights LLM lane.

No terminal? The repo also ships a Vercel-ready web UI (web/) and a 2:16 walkthrough video (video/mstari-demo.mp4). The hosted desk persists its demo queue, gate ledger and audit trail to a Neon Postgres database (via Vercel, DATABASE_URL) and falls back to in-memory session storage when unset — the header pill always says which backend is live.

Deploying to Vercel? The repo ships a root vercel.json that defines the two services deployed together: core — the deterministic Python scoring core as a read-only API (api/index.py: status page, /api/health, /api/discriminators, /api/triage?patient=P-001; gated tools are never exposed over HTTP) — and web — the Next.js Triage Desk. Deploy with the project Framework Preset = services (no other overrides needed), or the desk alone with Root Directory = web. Don't hand-edit the services file casually: an invalid one blocks every build (see git history).

One command (creates a virtualenv, installs dependencies, generates the bundled synthetic intake cards, runs the evaluation suite, then runs the full agent over a 12-patient OPD queue):

./run_demo.sh

Manual install, if you prefer to drive each step:

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python -m mstari.main evals        # 15 pass + 1 documented failure (F1)
python -m mstari.main demo         # full queue run, stops at the clinician gate

Optional — enable the open-weights lane (auto-detected by run_demo.sh; patient data never leaves the machine):

ollama pull qwen2.5:7b

6. Usage

# heuristic mode (no LLM needed) — clearly labelled in the summary
./run_demo.sh

# with the named clinician who will approve priorities at the gate
./run_demo.sh "Sr. A. Wanjiru (Triage Nurse)"

# override the Ollama model
MSTARI_LLM_MODEL=qwen2.5:7b ./run_demo.sh "Sr. A. Wanjiru (Triage Nurse)"

# run the evaluation suite on its own
python -m mstari.main evals

Artifacts land in runs/<run_id>/:

Artifact

What it is

queue_board.md

the cited queue: every patient, every priority, every source vital

audit_log.jsonl

one JSON line per tool call: inputs, outputs, timestamps

gate_ledger.json

every gate decision — block or approve, approver + timestamp

draft_cards/, confirmed_cards/, recheck_flags/

the paper trail per patient

LLM modes (honest by design). With Ollama running, run_demo.sh auto-selects the open-weights lane (qwen2.5:7b by default). The open-weights model performs the full task of reading each free-text complaint and mapping it to SATS discriminator codes; the score itself is always deterministic cited code (mstari/scale.py). No endpoint configured → deterministic heuristic mode. Either way the repo runs end-to-end.

How it works — the whole flow in one pass

  1. Ingestion — a nurse records vitals + a free-text complaint on an intake card; parse_intake_record loads and normalises it. Children ≤ 12 are escalated to a clinician, never scored by the adult chart.

  2. Extraction — the complaint is mapped onto the SATS discriminator list by an open-weights Qwen model (local via Ollama, or the challenge-provided ModelScope endpoint below). No endpoint → a deterministic keyword fallback runs and the backend is labelled on every card (honest mode).

  3. Cited scoring — mstari/scale.py computes TEWS and applies discriminator overrides; every component cites the vital behind it. The score is never the model's opinion.

  4. Human gate — the graph stops; confirm_priority refuses to publish without a named clinician (block-by-default, enforced inside the MCP server); approval records who / what / when in gate_ledger.json.

  5. Queue board — confirmed priorities only, RED → GREEN, TEWS descending, longest-waiting first. Worsening re-check vitals raise a gated second-look flag citing both vital sets — it never changes the priority.

  6. Audit — every tool call lands in audit_log.jsonl with inputs, outputs and timestamps.

Qwen API configuration (challenge-provided ModelScope endpoint)

Copy .env.example to .env (git-ignored; never commit real tokens) and set:

MSTARI_LLM_BASE_URL=https://api-inference.modelscope.ai/v1
MSTARI_LLM_MODEL=Qwen-Ambassador/Qwen3.8-plus
MSTARI_LLM_API_KEY=<your ModelScope token>

Available models: Qwen-Ambassador/Qwen3.7-Max, Qwen-Ambassador/Qwen3.8-Max, Qwen-Ambassador/Qwen3.8-plus, Qwen-Ambassador/Qwen3.7-Plus. The hosted UI reads the same config as MODELSCOPE_* variables — see web/.env.example.

7. Technology stack

Layer

Choice

Why

Language

Python 3.10+

the MCP SDK and LangGraph are first-class here

Orchestration

LangGraph (langgraph>=0.2)

open-source OSS framework; the graph is inspectable and the human gate is a real node

Agent–tool protocol

MCP (mcp>=1.2,<2, FastMCP)

our own server over stdio; every action is a genuine logged tool call

Borrowed MCP server

mcp-server-fetch (official reference server)

pulls live SATS reference pages; declared in mcp_config.json with a one-line justification

Open-weights model

Qwen 2.5 (7B) via Ollama, local

performs the complaint → discriminator task on-device; no patient data leaves the machine

Optional frontier lane

ModelScope API-inference (Qwen frontier models)

synthetic-data-only by policy; never sees real patient text (there is none)

Scoring core

deterministic mstari/scale.py

cited TEWS implementation of the published SATS adult chart

Tests

pytest

11 deterministic unit tests for the scoring core

Audit

JSONL + JSON artifacts

replayable run record per run id

Licence

MIT

OSI-approved, in LICENSE

8. Agent architecture

LangGraph pipeline with a clinician gate as a first-class node: plan → intake → extract → score → human_gate → publish → rank → summarize.

  • plan decides the run shape and may pull live scale references via the borrowed fetch server; intake normalises a synthetic intake card; extract is the LLM node (complaint text → SATS discriminator codes, keyword fallback if no model); score calls the deterministic scale and writes a DRAFT card; human_gate stops the graph dead without a named approver; publish/rank write the confirmed queue board; summarize prints the run summary and the honesty labels.

Division of labour (the safety core):

  1. The LLM reads — free-text complaint → discriminator codes. That is the model's one full task.

  2. The scale scores — mstari/scale.py is deterministic, cited code. The model never invents a priority.

  3. The clinician decides — gated tools block by default; approval is a named person plus a timestamp, recorded in the gate ledger.

  4. The guardrails sweep — diagnosis-like vocabulary in any LLM narrative is redacted and logged; prescription-like phrasing is hard-blocked.

Why an agent and not a script: each node decides its next call from the previous node's output, failures degrade gracefully per node, and the graph genuinely stops at the gate when no named clinician approves. Details in ARCHITECTURE.md.

9. MCP implementation

  • Our own server (mstari/mcp_server.py): a FastMCP server over stdio exposing six tools (below). Gate semantics live inside the server, not in the agent's prompt — confirm_priority and raise_recheck_flag refuse to act without a named clinician, no matter what the graph asks for.

  • A real MCP client (mstari/mcp_client.py): a background-loop stdio client so synchronous LangGraph nodes can call tools as genuine MCP calls. Every action the agent takes is therefore a real, logged MCP tool call — the same server any external MCP client (e.g. Claude Desktop) could attach to.

  • One borrowed server: the official mcp-server-fetch, declared alongside ours in mcp_config.json with an inline one-line justification: it pulls live SATS / Kenya Triage Scale reference pages and facility-directory updates at plan time — writing our own web crawler would be fragile, duplicated work for zero clinical value. Verified callable in evals (T15).

10. MCP tools / servers

Server: mstari (built by us) — mstari/mcp_server.py

Tool

Kind

Notes

parse_intake_record

read

normalises a synthetic intake card into scored fields

list_discriminators

read

the SATS adult discriminator list with citations

score_priority

acting

computes the cited TEWS priority, writes a DRAFT card

confirm_priority

acting, GATED

publishes a priority to the live queue — named clinician required

rank_queue

acting

writes the cited queue board

raise_recheck_flag

acting, GATED

deterioration second-look flag — named clinician required

Four of six tools act on the world; the two irreversible ones are gated. Six tools ≥ the track's three-tool minimum.

Server: fetch (borrowed — the one we did not write) — official mcp-server-fetch, one tool (fetch), justified inline in mcp_config.json and verified in evals (T15).

11. Human-in-the-loop workflow

The track's rule: irreversible actions need a named human approver. In Mstari the irreversible actions are publishing a priority to the live queue and raising a re-check flag on a real patient.

  1. The graph reaches human_gate with a DRAFT card and stops.

  2. confirm_priority is called without approval → the server blocks, and the block is logged to the gate ledger.

  3. With an approver (./run_demo.sh "Sr. A. Wanjiru (Triage Nurse)"), the gate records who, what, and when in gate_ledger.json, and the card is published to its queue position.

  4. Block-by-default is a property of the MCP server, not a prompt: the tool itself refuses ungated calls.

  5. Re-check flags are "a second look, not a verdict": the agent flags, the clinician re-examines — the agent never alters a confirmed priority.

12. Limitations

  • Negation handling (known, unfixed, on purpose): the keyword fallback extractor has no negation scope, so "denies chest pain" can over-triage to ORANGE. Documented as eval task F1 in EVALS.md; the error is in the safe direction, and it is reproducible.

  • English-only complaint text in the current discriminator mapping.

  • Adult chart only. Children ≤12 are escalated to a clinician, never scored by the adult chart; the paediatric chart is not implemented yet.

  • Two interpreted SATS chart cells (documented in data/ATTRIBUTION.md) where the source chart's wording required an interpretation; both are cited.

  • Synthetic data only. All intake cards are fabricated ("synthetic": true); there is no real-patient evaluation in this repository.

  • Frontier-model lane untested end-to-end pending a rotated ModelScope key; the lane is documented synthetic-only and the repo never depends on it.

  • No UI. The queue board is a markdown artifact; wiring a live dashboard or HMS integration is future work.

  • Single-facility scope. Queue semantics (bay naming, shift handover, multi-desk concurrency) are simplified.

13. Future improvements

  • Fix F1 properly: a negation-scope pass over the complaint text plus a second open-weights adjudicator vote before extraction is accepted.

  • Paediatric chart support: implement the SATS paediatric columns so children are scored by the right scale instead of blanket escalation.

  • Swahili (and code-switched) complaint intake for real OPD language realities, with the same cited-derivation discipline.

  • Delivery where the queue lives: SMS/WhatsApp rendering of the queue board for facilities without a shared screen.

  • FHIR / HIE integration: read intake cards from registration systems and write confirmed priorities back as observations.

  • Queue analytics: per-day time-to-see by colour band, gate turnaround times, and re-check outcomes for facility managers.

14. Other

Evaluation & known failure

Fifteen evaluation tasks pass; one failure (F1, negation handling) is documented and reproducible — see EVALS.md or run:

python -m mstari.main evals

Track compliance map

Requirement

Where

Own MCP server, 3+ tools, ≥1 acting

mstari/mcp_server.py — 6 tools, 4 acting, 2 gated

One MCP server we did not write + why

official mcp-server-fetch, wired in mcp_config.json (justification inline; verified in evals T15)

OSS orchestration

LangGraph (mstari/graph.py): plan → intake → extract → score → gate → publish → rank → summarize

One full task on open-weights model

complaint → discriminator extraction on Ollama Qwen (mstari/llm.py); patient data never leaves the machine

Logged tool call for every action

runs/<run_id>/audit_log.jsonl — inputs, outputs, timestamps

Gate on every irreversible action

mstari/gates.py + confirm_priority / raise_recheck_flag — named clinician, timestamped, blocked-by-default

Never diagnose / prescribe / decide

mstari/guardrails.py (lexicon scrub + hard block), deterministic score, clinician gate, citations everywhere

Grounded in an existing scale

SATS adult chart; sources + interpretation notes in data/ATTRIBUTION.md

OSI licence

MIT (LICENSE)

8+ eval tasks incl. one unfixed failure

EVALS.md — 15 pass, F1 intentionally unfixed

Architecture doc

ARCHITECTURE.md

Demo video (unedited, tool calls on screen)

video/mstari-demo.mp4 — cut from a real run's audit log (2:16); shot list in VIDEO_SCRIPT.md

Repository layout

mstari/            the agent: mcp_server, mcp_client, graph (LangGraph),
                   scale (SATS TEWS), llm, guardrails, gates, queue, evals
data/synthetic/    fabricated intake cards (no real patient data, ever)
data/references/   verbatim SATS chart source extract + ATTRIBUTION.md
docs/              architecture diagram (PNG + editable HTML source)
video/             demo walkthrough MP4 (every step incl. ingestion) + notes
web/               hosted demo mirror — Next.js UI, Vercel-ready, Neon storage
runs/              per-run artifacts: queue boards, audit log, gate ledger
tests/             deterministic unit tests for the scoring core
run_demo.sh        the one command

Notes on safety & data

  • All patient data in this repo is synthetic ("synthetic": true on every card). No PHI exists, is stored, or is transmitted. Default LLM lane is local (Ollama); the optional frontier lane is documented as synthetic-only.

  • The agent's failure posture is conservative: missing vitals → refuse to score; unknown discriminator codes → reject; child → escalate; gate → block by default.

  • Known limitations are documented, not hidden: see EVALS.md (negation handling, F1) and data/ATTRIBUTION.md (two interpreted chart cells).

Licence & attribution

MIT — see LICENSE. SATS adult chart source, interpretation notes and data provenance: data/ATTRIBUTION.md and data/references/sats_adult_chart_source.md.

Related MCP Connectors

Related MCP Servers