MCP tutorial¶
A walk through driving dhis2w-mcp from an LLM agent — from "the server isn't loaded" to a real end-to-end DHIS2 workflow with one controlled mutation and a rollback. Assumes you've installed the server per the overview and wired it into your MCP host.
This covers the full server (typed tools). The single-tool bridge for local models is driven differently (the model calls dhis2_cli([...]) and discovers commands via --help) — see its usage guide.
Each step shows what you ask the agent, which tool it should invoke, the shape of the response you should see, and how to recover when something goes wrong.
1. Confirm the server is loaded¶
In Claude Desktop / Claude Code / Cursor, ask the agent:
List the MCP tools you have for DHIS2.
Expect: a count plus a sampled list grouped by plugin (analytics, apps, customize, data, doctor, files, maintenance, messaging, metadata, profile, route, system, user — roughly 304 tools total).
Recovery — "zero tools":
- The host hasn't loaded the server. Reload the MCP panel (Claude Desktop: quit + reopen; Claude Code:
/mcp). - Check the host's MCP log for a startup error — usually "command not found" (binary not on
PATH) or a Python import failure (uv environment broken). - Run
dhis2w-mcp --versionyourself in a terminal. If that works but the host can't launch it, the host'scommand:path is wrong.
2. Smoke-test the server without touching DHIS2¶
Call
system_server_infoand show me the result.
The tool takes no arguments. Expected response (your version numbers will differ):
{
"active_plugin_tree": "v43",
"active_plugin_tree_source": "DHIS2_VERSION='v43' env",
"dhis2w_core_version": "0.10.0",
"dhis2w_mcp_version": "0.10.0",
"dhis2w_cli_version": null
}
This is a process-local introspection — no DHIS2 client is opened, no auth required. Use it to confirm which plugin tree the server bound + which versions are installed. Always run this first when you're not sure where the server is pointed.
3. Touch DHIS2 — the auth smoke test¶
Call
system_whoamito check who the active profile is logged in as.
Expected response: a typed Me model with the agent's perspective on the DHIS2 user (username, displayName, authorities, OU scopes, group memberships).
Recovery — auth error:
The agent gets a typed Dhis2ApiError envelope. The interesting fields:
status_code:401(bad credentials),403(good auth, wrong permission),404(wrong base URL — usually).message: human-readable line from DHIS2.conflicts: per-row error list (only on writes — empty onwhoami).
The same envelope shape comes back from every tool, so an agent learns it once.
| Symptom | Likely cause | Fix |
|---|---|---|
| 401 with "User account is locked" | Account disabled in DHIS2 | Have an admin unlock; or use a different profile |
| 401 with "Bad credentials" | PAT expired / password rotated | Re-run d2w profile add NAME --auth pat to update |
| 403 on every tool | Profile has zero DHIS2 authorities | Profile is using the wrong user — d2w profile verify to confirm |
NoProfileError: no DHIS2 profile is configured |
Server started before any profile was saved | Run d2w profile add NAME --url ... --auth pat --default once; restart the MCP host |
UnknownProfileError from a profile=... call |
Profile name doesn't exist in TOML | d2w profile list to see real names |
4. A real read-only workflow¶
Suppose you want the agent to find "how many WITH_REGISTRATION programs the active DHIS2 has, and which org units the first one is scoped to":
Use
metadata_program_listwithprogram_type="WITH_REGISTRATION"andpage_size=1, then take the first result'sidand callmetadata_program_getwithfields="id,name,organisationUnits[id,displayName]".
What the agent should do (and what you should see in the MCP host's tool-call log):
-
Tool:
metadata_program_listArgs:{"program_type": "WITH_REGISTRATION", "page_size": 1}Response shape:list[Program]— one element on success. -
Tool:
metadata_program_getArgs:{"uid": "<id from step 1>", "fields": "id,name,organisationUnits[id,displayName]"}Response shape: a typedProgramwith.organisationUnitspopulated as a list of references.
The agent narrates the result in prose; you can inspect the raw payloads via the MCP host's log if something looks off.
5. Targeting a different profile per call¶
The agent's default profile comes from DHIS2_PROFILE (set on the server's env: block) or the project / global TOML default. Override per call:
Same lookup, but on staging: pass
profile="staging"to both tools.
Every MCP tool accepts an optional profile: str | None kwarg. One running MCP server can target multiple DHIS2 stacks (local + staging + a play instance) without restart. d2w profile list shows what's available.
6. A controlled mutation + rollback¶
Read-only is safe; the first write is where you want to see the agent narrate before it acts.
Pick any DataElement on the active DHIS2 (
metadata_data_element_listwithpage_size=5). Show me its currentname, thenmetadata_data_element_renameit to append " (test)". Wait for me to confirm before reverting.
Sequence:
-
Tool:
metadata_data_element_listArgs:{"page_size": 5, "fields": "id,name,shortName"}Response: a list ofDataElementmodels — note the first row'sidandnameso you can revert. -
Tool:
metadata_data_element_renameArgs:{"uid": "<de-id>", "name": "<original> (test)"}Response: the updatedDataElementmodel with the new name. -
Verify: have the agent call
metadata_data_element_geton the same uid and confirm the rename landed. -
Rollback:
metadata_data_element_renameagain with{"uid": "<de-id>", "name": "<original>"}.
The rename verb is implemented for most metadata-authoring resources (DataElement, Indicator, Program, Category, CategoryCombo, OrganisationUnitLevel, …); metadata_<resource>_rename is the safest "minimal mutation" surface for trying out an agent's write path. Resources without a dedicated rename (e.g. plain OrganisationUnit) need a fuller update or patch call instead.
Recovery — write failure:
DHIS2 returns a WebMessageResponse with status="WARNING" or "ERROR". The agent sees a Dhis2ApiError whose .conflicts list contains per-field rejection reasons (e.g. "name must be unique"). Read the conflict, fix the args, retry — rename is idempotent on (uid, new_name), so retrying with the same args is safe.
If the agent can't reach DHIS2 mid-flow (network timeout), the first call may have succeeded — the verify step will show the rename did land. Always run the verify step before deciding to retry.
7. Version-sensitive tools¶
Some tools only register when the active plugin tree matches the DHIS2 server. The v43-only setters are the largest cluster:
metadata_program_set_labels(uid, enrollments_label, events_label, program_stages_label)metadata_program_set_change_log_enabled(uid, enabled)metadata_program_set_enrollment_category_combo(uid, category_combo_uid)
These are absent from the tool list on a v42-bound server. If the agent says "I don't see a metadata_program_set_labels tool", call system_server_info to confirm the active plugin tree, then either point the server at a v43 DHIS2 (with DHIS2_VERSION=v43 in the host's env: block) or use the v42 alternatives.
8. Watching long-running jobs¶
DHIS2 has several async endpoints (analytics refresh, metadata import, predictor runs, data-integrity scans). The MCP tools that kick those off return a TaskRef immediately; the agent should poll for completion before declaring success:
Run
maintenance_refresh_analytics, then pollmaintenance_task_statusevery 5 seconds until it reportsCOMPLETEDorFAILED. Tell me the elapsed time.
The poll body shape: {"job_type": "ANALYTICS_TABLE", "job_id": "<id from refresh response>"}. Returns the latest TaskCompletion snapshot — status + per-stage notification list.
If the agent doesn't poll, it'll report "refresh started" but the analytics tables won't actually be ready yet — subsequent queries will hit stale data.
Where next¶
- Reference — every tool with its parameter schema + description.
- Architecture — return-shape conventions, error handling, profile resolution.
- Examples — Python scripts that drive the in-process MCP server end-to-end (useful for snapshot-testing agent flows). Each
examples/v{N}/mcp/*.pyinvokes a real tool sequence — copy one as a template for your own scripted agent flow.