hivtools-mcp
OfficialClick on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@hivtools-mcpget the latest HIV prevalence statistics"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
hivtools-mcp
An HTTP API and MCP server that lets an LLM,
such as Claude through a claude.ai custom connector, answer questions using
modelled HIV estimates from Naomi, Spectrum and SHIPP. It gives the model two
tools: search_hiv_metadata turns plain-language terms into IDs, and
get_hiv_data returns the estimates for those IDs.
GitHub repository: https://github.com/hivtools/hivtools-mcp/
Documentation: https://hivtools.github.io/hivtools-mcp/
Contents: How it works · Running locally · Testing the MCP server · Deploying · Further documentation
How it works
Architecture
flowchart LR
subgraph clients["Clients"]
claude["claude.ai<br/>custom connector"]
curl["curl / browser"]
end
subgraph image["Docker image, run on Azure Container Apps"]
auth["Token check"]
mcp["MCP server at /mcp"]
knowledge[("Knowledge files")]
subgraph routes["API routes = MCP tools"]
search["GET /search<br/>search_hiv_metadata"]
data["GET /data<br/>get_hiv_data"]
end
duckdb["DuckDB, in memory"]
parquet[("Parquet dataset")]
auth --> mcp
auth --> routes
mcp -- "tool calls, in-process" --> routes
knowledge -. "server instructions" .-> mcp
knowledge -. "concepts, units, aliases" .-> search
routes --> duckdb --> parquet
end
subgraph build["make data, at build time"]
demo[("Demo inputs<br/>data-prep/raw-data/")]
private[("Private inputs<br/>hivtools-mcp-data repo")]
extract["extract_indicators.py<br/>+ extract_spectrum_shipp.R"]
demo --> extract
private --> extract
end
claude -- "MCP over HTTP" --> auth
curl -- "HTTP" --> auth
extract -- "copied into the image" --> parquetOne app, two interfaces.
app/main.pyis a FastAPI app.app/mcp.pygenerates the MCP server from that app's OpenAPI schema, so each route is also a tool. A route's docstring is the tool description the model reads.The data is built ahead of time.
make dataturns the model outputs into a Parquet dataset, which is copied into the Docker image. Nothing is fetched at runtime, so shipping new data means cutting a new release.Hand-written knowledge.
app/knowledge/holds the facts the model outputs don't include: concepts such as "treatment gap", units, age groups that are safe to sum, and the server instructions.One API token, sent as a standard bearer token (
Authorization: Bearer <token>), protects both the API and/mcp. Locally no token is set, so everything is open.
How an LLM uses the tools
flowchart TD
question(["User asks a question, e.g.<br/>'Where is the treatment gap largest in Malawi?'"])
search["<b>search_hiv_metadata</b><br/>one q per term:<br/>'treatment gap', 'Malawi'"]
ambiguous{"ambiguous?"}
choose["Choose on substance,<br/>or ask the user"]
answerable{"answerable:<br/>false?"}
cannot(["Say the data<br/>cannot answer it"])
plan["Take the IDs from the matches.<br/>For a concept, also follow<br/>its notes, coverage and<br/>default_disaggregation"]
get["<b>get_hiv_data</b><br/>resolved IDs + one source"]
rows{"Any rows?"}
diagnostic["Read the diagnostic:<br/>which value or filter is wrong,<br/>and what would work"]
read["Read meta first:<br/>unit, basis, source label"]
answer(["Answer in the source label's<br/>wording, with lower–upper<br/>intervals. Never sum<br/>overlapping rows."])
question --> search --> ambiguous
ambiguous -- yes --> choose --> answerable
ambiguous -- no --> answerable
answerable -- yes --> cannot
answerable -- no --> plan --> get --> rows
rows -- no --> diagnostic -- "fix the filter" --> get
rows -- yes --> read --> answerThe model can't guess IDs (the treatment gap is untreated_plhiv_num), so it
always searches first. The tool descriptions in app/search.py
and app/indicators.py steer it through this flow, and so do
the server instructions in
app/knowledge/instructions.md. Not every MCP
client loads server instructions, so anything the model must know goes in the
tool descriptions as well.
Related MCP server: Healthpoint MCP Server
Running locally
Requirements
Tool | Needed for |
Everything. It installs Python (3.10+) and the dependencies. | |
Git | Cloning the repos |
Node.js 22.19+ | |
Docker | Optional: building and running the image |
R | Optional: building the Spectrum and SHIPP files in the private data |
Access to hivtools-mcp-data | Optional: the private data. Ask a maintainer. |
One-time setup
git clone git@github.com:hivtools/hivtools-mcp.git
cd hivtools-mcp
make install # creates .venv with uv and installs the pre-commit hooks
make data # builds the public demo dataset into data-prep/naomi-data/Without make data the server still starts, but every query returns no rows.
Optional: private data. The data that isn't public lives in the private hivtools-mcp-data repo. You clone it into this repo (the folder is gitignored):
git clone git@github.com:hivtools/hivtools-mcp-data.git data-prep/private-data
make r-deps # the R packages that read Spectrum and SHIPP files
make data # now builds the demo data and the private dataThis repo is public. Never commit private inputs or anything built from them, and never print them in CI logs.
With the private data checked out, make test also checks the knowledge files
against it. docs/data.md covers how the dataset is built and how to
update the private data.
Running the server
make devThis serves the app at http://127.0.0.1:8000 and reloads when you change a file.
URL | What it is |
Interactive API docs | |
The MCP server (streamable HTTP) | |
An example search |
Every MCP tool call is logged in this terminal, with its arguments and the start of the response.
To switch auth on, as in production, set a token:
HIVTOOLS_MCP_API_TOKEN=some-token make dev. Settings are environment variables,
or can go in a .env file. See Configuration for the
full list.
Running the tests and checks
make test # pytest, with coverage
make check # lock file, pre-commit (ruff), ty type check, deptryCI runs both, and runs the tests on Python 3.10 to 3.14. make help lists every
target.
Running with Docker
docker build -t hivtools-mcp . # public demo data only
make docker # the dataset `make data` builds, private data included
docker run -p 80:80 -e HIVTOOLS_MCP_API_TOKEN=some-token hivtools-mcpThe app is then at http://127.0.0.1:80. The image refuses to start without a
token, because its data may not be public. For a throwaway local run, add
-e HIVTOOLS_MCP_REQUIRE_AUTH=false. For how data gets into the image, see
docs/data.md.
Testing the MCP server
Use the MCP Inspector, which needs Node.js 22.19 or later. It works against a local server or production:
Local | Production | |
URL |
|
|
| Only if you set |
|
For a local server, start it:
make dev.In another terminal, start the Inspector. It opens in your browser.
npx @modelcontextprotocol/inspectorAdd the server: click Add Servers → Add manually. Choose the streamable-http transport and enter the URL from the table. Add a header named
Authorizationwhose value isBearerfollowed by the key. Click Add.Switch the server's toggle on to connect, then open Tools. You should see Search HIV Metadata (
search_hiv_metadata) and Get HIV Data (get_hiv_data).Pick a tool, fill in its arguments and click Execute Tool. Arguments must be valid JSON, so put quotes around strings (
"MWI") and write lists like["Lilongwe"].
To see what the model sees, follow the flow above with the demo data:
search_hiv_metadatawithq=["treatment gap", "Lilongwe"]andcountry="MWI". The first result for each term is a concept or an area with its ID.get_hiv_datawith those IDs, for exampleindicator=["untreated_plhiv_num"]andarea_id=["MWI_3_13_demo"].Try a wrong ID, such as
area_id=["Lilongwe"], to see thediagnostic.
From the command line
The Inspector also has a CLI mode, which is handy for quick checks:
# List the tools
npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8000/mcp --method tools/list
# Call a tool
npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8000/mcp \
--method tools/call --tool-name search_hiv_metadata \
--tool-arg 'q=["Lilongwe"]' --tool-arg country=MWIIf auth is on, add --header "Authorization: Bearer <token>". Against production:
npx @modelcontextprotocol/inspector --cli https://hivtools.org/mcp --method tools/list \
--header "Authorization: Bearer $(terraform -chdir=infra output -raw api_token)"From Claude Desktop
Claude Desktop's local MCP servers expect a stdio command, not this server's
streamable-HTTP endpoint directly — bridge the two with
mcp-remote.
Start the local server:
make dev.In Claude Desktop, open Settings → Local MCP servers → Edit config and add an entry:
{ "mcpServers": { "hivtools-local": { "command": "npx", "args": ["-y", "mcp-remote", "http://localhost:8000/mcp", "--allow-http"] } } }If auth is on, append
"--header", "Authorization:Bearer <token>"toargs.Save and reconnect. Claude Desktop starts two
mcp-remoteprocesses per connector; the first time either needs to fetch a dependency, they can race on the shared npm cache and both fail withServer disconnected. View logs on the failed connector shows the real cause if this happens: annpm error ... EACCES ... mkdir _cacacheentry. It is npm cache corruption from the race, not a real permissions problem — clear it once and pre-populate the cache so the race can't recur, then reconnect:npm cache clean --force npx -y mcp-remote@latest --help
Getting the production API key
The key is generated by Terraform, so any of these gives you the same value:
Terraform, if you hold the state (see infra/README.md):
terraform -chdir=infra output -raw api_tokenAzure, if you have access to the subscription: in the portal, open the Container App
ca-hivtools-mcp-prod, then Settings → Secrets and show the value ofapi-token. Or from the command line:az containerapp secret show --subscription <subscription id> \ -g rg-hivtools-mcp-prod -n ca-hivtools-mcp-prod \ --secret-name api-token --query value -o tsv
Otherwise, ask whoever runs the deployment. It is also stored as the API_TOKEN
secret in GitHub, but GitHub never shows secret values again.
Deploying
Production runs on Azure Container Apps. Publishing a GitHub Release deploys it, and there are no manual steps.
What you need: write access to this repo, so you can merge to main and
publish releases. You don't need Azure access to release, only to
change the infrastructure.
Releasing a new version
In your PR, bump
versioninpyproject.toml:uv version --bump patch # or minor / major; also updates uv.lockMerge the PR to
main.Publish a GitHub Release. Its tag is the new version with a
vin front. You can do this in the GitHub UI (Releases → Draft a new release) or with:gh release create v0.3.1 --target main --generate-notesWatch the release-main workflow in the Actions tab, or with
gh run watch.
The release runs
.github/workflows/on-release-main.yml,
which:
Fails fast if the tag doesn't match the version in
pyproject.toml.Checks out the private data repo and builds the dataset with
make data.Builds the image and pushes it to Azure Container Registry, tagged
vX.Y.Z.Rolls a new Container Apps revision onto that image.
Smoke-tests the new revision: it checks the health and version endpoints, that
/datarefuses requests without the token, and that each source is served.Publishes the documentation site to GitHub Pages.
To check it afterwards, the workflow run's summary page links to the live app
(the production environment). https://<that host>/version should return the
new version.
Shipping new data
The data is built into the image, so new data also ships as a release:
Push the data change to hivtools-mcp-data. See Updating the private data.
Bump the version and publish a release, as above. A release tag can only be used once, so a data-only release still needs a version bump.
The release builds from the data repo's default branch, or from the
DATA_REPO_REF variable if it is set on the production environment. The image
registry is private, but anyone who can pull the image can read the data.
Changing the infrastructure
The Azure resources are managed with Terraform in infra/. CI only
validates it. Someone applies it by hand, which needs Azure access and the local
state file. infra/README.md covers this, plus the one-time
setup: provisioning, the GitHub variables and secrets the release needs, and
rotating the API token.
Further documentation
docs/data.md: the data sources,
datasets.yaml, the private data repo, how the Parquet dataset is built, and how it gets into the image.docs/api.md: HTTP API reference for
/searchand/data, authentication, and every configuration setting.infra/README.md: the Azure infrastructure and one-time deployment setup.
app/knowledge/: the hand-written knowledge files the tools serve.
CONTRIBUTING.md: how to contribute.
This server cannot be deployed
Maintenance
Related MCP Connectors
Hosted MCP server for the Healthie EHR & telehealth API: patients, appointments, charting, tasks.
Hosted MCP server exposing US hospital procedure cost data to AI assistants
Swagger Petstore API (v1.0.27) as MCP for testing and prototyping powered by the HAPI MCP server
Governed data discovery, exact queries, decisions, simulations, and runtime utilities over MCP.
Related MCP Servers
- FlicenseCqualityDmaintenanceMCP server that exposes the OpenMRS/Bahmni REST API as tools for Claude to interact with Bahmni instances.19-
- AlicenseAqualityCmaintenanceRead-only MCP server for licensed Healthpoint HL7 FHIR API access.101Apache 2.0
- AlicenseNot gradedqualityBmaintenanceEnables AI clients to securely access, query, and mutate normalized user-controlled health data (e.g., from Apple Health or Supabase) through a bounded set of MCP tools, with optional OAuth and sandboxed deployment.MIT
- AlicenseNot gradedqualityBmaintenanceMCP server for Kenya health facility and epidemiological data via KHIS/DHIS2.25 PyPIMIT