Documents / Decisions / adr-a18d0577739f
The environment an MCP client runs in is a tool, because it is not a command
Why docir_doctor is the second exception to one-tool-per-dispatcher-command, and why the composition and payload moved out of the CLI to get there.
Decision¶
docir_doctor joins docir_schema as an MCP tool with no dispatcher command behind it. The
report's composition and its payload shape move from the CLI into entry_points/doctor.py, and
both transports call them.
Why it cannot be a command¶
Every other tool is one Request through a RequestExecutor, which is what makes the CLI and
MCP unable to answer differently (MCP as a second transport over the dispatcher). doctor cannot join them, and not for
want of trying: it snapshots the environment before anything dispatches, because dispatching
is what replaces a daemon serving other code and builds a missing index. Both facts are gone by
the time a first reply returns.
So a doctor command would report the state after the repairs it exists to tell you about.
The ordering is the feature.
Why it is a tool anyway¶
An MCP-only agent could see none of its own environment: not the daemon, not the peers a
federated read was silently skipping, not the embedding model actually in force, not the thread
cap The embedding model takes every core, so warming, reindexing and searching saturate the CPU. docir_store_status covers the store half and is deliberately
"facts only the index can answer" — an environment fact does not belong in it.
The same argument docir_schema was excepted on: a thing an agent needs that is not a command.
The test asserting the tool surface names both by hand, so a third stays a decision.
What moved, and why that is the point¶
A second transport composing the report itself is the drift the one-implementation rule exists to prevent, and it would be invisible — two reports that merely disagree.
So build_report (snapshot, then store, then findings, in that order) and as_payload (the
section shape) live in doctor.py, with store_reply's coercion beside them because both
transports have to read an unanswerable store the same way. The CLI keeps only what is its
own: the spinner, and the choice between a table and JSON.
The store half still goes through the executor, so the report describes the daemon actually answering rather than a second container built beside it.
It answers when nothing else does¶
The composition root supplies diagnose on the store-unreadable path too. Explaining why a
store will not open is what doctor is for, so it is the one tool that must keep working when
every other one returns the refusal — the surface An unloadable schema kills the MCP server with no message kept alive, now with
something useful to say.
Consequences¶
An agent on either transport can answer "is this installation healthy, and what is it running with" in one call. What no tool can do is change it: the variables the report names are read where the server was launched, so the agent tells the person what to export.