v0.5.0
Added
-
validate_pvscan answer the other question about a display:view="file" | "display". The
tool has always reported what a.bobfile itself declares, attributed across every display that
embeds it. That is the right answer for a fragment and the wrong one for a screen: a display that
only composes embedded fragments declares nothing of its own, so it answeredtotal: 0while
resolving thousands of channels.view="display"asks the second question, what the file
resolves to when opened as a display, fragments included. Measured on a 257-display dataset, the
two views disagree on 54 files, 42 of which answeredtotal: 0under the default, the largest
hiding 5846 channels. The default stays"file", so nothing changes for existing callers.
Both views come out of the same inventory walk, so the second one is free. -
Every
file_pathresult now carriesshown_by_displayandshown_by_display_capped, and
underview="file"anotesentry says how many channels the display view adds. The counts let
a caller see which of the two questions was answered without running the other one. The capped
flag is computed separately from the file view's own cap verdict, which asks a different question
(see theFixedentry below). -
epics-init, a sixth console command: the configuration you need, without reading the
reference first. It prints the MCP client-configuration block for one of four deployment shapes
(sandbox,ioc-only,ioc-archiver,full;--listdescribes them) and then runs
epics-doctoragainst exactly that block, so generating a configuration and checking it are one
step. The block goes to stdout and everything else to stderr, soepics-init --preset X > .mcp.jsonyields a usable file.--set NAME=VALUEfills or adds a variable,--probe-pv NAMEis
passed to the doctor,--no-checkemits only. It introduces no newEPICS_MCP_*variable: the
presets set existing ones. Exit codes are the doctor's own (0/1/3), with2for a usage
error. Three behaviours worth knowing: a value still carrying a placeholder makes it REFUSE the
check and name it rather than report an unactionable failure (it reads the value in any case,
<ARCHIVER-HOST>as well as<archiver-host>, and does not fire on a named regex group such as
a write-gate pattern); a shape with no REST plane probed without--probe-pvis reported as
confirming nothing, because the live plane makes no network call in that case; andsandbox
searches BOTH ways a PVA client can, UDP broadcast to127.0.0.1and TCP unicast to
127.0.0.1:5075, so it reaches a soft IOC running natively on the host as well as one in a
container, which typically publishes only its PVA TCP port and no UDP search port. -
setup_epics_mcp, a third MCP prompt. The same walkthrough conversationally: it asks about
each service plane in turn and ends by naming theepics-initcommand to run. -
monitor_pvsays whether the channel was reachable. A newconnectionfield
(connected/disconnected/unknown) travels with the events, andconnection_detail
adds one sentence whenever there is something to explain. Zero events used to be ambiguous
between "the PV was quiet" and "the PV was never there", andget_pv_valuecontradicted this
tool on the second case by raisingPV_TIMEOUT, so the two disagreed about one fact.
connectedis claimed only where a delivered value proves it; a stream the server ended, or
one that reported an error, isunknownrather than a guess. A subscriptionRemoteErrornow
reaches the caller as well, where before it only went to the log and left an unexplained empty
result. The state is subscribed for rather than probed separately, so it describes the same run
as the events. Purely additive: existing fields are unchanged.
Removed
- The Olog read redaction is gone: every logbook read returns the whole entry.
search_logbook,
get_log_entry, the create/reply/update echoes andlist_log_attachmentsnow carrytitle,
description,owner(the author),source,propertiesand the raw attachments list for every
server, anddownload_log_attachmenthands the bytes back (size-capped) without an opt-in. The
redaction had been built against an assumed privacy rule that was never specified for this server,
and it cost the logbook its point: a search returned ids whose content the caller could not judge.
A deliberate prototype decision (2026-08-01), consequences stated indocs/safety.md; if a real
facility privacy specification ever arrives, it will be rebuilt against that specification. With
it go, user-visibly: the env varsEPICS_MCP_OLOG_ASSUME_TEST_DATAand
EPICS_MCP_OLOG_ALLOW_ATTACHMENT_DOWNLOAD(now unknown, the config warns if still set), the
error codesOLOG_WHOLE_MODE_REQUIREDandOLOG_ATTACHMENT_DOWNLOAD_DENIED, thewithheld
fields of the download/list results, and the doctor'sOlog free-text:line together with the
olog_freetext_withheldfield its privacy report carried, whichepics-doctor --jsontherefore
no longer emits. The whole-mode preconditions ofadd_log_attachment/update_log_entryare
replaced by the write gate itself:
its env + URL checks now run BEFORE the pre-write read, so a target the gate refuses is never
even read. Both write gates, the ChannelFinder redaction and the withheld-is-not-no semantics of
the other planes are unchanged. - The alarm twin of that redaction is gone too.
is_alarm_configuredreturns the authored
fields (description/guidance/displays/commands/actionsand the serializedconfig_msg)
with their values, exactly the handling instruction the withholding used to blank, and
get_alarm_historyevents carryuser/host/command/config_msgagain. The known-field
allowlists stay as structure: an unknown field a future logger version adds is still dropped.
Changed
validate_pvsrefuses an unusablefile_pathat once instead of walking the whole dataset
first. A path that is not a.bob, or that lies outsidedisplays_dir, used to run the full
display-PV inventory, tens of seconds on a large dataset, and only then return an empty result
(or, in the second case, the error it could have given straight away). Neither input can produce
a PV: the inventory reads.bobfiles only and resolves embedded targets against that same
collected set, so the outcome was settled before the first file was opened. Both are now
INVALID_INPUT, and the message names the way out (pv_namesfor a plain list of PVs).
A client that passed a non-.bobpath today received a successfultotal: 0, and will now
get an error. The suffix comparison folds case, soUPPER.BOBis still a display; and a
genuine.bobthat declares no realca/pvachannels is unchanged, that stays an honest
total: 0, not a refusal. Passingpv_namesas well makes the list win, and the file path is
neither read nor refused.coverage_auditreports a missing alarm tree before doing the work, not after. Asking for
the alarm plane without naming a tree (alarm_config) was already anINVALID_INPUT, but the
refusal arrived only once the display-PV walk had finished, tens of seconds on a large dataset,
for a verdict the arguments alone decide. Same defect as thevalidate_pvsone above, in the
sibling tool. The error itself is unchanged.- The
compare_machine_stateprompt no longer suggestsvalidate_pvsfor a non-display file.
With areference_filesuch as a CSV or a JSON snapshot it now tells the client to read the file
itself, because naming the tool would hand over a call the server is now certain to refuse. A
.bobreference file (in any capitalisation) is unaffected and still uses the tool. - Five capped arguments now reject a non-positive value instead of answering emptily.
monitor_pv.max_events,find_channels.max_resultsand thecontext_capof
crossplane_check,coverage_auditandfind_devicerequire>= 1, andmonitor_pv.duration
requires> 0. A cap of0did not fail before: it succeeded and returned nothing, which a
client cannot tell from "the thing you asked about does not exist".find_channelswas the
sharpest case, returning an empty channel list together withcapped: true, so the answer
claimed there was more while showing none. A client passing0today will now get a
validation error rather than an empty result. The four sibling caps that already carried this
bound stated the reason at the call site; it now holds everywhere, watched by a test over the
live tool registry rather than a list, so a future tool is covered the day it is registered. - Ten
timeoutarguments now reject zero and below.get_pv_value,get_pvs,get_pv_info,
set_pv_value,discover_pvs,diagnose_connection,find_channels,is_archived,
validate_pvsandfind_devicerequire> 0, which the other nineteen timeouts already did.
A client passingtimeout=0today will now get a validation error naming the argument,
before any request exists. This was deferred once as a mere inconsistency, on the assumption
that a zero timeout fails honestly rather than fabricating an answer. Measured over all ten,
that assumption held for five and was wrong for the other five, which returned a
plausible-looking result instead:find_deviceanswered "No operator-facing screen references
this device",validate_pvsreported the PV as disconnected,diagnose_connectionnamed a
cause,discover_pvsandget_pvscame back empty. Two of the five that did raise pointed at
the wrong thing:PV_TIMEOUTblames the device rather than the argument, andis_archived
surfacedINTERNALwith a server-side traceback for what is a caller input error. The registry
guard that watches this now covers every numeric argument rather than only the integer half,
and for anumberit requires an EXCLUSIVE lower bound, so a futurege=0cannot reintroduce
the same defect. epics-crossplane --helpandepics-coverage --helpnow answer on a core-only install. They
used to report the missing display engine and exit2instead, so on any install from a package
index, where that engine is never present, the first answer to--helpwas an instruction to
install something you do not need in order to read a help text. Both commands now parse their
arguments before asking for the engine, and--versionanswers there too, so every console
command explains itself everywhere. They still need the engine to do their work and still say
so when asked to do it.- A usage error on those two commands reports the engine, and its exit code is the engine's.
epics-coverage --nopeon a core-only install answers with the missing engine rather than with
the following arguments are required, because supplying the argument would not make the command
run either. The code is2where the engine is absent, as before, and1where the engine is
installed and fails to load, which is the code the same command returns on that install with
correct arguments. - The source distribution declares what it contains. It did not, and an undeclared sdist is not
"the tracked tree": the backend packs the working tree minus what version control ignores, so it
also packs any untracked file that happens to be lying there at build time. Measured on the 0.4.0
artifact: 192 files, which was every tracked file plus a stray log file, and it included all 81
test modules,scripts/,.github/andCLAUDE.md(instructions meant for an assistant working
in the repository, not for anyone installing the package). The sdist now carries the package, the
documentation and the standard metadata files, 96 files, and a test builds it and compares its
contents against version control in both directions, so neither a stray file nor a silently
dropped directory can reach a package index again. The wheel is unchanged.
Fixed
validate_pvsansweredtotal: 0without a word when the macro expansion had been cut short.
The honesty note that says the PV list is a lower bound was raised inside the loop that filters a
display's PVs down to this file's own resolved channels, so on a file where nothing survived that
filter the loop body never ran and the note could never appear. That is exactly the answer a
reader is most likely to take at face value: measured on a 257-display dataset, 9 files hold a
file view that provably grows with a larger context budget, and the 2 the tool stayed silent on
were both answeringtotal: 0, one of them resolving 5576 channels once the budget allowed it.
The verdict now also fires when the contexts reaching the file itself were dropped, on an empty
and a non-empty result alike, and it is guarded: a file that declares no PV at all keeps its
silent, exacttotal: 0, because calling that a lower bound would be a false statement. No file
that carried the note before loses it. Theview="display"verdict is unchanged.- The shipped status legend explained 9 of the 12 states
epics-doctorcan print, and two of its
marks not at all.disabled,infoanddisconnectedwere never named by their status name
anywhere in the guide, and the marks·andihad no legend entry, so an operator could see a
character the document travelling with the server did not explain. All twelve states now have a
row, every mark is explained, and the legend is held TOTAL againstPlaneStatus: a state that is
not documented is a failing test rather than a decision. Still not documented, and now recorded as
such: theOverall:verdict line and the privacy block. - The guide explained what a report line SAYS but never what it is CALLED, and one plane had no
entry at all. The guide's plane bullets are grouped by service and count six;epics-doctoris
grouped by check and prints seven, because the Archiver's management root and its retrieval root
are separate planes.archiver_retrievaltherefore appeared under no bullet, and a reader seeing
that line had nothing to look it up by. Both orderings were individually correct, which is why
prose alone never caught it. The guide now names all seven planes in the spelling the report uses,
and says which--jsonfield carries them. - The guide's advice for sizing
EPICS_MCP_READ_RATE_LIMITwas wrong for one of the two tools it
named, and unusable for the other. It said a multi-GET tool such ascoverage_auditor
crossplane_checkspends "several tokens per audited PV". Measured at the throttle itself,
crossplane_checkis not per-PV at all: it spends 2 tokens in total, the same for one display
PV or a thousand, and 3 when the IOC device name turns out not to be registered, which is the
finding it exists to report. Anyone who sized a limit from the old sentence over-provisioned for
that tool by whatever their PV count was, and would still have been caught out on the one run that
finds something.coverage_auditis per-PV, and now says how much: 1 + 2N with both per-PV
planes requested, rising to 1 + 3N when the audited PVs are not alarm-configured, because a
missed alarm lookup re-asks for the bare tree. There is no single per-PV figure, and the guide now
says so instead of implying one.
Internal
- The typography guard sees the doubled hyphen at the START and the END of a line, not only between
two spaces. That is where the form lands when a sentence wraps, and it was the one place the rule
could not look.scanpads each line with one space per side, so the rule table is unchanged; the
two occurrences this uncovered in the tree were corrected with it. The reach, the four legitimate
forms the widening also catches and the blind spots that remain are recorded in
docs/known-limits.mdsection 10. - Two guards hold the shipped guide's new plane inventory: the region against a real
run_doctor
run, and the plane-name literals inservices/doctor.pyagainst that same run rather than against
the guide, so a name spelled in the module that never reaches a report is reported as the dead
branch it is instead of as a documentation gap. The literal scan derives which call shapes carry a
plane name from the parameter name rather than from a list, after a hand-kept list was measured to
miss six positional literals. What neither guard holds is recorded indocs/known-limits.md
section 14.